Implement gambling
This commit is contained in:
352
blockchain.py
352
blockchain.py
@@ -5,8 +5,23 @@ import hashlib
|
|||||||
from multiprocessing import Lock
|
from multiprocessing import Lock
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@dataclass
|
n = 22152137184697602751949152182712806144286735269755991212091578018969267364854563246965161398309375840488156997640625085213985729183180764348127989435514689722834129288342499703533613231239853168289771769536412763137391054558055082146752229593979328251181647873233949602834141648160681711983351545692646009424518816069561938917629523175464947983950548802679152115205735609960641453864298194702935993896839374645356040490091081577992299773430144650589605043643969140352237968606446474316247592579560197155686719175897498255683642121505357781103123719079205647707696181709515150954235402701095586525936356219917713227143
|
||||||
|
|
||||||
class Transaction:
|
class Transaction:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NoTransaction(Transaction):
|
||||||
|
def is_valid(self):
|
||||||
|
return True
|
||||||
|
def is_valid_after_block(self, block):
|
||||||
|
return True
|
||||||
|
def get_transaction_raw(self):
|
||||||
|
return 149 * b"\0"
|
||||||
|
def is_empty(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PaymentTransaction(Transaction):
|
||||||
id: int
|
id: int
|
||||||
sender: bytes
|
sender: bytes
|
||||||
receiver: bytes
|
receiver: bytes
|
||||||
@@ -15,18 +30,18 @@ class Transaction:
|
|||||||
signature: bytes
|
signature: bytes
|
||||||
|
|
||||||
def from_bytes(transaction_raw):
|
def from_bytes(transaction_raw):
|
||||||
assert len(transaction_raw) == 148
|
return PaymentTransaction(
|
||||||
return Transaction(
|
id = int.from_bytes(transaction_raw[1:5], "big"),
|
||||||
id = int.from_bytes(transaction_raw[0:4], "big"),
|
sender = transaction_raw[5:37],
|
||||||
sender = transaction_raw[4:36],
|
receiver = transaction_raw[37:69],
|
||||||
receiver = transaction_raw[36:68],
|
amount = int.from_bytes(transaction_raw[69:77], "big"),
|
||||||
amount = int.from_bytes(transaction_raw[68:76], "big"),
|
transaction_fee = int.from_bytes(transaction_raw[77:85], "big"),
|
||||||
transaction_fee = int.from_bytes(transaction_raw[76:84], "big"),
|
signature = transaction_raw[85:149],
|
||||||
signature = transaction_raw[84:148],
|
|
||||||
)
|
)
|
||||||
def is_valid(self):
|
def is_valid(self):
|
||||||
sender_pubkey = Ed25519PublicKey.from_public_bytes(self.sender)
|
sender_pubkey = Ed25519PublicKey.from_public_bytes(self.sender)
|
||||||
msg = self.id.to_bytes(4, "big") + \
|
msg = b"\x01" + \
|
||||||
|
self.id.to_bytes(4, "big") + \
|
||||||
self.sender + \
|
self.sender + \
|
||||||
self.receiver + \
|
self.receiver + \
|
||||||
self.amount.to_bytes(8, "big") + \
|
self.amount.to_bytes(8, "big") + \
|
||||||
@@ -44,18 +59,178 @@ class Transaction:
|
|||||||
return False
|
return False
|
||||||
return balance >= self.amount + self.transaction_fee
|
return balance >= self.amount + self.transaction_fee
|
||||||
def get_transaction_raw(self):
|
def get_transaction_raw(self):
|
||||||
return self.id.to_bytes(4, "big") + \
|
return b"\x01" + \
|
||||||
|
self.id.to_bytes(4, "big") + \
|
||||||
self.sender + \
|
self.sender + \
|
||||||
self.receiver + \
|
self.receiver + \
|
||||||
self.amount.to_bytes(8, "big") + \
|
self.amount.to_bytes(8, "big") + \
|
||||||
self.transaction_fee.to_bytes(8, "big") + \
|
self.transaction_fee.to_bytes(8, "big") + \
|
||||||
self.signature
|
self.signature
|
||||||
|
def open_transactions_hash_data(self):
|
||||||
|
return b"\0" + \
|
||||||
|
self.transaction_fee.to_bytes(8, "big") + \
|
||||||
|
self.amount.to_bytes(8, "big") + \
|
||||||
|
self.sender + \
|
||||||
|
self.id.to_bytes(4, "big")
|
||||||
def sorting_id(self):
|
def sorting_id(self):
|
||||||
return (-self.transaction_fee, self.sender, self.id)
|
return (1, -self.transaction_fee, -self.amount, self.sender, self.id)
|
||||||
|
def is_empty(self):
|
||||||
|
return False
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return (self.id, self.sender, self.receiver, self.amount, self.transaction_fee) == \
|
return isinstance(other, PaymentTransaction) \
|
||||||
|
and (self.id, self.sender, self.receiver, self.amount, self.transaction_fee) == \
|
||||||
(other.id, other.sender, other.receiver, other.amount, other.transaction_fee)
|
(other.id, other.sender, other.receiver, other.amount, other.transaction_fee)
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GamblingTransaction(Transaction):
|
||||||
|
id: int
|
||||||
|
player: bytes
|
||||||
|
amount: int
|
||||||
|
transaction_fee: int
|
||||||
|
signature: bytes
|
||||||
|
|
||||||
|
def from_bytes(transaction_raw):
|
||||||
|
if transaction_raw[117:149] != 32 * b"\0":
|
||||||
|
return InvalidTransaction()
|
||||||
|
return GamblingTransaction(
|
||||||
|
id = int.from_bytes(transaction_raw[1:5], "big"),
|
||||||
|
player = transaction_raw[5:37],
|
||||||
|
amount = int.from_bytes(transaction_raw[37:45], "big"),
|
||||||
|
transaction_fee = int.from_bytes(transaction_raw[45:53], "big"),
|
||||||
|
signature = transaction_raw[53:117],
|
||||||
|
)
|
||||||
|
def is_valid(self):
|
||||||
|
player_pubkey = Ed25519PublicKey.from_public_bytes(self.player)
|
||||||
|
msg = b"\x02" + \
|
||||||
|
self.id.to_bytes(4, "big") + \
|
||||||
|
self.player + \
|
||||||
|
self.amount.to_bytes(8, "big") + \
|
||||||
|
self.transaction_fee.to_bytes(8, "big")
|
||||||
|
try:
|
||||||
|
player_pubkey.verify(self.signature, msg)
|
||||||
|
except InvalidSignature:
|
||||||
|
return False
|
||||||
|
return self.amount >= 1
|
||||||
|
def is_valid_after_block(self, block):
|
||||||
|
if (self.player, self.id) in block.used_transaction_ids:
|
||||||
|
return False
|
||||||
|
balance = block.balances.get(self.player)
|
||||||
|
if balance is None:
|
||||||
|
return False
|
||||||
|
return balance >= self.amount + self.transaction_fee
|
||||||
|
def get_transaction_raw(self):
|
||||||
|
return b"\x02" + \
|
||||||
|
self.id.to_bytes(4, "big") + \
|
||||||
|
self.player + \
|
||||||
|
self.amount.to_bytes(8, "big") + \
|
||||||
|
self.transaction_fee.to_bytes(8, "big") + \
|
||||||
|
self.signature + \
|
||||||
|
32 * b"\0"
|
||||||
|
def open_transactions_hash_data(self):
|
||||||
|
return b"\0" + \
|
||||||
|
self.transaction_fee.to_bytes(8, "big") + \
|
||||||
|
self.amount.to_bytes(8, "big") + \
|
||||||
|
self.player + \
|
||||||
|
self.id.to_bytes(4, "big")
|
||||||
|
def sorting_id(self):
|
||||||
|
return (1, -self.transaction_fee, -self.amount, self.player, self.id)
|
||||||
|
def is_empty(self):
|
||||||
|
return False
|
||||||
|
def __eq__(self, other):
|
||||||
|
return isinstance(other, GamblingTransaction) \
|
||||||
|
and (self.id, self.player, self.amount, self.transaction_fee) == \
|
||||||
|
(other.id, other.player, other.amount, other.transaction_fee)
|
||||||
|
|
||||||
|
def is_valid_group_element(x):
|
||||||
|
return x > 0 and x <= n//2
|
||||||
|
|
||||||
|
def normalize(x):
|
||||||
|
if x <= n//2:
|
||||||
|
return x
|
||||||
|
return n - x
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RevealTransaction:
|
||||||
|
revealer_pubkey: bytes
|
||||||
|
associated_proof_hash: bytes
|
||||||
|
reveal_value: bytes
|
||||||
|
intermediates: list
|
||||||
|
|
||||||
|
def from_bytes(transaction_raw, associated_proof_data):
|
||||||
|
if transaction_raw[65:149] != 84 * b"\0":
|
||||||
|
return InvalidTransaction()
|
||||||
|
return RevealTransaction(
|
||||||
|
revealer_pubkey = transaction_raw[1:33],
|
||||||
|
associated_proof_hash = transaction_raw[33:65],
|
||||||
|
reveal_value = associated_proof_data[0:256],
|
||||||
|
intermediates = [associated_proof_data[(i+1)*256:(i+2)*256] for i in range(20)],
|
||||||
|
)
|
||||||
|
def is_valid(self):
|
||||||
|
to_hash = self.get_associated_data()
|
||||||
|
return hashlib.sha256(to_hash).digest() == self.associated_proof_hash
|
||||||
|
def is_valid_after_block(self, block):
|
||||||
|
if len(block.pending_commitment_blocks) == 0:
|
||||||
|
return False
|
||||||
|
claim_R = int.from_bytes(self.reveal_value, "big")
|
||||||
|
if not is_valid_group_element(claim_R):
|
||||||
|
return False
|
||||||
|
intermediate_numbers = [int.from_bytes(i, "big") for i in self.intermediates]
|
||||||
|
for intermediate in intermediate_numbers:
|
||||||
|
if not is_valid_group_element(intermediate):
|
||||||
|
return False
|
||||||
|
claim_H = int.from_bytes(block.pending_commitment_blocks[0][0], "big")
|
||||||
|
for c in range(20):
|
||||||
|
i = 30 - c
|
||||||
|
claim_I = intermediate_numbers[c]
|
||||||
|
to_hash = self.revealer_pubkey + \
|
||||||
|
claim_H.to_bytes(256, "big") + \
|
||||||
|
claim_I.to_bytes(256, "big") + \
|
||||||
|
claim_R.to_bytes(256, "big") + \
|
||||||
|
i.to_bytes(1, "big")
|
||||||
|
e = int.from_bytes(hashlib.sha256(to_hash).digest(), "big")
|
||||||
|
new_H = normalize((claim_H * pow(claim_I, e, n)) % n)
|
||||||
|
new_R = normalize((claim_I * pow(claim_R, e, n)) % n)
|
||||||
|
claim_H = new_H
|
||||||
|
claim_R = new_R
|
||||||
|
return normalize(pow(claim_H, 2**1024, n)) == claim_R
|
||||||
|
def get_transaction_raw(self):
|
||||||
|
return b"\x03" + \
|
||||||
|
self.revealer_pubkey + \
|
||||||
|
self.associated_proof_hash + \
|
||||||
|
84 * b"\0"
|
||||||
|
def open_transactions_hash_data(self):
|
||||||
|
return b"\x01" + 52 * b"\0"
|
||||||
|
def get_associated_data(self):
|
||||||
|
return b"".join([self.reveal_value] + self.intermediates)
|
||||||
|
def sorting_id(self):
|
||||||
|
return (0,)
|
||||||
|
def is_empty(self):
|
||||||
|
return False
|
||||||
|
def __eq__(self, other):
|
||||||
|
return isinstance(other, RevealTransaction) \
|
||||||
|
and (self.revealer_pubkey, self.associated_proof_hash, self.reveal_value, self.intermediates) == \
|
||||||
|
(other.revealer_pubkey, other.associated_proof_hash, other.reveal_value, other.intermediates)
|
||||||
|
|
||||||
|
class InvalidTransaction:
|
||||||
|
def is_valid(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def associated_data_required(transaction):
|
||||||
|
return transaction[0] == 3 and transaction[65:149] == 84 * b"\0"
|
||||||
|
|
||||||
|
def transaction_from_bytes(transaction_raw, associated_data = None):
|
||||||
|
assert len(transaction_raw) == 149
|
||||||
|
if transaction_raw == 149 * b"\0":
|
||||||
|
return NoTransaction()
|
||||||
|
elif transaction_raw[0] == 1:
|
||||||
|
return PaymentTransaction.from_bytes(transaction_raw)
|
||||||
|
elif transaction_raw[0] == 2:
|
||||||
|
return GamblingTransaction.from_bytes(transaction_raw)
|
||||||
|
elif transaction_raw[0] == 3:
|
||||||
|
return RevealTransaction.from_bytes(transaction_raw, associated_data)
|
||||||
|
else:
|
||||||
|
return InvalidTransaction()
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Block:
|
class Block:
|
||||||
nonce: int
|
nonce: int
|
||||||
@@ -66,35 +241,37 @@ class Block:
|
|||||||
miner_pubkey: bytes
|
miner_pubkey: bytes
|
||||||
transaction: Transaction
|
transaction: Transaction
|
||||||
own_hash: bytes
|
own_hash: bytes
|
||||||
|
pending_commitment_blocks: list
|
||||||
|
pending_gambling_transactions: list
|
||||||
balances: dict
|
balances: dict
|
||||||
# (sender_pubkey, id) tuples
|
# (sender_pubkey, id) tuples
|
||||||
used_transaction_ids: set
|
used_transaction_ids: set
|
||||||
block_number: int
|
block_number: int
|
||||||
|
persist_address: int
|
||||||
valid: bool
|
valid: bool
|
||||||
|
|
||||||
def from_bytes(block_raw):
|
def from_bytes(block_raw, associated_data = None):
|
||||||
assert len(block_raw) == 292
|
assert len(block_raw) == 293
|
||||||
transaction_raw = block_raw[0:148]
|
transaction_raw = block_raw[0:149]
|
||||||
if transaction_raw == 148 * b"\0":
|
transaction = transaction_from_bytes(transaction_raw, associated_data)
|
||||||
transaction = None
|
|
||||||
else:
|
|
||||||
transaction = Transaction.from_bytes(transaction_raw)
|
|
||||||
return Block(
|
return Block(
|
||||||
transaction = transaction,
|
transaction = transaction,
|
||||||
message = block_raw[148:180],
|
message = block_raw[149:181],
|
||||||
miner_pubkey = block_raw[180:212],
|
miner_pubkey = block_raw[181:213],
|
||||||
previous_hash = block_raw[212:244],
|
previous_hash = block_raw[213:245],
|
||||||
timestamp = int.from_bytes(block_raw[244:252], "big"),
|
timestamp = int.from_bytes(block_raw[245:253], "big"),
|
||||||
difficulty_sum = int.from_bytes(block_raw[252:284], "big"),
|
difficulty_sum = int.from_bytes(block_raw[253:285], "big"),
|
||||||
nonce = int.from_bytes(block_raw[284:292], "big"),
|
nonce = int.from_bytes(block_raw[285:293], "big"),
|
||||||
own_hash = hashlib.sha256(block_raw).digest(),
|
own_hash = hashlib.sha256(block_raw).digest(),
|
||||||
|
pending_commitment_blocks = None,
|
||||||
|
pending_gambling_transactions = None,
|
||||||
balances = None,
|
balances = None,
|
||||||
used_transaction_ids = None,
|
used_transaction_ids = None,
|
||||||
block_number = None,
|
block_number = None,
|
||||||
|
persist_address = None,
|
||||||
valid = False,
|
valid = False,
|
||||||
)
|
)
|
||||||
def validate(self, blockchain):
|
def validate(self, blockchain):
|
||||||
if self.transaction is not None:
|
|
||||||
if not self.transaction.is_valid():
|
if not self.transaction.is_valid():
|
||||||
return False
|
return False
|
||||||
if self.previous_hash != 32 * b"\0":
|
if self.previous_hash != 32 * b"\0":
|
||||||
@@ -107,11 +284,11 @@ class Block:
|
|||||||
return False
|
return False
|
||||||
if self.timestamp > time.time():
|
if self.timestamp > time.time():
|
||||||
return False
|
return False
|
||||||
if self.transaction is not None and not self.transaction.is_valid_after_block(prev_block):
|
if not self.transaction.is_valid_after_block(prev_block):
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
prev_block = None
|
prev_block = None
|
||||||
if self.transaction is not None:
|
if not self.transaction.is_empty():
|
||||||
return False
|
return False
|
||||||
# check for the correct miner pubkey - which will become public at launch day
|
# check for the correct miner pubkey - which will become public at launch day
|
||||||
h = hashlib.sha256(self.miner_pubkey).hexdigest()
|
h = hashlib.sha256(self.miner_pubkey).hexdigest()
|
||||||
@@ -127,10 +304,28 @@ class Block:
|
|||||||
return False
|
return False
|
||||||
self.valid = int.from_bytes(self.own_hash, "big") * block_difficulty < 2**256
|
self.valid = int.from_bytes(self.own_hash, "big") * block_difficulty < 2**256
|
||||||
if self.valid:
|
if self.valid:
|
||||||
|
self.calculate_block_number(prev_block)
|
||||||
|
self.calculate_pending_gambling_transactions(prev_block)
|
||||||
self.calculate_balances(prev_block)
|
self.calculate_balances(prev_block)
|
||||||
self.calculate_used_transaction_ids(prev_block)
|
self.calculate_used_transaction_ids(prev_block)
|
||||||
self.calculate_block_number(prev_block)
|
self.calculate_persist_address(prev_block)
|
||||||
return self.valid
|
return self.valid
|
||||||
|
def calculate_pending_gambling_transactions(self, prev_block):
|
||||||
|
if prev_block is None:
|
||||||
|
self.pending_commitment_blocks = []
|
||||||
|
self.pending_gambling_transactions = []
|
||||||
|
return
|
||||||
|
pending_commitment_blocks = prev_block.pending_commitment_blocks.copy()
|
||||||
|
pending_gambling_transactions = prev_block.pending_gambling_transactions.copy()
|
||||||
|
if isinstance(self.transaction, GamblingTransaction):
|
||||||
|
pending_gambling_transactions.append(self.transaction)
|
||||||
|
if self.block_number % 256 == 255:
|
||||||
|
pending_commitment_blocks.append((self.own_hash, pending_gambling_transactions))
|
||||||
|
pending_gambling_transactions = []
|
||||||
|
if isinstance(self.transaction, RevealTransaction):
|
||||||
|
pending_commitment_blocks = pending_commitment_blocks[1:]
|
||||||
|
self.pending_commitment_blocks = pending_commitment_blocks
|
||||||
|
self.pending_gambling_transactions = pending_gambling_transactions
|
||||||
def calculate_balances(self, prev_block):
|
def calculate_balances(self, prev_block):
|
||||||
if prev_block is None:
|
if prev_block is None:
|
||||||
self.balances = {
|
self.balances = {
|
||||||
@@ -139,13 +334,29 @@ class Block:
|
|||||||
return
|
return
|
||||||
balances = prev_block.balances.copy()
|
balances = prev_block.balances.copy()
|
||||||
balances.setdefault(self.miner_pubkey, 0)
|
balances.setdefault(self.miner_pubkey, 0)
|
||||||
balances.setdefault(t.receiver, 0)
|
|
||||||
balances[self.miner_pubkey] += 100
|
balances[self.miner_pubkey] += 100
|
||||||
t = self.transaction
|
t = self.transaction
|
||||||
if t is not None:
|
if isinstance(t, PaymentTransaction):
|
||||||
balances[self.miner_pubkey] += t.transaction_fee
|
balances[self.miner_pubkey] += t.transaction_fee
|
||||||
balances[t.sender] -= (t.amount + t.transaction_fee)
|
balances[t.sender] -= (t.amount + t.transaction_fee)
|
||||||
|
balances.setdefault(t.receiver, 0)
|
||||||
balances[t.receiver] += t.amount
|
balances[t.receiver] += t.amount
|
||||||
|
elif isinstance(t, GamblingTransaction):
|
||||||
|
balances[self.miner_pubkey] += t.transaction_fee
|
||||||
|
balances[t.player] -= (t.amount + t.transaction_fee)
|
||||||
|
elif isinstance(t, RevealTransaction):
|
||||||
|
balances[self.miner_pubkey] += 100
|
||||||
|
balances.setdefault(t.revealer_pubkey, 0)
|
||||||
|
balances[t.revealer_pubkey] += 1500
|
||||||
|
revealed_gamblings = prev_block.pending_commitment_blocks[0][1]
|
||||||
|
for transaction in revealed_gamblings:
|
||||||
|
to_hash = transaction.id.to_bytes(4, "big") + \
|
||||||
|
transaction.player + \
|
||||||
|
t.reveal_value
|
||||||
|
h = hashlib.sha256(to_hash).digest()
|
||||||
|
if h[0] < 0x80:
|
||||||
|
continue
|
||||||
|
balances[transaction.player] += 2 * transaction.amount
|
||||||
self.balances = balances
|
self.balances = balances
|
||||||
def calculate_used_transaction_ids(self, prev_block):
|
def calculate_used_transaction_ids(self, prev_block):
|
||||||
if prev_block is None:
|
if prev_block is None:
|
||||||
@@ -153,14 +364,23 @@ class Block:
|
|||||||
return
|
return
|
||||||
used_transaction_ids = prev_block.used_transaction_ids.copy()
|
used_transaction_ids = prev_block.used_transaction_ids.copy()
|
||||||
t = self.transaction
|
t = self.transaction
|
||||||
if t is not None:
|
if isinstance(t, PaymentTransaction):
|
||||||
used_transaction_ids.add((t.sender, t.id))
|
used_transaction_ids.add((t.sender, t.id))
|
||||||
|
elif isinstance(t, GamblingTransaction):
|
||||||
|
used_transaction_ids.add((t.player, t.id))
|
||||||
self.used_transaction_ids = used_transaction_ids
|
self.used_transaction_ids = used_transaction_ids
|
||||||
def calculate_block_number(self, prev_block):
|
def calculate_block_number(self, prev_block):
|
||||||
if prev_block is None:
|
if prev_block is None:
|
||||||
self.block_number = 0
|
self.block_number = 0
|
||||||
else:
|
else:
|
||||||
self.block_number = prev_block.block_number + 1
|
self.block_number = prev_block.block_number + 1
|
||||||
|
def calculate_persist_address(self, prev_block):
|
||||||
|
if prev_block is None:
|
||||||
|
self.persist_address = 0
|
||||||
|
else:
|
||||||
|
self.persist_address = prev_block.persist_address + 293
|
||||||
|
if isinstance(prev_block.transaction, RevealTransaction):
|
||||||
|
self.persist_address += 5376
|
||||||
def get_difficulty_info(self, steps, blockchain):
|
def get_difficulty_info(self, steps, blockchain):
|
||||||
if steps == 0:
|
if steps == 0:
|
||||||
return self.difficulty_sum, self.timestamp
|
return self.difficulty_sum, self.timestamp
|
||||||
@@ -172,11 +392,7 @@ class Block:
|
|||||||
previous_block = blockchain.get_block(self.previous_hash)
|
previous_block = blockchain.get_block(self.previous_hash)
|
||||||
return previous_block.get_difficulty_info(steps-1, blockchain)
|
return previous_block.get_difficulty_info(steps-1, blockchain)
|
||||||
def get_block_raw(self):
|
def get_block_raw(self):
|
||||||
if self.transaction is None:
|
return self.transaction.get_transaction_raw() + \
|
||||||
transaction = 148 * b"\0"
|
|
||||||
else:
|
|
||||||
transaction = self.transaction.get_transaction_raw()
|
|
||||||
return transaction + \
|
|
||||||
self.message + \
|
self.message + \
|
||||||
self.miner_pubkey + \
|
self.miner_pubkey + \
|
||||||
self.previous_hash + \
|
self.previous_hash + \
|
||||||
@@ -216,12 +432,12 @@ class OpenTransactions:
|
|||||||
return None
|
return None
|
||||||
return self.__open_transactions[i]
|
return self.__open_transactions[i]
|
||||||
def __has_space(self, transaction):
|
def __has_space(self, transaction):
|
||||||
if len(self.__open_transactions) < 1000:
|
if len(self.__open_transactions) < 1024:
|
||||||
return True
|
return True
|
||||||
return transaction.sorting_id() < self.__open_transactions[-1].sorting_id()
|
return transaction.sorting_id() < self.__open_transactions[-1].sorting_id()
|
||||||
def __cleanup(self):
|
def __cleanup(self):
|
||||||
# sort everything
|
# sort everything
|
||||||
self.__open_transactions.sort(key = Transaction.sorting_id)
|
self.__open_transactions.sort(key = lambda t: t.sorting_id())
|
||||||
# drop out invalid ones
|
# drop out invalid ones
|
||||||
# - reused ids
|
# - reused ids
|
||||||
# - paying more money than available
|
# - paying more money than available
|
||||||
@@ -232,15 +448,30 @@ class OpenTransactions:
|
|||||||
return
|
return
|
||||||
used_transaction_ids = latest_block.used_transaction_ids.copy()
|
used_transaction_ids = latest_block.used_transaction_ids.copy()
|
||||||
balances = latest_block.balances.copy()
|
balances = latest_block.balances.copy()
|
||||||
|
contains_reveal_transaction = False
|
||||||
def is_valid(transaction):
|
def is_valid(transaction):
|
||||||
|
nonlocal contains_reveal_transaction
|
||||||
|
if isinstance(transaction, PaymentTransaction):
|
||||||
sender_tuple = (transaction.sender, transaction.id)
|
sender_tuple = (transaction.sender, transaction.id)
|
||||||
|
elif isinstance(transaction, GamblingTransaction):
|
||||||
|
sender_tuple = (transaction.player, transaction.id)
|
||||||
|
elif isinstance(transaction, RevealTransaction):
|
||||||
|
latest_block = self.__blockchain.get_latest_block()
|
||||||
|
if latest_block is None:
|
||||||
|
return False
|
||||||
|
if not transaction.is_valid_after_block(latest_block):
|
||||||
|
return False
|
||||||
|
if contains_reveal_transaction:
|
||||||
|
return False
|
||||||
|
contains_reveal_transaction = True
|
||||||
|
return True
|
||||||
if sender_tuple in used_transaction_ids:
|
if sender_tuple in used_transaction_ids:
|
||||||
return False
|
return False
|
||||||
balance = balances.get(transaction.sender) or 0
|
balance = balances.get(sender_tuple[0]) or 0
|
||||||
if transaction.amount + transaction.transaction_fee > balance:
|
if transaction.amount + transaction.transaction_fee > balance:
|
||||||
return False
|
return False
|
||||||
used_transaction_ids.add(sender_tuple)
|
used_transaction_ids.add(sender_tuple)
|
||||||
balances[transaction.sender] = balance - transaction.amount - transaction.transaction_fee
|
balances[sender_tuple[0]] = balance - transaction.amount - transaction.transaction_fee
|
||||||
return True
|
return True
|
||||||
self.__open_transactions = [transaction for transaction in self.__open_transactions if is_valid(transaction)]
|
self.__open_transactions = [transaction for transaction in self.__open_transactions if is_valid(transaction)]
|
||||||
# limit to 1024
|
# limit to 1024
|
||||||
@@ -251,12 +482,10 @@ class OpenTransactions:
|
|||||||
current_hash = 32 * b"\0"
|
current_hash = 32 * b"\0"
|
||||||
for i in range(1024):
|
for i in range(1024):
|
||||||
if i >= len(self.__open_transactions):
|
if i >= len(self.__open_transactions):
|
||||||
transaction_data = 44 * b"\0"
|
transaction_data = 53 * b"\0"
|
||||||
else:
|
else:
|
||||||
transaction = self.__open_transactions[i]
|
transaction = self.__open_transactions[i]
|
||||||
transaction_data = transaction.transaction_fee.to_bytes(8, "big") + \
|
transaction_data = transaction.open_transactions_hash_data()
|
||||||
transaction.sender + \
|
|
||||||
transaction.id.to_bytes(4, "big")
|
|
||||||
current_hash = hashlib.sha256(current_hash + transaction_data).digest()
|
current_hash = hashlib.sha256(current_hash + transaction_data).digest()
|
||||||
self.__hashes.append(current_hash)
|
self.__hashes.append(current_hash)
|
||||||
|
|
||||||
@@ -265,6 +494,7 @@ class Blockchain:
|
|||||||
# maps block hashes to block instances
|
# maps block hashes to block instances
|
||||||
self.__block_map = {}
|
self.__block_map = {}
|
||||||
self.__latest_block_hash = None
|
self.__latest_block_hash = None
|
||||||
|
self.__associated_data = {}
|
||||||
self.__lock = Lock()
|
self.__lock = Lock()
|
||||||
self.open_transactions = OpenTransactions(self)
|
self.open_transactions = OpenTransactions(self)
|
||||||
self.__load_blocks_from_disk()
|
self.__load_blocks_from_disk()
|
||||||
@@ -273,10 +503,16 @@ class Blockchain:
|
|||||||
try:
|
try:
|
||||||
with open("blockchain", "rb") as f:
|
with open("blockchain", "rb") as f:
|
||||||
while True:
|
while True:
|
||||||
block = f.read(292)
|
block = f.read(293)
|
||||||
if len(block) < 292:
|
if len(block) < 293:
|
||||||
break
|
break
|
||||||
block_obj = self.add_block(block)
|
if associated_data_required(block[0:149]):
|
||||||
|
associated_data = f.read(5376)
|
||||||
|
if len(associated_data) < 5376:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
associated_data = None
|
||||||
|
block_obj = self.add_block(block, associated_data)
|
||||||
if not block_obj.validate(self):
|
if not block_obj.validate(self):
|
||||||
break
|
break
|
||||||
last_valid = block_obj
|
last_valid = block_obj
|
||||||
@@ -319,16 +555,18 @@ class Blockchain:
|
|||||||
new_block = self.__block_map[new_block.previous_hash]
|
new_block = self.__block_map[new_block.previous_hash]
|
||||||
if old_block is not None and old_block.block_number > new_block.block_number:
|
if old_block is not None and old_block.block_number > new_block.block_number:
|
||||||
old_block = self.__block_map[old_block.previous_hash]
|
old_block = self.__block_map[old_block.previous_hash]
|
||||||
start_block_number = block_list[-1].block_number
|
start_addr = block_list[-1].persist_address
|
||||||
start_addr = start_block_number * 292
|
|
||||||
open_mode = "wb" if start_addr == 0 else "r+b"
|
open_mode = "wb" if start_addr == 0 else "r+b"
|
||||||
with open("blockchain", open_mode) as f:
|
with open("blockchain", open_mode) as f:
|
||||||
f.seek(start_addr)
|
f.seek(start_addr)
|
||||||
for block in reversed(block_list):
|
for block in reversed(block_list):
|
||||||
f.write(block.get_block_raw())
|
f.write(block.get_block_raw())
|
||||||
def add_block(self, block_raw):
|
if isinstance(block.transaction, RevealTransaction):
|
||||||
|
f.write(block.transaction.get_associated_data())
|
||||||
|
f.truncate()
|
||||||
|
def add_block(self, block_raw, associated_data = None):
|
||||||
with self.__lock:
|
with self.__lock:
|
||||||
block = Block.from_bytes(block_raw)
|
block = Block.from_bytes(block_raw, associated_data)
|
||||||
if block.own_hash not in self.__block_map:
|
if block.own_hash not in self.__block_map:
|
||||||
self.__block_map[block.own_hash] = block
|
self.__block_map[block.own_hash] = block
|
||||||
return self.__block_map[block.own_hash]
|
return self.__block_map[block.own_hash]
|
||||||
@@ -347,3 +585,11 @@ class Blockchain:
|
|||||||
if self.__latest_block_hash is None:
|
if self.__latest_block_hash is None:
|
||||||
return None
|
return None
|
||||||
return self.__block_map[self.__latest_block_hash]
|
return self.__block_map[self.__latest_block_hash]
|
||||||
|
def cache_associated_data(self, transaction):
|
||||||
|
if not isinstance(transaction, RevealTransaction):
|
||||||
|
return
|
||||||
|
with self.__lock:
|
||||||
|
self.__associated_data[transaction.associated_proof_hash] = transaction.get_associated_data()
|
||||||
|
def get_associated_data(self, associated_proof_hash):
|
||||||
|
with self.__lock:
|
||||||
|
return self.__associated_data.get(associated_proof_hash, None)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
The carrotcoin cryptocurrency is a bitcoin-like currency with proof-of-work, created just for learning purposes.
|
The carrotcoin cryptocurrency is a bitcoin-like currency with proof-of-work, created just for learning purposes.
|
||||||
|
|
||||||
|
A special feature is its builtin option for gambling.
|
||||||
|
|
||||||
# The currency
|
# The currency
|
||||||
|
|
||||||
One carrotcoin (abbreviated cc) consists of 100 cents. Internally, only cent amounts are processed using integer fields. The recommended notation for an amount is like in this example:
|
One carrotcoin (abbreviated cc) consists of 100 cents. Internally, only cent amounts are processed using integer fields. The recommended notation for an amount is like in this example:
|
||||||
@@ -93,6 +95,7 @@ If a payment transaction is included, the following happens additionally:
|
|||||||
If a gambling transaction is included, the following happens additionally:
|
If a gambling transaction is included, the following happens additionally:
|
||||||
|
|
||||||
- The player needs to pay "gambling amount" + "transaction fee"
|
- The player needs to pay "gambling amount" + "transaction fee"
|
||||||
|
- The miner gets "transaction fee" on top of the reward.
|
||||||
|
|
||||||
If a gambling reveal transaction is included, the following happens additionally:
|
If a gambling reveal transaction is included, the following happens additionally:
|
||||||
|
|
||||||
@@ -182,13 +185,13 @@ Technically, gambling happens in the following steps:
|
|||||||
|
|
||||||
## Gambling transaction
|
## Gambling transaction
|
||||||
|
|
||||||
A user can create a gambling transaction at any time to participate in the gambling process. Such transactions are described above in the section "Blockchain" / "transaction datastructure" / "gambling transaction". The id and player fields (along with other information) will be used to calculate, if the player wins.
|
A user can create a gambling transaction at any time to participate in the gambling process. Such transactions are described above in the section "Blockchain" / "transaction datastructure" / "gambling transaction". The id and player fields (along with other information) will be used to calculate if the player wins.
|
||||||
|
|
||||||
## Gambling commitment blocks
|
## Gambling commitment blocks
|
||||||
|
|
||||||
To find out which block is a gambling commitment block, you need to number all blocks of the blockchain. The first block (this is the one with 32 nullbytes in the "previous hash" field) gets block number 0. From there on, just count up. (The block number of a block is the block number of its previous block plus one.)
|
To find out which block is a gambling commitment block, you need to number all blocks of the blockchain. The first block (this is the one with 32 nullbytes in the "previous hash" field) gets block number 0. From there on, just count up. (The block number of a block is the block number of its previous block plus one.)
|
||||||
|
|
||||||
Every block with a block number of 255 (modulo 256) is a gambling commitment block. Its hash value will also be relevant to calculate, if a player wins.
|
Every block with a block number of 255 (modulo 256) is a gambling commitment block. Its hash value will also be relevant to calculate if a player wins.
|
||||||
|
|
||||||
## Decision of winning / losing
|
## Decision of winning / losing
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ Node to Node communication happens over IPv6 only.
|
|||||||
|
|
||||||
When starting a node process, it tries to claim udp port 62039. If this port is not free, any other port is chosen.
|
When starting a node process, it tries to claim udp port 62039. If this port is not free, any other port is chosen.
|
||||||
|
|
||||||
|
### Version fields
|
||||||
|
|
||||||
|
Every packet starts with a 4-byte header containing two version fields. (A 2-byte "protocol version" and a 2-byte "capable version".)
|
||||||
|
Participants should fill both fields with 0 when sending messages. When receiving a messages with "protocol version" != 0, the message should be ignored. The "capable version" must not be checked for incoming messages, so messages are processed regardless of the value they contain as "capable version".
|
||||||
|
|
||||||
|
This is the current behaviour for all "protocol version 0" participants and should allow to extend the protocol in the future, if this becomes necessary.
|
||||||
|
|
||||||
## Node behaviour
|
## Node behaviour
|
||||||
|
|
||||||
### Peers
|
### Peers
|
||||||
@@ -43,14 +50,19 @@ Each node keeps a list of up to 1024 open transactions. (That are valid but not
|
|||||||
The list is sorted by the following criteria:
|
The list is sorted by the following criteria:
|
||||||
|
|
||||||
- transaction fee, decreasing
|
- transaction fee, decreasing
|
||||||
|
- sent or played amount, decreasing
|
||||||
- sender pubkey, increasing
|
- sender pubkey, increasing
|
||||||
- id, increasing
|
- id, increasing
|
||||||
|
|
||||||
These sorting criteria form a 3-tuple.
|
These sorting criteria form a 4-tuple.
|
||||||
|
|
||||||
Only one transaction per (sender pubkey, id) tuple stays in the list. If a transaction with the same tuple but greater transaction fee is received, it replaces the current transaction. If the transaction fee is equal or smaller, the new transaction is ignored.
|
Only one transaction per (sender pubkey, id) tuple stays in the list. If a transaction with the same (sender pubkey, id) tuple but greater transaction fee or same fee but greater amount is received, it replaces the current transaction. In other cases (smaller fee or neither the fee nor the amount increased), the new transaction is ignored.
|
||||||
|
|
||||||
If one sender created multiple transactions, it must have a large enough balance for all transactions, otherwise the excess ones (as defined by the sorting criteria above) are removed from the list.
|
By choosing different ids, one sender can put multiple transactions into the list.
|
||||||
|
|
||||||
|
Each sender must have a large enough balance for all transactions, otherwise the excess ones (as defined by the sorting criteria above) are removed from the list.
|
||||||
|
|
||||||
|
At most one (the next pending) gambling reveal transaction can be part of the list and will be the first one, before all other transactions.
|
||||||
|
|
||||||
If the list grows above 1024 entries, a node may either remove excess ones or keep them in a local list. Within the Node to Node communication, only the first 1024 entries will be synced.
|
If the list grows above 1024 entries, a node may either remove excess ones or keep them in a local list. Within the Node to Node communication, only the first 1024 entries will be synced.
|
||||||
|
|
||||||
@@ -60,13 +72,17 @@ The hash of each open transaction is a SHA256, calculated over the following dat
|
|||||||
| content | length |
|
| content | length |
|
||||||
|---|---|
|
|---|---|
|
||||||
| previous hash | 32 |
|
| previous hash | 32 |
|
||||||
|
| is reveal transaction (bool) | 1 |
|
||||||
| transaction fee (BE) | 8 |
|
| transaction fee (BE) | 8 |
|
||||||
| sender pubkey | 32 |
|
| amount (BE) | 8 |
|
||||||
|
| sender / player pubkey | 32 |
|
||||||
| id | 4 |
|
| id | 4 |
|
||||||
|
|
||||||
The "previous hash" consists of 32 nullbytes for the first transaction in the list. For all other entries, it is the calculated hash value of the open transaction entry directly before in the list.
|
The "previous hash" consists of 32 nullbytes for the first transaction in the list. For all other entries, it is the calculated hash value of the open transaction entry directly before in the list.
|
||||||
|
|
||||||
If the list contains less than 1024 entries, set "transaction fee", "sender pubkey" and "id" to nullbytes for the hash calculation of all following entries.
|
The field "is reveal transaction" is 0x01 for the first transaction if it is a reveal transaction and 0x00 for all other transactions. Reveal transactions have "transaction fee", "amount", "sender / player pubkey" and "id" set to nullbytes.
|
||||||
|
|
||||||
|
If the list contains less than 1024 entries, set "is reveal transaction", "transaction fee", "amount", "sender / player pubkey" and "id" to nullbytes for the hash calculation of all following entries.
|
||||||
|
|
||||||
## Node to Node message packet formats
|
## Node to Node message packet formats
|
||||||
|
|
||||||
@@ -76,7 +92,7 @@ If the list contains less than 1024 entries, set "transaction fee", "sender pubk
|
|||||||
|---|---|
|
|---|---|
|
||||||
| protocol version = 0 (BE) | 2 |
|
| protocol version = 0 (BE) | 2 |
|
||||||
| capable version = 0 (BE) | 2 |
|
| capable version = 0 (BE) | 2 |
|
||||||
| type = 0 (BE) | 1 |
|
| type = 0 (dec) | 1 |
|
||||||
| difficulty sum of second last known block (BE) | 32 |
|
| difficulty sum of second last known block (BE) | 32 |
|
||||||
| hash value of open transaction # 1023 | 32 |
|
| hash value of open transaction # 1023 | 32 |
|
||||||
| partner IPv6 | 16 |
|
| partner IPv6 | 16 |
|
||||||
@@ -92,7 +108,7 @@ partner IPv6 and partner port may be nullbytes (no partner included).
|
|||||||
|---|---|
|
|---|---|
|
||||||
| protocol version = 0 (BE) | 2 |
|
| protocol version = 0 (BE) | 2 |
|
||||||
| capable version = 0 (BE) | 2 |
|
| capable version = 0 (BE) | 2 |
|
||||||
| type = 1 (BE) | 1 |
|
| type = 1 (dec) | 1 |
|
||||||
| block hash | 32 |
|
| block hash | 32 |
|
||||||
|
|
||||||
A block request is sent from node A to node B in order to transfer a block from node B to node A.
|
A block request is sent from node A to node B in order to transfer a block from node B to node A.
|
||||||
@@ -106,8 +122,8 @@ If "block hash" consists of 32 nullbytes, node A wants node B to send the newest
|
|||||||
|---|---|
|
|---|---|
|
||||||
| protocol version = 0 (BE) | 2 |
|
| protocol version = 0 (BE) | 2 |
|
||||||
| capable version = 0 (BE) | 2 |
|
| capable version = 0 (BE) | 2 |
|
||||||
| type = 2 (BE) | 1 |
|
| type = 2 (dec) | 1 |
|
||||||
| block | 292 |
|
| block | 293 |
|
||||||
|
|
||||||
A "block transfer" message is sent back in response to a "block request" message.
|
A "block transfer" message is sent back in response to a "block request" message.
|
||||||
|
|
||||||
@@ -117,7 +133,7 @@ A "block transfer" message is sent back in response to a "block request" message
|
|||||||
|---|---|
|
|---|---|
|
||||||
| protocol version = 0 (BE) | 2 |
|
| protocol version = 0 (BE) | 2 |
|
||||||
| capable version = 0 (BE) | 2 |
|
| capable version = 0 (BE) | 2 |
|
||||||
| type = 3 (BE) | 1 |
|
| type = 3 (dec) | 1 |
|
||||||
| list position (0 <= x < 1024) (BE) | 2 |
|
| list position (0 <= x < 1024) (BE) | 2 |
|
||||||
|
|
||||||
### open transaction list hash response
|
### open transaction list hash response
|
||||||
@@ -126,7 +142,7 @@ A "block transfer" message is sent back in response to a "block request" message
|
|||||||
|---|---|
|
|---|---|
|
||||||
| protocol version = 0 (BE) | 2 |
|
| protocol version = 0 (BE) | 2 |
|
||||||
| capable version = 0 (BE) | 2 |
|
| capable version = 0 (BE) | 2 |
|
||||||
| type = 4 (BE) | 1 |
|
| type = 4 (dec) | 1 |
|
||||||
| list position (0 <= x < 1024) (BE) | 2 |
|
| list position (0 <= x < 1024) (BE) | 2 |
|
||||||
| "open transaction" hash value | 32 |
|
| "open transaction" hash value | 32 |
|
||||||
|
|
||||||
@@ -136,7 +152,7 @@ A "block transfer" message is sent back in response to a "block request" message
|
|||||||
|---|---|
|
|---|---|
|
||||||
| protocol version = 0 (BE) | 2 |
|
| protocol version = 0 (BE) | 2 |
|
||||||
| capable version = 0 (BE) | 2 |
|
| capable version = 0 (BE) | 2 |
|
||||||
| type = 5 (BE) | 1 |
|
| type = 5 (dec) | 1 |
|
||||||
| list position (0 <= x < 1024) (BE) | 2 |
|
| list position (0 <= x < 1024) (BE) | 2 |
|
||||||
|
|
||||||
### open transaction response
|
### open transaction response
|
||||||
@@ -145,9 +161,9 @@ A "block transfer" message is sent back in response to a "block request" message
|
|||||||
|---|---|
|
|---|---|
|
||||||
| protocol version = 0 (BE) | 2 |
|
| protocol version = 0 (BE) | 2 |
|
||||||
| capable version = 0 (BE) | 2 |
|
| capable version = 0 (BE) | 2 |
|
||||||
| type = 6 (BE) | 1 |
|
| type = 6 (dec) | 1 |
|
||||||
| list position (0 <= x < 1024) (BE) | 2 |
|
| list position (0 <= x < 1024) (BE) | 2 |
|
||||||
| transaction | 148 |
|
| transaction | 149 |
|
||||||
|
|
||||||
## Client to Node message packet formats
|
## Client to Node message packet formats
|
||||||
|
|
||||||
@@ -157,8 +173,8 @@ A "block transfer" message is sent back in response to a "block request" message
|
|||||||
|---|---|
|
|---|---|
|
||||||
| protocol version = 0 (BE) | 2 |
|
| protocol version = 0 (BE) | 2 |
|
||||||
| capable version = 0 (BE) | 2 |
|
| capable version = 0 (BE) | 2 |
|
||||||
| type = 7 (BE) | 1 |
|
| type = 7 (dec) | 1 |
|
||||||
| padding (nullbytes) | 252 |
|
| padding (nullbytes) | 253 |
|
||||||
|
|
||||||
The node should answer to a "Mining task request" with a "Mining task response"
|
The node should answer to a "Mining task request" with a "Mining task response"
|
||||||
|
|
||||||
@@ -168,8 +184,8 @@ The node should answer to a "Mining task request" with a "Mining task response"
|
|||||||
|---|---|
|
|---|---|
|
||||||
| protocol version = 0 (BE) | 2 |
|
| protocol version = 0 (BE) | 2 |
|
||||||
| capable version = 0 (BE) | 2 |
|
| capable version = 0 (BE) | 2 |
|
||||||
| type = 8 (BE) | 1 |
|
| type = 8 (dec) | 1 |
|
||||||
| transaction (optional) | 148 |
|
| transaction | 149 |
|
||||||
| previous hash | 32 |
|
| previous hash | 32 |
|
||||||
| timestamp (unix time in seconds, BE) | 8 |
|
| timestamp (unix time in seconds, BE) | 8 |
|
||||||
| difficulty sum (BE) | 32 |
|
| difficulty sum (BE) | 32 |
|
||||||
@@ -181,23 +197,130 @@ The miner fills "nonce", "message" and "miner pubkey" on its own.
|
|||||||
|
|
||||||
When a miner finds a block, it sends a "block transfer" message to the node.
|
When a miner finds a block, it sends a "block transfer" message to the node.
|
||||||
|
|
||||||
### Payment request (Client -> Node)
|
### Transaction request (Client -> Node)
|
||||||
|
|
||||||
| content | size (bytes) |
|
| content | size (bytes) |
|
||||||
|---|---|
|
|---|---|
|
||||||
| protocol version = 0 (BE) | 2 |
|
| protocol version = 0 (BE) | 2 |
|
||||||
| capable version = 0 (BE) | 2 |
|
| capable version = 0 (BE) | 2 |
|
||||||
| type = 9 (BE) | 1 |
|
| type = 9 (dec) | 1 |
|
||||||
| transaction | 148 |
|
| transaction | 149 |
|
||||||
|
|
||||||
The node should answer to a "Payment request" with a "Payment request received" if the contained transaction is formally correct (see "validity / transaction" in the blockchain specification).
|
The transaction can be of any type. (Payment, gambling, gambling reveal)
|
||||||
|
|
||||||
A response is always sent back in this case, even if the transaction cannot be applied to the blockchain right now.
|
The node should answer to a "Transaction request" with a "Transaction request received" if the contained transaction is formally correct (see "validity / transaction" in the blockchain specification).
|
||||||
|
|
||||||
### Payment request received (Node -> Client)
|
To validate a gambling reveal transaction, the node will first ask the client for the associated proof (see "Associated revealing proof request" and "Associated revealing proof response" messages) before sending back a "Transaction request received" response.
|
||||||
|
|
||||||
|
A response is always sent back for valid transaction data structures, even if the transaction cannot be applied to the blockchain (e.g. because the sender used the transaction id before or has not enough money). For gambling reveal transactions, the request is confirmed if the proof was received, the proof hash matches and all numbers are correctly in range. (0 < I < n/2) The check if the reveal proof matches the oldest not yet revealed commitment block, is done after confirming with "Transaction request received".
|
||||||
|
|
||||||
|
### Transaction request received (Node -> Client)
|
||||||
|
|
||||||
| content | size (bytes) |
|
| content | size (bytes) |
|
||||||
|---|---|
|
|---|---|
|
||||||
| protocol version = 0 (BE) | 2 |
|
| protocol version = 0 (BE) | 2 |
|
||||||
| capable version = 0 (BE) | 2 |
|
| capable version = 0 (BE) | 2 |
|
||||||
| type = 10 (BE) | 1 |
|
| type = 10 (dec) | 1 |
|
||||||
|
|
||||||
|
### Reveal mining task request (Client -> Node)
|
||||||
|
|
||||||
|
| content | size (bytes) |
|
||||||
|
|---|---|
|
||||||
|
| protocol version = 0 (BE) | 2 |
|
||||||
|
| capable version = 0 (BE) | 2 |
|
||||||
|
| type = 11 (dec) | 1 |
|
||||||
|
| padding (nullbytes) | 32 |
|
||||||
|
|
||||||
|
The node should answer to a "Reveal mining task request" with a "Reveal mining task response"
|
||||||
|
|
||||||
|
### Reveal mining task response (Node -> Client)
|
||||||
|
|
||||||
|
| content | size (bytes) |
|
||||||
|
|---|---|
|
||||||
|
| protocol version = 0 (BE) | 2 |
|
||||||
|
| capable version = 0 (BE) | 2 |
|
||||||
|
| type = 12 (dec) | 1 |
|
||||||
|
| commitment hash | 32 |
|
||||||
|
|
||||||
|
The commitment hash is either 32 nullbytes ("nothing to do for reveal miners at the moment") or it contains the hash of the oldest not yet revealed gambling commitment block.
|
||||||
|
|
||||||
|
See blockchain.md / section Gambling for the mathematical background what to do with the commitment hash "H".
|
||||||
|
|
||||||
|
When the miner is done forming a proof, he is expected to create a gambling reveal transaction and send it to a node using a "Transaction request" message. It should also be ready to receive and answer "Associated revealing proof request" messages from that node.
|
||||||
|
|
||||||
|
## General packet formats
|
||||||
|
|
||||||
|
These packets can be used in both situations (For Client <-> Node communication and for Node <-> Node communication.)
|
||||||
|
|
||||||
|
### Ping
|
||||||
|
|
||||||
|
| content | size (bytes) |
|
||||||
|
|---|---|
|
||||||
|
| protocol version = 0 (BE) | 2 |
|
||||||
|
| capable version = 0 (BE) | 2 |
|
||||||
|
| type = 13 (dec) | 1 |
|
||||||
|
| ASCII string "R u carrotcoin?" | 15 |
|
||||||
|
| nonce (arbitrary value) | 8 |
|
||||||
|
|
||||||
|
The ASCII string "R u carrotcoin?" is fixed and messages with a different content in this part of the message should be ignored.
|
||||||
|
|
||||||
|
When receiving a valid Ping message, a Pong message with the same nonce value must be sent back to the sender.
|
||||||
|
|
||||||
|
### Pong
|
||||||
|
|
||||||
|
| content | size (bytes) |
|
||||||
|
|---|---|
|
||||||
|
| protocol version = 0 (BE) | 2 |
|
||||||
|
| capable version = 0 (BE) | 2 |
|
||||||
|
| type = 14 (dec) | 1 |
|
||||||
|
| ASCII string "I m carrotcoin!" | 15 |
|
||||||
|
| nonce (same value as in the Ping) | 8 |
|
||||||
|
|
||||||
|
An answer message to a Ping request. See Ping for implementation details.
|
||||||
|
|
||||||
|
### Associated revealing proof request
|
||||||
|
|
||||||
|
As described in the blockchain specification, a reveal transaction requires an "associated revealing proof" to be fully validated.
|
||||||
|
|
||||||
|
There are 3 possible situations when a participant A transfers a reveal transaction to a participant B:
|
||||||
|
|
||||||
|
1. A reveal mining client (A) finished calculating the proof and sends the transaction to some node (B).
|
||||||
|
2. A node (A) further spreads this transaction to another node (B).
|
||||||
|
3. A node (A) transfers a block with a reveal transaction to another node (B).
|
||||||
|
|
||||||
|
In each case, if B does not know the associated revealing proof, it needs to ask A for it with the following message:
|
||||||
|
|
||||||
|
| content | size (bytes) |
|
||||||
|
|---|---|
|
||||||
|
| protocol version = 0 (BE) | 2 |
|
||||||
|
| capable version = 0 (BE) | 2 |
|
||||||
|
| type = 15 (dec) | 1 |
|
||||||
|
| parts bitfield | 1 |
|
||||||
|
| associated revealing proof (hash) | 32 |
|
||||||
|
|
||||||
|
The associated revealing proof described in the blockchain has a total length of 5376 bytes. For transmission, it is equally divided into 6 parts with 896 bytes each. These parts are numbered from "part 0" (the first 896 bytes) to "part 5" (the last 896 bytes).
|
||||||
|
|
||||||
|
A sha256 hash value of the entire 5376 byte datastructure is the identifier that is used inside the transaction and used in this request to identify the proof being requested.
|
||||||
|
|
||||||
|
The "parts bitfield" describes which parts should be sent back. 0x01 means "part 0", 0x02 means "part 1", ..., 0x20 means "part 5". To request all 6 parts (which might be a typical request), the "parts bitfield" will have value 0x3f. If a retransmission is needed, the "parts bitfield" can describe exactly, which parts are still missing.
|
||||||
|
|
||||||
|
When node A sent a reveal transation to node B, it should answer such associated revealing proof requests from B with all required parts, each part transmitted as an individual UDP packet of type "Associated revealing proof response".
|
||||||
|
|
||||||
|
### Associated revealing proof response
|
||||||
|
|
||||||
|
| content | size (bytes) |
|
||||||
|
|---|---|
|
||||||
|
| protocol version = 0 (BE) | 2 |
|
||||||
|
| capable version = 0 (BE) | 2 |
|
||||||
|
| type = 16 (dec) | 1 |
|
||||||
|
| part number | 1 |
|
||||||
|
| associated revealing proof (hash) | 32 |
|
||||||
|
| proof partial data | 896 |
|
||||||
|
|
||||||
|
This message is sent back as response to an associated revealing proof request. See above for implementation details.
|
||||||
|
|
||||||
|
The "part number" describes where this part belongs to and has a value from 0 to 5 (inclusive).
|
||||||
|
|
||||||
|
The "associated revealing proof (hash)" identifies the entire 5376-byte proof and is the same as in the request.
|
||||||
|
|
||||||
|
The "proof partial data" contains the actual information (1/6 of the entire proof in each message).
|
||||||
|
|||||||
@@ -19,12 +19,15 @@ def main():
|
|||||||
try:
|
try:
|
||||||
with open("blockchain", "rb") as f:
|
with open("blockchain", "rb") as f:
|
||||||
while True:
|
while True:
|
||||||
block = f.read(292)
|
block = f.read(293)
|
||||||
if len(block) != 292:
|
if len(block) != 293:
|
||||||
break
|
break
|
||||||
timestamp = int.from_bytes(block[244:252], "big")
|
if block[0] == 3:
|
||||||
|
# Reveal Transaction, skip the associated data
|
||||||
|
f.read(5376)
|
||||||
|
timestamp = int.from_bytes(block[245:253], "big")
|
||||||
time_info = time.strftime("%d.%m.%Y %H:%M:%S", time.localtime(timestamp))
|
time_info = time.strftime("%d.%m.%Y %H:%M:%S", time.localtime(timestamp))
|
||||||
message = prepare_message(block[148:180])
|
message = prepare_message(block[149:181])
|
||||||
print(f"[{time_info}] {message}")
|
print(f"[{time_info}] {message}")
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print("Found no blockchain file", file=sys.stderr)
|
print("Found no blockchain file", file=sys.stderr)
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ def get_transaction(s, i):
|
|||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
msg, sender = s.recvfrom(4096)
|
msg, sender = s.recvfrom(4096)
|
||||||
if sender[0:2] == ("::1", 62039) and len(msg) == 155 and msg[0:5] == b"\0\0\0\0\x06" and msg[5:7] == i.to_bytes(2, "big"):
|
if sender[0:2] == ("::1", 62039) and len(msg) == 156 and msg[0:5] == b"\0\0\0\0\x06" and msg[5:7] == i.to_bytes(2, "big"):
|
||||||
return msg[7:155]
|
return msg[7:156]
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
@@ -32,12 +32,24 @@ def main():
|
|||||||
if transaction is None:
|
if transaction is None:
|
||||||
print("- no response from local node -", file=sys.stderr)
|
print("- no response from local node -", file=sys.stderr)
|
||||||
exit(1)
|
exit(1)
|
||||||
if transaction == 148 * b"\0":
|
if transaction == 149 * b"\0":
|
||||||
return
|
return
|
||||||
sender = format_addr(transaction[4:36])
|
if transaction[0] == 1:
|
||||||
receiver = format_addr(transaction[36:68])
|
sender = format_addr(transaction[5:37])
|
||||||
amount = format_amount(transaction[68:76])
|
receiver = format_addr(transaction[37:69])
|
||||||
fee = format_amount(transaction[76:84])
|
amount = format_amount(transaction[69:77])
|
||||||
|
fee = format_amount(transaction[77:85])
|
||||||
|
elif transaction[0] == 2:
|
||||||
|
sender = format_addr(transaction[5:37])
|
||||||
|
receiver = "(gambling)" + 33 * " "
|
||||||
|
amount = format_amount(transaction[37:45])
|
||||||
|
fee = format_amount(transaction[45:53])
|
||||||
|
elif transaction[0] == 3:
|
||||||
|
print("- reveal transaction -")
|
||||||
|
sender = None
|
||||||
|
else:
|
||||||
|
sender = None
|
||||||
|
if sender is not None:
|
||||||
print(f"{sender} {receiver} {amount:>13} {fee:>10}")
|
print(f"{sender} {receiver} {amount:>13} {fee:>10}")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -131,20 +131,20 @@ fn main() {
|
|||||||
work_counter: 0,
|
work_counter: 0,
|
||||||
}));
|
}));
|
||||||
let (results_tx, results_rx) = mpsc::channel();
|
let (results_tx, results_rx) = mpsc::channel();
|
||||||
let mut request = vec![0u8; 257];
|
let mut request = vec![0u8; 258];
|
||||||
request[4] = 7;
|
request[4] = 7;
|
||||||
loop {
|
loop {
|
||||||
let mut recv_buffer = vec![0u8; 512];
|
let mut recv_buffer = vec![0u8; 512];
|
||||||
loop {
|
loop {
|
||||||
match socket.recv(&mut recv_buffer) {
|
match socket.recv(&mut recv_buffer) {
|
||||||
Ok(size) => {
|
Ok(size) => {
|
||||||
if size == 257 && &recv_buffer[0..5] == &[0, 0, 0, 0, 8] {
|
if size == 258 && &recv_buffer[0..5] == &[0, 0, 0, 0, 8] {
|
||||||
let transaction = &recv_buffer[5..153];
|
let transaction = &recv_buffer[5..154];
|
||||||
let previous_hash = &recv_buffer[153..185];
|
let previous_hash = &recv_buffer[154..186];
|
||||||
let timestamp = &recv_buffer[185..193];
|
let timestamp = &recv_buffer[186..194];
|
||||||
let difficulty_sum = &recv_buffer[193..225];
|
let difficulty_sum = &recv_buffer[194..226];
|
||||||
let threshold = &recv_buffer[225..257];
|
let threshold = &recv_buffer[226..258];
|
||||||
let mut prefix = Vec::with_capacity(292);
|
let mut prefix = Vec::with_capacity(293);
|
||||||
prefix.extend_from_slice(transaction);
|
prefix.extend_from_slice(transaction);
|
||||||
prefix.extend_from_slice(&message);
|
prefix.extend_from_slice(&message);
|
||||||
prefix.extend_from_slice(&public_key);
|
prefix.extend_from_slice(&public_key);
|
||||||
|
|||||||
181
node.py
181
node.py
@@ -1,6 +1,6 @@
|
|||||||
#! /usr/bin/env python3
|
#! /usr/bin/env python3
|
||||||
|
|
||||||
import blockchain, hashlib, observer, random, socket, sys, threading, time
|
import blockchain, hashlib, observer, random, secrets, socket, sys, threading, time
|
||||||
from _queue import Empty
|
from _queue import Empty
|
||||||
|
|
||||||
DEFAULT_PORT = 62039
|
DEFAULT_PORT = 62039
|
||||||
@@ -101,15 +101,14 @@ def send_heartbeat(node, peer, b):
|
|||||||
node.node_socket.sendto(heartbeat_msg, (peer.ipv6, peer.port))
|
node.node_socket.sendto(heartbeat_msg, (peer.ipv6, peer.port))
|
||||||
|
|
||||||
def define_partnership(peers):
|
def define_partnership(peers):
|
||||||
|
for peer in peers:
|
||||||
|
peer.partner = None
|
||||||
peers_to_pair = [peer for peer in peers if peer.lifetime_counter >= 8]
|
peers_to_pair = [peer for peer in peers if peer.lifetime_counter >= 8]
|
||||||
random.shuffle(peers_to_pair)
|
random.shuffle(peers_to_pair)
|
||||||
pairing_count = len(peers_to_pair) // 2
|
pairing_count = len(peers_to_pair) // 2
|
||||||
for pair_idx in range(pairing_count):
|
for pair_idx in range(pairing_count):
|
||||||
peers_to_pair[2*pair_idx].partner = peers_to_pair[2*pair_idx+1]
|
peers_to_pair[2*pair_idx].partner = peers_to_pair[2*pair_idx+1]
|
||||||
peers_to_pair[2*pair_idx+1].partner = peers_to_pair[2*pair_idx]
|
peers_to_pair[2*pair_idx+1].partner = peers_to_pair[2*pair_idx]
|
||||||
# in case of an odd count, the last one will remain without partner
|
|
||||||
if pairing_count % 2 == 1:
|
|
||||||
peers_to_pair[-1].partner = None
|
|
||||||
|
|
||||||
def heartbeat(node, b):
|
def heartbeat(node, b):
|
||||||
while True:
|
while True:
|
||||||
@@ -216,6 +215,30 @@ def transfer_block(addr, node, receive_observer, b):
|
|||||||
except NoReponseException:
|
except NoReponseException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def get_associated_data(node, receive_observer, b, addr, associated_data_hash):
|
||||||
|
cached_data = b.get_associated_data(associated_data_hash)
|
||||||
|
if cached_data is not None:
|
||||||
|
return cached_data
|
||||||
|
subscription = receive_observer.listen((addr[0:2], "associated data", associated_data_hash))
|
||||||
|
fragments = 6 * [None]
|
||||||
|
for _ in range(10):
|
||||||
|
request_bitfield = 0
|
||||||
|
for i, fragment in enumerate(fragments):
|
||||||
|
if fragment is None:
|
||||||
|
request_bitfield |= (1 << i)
|
||||||
|
request = b"\0\0\0\0\x0f" + bytes([request_bitfield]) + associated_data_hash
|
||||||
|
node.node_socket.sendto(request, addr)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
response = subscription.receive(1)
|
||||||
|
if fragments[response["part_number"]] is None:
|
||||||
|
fragments[response["part_number"]] = response["fragment"]
|
||||||
|
if None not in fragments:
|
||||||
|
return b"".join(fragments)
|
||||||
|
except Empty:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
def compare_open_transactions(addr, node, receive_observer, b):
|
def compare_open_transactions(addr, node, receive_observer, b):
|
||||||
try:
|
try:
|
||||||
cursor = 511
|
cursor = 511
|
||||||
@@ -238,9 +261,16 @@ def compare_open_transactions(addr, node, receive_observer, b):
|
|||||||
def transaction_condition(response):
|
def transaction_condition(response):
|
||||||
return response["position"] == cursor
|
return response["position"] == cursor
|
||||||
remote_transaction = request_retry(node, addr, request, subscription, transaction_condition)
|
remote_transaction = request_retry(node, addr, request, subscription, transaction_condition)
|
||||||
parsed_transaction = blockchain.Transaction.from_bytes(remote_transaction["transaction"])
|
if blockchain.associated_data_required(remote_transaction["transaction"]):
|
||||||
if not parsed_transaction.is_valid():
|
associated_data = get_associated_data(node, receive_observer, b, addr, remote_transaction["transaction"][33:65])
|
||||||
|
if associated_data is None:
|
||||||
return
|
return
|
||||||
|
else:
|
||||||
|
associated_data = None
|
||||||
|
parsed_transaction = blockchain.transaction_from_bytes(remote_transaction["transaction"], associated_data)
|
||||||
|
if not parsed_transaction.is_valid() or parsed_transaction.is_empty():
|
||||||
|
return
|
||||||
|
b.cache_associated_data(parsed_transaction)
|
||||||
b.open_transactions.add(parsed_transaction)
|
b.open_transactions.add(parsed_transaction)
|
||||||
except NoReponseException:
|
except NoReponseException:
|
||||||
pass
|
pass
|
||||||
@@ -252,7 +282,7 @@ def receiver(node, b):
|
|||||||
sender = describe(addr[0], addr[1])
|
sender = describe(addr[0], addr[1])
|
||||||
msg_len = len(msg)
|
msg_len = len(msg)
|
||||||
if msg_len < 4:
|
if msg_len < 4:
|
||||||
log("Got a udp message from {sender} that was too short.")
|
log(f"Got a udp message from {sender} that was too short.")
|
||||||
continue
|
continue
|
||||||
version = int.from_bytes(msg[0:2], "big")
|
version = int.from_bytes(msg[0:2], "big")
|
||||||
if version != 0:
|
if version != 0:
|
||||||
@@ -300,16 +330,26 @@ def receiver(node, b):
|
|||||||
node.node_socket.sendto(response_msg, addr)
|
node.node_socket.sendto(response_msg, addr)
|
||||||
elif msg_type == 2:
|
elif msg_type == 2:
|
||||||
# block transfer
|
# block transfer
|
||||||
if msg_len != 297:
|
if msg_len != 298:
|
||||||
log(f"Got a block transfer of wrong length ({msg_len} bytes from {sender}, but expected 297 bytes)")
|
log(f"Got a block transfer of wrong length ({msg_len} bytes from {sender}, but expected 298 bytes)")
|
||||||
continue
|
continue
|
||||||
block_raw = msg[5:297]
|
def handle_block_transfer(addr, msg):
|
||||||
new_block = b.add_block(block_raw)
|
if blockchain.associated_data_required(msg[5:154]):
|
||||||
|
associated_data = get_associated_data(node, receive_observer, b, addr, msg[38:70])
|
||||||
|
if associated_data is None:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
associated_data = None
|
||||||
|
new_block = b.add_block(msg[5:298], associated_data)
|
||||||
block_hash = new_block.own_hash
|
block_hash = new_block.own_hash
|
||||||
if new_block.validate(b) and b.set_latest_block(block_hash):
|
if new_block.validate(b):
|
||||||
|
b.cache_associated_data(new_block.transaction)
|
||||||
|
if b.set_latest_block(block_hash):
|
||||||
log("Got a new block")
|
log("Got a new block")
|
||||||
identifier = (addr[0:2], "block transfer")
|
identifier = (addr[0:2], "block transfer")
|
||||||
receive_observer.publish(identifier, block_hash)
|
receive_observer.publish(identifier, block_hash)
|
||||||
|
# Handle this in a thread because asynchronous back-requests might be required
|
||||||
|
threading.Thread(target=handle_block_transfer, args=(addr, msg)).start()
|
||||||
elif msg_type == 3:
|
elif msg_type == 3:
|
||||||
# open transaction list hash request
|
# open transaction list hash request
|
||||||
if msg_len != 7:
|
if msg_len != 7:
|
||||||
@@ -344,32 +384,32 @@ def receiver(node, b):
|
|||||||
continue
|
continue
|
||||||
transaction = b.open_transactions.get_transaction(list_position)
|
transaction = b.open_transactions.get_transaction(list_position)
|
||||||
if transaction is None:
|
if transaction is None:
|
||||||
transaction_raw = 148 * b"\0"
|
transaction_raw = 149 * b"\0"
|
||||||
else:
|
else:
|
||||||
transaction_raw = transaction.get_transaction_raw()
|
transaction_raw = transaction.get_transaction_raw()
|
||||||
response = b"\0\0\0\0\x06" + list_position.to_bytes(2, "big") + transaction_raw
|
response = b"\0\0\0\0\x06" + list_position.to_bytes(2, "big") + transaction_raw
|
||||||
node.node_socket.sendto(response, addr)
|
node.node_socket.sendto(response, addr)
|
||||||
elif msg_type == 6:
|
elif msg_type == 6:
|
||||||
# open transaction response
|
# open transaction response
|
||||||
if msg_len != 155:
|
if msg_len != 156:
|
||||||
log(f"Got an open transaction list hash response of wrong length ({msg_len} bytes from {sender}, but expected 155 bytes)")
|
log(f"Got an open transaction list hash response of wrong length ({msg_len} bytes from {sender}, but expected 156 bytes)")
|
||||||
continue
|
continue
|
||||||
event_obj = {
|
event_obj = {
|
||||||
"position": int.from_bytes(msg[5:7], "big"),
|
"position": int.from_bytes(msg[5:7], "big"),
|
||||||
"transaction": msg[7:155],
|
"transaction": msg[7:156],
|
||||||
}
|
}
|
||||||
identifier = (addr[0:2], "transaction")
|
identifier = (addr[0:2], "transaction")
|
||||||
receive_observer.publish(identifier, event_obj)
|
receive_observer.publish(identifier, event_obj)
|
||||||
elif msg_type == 7:
|
elif msg_type == 7:
|
||||||
# mining task request
|
# mining task request
|
||||||
if msg_len != 257:
|
if msg_len != 258:
|
||||||
log(f"Got a mining task request of wrong length ({msg_len} bytes from {sender}, but expected 257 bytes)")
|
log(f"Got a mining task request of wrong length ({msg_len} bytes from {sender}, but expected 258 bytes)")
|
||||||
continue
|
continue
|
||||||
transaction = b.open_transactions.get_transaction(0)
|
transaction = b.open_transactions.get_transaction(0)
|
||||||
if transaction is not None:
|
if transaction is not None:
|
||||||
transaction_raw = transaction.get_transaction_raw()
|
transaction_raw = transaction.get_transaction_raw()
|
||||||
else:
|
else:
|
||||||
transaction_raw = 148 * b"\0"
|
transaction_raw = 149 * b"\0"
|
||||||
t = int(time.time())
|
t = int(time.time())
|
||||||
timestamp_raw = t.to_bytes(8, "big")
|
timestamp_raw = t.to_bytes(8, "big")
|
||||||
latest_block = b.get_latest_block()
|
latest_block = b.get_latest_block()
|
||||||
@@ -395,15 +435,106 @@ def receiver(node, b):
|
|||||||
threshold.to_bytes(32, "big")
|
threshold.to_bytes(32, "big")
|
||||||
node.node_socket.sendto(response, addr)
|
node.node_socket.sendto(response, addr)
|
||||||
elif msg_type == 9:
|
elif msg_type == 9:
|
||||||
# payment request
|
# transaction request
|
||||||
if msg_len != 153:
|
if msg_len != 154:
|
||||||
log(f"Got a payment of wrong length ({msg_len} bytes from {sender}, but expected 153 bytes)")
|
log(f"Got a transaction of wrong length ({msg_len} bytes from {sender}, but expected 154 bytes)")
|
||||||
continue
|
|
||||||
parsed_transaction = blockchain.Transaction.from_bytes(msg[5:153])
|
|
||||||
if not parsed_transaction.is_valid():
|
|
||||||
continue
|
continue
|
||||||
|
def handle_transaction_request(msg, addr):
|
||||||
|
if blockchain.associated_data_required(msg[5:154]):
|
||||||
|
associated_data = get_associated_data(node, receive_observer, b, addr, msg[38:70])
|
||||||
|
if associated_data is None:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
associated_data = None
|
||||||
|
parsed_transaction = blockchain.transaction_from_bytes(msg[5:154], associated_data)
|
||||||
|
if not parsed_transaction.is_valid() or parsed_transaction.is_empty():
|
||||||
|
return
|
||||||
|
b.cache_associated_data(parsed_transaction)
|
||||||
b.open_transactions.add(parsed_transaction)
|
b.open_transactions.add(parsed_transaction)
|
||||||
node.node_socket.sendto(b"\0\0\0\0\x0a", addr)
|
node.node_socket.sendto(b"\0\0\0\0\x0a", addr)
|
||||||
|
# Handle this in a thread because asynchronous back-requests might be required
|
||||||
|
threading.Thread(target=handle_transaction_request, args=(msg, addr)).start()
|
||||||
|
elif msg_type == 11:
|
||||||
|
# reveal mining task request
|
||||||
|
if msg_len != 37:
|
||||||
|
log(f"Got a reveal mining task request of wrong length ({msg_len} bytes from {sender}, but expected 37 bytes)")
|
||||||
|
continue
|
||||||
|
next_reveal_hash = 32 * b"\0"
|
||||||
|
latest_block = b.get_latest_block()
|
||||||
|
if latest_block is not None and len(latest_block.pending_commitment_blocks) > 0:
|
||||||
|
next_reveal_hash = latest_block.pending_commitment_blocks[0][0]
|
||||||
|
node.node_socket.sendto(b"\0\0\0\0\x0c" + next_reveal_hash, addr)
|
||||||
|
elif msg_type == 13:
|
||||||
|
# Ping
|
||||||
|
if msg_len != 28:
|
||||||
|
log(f"Got a Ping message of wrong length ({msg_len} bytes from {sender}, but expected 28 bytes)")
|
||||||
|
continue
|
||||||
|
if msg[5:20] != "R u carrotcoin?".encode():
|
||||||
|
# Wrong Ping question, ignore
|
||||||
|
continue
|
||||||
|
nonce = msg[20:28]
|
||||||
|
node.node_socket.sendto(b"\0\0\0\0\x0e" + "I m carrotcoin!".encode() + nonce, addr)
|
||||||
|
elif msg_type == 14:
|
||||||
|
# Pong
|
||||||
|
if msg_len != 28:
|
||||||
|
log(f"Got a Pong message of wrong length ({msg_len} bytes from {sender}, but expected 28 bytes)")
|
||||||
|
continue
|
||||||
|
if msg[5:20] != "I m carrotcoin!".encode():
|
||||||
|
# Wrong Pong answer, ignore
|
||||||
|
continue
|
||||||
|
nonce = msg[20:28]
|
||||||
|
identifier = (addr[0:2], "pong")
|
||||||
|
receive_observer.publish(identifier, nonce)
|
||||||
|
elif msg_type == 15:
|
||||||
|
# Associated revealing proof request
|
||||||
|
if msg_len != 38:
|
||||||
|
log(f"Got an associated revealing proof request of wrong length ({msg_len} bytes from {sender}, but expected exactly 38 bytes)")
|
||||||
|
continue
|
||||||
|
parts_bitfield = msg[5]
|
||||||
|
associated_proof_hash = msg[6:38]
|
||||||
|
proof = b.get_associated_data(associated_proof_hash)
|
||||||
|
if proof is None:
|
||||||
|
# Cannot send anything, proof is unknown
|
||||||
|
continue
|
||||||
|
def handle_associated_relvealing_proof_request(addr, parts_bitfield, associated_proof_hash, proof):
|
||||||
|
# Ping first, otherwise this protocol part would be a really strong ddos reflection amplifier
|
||||||
|
subscription = receive_observer.listen((addr[0:2], "pong"))
|
||||||
|
nonce = secrets.randbits(64).to_bytes(8, "big")
|
||||||
|
ping = b"\0\0\0\0\x0dR u carrotcoin?" + nonce
|
||||||
|
node.node_socket.sendto(ping, addr[0:2])
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
pong_nonce = subscription.receive(5)
|
||||||
|
if pong_nonce == nonce:
|
||||||
|
break
|
||||||
|
except Empty:
|
||||||
|
# No response to ping, ignore the initial request
|
||||||
|
return
|
||||||
|
for part_number in range(6):
|
||||||
|
if parts_bitfield & (1 << part_number) == 0:
|
||||||
|
continue
|
||||||
|
revealing_proof_response = b"\0\0\0\0\x10" + \
|
||||||
|
bytes([part_number]) + \
|
||||||
|
associated_proof_hash + \
|
||||||
|
proof[part_number*896:(part_number+1)*896]
|
||||||
|
node.node_socket.sendto(revealing_proof_response, addr[0:2])
|
||||||
|
# Start a thread because of the asynchronous ping
|
||||||
|
threading.Thread(target=handle_associated_relvealing_proof_request, args=(addr, parts_bitfield, associated_proof_hash, proof)).start()
|
||||||
|
elif msg_type == 16:
|
||||||
|
# Associated revealing proof response
|
||||||
|
if msg_len != 934:
|
||||||
|
log(f"Got an associated revealing proof response of wrong length ({msg_len} bytes from {sender}, but expected exactly 934 bytes)")
|
||||||
|
continue
|
||||||
|
part_number = msg[5]
|
||||||
|
if part_number not in range(6):
|
||||||
|
continue
|
||||||
|
associated_data_hash = msg[6:38]
|
||||||
|
fragment = msg[38:934]
|
||||||
|
identifier = (addr[0:2], "associated data", associated_data_hash)
|
||||||
|
receive_observer.publish(identifier, {
|
||||||
|
"part_number": part_number,
|
||||||
|
"fragment": fragment
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
log(f"Got a udp message of unknown type from {sender}. (type {msg_type})")
|
log(f"Got a udp message of unknown type from {sender}. (type {msg_type})")
|
||||||
|
|
||||||
|
|||||||
1
reveal-miner/.gitignore
vendored
Normal file
1
reveal-miner/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/progress
|
||||||
195
reveal-miner/mine.py
Executable file
195
reveal-miner/mine.py
Executable file
@@ -0,0 +1,195 @@
|
|||||||
|
#! /usr/bin/env python3
|
||||||
|
|
||||||
|
import base64, hashlib, os, queue, socket, sys, threading, time
|
||||||
|
|
||||||
|
n = 22152137184697602751949152182712806144286735269755991212091578018969267364854563246965161398309375840488156997640625085213985729183180764348127989435514689722834129288342499703533613231239853168289771769536412763137391054558055082146752229593979328251181647873233949602834141648160681711983351545692646009424518816069561938917629523175464947983950548802679152115205735609960641453864298194702935993896839374645356040490091081577992299773430144650589605043643969140352237968606446474316247592579560197155686719175897498255683642121505357781103123719079205647707696181709515150954235402701095586525936356219917713227143
|
||||||
|
|
||||||
|
def log(msg):
|
||||||
|
time_info = time.strftime("%d.%m.%Y %H:%M:%S")
|
||||||
|
print(f"[{time_info}] {msg}")
|
||||||
|
|
||||||
|
def load_progress():
|
||||||
|
try:
|
||||||
|
f = open("progress", "r+b")
|
||||||
|
except FileNotFoundError:
|
||||||
|
f = open("progress", "w+b")
|
||||||
|
return [f, 0, 0, 0]
|
||||||
|
length = f.seek(0, os.SEEK_END)
|
||||||
|
if length < 256:
|
||||||
|
return [f, 0, 0, 0]
|
||||||
|
f.seek(0)
|
||||||
|
begin = int.from_bytes(f.read(256), "big")
|
||||||
|
position = length // 256 - 1
|
||||||
|
f.seek(256 * position)
|
||||||
|
current_value = int.from_bytes(f.read(256), "big")
|
||||||
|
return [f, begin, position, current_value]
|
||||||
|
|
||||||
|
def normalize(x):
|
||||||
|
if x > n // 2:
|
||||||
|
return n - x
|
||||||
|
return x
|
||||||
|
|
||||||
|
def reduce_milestones(a, e):
|
||||||
|
half_length = len(a) // 2
|
||||||
|
return [(a[i] * pow(a[half_length+i], e, n)) % n for i in range(half_length+1)]
|
||||||
|
|
||||||
|
def finalize(f, revealer_pubkey):
|
||||||
|
f.seek(0)
|
||||||
|
milestones = [int.from_bytes(f.read(256), "big") for _ in range(2**15 + 1)]
|
||||||
|
final_R = normalize(milestones[-1])
|
||||||
|
intermediates = []
|
||||||
|
for i in range(30, 10, -1):
|
||||||
|
H = normalize(milestones[0])
|
||||||
|
R = normalize(milestones[-1])
|
||||||
|
if i > 15:
|
||||||
|
I = normalize(milestones[len(milestones) // 2])
|
||||||
|
else:
|
||||||
|
I = normalize(pow(H, 2**2**(i-1), n))
|
||||||
|
milestones = [milestones[0], I, milestones[-1]]
|
||||||
|
hash_data = revealer_pubkey + \
|
||||||
|
H.to_bytes(256, "big") + \
|
||||||
|
I.to_bytes(256, "big") + \
|
||||||
|
R.to_bytes(256, "big") + \
|
||||||
|
i.to_bytes(1, "big")
|
||||||
|
hash_value = hashlib.sha256(hash_data).digest()
|
||||||
|
e = int.from_bytes(hash_value, "big")
|
||||||
|
milestones = reduce_milestones(milestones, e)
|
||||||
|
intermediates.append(I)
|
||||||
|
proof_numbers = [final_R] + intermediates
|
||||||
|
return [x.to_bytes(256, "big") for x in proof_numbers]
|
||||||
|
|
||||||
|
def send_transaction(revealer_pubkey, proof):
|
||||||
|
proof_block = b"".join(proof)
|
||||||
|
associated_proof_hash = hashlib.sha256(proof_block).digest()
|
||||||
|
transaction = b"\x03" + revealer_pubkey + associated_proof_hash + 84 * b"\0"
|
||||||
|
|
||||||
|
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||||
|
s.connect(("::1", 62039))
|
||||||
|
|
||||||
|
attempts = 10
|
||||||
|
next_attempt = time.time()
|
||||||
|
while True:
|
||||||
|
timeout = next_attempt - time.time()
|
||||||
|
if timeout > 0:
|
||||||
|
s.settimeout(timeout)
|
||||||
|
try:
|
||||||
|
msg = s.recv(4096)
|
||||||
|
if len(msg) == 38 and msg[0:2] == b"\0\0" and msg[4] == 15 and msg[6:38] == associated_proof_hash:
|
||||||
|
bitfield = msg[5]
|
||||||
|
for bitpos in range(6):
|
||||||
|
if (bitfield & (1 << bitpos)) != 0:
|
||||||
|
begin = 896 * bitpos
|
||||||
|
end = 896 * (bitpos + 1)
|
||||||
|
reply = b"\0\0\0\0\x10" + bytes([bitpos]) + associated_proof_hash + proof_block[begin:end]
|
||||||
|
s.send(reply)
|
||||||
|
elif len(msg) == 5 and msg[0:2] == b"\0\0" and msg[4] == 10:
|
||||||
|
return True
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
elif attempts == 0:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
attempts -= 1
|
||||||
|
next_attempt += 1.0
|
||||||
|
transaction_request = b"\0\0\0\0\x09" + transaction
|
||||||
|
s.send(transaction_request)
|
||||||
|
|
||||||
|
def mining_loop(event_queue, revealer_pubkey):
|
||||||
|
step_exponent = 2**2**15
|
||||||
|
f, begin, position, value = load_progress()
|
||||||
|
last_status = position * 100 // 2**15
|
||||||
|
proof = None
|
||||||
|
sent = False
|
||||||
|
def reset():
|
||||||
|
nonlocal f, begin, position, value, last_status, proof, sent
|
||||||
|
f.seek(0)
|
||||||
|
f.truncate()
|
||||||
|
if begin != 0:
|
||||||
|
f.write(begin.to_bytes(256, "big"))
|
||||||
|
position = 0
|
||||||
|
last_status = 0
|
||||||
|
value = begin
|
||||||
|
proof = None
|
||||||
|
sent = False
|
||||||
|
while True:
|
||||||
|
if begin == 0:
|
||||||
|
begin = event_queue.get()
|
||||||
|
reset()
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
new_begin = event_queue.get(0)
|
||||||
|
if new_begin != begin:
|
||||||
|
begin = new_begin
|
||||||
|
if begin == 0:
|
||||||
|
log("No task right now")
|
||||||
|
else:
|
||||||
|
log("Got a new revealing task")
|
||||||
|
reset()
|
||||||
|
continue
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
if position < 2**15:
|
||||||
|
value = pow(value, step_exponent, n)
|
||||||
|
f.write(value.to_bytes(256, "big"))
|
||||||
|
position += 1
|
||||||
|
new_status = position * 100 // 2**15
|
||||||
|
if new_status != last_status:
|
||||||
|
log(f"{new_status} %")
|
||||||
|
last_status = new_status
|
||||||
|
elif proof is None:
|
||||||
|
log("finalizing")
|
||||||
|
proof = finalize(f, revealer_pubkey)
|
||||||
|
elif not sent:
|
||||||
|
log("Sending reveal transaction")
|
||||||
|
sent = send_transaction(revealer_pubkey, proof)
|
||||||
|
if sent:
|
||||||
|
log("Transaction has been received by our node")
|
||||||
|
else:
|
||||||
|
log("(no response from the node)")
|
||||||
|
else:
|
||||||
|
new_begin = event_queue.get()
|
||||||
|
if new_begin == begin:
|
||||||
|
continue
|
||||||
|
begin = new_begin
|
||||||
|
if begin == 0:
|
||||||
|
log("No task right now")
|
||||||
|
else:
|
||||||
|
log("Got a new revealing task")
|
||||||
|
reset()
|
||||||
|
|
||||||
|
# communicate with node, send change events
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: ./mine.py <wallet address>", file=sys.stderr)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
revealer_pubkey = base64.b64decode(sys.argv[1])
|
||||||
|
assert len(revealer_pubkey) == 32
|
||||||
|
|
||||||
|
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||||
|
s.connect(("::1", 62039))
|
||||||
|
s.settimeout(1)
|
||||||
|
|
||||||
|
event_queue = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
request = b"\0\0\0\0\x0b" + 32 * b"\0"
|
||||||
|
s.send(request)
|
||||||
|
try:
|
||||||
|
response = s.recv(4096)
|
||||||
|
if len(response) == 37 and response[0:5] == b"\0\0\0\0\x0c":
|
||||||
|
begin = int.from_bytes(response[5:37], "big")
|
||||||
|
if event_queue is None:
|
||||||
|
event_queue = queue.Queue()
|
||||||
|
threading.Thread(target=mining_loop, args=(event_queue, revealer_pubkey)).start()
|
||||||
|
if begin == 0:
|
||||||
|
log("node is available, waiting for a reveal mining task")
|
||||||
|
else:
|
||||||
|
log("node is available, starting reveal mining process")
|
||||||
|
event_queue.put(begin)
|
||||||
|
time.sleep(1)
|
||||||
|
except TimeoutError:
|
||||||
|
log("Got no response from the node")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
111
wallet.py
111
wallet.py
@@ -1,6 +1,6 @@
|
|||||||
#! /usr/bin/env python3
|
#! /usr/bin/env python3
|
||||||
|
|
||||||
import base64, socket, sys, time
|
import base64, hashlib, socket, sys, time
|
||||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
||||||
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, PublicFormat, NoEncryption
|
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, PublicFormat, NoEncryption
|
||||||
|
|
||||||
@@ -27,33 +27,75 @@ def write_transaction(timestamp, message, amount):
|
|||||||
|
|
||||||
def show_balance(public_key):
|
def show_balance(public_key):
|
||||||
public_key_raw = public_key.public_bytes(Encoding.Raw, PublicFormat.Raw)
|
public_key_raw = public_key.public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||||
|
try:
|
||||||
with open("blockchain", "rb") as f:
|
with open("blockchain", "rb") as f:
|
||||||
total_amount = 0
|
total_amount = 0
|
||||||
|
block_counter = 0
|
||||||
|
pending_bets = [[]]
|
||||||
while True:
|
while True:
|
||||||
block = f.read(292)
|
block = f.read(293)
|
||||||
if len(block) != 292:
|
if len(block) != 293:
|
||||||
break
|
break
|
||||||
miner = block[180:212]
|
miner = block[181:213]
|
||||||
timestamp = int.from_bytes(block[244:252], "big")
|
timestamp = int.from_bytes(block[245:253], "big")
|
||||||
if block[0:148] != 148 * b"\0":
|
if block[0] == 1:
|
||||||
sender = block[4:36]
|
# Payment
|
||||||
receiver = block[36:68]
|
sender = block[5:37]
|
||||||
amount = int.from_bytes(block[68:76], "big")
|
receiver = block[37:69]
|
||||||
fee = int.from_bytes(block[76:84], "big")
|
amount = int.from_bytes(block[69:77], "big")
|
||||||
|
fee = int.from_bytes(block[77:85], "big")
|
||||||
if sender == public_key_raw:
|
if sender == public_key_raw:
|
||||||
write_transaction(timestamp, format_address(receiver), - amount - fee)
|
write_transaction(timestamp, format_address(receiver), - amount)
|
||||||
|
if fee > 0:
|
||||||
|
write_transaction(timestamp, 39 * " " + "(fee)", - fee)
|
||||||
total_amount -= (amount + fee)
|
total_amount -= (amount + fee)
|
||||||
if receiver == public_key_raw:
|
if receiver == public_key_raw:
|
||||||
write_transaction(timestamp, format_address(sender), amount)
|
write_transaction(timestamp, format_address(sender), amount)
|
||||||
total_amount += amount
|
total_amount += amount
|
||||||
|
elif block[0] == 2:
|
||||||
|
# Gambling
|
||||||
|
transaction_id = block[1:5]
|
||||||
|
player = block[5:37]
|
||||||
|
amount = int.from_bytes(block[37:45], "big")
|
||||||
|
fee = int.from_bytes(block[45:53], "big")
|
||||||
|
if player == public_key_raw:
|
||||||
|
write_transaction(timestamp, "- gambling -", -amount)
|
||||||
|
if fee > 0:
|
||||||
|
write_transaction(timestamp, 39 * " " + "(fee)", - fee)
|
||||||
|
total_amount -= (amount + fee)
|
||||||
|
pending_bets[-1].append((transaction_id, amount))
|
||||||
|
elif block[0] == 3:
|
||||||
|
# Reveal transaction
|
||||||
|
reveal_info = f.read(5376)
|
||||||
|
revealer = block[1:33]
|
||||||
|
R = reveal_info[0:256]
|
||||||
|
relevant_bets = pending_bets[0]
|
||||||
|
pending_bets = pending_bets[1:]
|
||||||
|
fee = 100
|
||||||
|
for (transaction_id, gambling_amount) in relevant_bets:
|
||||||
|
to_hash = transaction_id + public_key_raw + R
|
||||||
|
if hashlib.sha256(to_hash).digest()[0] >= 0x80:
|
||||||
|
write_transaction(timestamp, f"Won gambling with {format_amount(gambling_amount, 0, False)}", 2 * gambling_amount)
|
||||||
|
total_amount += 2 * gambling_amount
|
||||||
|
else:
|
||||||
|
write_transaction(timestamp, f"Lost gambling with {format_amount(gambling_amount, 0, False)}", 0)
|
||||||
|
if revealer == public_key_raw:
|
||||||
|
write_transaction(timestamp, "revealing reward", 1500)
|
||||||
|
total_amount += 1500
|
||||||
else:
|
else:
|
||||||
fee = 0
|
fee = 0
|
||||||
if miner == public_key_raw:
|
if miner == public_key_raw:
|
||||||
write_transaction(timestamp, "mining reward", 100 + fee)
|
write_transaction(timestamp, "mining reward", 100 + fee)
|
||||||
total_amount += 100 + fee
|
total_amount += 100 + fee
|
||||||
|
if block_counter % 256 == 255:
|
||||||
|
pending_bets.append([])
|
||||||
|
block_counter += 1
|
||||||
print(81 * "\u2500")
|
print(81 * "\u2500")
|
||||||
amount_string = f"\U0001f955 \x1b[1;37m{format_amount(total_amount, 41, False)}\x1b[0m"
|
amount_string = f"\U0001f955 \x1b[1;37m{format_amount(total_amount, 41, False)}\x1b[0m"
|
||||||
print(21 * " " + f"\x1b[1;37mYour balance:\x1b[0m{amount_string}")
|
print(21 * " " + f"\x1b[1;37mYour balance:\x1b[0m{amount_string}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("File \"blockchain\" not found.\nThis wallet script requires a running node with at least 1 block in the current directory.", file=sys.stderr)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
def parse_amount(amount):
|
def parse_amount(amount):
|
||||||
amount = amount.replace(",", ".")
|
amount = amount.replace(",", ".")
|
||||||
@@ -61,10 +103,12 @@ def parse_amount(amount):
|
|||||||
if len(parts) == 1:
|
if len(parts) == 1:
|
||||||
return int(parts[0]) * 100
|
return int(parts[0]) * 100
|
||||||
elif len(parts) == 2:
|
elif len(parts) == 2:
|
||||||
if len(parts[1]) > 2:
|
if len(parts[1]) == 0 or len(parts[1]) > 2:
|
||||||
raise Exception(f"Invalid amount: {amount}")
|
raise Exception(f"Invalid amount: {amount}")
|
||||||
coins = int(parts[0])
|
coins = int(parts[0]) if parts[0] != "" else 0
|
||||||
cents = int(parts[1])
|
cents = int(parts[1]) if parts[1] != "" else 0
|
||||||
|
if len(parts[1]) == 1:
|
||||||
|
cents *= 10
|
||||||
return coins * 100 + cents
|
return coins * 100 + cents
|
||||||
raise Exception(f"Invalid amount: {amount}")
|
raise Exception(f"Invalid amount: {amount}")
|
||||||
|
|
||||||
@@ -81,14 +125,17 @@ def find_free_id(public_key_raw):
|
|||||||
with open("blockchain", "rb") as f:
|
with open("blockchain", "rb") as f:
|
||||||
used_ids = set()
|
used_ids = set()
|
||||||
while True:
|
while True:
|
||||||
block = f.read(292)
|
block = f.read(293)
|
||||||
if len(block) != 292:
|
if len(block) != 293:
|
||||||
break
|
break
|
||||||
if block[0:148] != 148 * b"\0":
|
transaction_type = block[0]
|
||||||
transaction_id = int.from_bytes(block[0:4], "big")
|
if transaction_type in (1, 2):
|
||||||
sender = block[4:36]
|
transaction_id = int.from_bytes(block[1:5], "big")
|
||||||
|
sender = block[5:37]
|
||||||
if sender == public_key_raw:
|
if sender == public_key_raw:
|
||||||
used_ids.add(transaction_id)
|
used_ids.add(transaction_id)
|
||||||
|
elif transaction_type == 3:
|
||||||
|
f.read(5376)
|
||||||
for possible_id in range(0, 2**32):
|
for possible_id in range(0, 2**32):
|
||||||
if possible_id not in used_ids:
|
if possible_id not in used_ids:
|
||||||
return possible_id
|
return possible_id
|
||||||
@@ -106,15 +153,34 @@ def send_payment(private_key, target, amount, fee):
|
|||||||
fee = parse_amount_checked(fee)
|
fee = parse_amount_checked(fee)
|
||||||
public_key_raw = private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
public_key_raw = private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||||
transaction_id = find_free_id(public_key_raw)
|
transaction_id = find_free_id(public_key_raw)
|
||||||
transaction_prefix = transaction_id.to_bytes(4, "big") + \
|
transaction_prefix = b"\x01" + \
|
||||||
|
transaction_id.to_bytes(4, "big") + \
|
||||||
public_key_raw + \
|
public_key_raw + \
|
||||||
target_raw + \
|
target_raw + \
|
||||||
amount.to_bytes(8, "big") + \
|
amount.to_bytes(8, "big") + \
|
||||||
fee.to_bytes(8, "big")
|
fee.to_bytes(8, "big")
|
||||||
signature = private_key.sign(transaction_prefix)
|
signature = private_key.sign(transaction_prefix)
|
||||||
transaction = transaction_prefix + signature
|
transaction = transaction_prefix + signature
|
||||||
request = b"\0\0\0\0\x09" + transaction
|
send_transaction(transaction)
|
||||||
|
|
||||||
|
def gamble(private_key, amount, fee):
|
||||||
|
amount = parse_amount_checked(amount)
|
||||||
|
if amount == 0:
|
||||||
|
raise Exception("Amount must not be zero")
|
||||||
|
fee = parse_amount_checked(fee)
|
||||||
|
public_key_raw = private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||||
|
transaction_id = find_free_id(public_key_raw)
|
||||||
|
transaction_prefix = b"\x02" + \
|
||||||
|
transaction_id.to_bytes(4, "big") + \
|
||||||
|
public_key_raw + \
|
||||||
|
amount.to_bytes(8, "big") + \
|
||||||
|
fee.to_bytes(8, "big")
|
||||||
|
signature = private_key.sign(transaction_prefix)
|
||||||
|
transaction = transaction_prefix + signature + 32 * b"\0"
|
||||||
|
send_transaction(transaction)
|
||||||
|
|
||||||
|
def send_transaction(transaction):
|
||||||
|
request = b"\0\0\0\0\x09" + transaction
|
||||||
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||||
try:
|
try:
|
||||||
s.connect(("::1", 62039))
|
s.connect(("::1", 62039))
|
||||||
@@ -136,6 +202,7 @@ def usage_info():
|
|||||||
print("Usage:", file=sys.stderr)
|
print("Usage:", file=sys.stderr)
|
||||||
print(" ./wallet.py # see your past transactions and balance", file=sys.stderr)
|
print(" ./wallet.py # see your past transactions and balance", file=sys.stderr)
|
||||||
print(" ./wallet.py pay <target> <amount> <fee> # send carrotcoins to someone else", file=sys.stderr)
|
print(" ./wallet.py pay <target> <amount> <fee> # send carrotcoins to someone else", file=sys.stderr)
|
||||||
|
print(" ./wallet.py gamble <amount> <fee> # set an arbitrary amount on a 50:50 bet", file=sys.stderr)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
try:
|
try:
|
||||||
@@ -156,6 +223,8 @@ def main():
|
|||||||
show_balance(public_key)
|
show_balance(public_key)
|
||||||
elif len(sys.argv) == 5 and sys.argv[1] == "pay":
|
elif len(sys.argv) == 5 and sys.argv[1] == "pay":
|
||||||
send_payment(private_key, *sys.argv[2:5])
|
send_payment(private_key, *sys.argv[2:5])
|
||||||
|
elif len(sys.argv) == 4 and sys.argv[1] == "gamble":
|
||||||
|
gamble(private_key, *sys.argv[2:4])
|
||||||
else:
|
else:
|
||||||
usage_info()
|
usage_info()
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user