def get_minimum_fee( self, minimum_fee: Tuple[int, int], inputs: int, outputs: int, mixins: List[List[int]], extra: int, fee: int, ) -> int: """ Calculates the minimum fee via the passed in: - Minimum fee (fee per byte, quantization mask) - Number of inputs - Number of outputs - Mixins - Extra length - Fee (a bit ironic, yet the fee changes the serialization length) """ # Calculate the Transaction length. length: int = len(to_var_int(inputs)) + (803 * inputs) for i in mixins: for v in i: length += len(to_var_int(v)) length += len(to_var_int(outputs)) + (74 * outputs) length += len(to_var_int(extra)) + extra length += len(to_var_int(fee)) length += 64 * min(outputs, 4) length += 614 # Calculate and return the minimum fee. return (((length * minimum_fee[0]) + minimum_fee[1] - 1) // minimum_fee[1] * minimum_fee[1])
def padded_varint_serializations_test() -> None: for _ in range(500): num: int = randint(0, 2**16) var_int: bytes = to_var_int(num) length: int = len(var_int) rpc_var_int: bytes = to_rpc_var_int(num) rpc_length: int = len(rpc_var_int) before: int = randint(0, 32) for i in range(before): var_int = bytes([randint(0, 255)]) + var_int rpc_var_int = bytes([randint(0, 255)]) + rpc_var_int for i in range(randint(0, 32)): var_int += bytes([randint(0, 255)]) rpc_var_int += bytes([randint(0, 255)]) res: Tuple[int, int] = from_var_int(var_int, before) assert res[0] == num assert res[1] == before + length rpc_res: Tuple[int, int] = from_rpc_var_int(rpc_var_int, before) assert res[0] == num assert res[1] == before + length
def multiple_Rs_test(harness: Harness) -> None: # Wallet. wallet: Wallet = Wallet(harness.crypto, urandom(32)) # WatchWallet. watch: WatchWallet = WatchWallet( harness.crypto, harness.rpc, wallet.private_view_key, wallet.public_spend_key, harness.rpc.get_block_count() - 1, ) # Test multiple Rs. indexes: List[Tuple[int, int]] = [(0, 0)] amounts: List[int] = [] txs: List[bytes] = [] for _ in range(5): indexes.append((randint(0, 300), randint(0, 300))) amounts.append(randint(ATOMIC_XMR, 40 * ATOMIC_XMR)) txs.append(harness.send(watch.new_address(indexes[-1]), amounts[-1])) # Get the Transaction. tx: Transaction = watch.rpc.get_transaction(txs[-1]) # Add multiple other Rs to the Transaction. for _ in range(3): tx.Rs.append(public_from_secret(Hs(urandom(32)))) # Check the other Rs had no affect. spendable: Tuple[List[bytes], Dict[OutputIndex, OutputInfo]] = watch.can_spend(tx) assert not spendable[0] assert len(spendable[1]) == 1 assert list(spendable[1].keys())[0].tx_hash == txs[-1] assert spendable[1][list(spendable[1].keys())[0]].amount == amounts[-1] # Test multiple identical Rs. for _ in range(5): # Send to a random index. indexes.append((randint(0, 300), randint(0, 300))) amounts.append(randint(ATOMIC_XMR, 5 * ATOMIC_XMR)) txs.append(harness.send(watch.new_address(indexes[-1]), amounts[-1])) # Manually get the Transaction's JSON. tx_json: Dict[str, Any] = json.loads( watch.rpc.rpc_request( "get_transactions", { "txs_hashes": [txs[-1].hex()], "decode_as_json": True }, )["txs"][0]["as_json"]) # Create a Transaction from it. tx: Transaction = Transaction(txs[-1], tx_json) # Get a duplicate list of Rs. Rs: List[bytes] = tx.Rs * 2 # Use the Rs and tx to craft a new extra in tx_json. extra: bytes = bytes([0x01]) + Rs[0] # Add the other Rs. extra += bytes([0x04]) + to_var_int(len(Rs) - 1) for R in Rs[1:]: extra += R # Store it in tx_json. tx_json["extra"] = [] for b in range(len(extra)): tx_json["extra"].append(extra[b]) # Parse the modified JSON. modified_tx: Transaction = Transaction(txs[-1], tx_json) # Check the duplicate Rs were stripped. assert tx.Rs == modified_tx.Rs spendable: Tuple[List[bytes], Dict[OutputIndex, OutputInfo]] = watch.can_spend(tx) assert not spendable[0] assert len(spendable[1]) == 1 assert list(spendable[1].keys())[0].tx_hash == txs[-1] assert spendable[1][list(spendable[1].keys())[0]].amount == amounts[-1] # Send back to the master wallet. harness.return_funds(wallet, watch, sum(amounts))
def spendable_transaction( self, inputs: List[OutputInfo], mixins: List[List[int]], outputs: List[SpendableOutput], ring: List[List[List[bytes]]], change: SpendableOutput, fee: int, ) -> MoneroSpendableTransaction: """Create a MoneroSpendableTransaction.""" # Clone the arguments. inputs = list(inputs) outputs = list(outputs) # Calculate the Transaction's amount. amount: int = 0 for input_i in inputs: amount += input_i.amount for output in outputs: amount -= output.amount amount -= fee # Verify the outputs, change output (if needed), and fee are payable. if amount < 0: raise Exception( "Transaction doesn't have enough of an amount to pay " + "all the outputs, a change output (if needed), and the fee.") elif amount == 0: if len(outputs) < 2: raise Exception( "Transaction doesn't have enough to create a second output." ) else: # Add the change output. change.amount = amount outputs.append(change) # Shuffle the outputs. shuffle(outputs) # Embed the mixins and ring into the inputs. for i in range(len(inputs)): inputs[i].mixins = [] index_sum: int = 0 for index in mixins[i]: inputs[i].mixins.append(index - index_sum) index_sum += inputs[i].mixins[-1] inputs[i].ring = ring[i] # Create an r. r: bytes = ed.Hs(urandom(32)) # Create the actual output key and the output amounts. Rs: List[bytes] = [] rA8s: List[bytes] = [] amount_keys: List[bytes] = [] output_keys: List[bytes] = [] output_amounts: List[int] = [] for o in range(len(outputs)): rA8s.append(self.create_shared_key(r, outputs[o].view_key)) amount_keys.append(ed.Hs(rA8s[-1] + to_var_int(o))) output_keys.append( ed.encodepoint( ed.add_compressed( ed.scalarmult(ed.B, ed.decodeint(amount_keys[-1])), ed.decodepoint(outputs[o].spend_key), ))) rG: bytes if outputs[o].network == self.network_bytes_property[2]: rG = ed.encodepoint( ed.scalarmult(ed.decodepoint(outputs[o].spend_key), ed.decodeint(r))) else: rG = ed.encodepoint(ed.scalarmult(ed.B, ed.decodeint(r))) Rs.append(rG) output_amounts.append(outputs[o].amount) # Deduplicate the Rs. Rs = list(set(Rs)) # Create an extra. extra: bytes = bytes([0x01]) + Rs[0] # Add the other Rs. if len(Rs) > 1: extra += bytes([0x04]) + to_var_int(len(Rs) - 1) for R in Rs[1:]: extra += R # Add the payment IDs. extra_payment_IDs: bytes = bytes() for o in range(len(outputs)): potential_payment_id: Optional[bytes] = outputs[o].payment_id if potential_payment_id: extra_payment_IDs += bytes([0x01]) + (int.from_bytes( potential_payment_id, byteorder="little") ^ int.from_bytes( ed.H(rA8s[o] + bytes([0x8D]))[0:8], byteorder="little")).to_bytes(8, byteorder="little") if extra_payment_IDs: extra += (bytes([0x02]) + to_var_int(len(extra_payment_IDs)) + extra_payment_IDs) return MoneroSpendableTransaction(inputs, amount_keys, output_keys, output_amounts, extra, fee)
def can_spend_output( self, unique_factors: Dict[bytes, Tuple[int, int]], shared_key: bytes, tx: Transaction, o: int, ) -> Optional[MoneroOutputInfo]: """Checks if an output is spendable and returns the relevant info.""" # Grab the output. output = tx.outputs[o] # Transaction one time keys are defined as P = Hs(H8Ra || i)G + B. # This is rewrittable as B = P - Hs(8Ra || i) G. # Hs(8Ra || i) amount_key: bytes = ed.Hs(shared_key + to_var_int(o)) # P - Hs(8Ra || i)G amount_key_G: ed.CompressedPoint = ed.scalarmult( ed.B, ed.decodeint(amount_key)) # Make it negative so it can be subtracted by adding it. amount_key_G = (-amount_key_G[0], amount_key_G[1]) spend_key: bytes = ed.encodepoint( ed.add_compressed(ed.decodepoint(output.key), amount_key_G)) # We now have the spend key of the Transaction. if spend_key in unique_factors: # Get the amount. amount: int = 0 if isinstance(output, MinerOutput): amount = output.amount else: # Decrypt the amount. amount = int.from_bytes( output.amount, byteorder="little") ^ int.from_bytes( ed.H(b"amount" + amount_key)[0:8], byteorder="little") commitment: bytes = ed.COMMITMENT_MASK if isinstance(output, Output): # The encrypted amount is malleable. # We need to rebuild the commitment to verify it's accurate. commitment = ed.Hs(b"commitment_mask" + amount_key) if (ed.encodepoint( ed.add_compressed( ed.scalarmult( ed.B, ed.decodeint(commitment), ), ed.scalarmult(ed.C, amount), )) != output.commitment): return None return MoneroOutputInfo( OutputIndex(tx.tx_hash, o), tx.unlock_time, amount, spend_key, (unique_factors[spend_key][0], unique_factors[spend_key][1]), amount_key, commitment, ) return None
def serialize(self) -> Tuple[bytes, bytes]: """Serialize a MoneroSpendableTransaction.""" # Serialize the version and lock time. prefix: bytes = bytes([2, 0]) prefix += to_var_int(len(self.inputs)) for input_i in self.inputs: prefix += bytes([2, 0]) prefix += to_var_int(len(input_i.mixins)) for mixin in input_i.mixins: prefix += to_var_int(mixin) prefix += input_i.image prefix += to_var_int(len(self.output_keys)) for o in range(len(self.output_keys)): prefix += bytes([0, 2]) prefix += self.output_keys[o] prefix += to_var_int(len(self.extra)) prefix += self.extra if self.signatures is None: return (ed.H(prefix), bytes()) # RangeCT below. All of our Transactions are Simple Padded Bulletproofs (type 4). base: bytes = bytes([5]) base += to_var_int(self.fee) for o in range(len(self.output_keys)): for i in range(8): base += bytes([self.signatures.ecdh_info[o].amount[i]]) for out_public_key in self.signatures.out_public_keys: for i in range(32): base += bytes([out_public_key.mask[i]]) # Prunable info. prunable: bytes = to_var_int(len( self.signatures.prunable.bulletproofs)) for bulletproof in self.signatures.prunable.bulletproofs: for i in range(32): prunable += bytes([bulletproof.capital_a[i]]) for i in range(32): prunable += bytes([bulletproof.s[i]]) for i in range(32): prunable += bytes([bulletproof.t1[i]]) for i in range(32): prunable += bytes([bulletproof.t2[i]]) for i in range(32): prunable += bytes([bulletproof.taux[i]]) for i in range(32): prunable += bytes([bulletproof.mu[i]]) prunable += to_var_int(len(bulletproof.l)) for l in bulletproof.l: for i in range(32): prunable += bytes([l[i]]) prunable += to_var_int(len(bulletproof.r)) for r in bulletproof.r: for i in range(32): prunable += bytes([r[i]]) for i in range(32): prunable += bytes([bulletproof.a[i]]) for i in range(32): prunable += bytes([bulletproof.b[i]]) for i in range(32): prunable += bytes([bulletproof.t[i]]) for cl in self.signatures.prunable.CLSAGs: for s in cl.s: for i in range(32): prunable += bytes([s[i]]) for i in range(32): prunable += bytes([cl.c1[i]]) for i in range(32): prunable += bytes([cl.D[i]]) for pseudo_out in self.signatures.prunable.pseudo_outs: for i in range(32): prunable += bytes([pseudo_out[i]]) return ( ed.H(ed.H(prefix) + ed.H(base) + ed.H(prunable)), prefix + base + prunable, )
def varint_serializations_test() -> None: for _ in range(500): num: int = randint(0, 2**16) assert from_var_int(to_var_int(num), 0)[0] == num assert from_rpc_var_int(to_rpc_var_int(num), 0)[0] == num