blockchainkit.network#
Peer-to-peer networks: topologies, logical time, dissemination, overlays, and block relay.
Peer graphs, logical clocks, gossip and reliable broadcast, replication under partitions, Kademlia and its attacks, block relay, forks, and transaction privacy.
Every public name below is re-exported by the subpackage: import it as
bk.network.<name>. The plotting helpers are the exception: import them
explicitly from blockchainkit.network.visualizers, which loads Matplotlib.
Types and results#
Event records and result containers for blockchainkit.network.
- class blockchainkit.network.core.base.Delivery(time, sender, recipient, payload)[source]#
Bases:
objectFirst receipt of a byte payload at a peer at integer simulation time.
- class blockchainkit.network.core.base.RumorRun(informed, n)[source]#
Bases:
objectHow a rumor spread, round by synchronous round.
- Variables:
- Parameters:
- class blockchainkit.network.core.base.BroadcastResult(delivered, agreement, totality, messages)[source]#
Bases:
objectOutcome of a Byzantine reliable-broadcast run.
- Variables:
- Parameters:
- class blockchainkit.network.core.base.Operation(kind, replica, value, ok)[source]#
Bases:
objectOne client request to a replicated register and its outcome.
- Variables:
- Parameters:
- class blockchainkit.network.core.base.LookupResult(path)[source]#
Bases:
objectRoute of a Kademlia lookup.
- class blockchainkit.network.core.base.RelayCost(messages, bytes, completion)[source]#
Bases:
objectTraffic and time to relay one block from one source to every reachable peer.
- Variables:
- Parameters:
- class blockchainkit.network.core.base.CompactBlockResult(full_bytes, compact_bytes, missing, round_trips)[source]#
Bases:
objectBytes needed to relay one block in full and as a compact block (BIP 152).
- Variables:
full_bytes (
int) – Header plus every transaction.compact_bytes (
int) – Header, nonce, short IDs, and the transactions the receiver lacked.missing (
int) – Transactions the receiver had to request: absent from its mempool, or ambiguous because their short ID collided.round_trips (
int) – 1 if the mempool covered the block, 2 if a request was needed.
- Parameters:
Constructions and protocols#
Deterministic discrete-event gossip; no sockets, wall clock, or background threads.
- class blockchainkit.network.systems.gossip.SimulatedNetwork(peers, *, seed=0, on_receive=None)[source]#
Bases:
objectAn undirected peer graph with seeded link delays and duplicate suppression.
- Parameters:
peers (
collections.abc.Iterableofstr) – Unique nonempty peer names. Initially no links exist.seed (
int) – Private PRNG seed; construction does not modify global randomness.on_receive (
collections.abc.Callable, optional) – Callback for each first delivery, including the originating peer. Return False to reject that payload at that peer and stop forwarding. Returning None or True accepts it. Rejections remain seen.
Notes
Disconnecting a link drops its in-flight messages. Reconnecting does not automatically synchronize old data: call
broadcastagain. Latency is sampled at send time, inclusive of both endpoints, and is at least one tick.- classmethod from_graph(graph, *, latency=(1, 1), seed=0, on_receive=None)[source]#
Build a network whose peers
"0","1", … are linked likegraph.>>> from blockchainkit.network import SimulatedNetwork, ring_lattice >>> SimulatedNetwork.from_graph(ring_lattice(8, 2)) SimulatedNetwork(peers=8, links=8, time=0, pending=0)
- property messages_sent: int#
Messages handed to links so far, including duplicates that receivers discard.
- connect(left, right, *, latency=(1, 1))[source]#
Create or replace a link with inclusive integer latency bounds.
- disconnect(left, right)[source]#
Remove a link, invalidating all in-flight events sent on that link.
- broadcast(sender, payload)[source]#
Originate or retransmit bytes to neighbors; receivers suppress duplicates.
Peer graphs: random (Erdős–Rényi 1959), small-world (Watts–Strogatz 1998), and scale-free (Barabási–Albert 1999).
Who is connected to whom decides how fast news spreads, how many links an attacker must cut, and how much a single peer can see. These generators build the three classic families so experiments can compare them on equal terms.
- class blockchainkit.network.systems.topology.Graph(n, edges)[source]#
Bases:
objectAn immutable undirected simple graph on the nodes
0, 1, ..., n - 1.- Parameters:
n (
int) – Number of nodes, at least 1.edges (
collections.abc.Iterableoftupleofint) – Unordered pairs of distinct nodes; duplicates are merged.
Examples
>>> from blockchainkit.network import Graph >>> path = Graph(3, [(0, 1), (1, 2)]) >>> path.distances(0) (0, 1, 2) >>> path.is_connected() True
- property edges: tuple[tuple[int, int], ...]#
Every edge once, as
(smaller, larger), in sorted order.
- blockchainkit.network.systems.topology.complete_graph(n)[source]#
Every pair of the
nnodes linked: the setting of classic rumor-spreading results.>>> from blockchainkit.network import complete_graph >>> len(complete_graph(5).edges) 10
- blockchainkit.network.systems.topology.erdos_renyi(n, p, *, seed=0)[source]#
Random graph G(n, p): each of the n(n-1)/2 possible links exists with probability p.
Erdős and Rényi showed that connectivity appears abruptly: for large n, G(n, p) is almost surely disconnected when
p < (1 - e) ln n / nand almost surely connected whenp > (1 + e) ln n / n.>>> from blockchainkit.network import erdos_renyi >>> erdos_renyi(30, 1.0).is_connected() True
- blockchainkit.network.systems.topology.ring_lattice(n, k)[source]#
Ring where each node links to its
knearest nodes,k / 2on each side.>>> from blockchainkit.network import ring_lattice >>> ring_lattice(6, 2).neighbors(0) (1, 5)
- blockchainkit.network.systems.topology.watts_strogatz(n, k, beta, *, seed=0)[source]#
Small-world graph: rewire each ring-lattice edge with probability
beta.Following Watts and Strogatz, each edge
(u, u + j)ofring_lattice()keeps its endpointuand, with probabilitybeta, moves its other end to a uniformly random node, avoiding self-loops and duplicate links. A few shortcuts (smallbeta) shrink the average path length almost to that of a random graph while the clustering stays close to the lattice’s.>>> from blockchainkit.network import watts_strogatz >>> len(watts_strogatz(20, 4, 0.3, seed=1).edges) 40
- blockchainkit.network.systems.topology.barabasi_albert(n, m, *, seed=0)[source]#
Scale-free graph by preferential attachment.
Start from
m + 1fully linked nodes. Each new node links tomdistinct existing nodes, chosen with probability proportional to their current degree (“the rich get richer”). The degree distribution approaches the power lawP(k) ~ k^-3: a few hubs, many small nodes.>>> from blockchainkit.network import barabasi_albert >>> g = barabasi_albert(50, 2, seed=3) >>> len(g.edges) == 3 + 2 * (50 - 3) True
Logical clocks: Lamport timestamps (1978) and vector clocks (Fidge, Mattern 1988).
Peers have no shared clock, so “which happened first?” has no physical answer. Lamport defined happened before from the messages themselves: an event precedes later events at the same process, a send precedes its receive, and the relation is transitive. Two events related neither way are concurrent.
A history is a sequence of processes, each a sequence of events. An event is
a pair (kind, label): ("local", name), ("send", message) or
("receive", message). Each message label is sent once and received at
most once.
- blockchainkit.network.systems.clocks.Event#
(kind, label)with kind"local","send"or"receive".
- blockchainkit.network.systems.clocks.lamport_timestamps(processes)[source]#
Assign Lamport clock values to every event.
Each process keeps a counter. It increments the counter before each event, attaches it to every message it sends, and on receipt jumps to
max(own, received) + 1. The result satisfies the clock condition: if a happened before b, thenC(a) < C(b). The converse does not hold.- Returns:
result[p][i]is the timestamp of eventiat processp.- Return type:
- Parameters:
Examples
>>> from blockchainkit.network import lamport_timestamps >>> lamport_timestamps([[("send", "m")], [("local", "x"), ("local", "y"), ("receive", "m")]]) ((1,), (1, 2, 3))
- blockchainkit.network.systems.clocks.vector_timestamps(processes)[source]#
Assign vector clock values to every event.
Each process
pkeeps one counter per process. It increments entrypbefore each event and, on receipt, first takes the entrywise maximum with the vector carried by the message. Entryqof an event’s vector counts the events atqthat happened before or at it, so vectors characterize causality exactly: seehappened_before().Examples
>>> from blockchainkit.network import vector_timestamps >>> vector_timestamps([[("send", "m")], [("local", "x"), ("receive", "m")]]) (((1, 0),), ((0, 1), (1, 2)))
- blockchainkit.network.systems.clocks.happened_before(a, b)[source]#
True if the event stamped
ahappened before the event stampedb.For vector timestamps,
a -> bexactly whena <= bentrywise anda != b.>>> from blockchainkit.network import happened_before >>> happened_before((1, 0), (1, 2)), happened_before((1, 0), (0, 1)) (True, False)
- blockchainkit.network.systems.clocks.concurrent(a, b)[source]#
True if neither vector-stamped event happened before the other (and they differ).
>>> from blockchainkit.network import concurrent >>> concurrent((1, 0), (0, 1)) True
Epidemic dissemination: push, pull and push-pull rumor spreading.
Each round, every peer calls one neighbor chosen uniformly at random. In
push, informed callers tell the callee; in pull, uninformed callers ask
the callee and learn the rumor if it knows it; push-pull does both. Demers
et al. (1987) compared these for replicated databases. On the complete graph,
push alone informs everyone in about log2 n + ln n rounds (Frieze and
Grimmett 1985, Pittel 1987).
- blockchainkit.network.systems.epidemics.MODES = ('push', 'pull', 'push-pull')#
The supported exchange modes.
- blockchainkit.network.systems.epidemics.pittel_rounds(n)[source]#
Rounds push gossip needs on the complete graph:
log2 n + ln n, up to O(1).The
log2 nterm is the doubling phase, while few peers know the rumor; theln nterm is the coupon-collector tail, while the last uninformed peers wait to be called.>>> from blockchainkit.network import pittel_rounds >>> round(pittel_rounds(1024), 2) 16.93
- blockchainkit.network.systems.epidemics.spread_rumor(network, *, mode='push', source=0, seed=0, max_rounds=10000)[source]#
Simulate synchronous random-call rumor spreading.
- Parameters:
network (
blockchainkit.network.systems.topology.Graphorint) – The peer graph, or an integernfor the complete graph onnpeers (simulated without building its n(n-1)/2 edges).mode (
{"push", "pull", "push-pull"}) – Who learns from each call; see the module docstring.source (
int) – The peer that knows the rumor at round 0.seed (
int) – Seed for the random calls.max_rounds (
int) – Stop after this many rounds even if peers remain uninformed.
- Returns:
Informed counts per round. The run also stops once every peer that the source can reach is informed.
- Return type:
Examples
>>> from blockchainkit.network import spread_rumor >>> run = spread_rumor(1000, seed=1) >>> run.complete, run.informed[:4] (True, (1, 2, 4, 8))
Byzantine reliable broadcast (Bracha 1987).
A sender wants every correct process to deliver the same value, even if the sender lies by telling different processes different things. Bracha’s protocol adds two all-to-all phases on top of the sender’s message:
Echo. On the sender’s value, a process tells everyone “I got v”.
Ready. On
ceil((n + t + 1) / 2)echoes of v, ort + 1readies of v (amplification), a process tells everyone “ready for v”, once.Deliver. On
2t + 1readies of v, a process delivers v.
Any two echo quorums overlap in a correct process, which echoes only once,
so correct processes never become ready for different values. Amplification
makes delivery all-or-nothing. Both arguments need n > 3t.
- blockchainkit.network.systems.broadcast.reliable_broadcast(n, faulty, proposals, *, tolerance=None)[source]#
Run Bracha’s broadcast in synchronous rounds from process 0.
- Parameters:
n (
int) – Number of processes, at least 2.faulty (
collections.abc.Iterableofint) – Byzantine processes. A faulty sender sendsproposals[r]to each processr. Faulty processes collude with that split: to each correct processrthey echo and readyproposals[r], the strongest support they can give to a disagreement.proposals (
collections.abc.Sequenceofstr) – One value per process; entries at faulty processes are ignored. If the sender is correct, all correct entries must equalproposals[0].tolerance (
int, optional) – Thetused in the thresholds; defaults to(n - 1) // 3, the most the protocol can tolerate. Running with more faulty processes thantoleranceshows the bound is needed.
- Return type:
Examples
>>> from blockchainkit.network import reliable_broadcast >>> result = reliable_broadcast(4, {0}, ["a", "a", "b", "b"]) >>> result.agreement, result.totality, sorted(set(result.delivered.values())) (True, True, ['b'])
The CAP trade-off (Brewer 2000; Gilbert and Lynch 2002) on a replicated register.
A register is copied on several replicas. When the network partitions them, a replica that cannot reach the others must choose: answer from what it knows (stay available, risking stale reads and lost writes) or refuse (stay consistent). Gilbert and Lynch proved no system can guarantee both during a partition. A blockchain makes the same choice: Bitcoin keeps producing blocks on both sides of a split and reconciles later.
- blockchainkit.network.systems.replication.MODES = ('consistent', 'available')#
The two policies a replica can follow during a partition.
- class blockchainkit.network.systems.replication.ReplicatedRegister(replicas, *, mode='consistent', initial='')[source]#
Bases:
objectA string register replicated on
replicasnodes that can be partitioned.- Parameters:
replicas (
int) – Number of replicas, at least 1; they are numbered from 0.mode (
{"consistent", "available"}) –"consistent": a request succeeds only if the contacted replica reaches a strict majority, and it then writes to or reads from every reachable replica. Any two majorities overlap, so a read sees the latest successful write."available": every request succeeds using the replicas the contacted one can reach.initial (
str) – The value every replica starts with.
Notes
Every value carries a version
(counter, replica); a write’s counter is one more than the highest it can see.heal()reconnects all replicas and keeps the highest version (last writer wins), so in available mode a write made on the losing side of a partition is lost.Examples
>>> from blockchainkit.network import ReplicatedRegister >>> register = ReplicatedRegister(3, mode="consistent") >>> register.partition([0], [1, 2]) >>> register.write(0, "x").ok, register.write(1, "y").ok (False, True)
- partition(*groups)[source]#
Split the replicas into groups that can talk only within themselves.
The groups must cover every replica exactly once.
Kademlia routing by XOR distance (Maymounkov and Mazières 2002).
Every node has a bits-bit identifier, and the distance between two
identifiers is their bitwise XOR read as an integer. A node sorts the nodes
it knows into k-buckets: bucket i holds up to k nodes whose
distance lies in [2**i, 2**(i+1)), the nodes whose identifier first
differs from its own at bit i. Knowing a few nodes at every scale lets a
lookup halve the remaining distance at each hop, so it takes about
log2 n hops among n nodes.
- blockchainkit.network.systems.kademlia.xor_distance(a, b)[source]#
Kademlia’s distance:
a XOR b. It is symmetric and zero only fora == b.>>> from blockchainkit.network import xor_distance >>> xor_distance(0b1010, 0b0110) 12
- blockchainkit.network.systems.kademlia.node_id(key, bits)[source]#
Derive a
bits-bit identifier from a public key: the top bits of its SHA-256.Assigning identifiers by hash means a node cannot simply pick a position in the identifier space; it has to search for keys. See the Sybil experiment.
>>> from blockchainkit.network import node_id >>> node_id(b"alice", 8) < 2**8 True
- class blockchainkit.network.systems.kademlia.KademliaNetwork(ids, *, bits, k=8, seed=0)[source]#
Bases:
objectNodes with idealized k-bucket routing tables.
- Parameters:
ids (
collections.abc.Iterableofint) – Distinct node identifiers in[0, 2**bits).bits (
int) – Identifier length.k (
int) – Bucket capacity. Each bucket is filled with up toknodes drawn uniformly from all nodes at that distance scale.seed (
int) – Seed for filling the buckets.
Notes
Real Kademlia nodes fill their buckets gradually from the traffic they see and prefer long-lived contacts. Here every table is filled at once from global knowledge, which isolates the routing geometry.
Examples
>>> from blockchainkit.network import KademliaNetwork >>> net = KademliaNetwork([0b000, 0b011, 0b100, 0b110], bits=3, k=1) >>> net.lookup(0b000, 0b111).path (0, 6)
- closest(target, count)[source]#
The
countnodes closest totargetby XOR distance (global truth).In Kademlia these nodes store the value with key
target.
- lookup(source, target)[source]#
Route greedily from
sourcetowardtarget.At each hop the current node forwards to the contact in its buckets closest to
target, while that is closer than itself. If the current node differs at bitifrom the true closest node, its bucketiis nonempty and every contact there is closer to the target, so the route always ends at the closest node.- Parameters:
- Return type:
Eclipse attacks on peer selection (Heilman, Kendler, Zohar and Goldberg 2015).
A node picks its outbound connections at random from a table of addresses it has heard of. An attacker who fills that table with its own addresses can make every connection land on an attacker: the node is eclipsed, and sees only the blocks and transactions the attacker chooses to show it.
Bitcoin’s defense, strengthened after the 2015 paper, is bucketing: an address goes to a bucket determined by a secret hash of its network group (its /16 IP prefix), and each group can reach only a few buckets. An attacker with many addresses but few groups can then fill only a small part of the table, however many addresses it sends.
- blockchainkit.network.systems.addresses.eclipse_probability(attacker_fraction, outbound)[source]#
Chance that all
outboundconnections land on attackers:f ** outbound.Each connection is modeled as an independent draw from a table in which a fraction
fof the entries belong to the attacker.>>> from blockchainkit.network import eclipse_probability >>> eclipse_probability(0.5, 8) 0.00390625
- class blockchainkit.network.systems.addresses.AddressManager(*, buckets=64, bucket_size=16, buckets_per_group=None, seed=0)[source]#
Bases:
objectA node’s table of known peer addresses, with optional group bucketing.
- Parameters:
buckets (
int) – Number of buckets.bucket_size (
int) – Capacity of each bucket. Adding to a full bucket evicts a random entry, so a flood of new addresses pushes old ones out.buckets_per_group (
int, optional) – If given, each network group can place addresses in only this many buckets (Bitcoin’s defense). If omitted, an address may land in any bucket.seed (
int) – Seeds both the node’s secret bucketing key and its random choices.
Examples
>>> from blockchainkit.network import AddressManager >>> table = AddressManager(buckets=4, bucket_size=2, seed=1) >>> table.add("10.0.0.1", "10.0") >>> table.addresses ('10.0.0.1',)
- bucket_of(address, group)[source]#
The bucket an address is stored in, derived from the node’s secret key.
Block relay: flooding, inv/getdata announcements (Bitcoin 2009), compact blocks (BIP 152, 2016).
Flooding sends the full block over every link. Bitcoin’s original protocol
instead announces the block’s hash with an inv message and sends the
block only to peers that ask with getdata, so each peer downloads it
once, at the price of a round trip per hop. Compact blocks go further: most
transactions are already in the receiver’s mempool, so the sender lists
short transaction IDs and the receiver rebuilds the block locally.
- blockchainkit.network.systems.relay.ANNOUNCE_SIZE = 61#
Bytes in an
invorgetdatafor one item: a 24-byte message header, a one-byte count, and a 36-byte inventory vector.- Type:
- blockchainkit.network.systems.relay.MODES = ('flood', 'announce')#
The relay strategies compared by
relay_cost().
- blockchainkit.network.systems.relay.relay_cost(graph, *, size, mode='flood', source=0)[source]#
Count the traffic to relay a
size-byte block fromsourceto its component.Every peer, on first receiving the block, passes it on to each neighbor except the one it came from, so
2E - (n - 1)messages cross the links of a connected graph with E edges and n peers."flood": each such message carries the whole block, and the block reaches a peer at distance d after d link latencies."announce": each such message is aninv; a peer that lacks the block answers its firstinvwith agetdataand receives the block. Every peer downloads the block once, but each hop costs three latencies.
Examples
>>> from blockchainkit.network import complete_graph, relay_cost >>> relay_cost(complete_graph(10), size=1_000_000).bytes 81000000
- blockchainkit.network.systems.relay.short_id(transaction, nonce, size=6)[source]#
A
size-byte transaction ID salted by the block’snonce.Salting with a per-block nonce stops an attacker from crafting transactions whose short IDs collide in every block. BIP 152 uses SipHash keyed by the header and nonce; this teaching version uses SHA-256.
>>> from blockchainkit.network import short_id >>> len(short_id(b"tx", nonce=7)) 6
- blockchainkit.network.systems.relay.compact_block_relay(block, mempool, *, nonce=0, short_id_size=6)[source]#
Compare sending
blockin full with sending it as a compact block.The compact form costs the header, an 8-byte nonce, and one short ID per transaction. The receiver matches short IDs against its mempool. A transaction with no match, or with an ambiguous or wrong match, must be requested in a second round trip: a request of
ANNOUNCE_SIZEbytes plus 2 bytes per index, then the transactions themselves.Examples
>>> from blockchainkit.network import compact_block_relay >>> block = [bytes([i]) * 250 for i in range(100)] >>> result = compact_block_relay(block, block[:90]) >>> result.missing, result.round_trips, result.full_bytes (10, 2, 25080)
Propagation delay and forks (Decker and Wattenhofer 2013).
A block takes time to reach the other miners. Until it arrives, they keep
mining on the old tip, and if one of them finds a block in that window the
chain forks. Block discovery is a Poisson process, so with propagation delay
tau and mean block interval T the chance that a block is followed by a
competitor within tau is 1 - exp(-tau / T). Shorter intervals or
slower relay mean more forks, wasted work, and an easier time for attackers.
- blockchainkit.network.systems.propagation.fork_rate(delay, interval)[source]#
Probability that a competing block appears before a block has propagated.
- Parameters:
- Return type:
Examples
>>> from blockchainkit.network import fork_rate >>> round(fork_rate(12.6, 600), 4) 0.0208
- blockchainkit.network.systems.propagation.simulate_fork_rate(delay, interval, *, blocks=10000, seed=0)[source]#
Measure the fork rate on a simulated chain of Poisson block discoveries.
Draws exponential gaps with mean
intervalbetween successive blocks and counts the blocks whose successor was found less thandelaylater, when its miner could not yet have seen them.>>> from blockchainkit.network import simulate_fork_rate >>> simulate_fork_rate(0.0, 600, blocks=100) 0.0
Transaction-origin privacy: diffusion versus Dandelion (Bojja Venkatakrishnan, Fanti and Viswanath 2017).
When a peer broadcasts its transaction, it is usually the first to send it.
Spy peers that connect widely record who first sent them each
transaction and guess that peer is the origin: the first-spy estimator.
Dandelion first passes the transaction along a random path (the stem),
each hop continuing with probability q, and only then diffuses it. The
spies then see the end of the stem, far from the origin.
- blockchainkit.network.systems.privacy.MODES = ('diffusion', 'dandelion')#
The broadcast strategies compared by
first_spy_precision().
- blockchainkit.network.systems.privacy.first_spy_precision(graph, spies, *, mode='diffusion', trials=200, stem_probability=0.9, seed=0)[source]#
Fraction of broadcasts whose origin the first-spy estimator identifies.
Each trial picks an honest origin uniformly at random.
"diffusion": the origin sends to all neighbors, and each peer forwards on first receipt, with independent Exp(1) delay per message."dandelion": the transaction first walks to a random neighbor; after each hop it continues the stem with probabilitystem_probabilityand otherwise diffuses from where it is. A spy on the stem guesses the peer that handed it the transaction.
The original Dandelion routes stems over a dedicated line-shaped anonymity graph; here the stem is a random walk on
graphitself.Examples
>>> from blockchainkit.network import complete_graph, first_spy_precision >>> first_spy_precision(complete_graph(2), {1}, trials=10) 1.0
Plotting#
Plotting helpers for blockchainkit.network: graphs, space-time diagrams, and spreading.
- blockchainkit.network.visualizers.plots.plot_gossip_timeline(deliveries, *, payload=None, ax=None)[source]#
Show when each peer first accepted a payload, earliest at the top.
- Parameters:
deliveries (
collections.abc.Sequenceofblockchainkit.network.core.base.Delivery) – UsuallySimulatedNetwork.deliveries.payload (
bytes, optional) – Payload to plot; required when deliveries contain several payloads.ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type:
- blockchainkit.network.visualizers.plots.plot_graph(graph, *, highlight=(), ax=None)[source]#
Draw a peer graph with its nodes on a circle.
A circle shows the structure of ring lattices and small worlds directly: lattice links hug the rim and rewired shortcuts cut across it.
- Parameters:
graph (
blockchainkit.network.systems.topology.Graph) – The graph to draw.highlight (
collections.abc.Iterableofint) – Nodes to draw in a contrasting color, such as spies or hubs.ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type:
- blockchainkit.network.visualizers.plots.plot_rumor_spread(runs, *, ax=None)[source]#
Plot the fraction of peers still uninformed after each round, on a log scale.
On a log scale, push’s slow final phase shows as a straight line (the residue shrinks by a constant factor per round), while pull’s accelerating fall shows the residue squaring each round.
- Parameters:
runs (
collections.abc.Mappingofstrtoblockchainkit.network.core.base.RumorRun) – Runs to compare, keyed by their legend label.ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type:
- blockchainkit.network.visualizers.plots.plot_space_time(processes, *, ax=None)[source]#
Draw Lamport’s space-time diagram, labeling each event with its Lamport clock.
Each process is a horizontal line, time runs left to right, and each message is an arrow from its send to its receive. An event is placed at its Lamport timestamp, so every arrow points forward in time.
- Parameters:
processes (
collections.abc.Sequenceofcollections.abc.SequenceofEvent) – A history as accepted bylamport_timestamps().ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type: