Compare commits

...

3 Commits

Author SHA1 Message Date
Daira Emma Hopwood 828c766baf
Merge 4ad740533a into e726693d5a 2023-10-21 18:59:14 +00:00
Daira Emma Hopwood 4ad740533a Replace bc-demo with a call to the test, to avoid duplicating code.
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-21 19:59:04 +01:00
Daira Emma Hopwood 4af1b6a02b Implement abstractions for a shielded protocol.
Signed-off-by: Daira Emma Hopwood <daira@jacaranda.org>
2023-10-21 19:53:42 +01:00
4 changed files with 219 additions and 67 deletions

View File

@ -1,13 +1,34 @@
"""
Abstractions for best-chain transactions, contexts, and blocks.
The transaction protocol is assumed to have a Bitcoin-like TXO-based
transparent component, and a Zcash-like note-based shielded component.
A block consists of a coinbase transaction which can issue new funds,
and zero or more non-coinbase transactions which cannot issue funds.
Each block chains to its parent block.
A transaction pays a transparent fee. Each block's coinbase transaction
collects the fees paid by the other transactions in the block.
The simulation of the shielded protocol does not attempt to model any
actual privacy properties.
"""
from collections import deque
from dataclasses import dataclass
from enum import Enum, auto
from itertools import chain
from sys import version_info
from ..util import Unique
class BlockHash(Unique):
"""Unique value representing a best-chain block hash."""
pass
class BCTransaction:
"""A transaction for a best-chain protocol."""
@ -17,37 +38,96 @@ class BCTransaction:
index: int
value: int
def __init__(self, inputs, output_values, fee, issuance=0):
@dataclass(eq=False)
class _Note(Unique):
"""
Constructs a `BCTransaction` with the given inputs, output values, fee,
and (if it is a coinbase transaction) issuance.
The elements of `inputs` are TXO objects obtained from the `output` method
of another `BCTransaction`.
For a coinbase transaction, pass `inputs=[]`, and `fee` as a negative value
of magnitude equal to the total amount of fees paid by other transactions
in the block.
A shielded note. Unlike in the actual protocol, we conflate notes, note
commitments, and nullifiers. This will be sufficient because we don't
need to maintain any actual privacy.
This is not a frozen dataclass; its identity is important, and models the
fact that each note has a unique commitment and nullifier in the actual
protocol.
"""
value: int
def __init__(self, transparent_inputs, transparent_output_values, shielded_inputs,
shielded_output_values, fee, anchor=None, issuance=0):
"""
Constructs a `BCTransaction` with the given transparent inputs, transparent
output values, anchor, shielded inputs, shielded output values, fee, and
(if it is a coinbase transaction) issuance.
The elements of `transparent_inputs` are TXO objects obtained from the
`transparent_output` method of another `BCTransaction`. The elements of
`shielded_inputs` are Note objects obtained from the `shielded_output`
method of another `BCTransaction`. The TXO and Note classes are private,
and these objects should not be constructed directly.
The anchor is modelled as a `BCContext` such that
`anchor.can_spend(shielded_inputs)`. If there are no shielded inputs,
`anchor` must be `None`. The anchor object must not be modified after
passing it to this constructor (copy it if necessary).
For a coinbase transaction, pass `[]` for `transparent_inputs` and
`shielded_inputs`, and pass `fee` as a negative value of magnitude equal
to the total amount of fees paid by other transactions in the block.
"""
assert issuance >= 0
assert fee >= 0 or len(inputs) == 0
assert issuance == 0 or len(inputs) == 0
assert all((isinstance(txin, self._TXO) for txin in inputs))
assert sum((txin.value for txin in inputs)) + issuance == sum(output_values) + fee
self.inputs = inputs
self.outputs = [self._TXO(self, i, v) for (i, v) in enumerate(output_values)]
coinbase = len(transparent_inputs) + len(shielded_inputs) == 0
assert fee >= 0 or coinbase
assert issuance == 0 or coinbase
assert all((v >= 0 for v in chain(transparent_output_values, shielded_output_values)))
assert all((isinstance(txin, self._TXO) for txin in transparent_inputs))
assert all((isinstance(note, self._Note) for note in shielded_inputs))
assert (
sum((txin.value for txin in transparent_inputs))
+ sum((note.value for note in shielded_inputs))
+ issuance ==
sum(transparent_output_values)
+ sum(shielded_output_values)
+ fee
)
assert anchor is None if len(shielded_inputs) == 0 else anchor.can_spend(shielded_inputs)
self.transparent_inputs = transparent_inputs
self.transparent_outputs = [self._TXO(self, i, v)
for (i, v) in enumerate(transparent_output_values)]
self.shielded_inputs = shielded_inputs
self.shielded_outputs = [self._Note(v) for v in shielded_output_values]
self.fee = fee
self.anchor = anchor
self.issuance = issuance
def input(self, index):
"""Returns the input with the given index."""
return self.inputs[index]
def transparent_input(self, index):
"""Returns the transparent input TXO with the given index."""
return self.transparent_inputs[index]
def output(self, index):
"""Returns the output with the given index."""
return self.outputs[index]
def transparent_output(self, index):
"""Returns the transparent output TXO with the given index."""
return self.transparent_outputs[index]
def shielded_input(self, index):
"""Returns the shielded input note with the given index."""
return self.shielded_inputs[index]
def shielded_output(self, index):
"""Returns the shielded output note with the given index."""
return self.shielded_outputs[index]
def is_coinbase(self):
"""Returns `True` if this is a coinbase transaction (it has no inputs)."""
return len(self.inputs) == 0
"""
Returns `True` if this is a coinbase transaction (it has no inputs).
"""
return len(self.transparent_inputs) + len(self.shielded_inputs) == 0
class Spentness(Enum):
"""The spentness status of a note."""
Unspent = auto()
"""The note is unspent."""
Spent = auto()
"""The note is spent."""
class BCContext:
@ -56,39 +136,82 @@ class BCContext:
best-chain protocol.
"""
assert version_info >= (3, 7), "This code relies on insertion-ordered dicts."
def __init__(self):
"""Constructs an empty `BCContext`."""
self.transactions = deque()
self.utxo_set = set()
self.transactions = deque() # of BCTransaction
self.utxo_set = set() # of BCTransaction._TXO
# Since dicts are insertion-ordered, this models the sequence in which
# notes are committed as well as their spentness.
self.notes = {} # Note -> Spent | Unspent
self.total_issuance = 0
def committed_notes(self):
"""
Returns a list of (`Note`, `Spentness`) for notes added to this context,
preserving the commitment order.
"""
return list(self.notes.items())
def can_spend(self, tospend):
"""Can all of the notes in `tospend` be spent in this context?"""
return all((self.notes.get(note) == Spentness.Unspent for note in tospend))
def _check(self, tx):
"""
Checks whether `tx` is valid. To avoid recomputation, this returns
a pair of the validity, and the set of transparent inputs of `tx`.
"""
txins = set(tx.transparent_inputs)
valid = txins.issubset(self.utxo_set) and self.can_spend(tx.shielded_inputs)
return (valid, txins)
def is_valid(self, tx):
"""Is `tx` valid in this context?"""
return set(tx.inputs).issubset(self.utxo_set)
return self._check(tx)[0]
def add_if_valid(self, tx):
"""
If `tx` is valid in this context, add it to the context and return `True`.
Otherwise leave the context unchanged and return `False`.
"""
txins = set(tx.inputs)
valid = txins.issubset(self.utxo_set)
(valid, txins) = self._check(tx)
if valid:
self.utxo_set -= txins
self.utxo_set |= set(tx.outputs)
self.utxo_set |= set(tx.transparent_outputs)
for note in tx.shielded_inputs:
self.notes[note] = Spentness.Spent
for note in tx.shielded_outputs:
assert note not in self.notes
self.notes[note] = Spentness.Unspent
self.total_issuance += tx.issuance
self.transactions.append(tx)
return valid
def copy(self):
"""Returns an independent copy of this `BCContext`."""
ctx = BCContext()
ctx.transactions = self.transactions.copy()
ctx.utxo_set = self.utxo_set.copy()
ctx.notes = self.notes.copy()
ctx.total_issuance = self.total_issuance
return ctx
class BCBlock:
"""A block in a best-chain protocol."""
def __init__(self, parent, added_score, transactions, allow_invalid=False):
"""
Constructs a `BCBlock` with the given parent block, score relative to the parent,
and transactions.
Constructs a `BCBlock` with the given parent block, score relative to the
parent, and sequence of transactions. `transactions` must not be modified
after passing it to this constructor (copy it if necessary).
If `allow_invalid` is set, the block need not be valid.
Use `parent=None` to construct the genesis block.
"""
@ -96,19 +219,24 @@ class BCBlock:
self.parent = parent
self.score = (0 if parent is None else self.parent.score) + added_score
self.transactions = transactions
self.hash = Unique()
assert allow_invalid or self.is_noncontextually_valid()
self.hash = BlockHash()
if not allow_invalid:
self.assert_noncontextually_valid()
def assert_noncontextually_valid(self):
"""Assert that non-contextual consensus rules are satisfied for this block."""
assert len(self.transactions) > 0
assert self.transactions[0].is_coinbase()
assert not any((tx.is_coinbase() for tx in self.transactions[1:]))
assert sum((tx.fee for tx in self.transactions)) == 0
def is_noncontextually_valid(self):
"""
Are non-contextual consensus rules satisfied for this block?
"""
return (
len(self.transactions) > 0 and
self.transactions[0].is_coinbase() and
not any((tx.is_coinbase() for tx in self.transactions[1:])) and
sum((tx.fee for tx in self.transactions)) == 0
)
"""Are non-contextual consensus rules satisfied for this block?"""
try:
self.assert_noncontextually_valid()
return True
except AssertionError:
return False
@dataclass
@ -125,7 +253,7 @@ class BCProtocol:
"""The type of blocks for this protocol."""
__all__ = ['BCTransaction', 'BCContext', 'BCBlock', 'BCProtocol']
__all__ = ['BCTransaction', 'BCContext', 'BCBlock', 'BCProtocol', 'BlockHash', 'Spentness']
import unittest
@ -134,14 +262,38 @@ import unittest
class TestBC(unittest.TestCase):
def test_basic(self):
ctx = BCContext()
issuance_tx0 = BCTransaction([], [10], 0, issuance=10)
assert ctx.add_if_valid(issuance_tx0)
genesis = BCBlock(None, 1, [issuance_tx0])
assert genesis.score == 1
coinbase_tx0 = BCTransaction([], [10], [], [], 0, issuance=10)
self.assertTrue(ctx.add_if_valid(coinbase_tx0))
genesis = BCBlock(None, 1, [coinbase_tx0])
self.assertEqual(genesis.score, 1)
self.assertEqual(ctx.total_issuance, 10)
issuance_tx1 = BCTransaction([], [6], -1, issuance=5)
spend_tx = BCTransaction([issuance_tx0.output(0)], [9], 1)
assert ctx.add_if_valid(issuance_tx1)
assert ctx.add_if_valid(spend_tx)
block1 = BCBlock(genesis, 1, [issuance_tx1, spend_tx])
assert block1.score == 2
coinbase_tx1 = BCTransaction([], [6], [], [], -1, issuance=5)
spend_tx = BCTransaction([coinbase_tx0.transparent_output(0)], [9], [], [], 1)
self.assertTrue(ctx.add_if_valid(coinbase_tx1))
self.assertTrue(ctx.add_if_valid(spend_tx))
block1 = BCBlock(genesis, 1, [coinbase_tx1, spend_tx])
self.assertEqual(block1.score, 2)
self.assertEqual(ctx.total_issuance, 15)
coinbase_tx2 = BCTransaction([], [6], [], [], -1, issuance=5)
shielding_tx = BCTransaction([coinbase_tx1.transparent_output(0), spend_tx.transparent_output(0)],
[], [], [8, 6], 1)
self.assertTrue(ctx.add_if_valid(coinbase_tx2))
self.assertTrue(ctx.add_if_valid(shielding_tx))
block2 = BCBlock(block1, 2, [coinbase_tx2, shielding_tx])
block2_anchor = ctx.copy()
self.assertEqual(block2.score, 4)
self.assertEqual(ctx.total_issuance, 20)
coinbase_tx3 = BCTransaction([], [7], [], [], -2, issuance=5)
shielded_tx = BCTransaction([], [], [shielding_tx.shielded_output(0)], [7], 1,
anchor=block2_anchor)
deshielding_tx = BCTransaction([], [5], [shielding_tx.shielded_output(1)], [], 1,
anchor=block2_anchor)
self.assertTrue(ctx.add_if_valid(coinbase_tx3))
self.assertTrue(ctx.add_if_valid(shielded_tx))
self.assertTrue(ctx.add_if_valid(deshielding_tx))
block3 = BCBlock(block2, 3, [coinbase_tx3, shielded_tx, deshielding_tx])
self.assertEqual(block3.score, 7)
self.assertEqual(ctx.total_issuance, 25)

View File

@ -1,18 +1,8 @@
from . import BCBlock, BCContext, BCTransaction
import unittest
def run():
"""
Runs the demo.
"""
ctx = BCContext()
issuance_tx0 = BCTransaction([], [10], 0, issuance=10)
assert ctx.add_if_valid(issuance_tx0)
genesis = BCBlock(None, 1, [issuance_tx0])
assert genesis.score == 1
issuance_tx1 = BCTransaction([], [6], -1, issuance=5)
spend_tx = BCTransaction([issuance_tx0.output(0)], [9], 1)
assert ctx.add_if_valid(issuance_tx1)
assert ctx.add_if_valid(spend_tx)
block1 = BCBlock(genesis, 1, [issuance_tx1, spend_tx])
assert block1.score == 2
unittest.main(module='simtfl.bc', verbosity=2)

View File

@ -162,6 +162,7 @@ class SenderTestNode(PassiveNode):
# is sent at time 3 and received at time 11.
yield from self.send(0, PayloadMessage(3), delay=8)
class TestFramework(unittest.TestCase):
def _test_node(self, receiver_node, expected):
network = Network(Environment())

View File

@ -12,5 +12,14 @@ def skip():
class Unique:
"""Represents a unique value."""
pass
"""
Represents a unique value.
Instances of this class are hashable. When subclassing as a dataclass, use
`@dataclass(eq=False)` to preserve hashability.
"""
def __eq__(self, other):
return self == other
def __hash__(self):
return id(self)