Ethernet · Module 25
"MAC Addresses Are Globally Unique Forever"
The birthday arithmetic everybody quotes uses 46 free bits; the generator in every hypervisor uses 24 — a factor of 1 335 907 — and a table indexed by the address cannot record the collision.
Chapter 5.3 §4 already did the birthday arithmetic and got a reassuring answer. This chapter shows that the arithmetic was done on the wrong space, and that the factor between the two is over a million.
The myth has three words in it and each is a separate error.
| Word | What it asserts | Section |
|---|---|---|
| "globally" | uniqueness across all deployments | 2 — the space is not 2⁴⁶; it is usually 2²⁴ |
| "unique" | a property of the value | 6 — the birthday bound assumes independence and manufacturing correlates |
| "forever" | once assigned, never reused | 8 — a virtual interface's address is reaped and reissued in seconds |
And the fourth error is not in the sentence at all: it is in what happens when the claim fails.
A duplicate address cannot be represented in a MAC table, because the table is indexed by the address. It appears as a rewrite, and a rewrite is the structure's specified behaviour.
That is Section 20's rejected property and it is the chapter's central result.
1. Scope — Three Words, Three Separate Errors
This chapter owns four derivations and a detector priced at real scale.
| What is derived | |
|---|---|
| Sections 2 to 5 | the implemented address space and the birthday bound on it — 94.92% at ten thousand virtual interfaces |
| Sections 6 to 7 | why the birthday bound is the optimistic case |
| Sections 8 to 9 | what "forever" means when addresses are recycled every 3.6 seconds |
| Sections 10 to 13 | the detection a switch would need, at 128k entries, and where the evidence goes without it |
What this chapter does not repeat. Chapter 5.3 established the address layout, the I/G and U/L flags, the locally administered mechanism and a flap-rate duplicate detector over sixteen entries — and its rejected property was the assertion that treats a single port change as the fault. This chapter starts where that one stopped: at data-centre scale, with a real table, and with the question of what the table can represent.
2. The Space Is Not 2⁴⁶
Take 48 bits and watch a real implementation remove them.
| Step | Why | Bits left | Values |
|---|---|---|---|
| start | a 48-bit address | 48 | 2.81 × 10¹⁴ |
| I/G = 0, U/L = 1 | a unicast, locally administered address | 46 | 7.04 × 10¹³ |
| first octet fixed at 0x02 | the conventional LAA first octet, hard-coded in most generators | 40 | 1.10 × 10¹² |
| a 24-bit vendor or product prefix | so addresses are recognisable as this product's | 24 | 1.68 × 10⁷ |
| an 8-bit field encoding a host or slot | operators like structure — Chapter 5.3 §4's third case | 16 | 65 536 |
Rows three and four are what nearly every hypervisor and container runtime actually does, and they are not mistakes: a recognisable prefix makes traffic attributable, which is worth having. Row five is a deliberate choice some operators make and it is worth having too.
But each row is a factor of 2⁶ or 2¹⁶ removed from the space the arithmetic assumed.
Between the space people compute over and the space their software draws from there are 22 bits — a factor of four million — and the birthday bound is quadratic in
nagainst a linearN, so the effect on the probability is dramatic.
Three deployments, one generator, three answers.
| Deployment | Addresses | P(collision), 2⁴⁶ | P(collision), 2²⁴ |
|---|---|---|---|
| a rack of 256 virtual interfaces | 256 | 4.66 × 10⁻¹⁰ | 0.195% |
| a row, 1 024 | 1 024 | 7.45 × 10⁻⁹ | 3.08% |
| a hall, 10 000 | 10 000 | 7.11 × 10⁻⁷ | 94.92% |
| a fleet, 10⁶ | 1 000 000 | 7.08 × 10⁻³ | essentially 1 |
Row three is the chapter's headline. The same deployment is one-in-1.4-million safe under the arithmetic people quote and better-than-even under the arithmetic their hypervisor implements — and the ratio between the two probabilities is 1 335 907.
And the crossover is worth knowing as a rule of thumb.
50% collision probability at N = 2^24 occurs at n ≈ 4 823
50% collision probability at N = 2^46 occurs at n ≈ 9 876 831Five thousand virtual interfaces, which is a medium-sized cluster, against ten million, which is a national-scale fleet.
3. RTL 1 — The Uniqueness Package and the Address-Space Model
// ---------------------------------------------------------------------
// uniqmyth_pkg -- the address-space and collision arithmetic, with the
// free-bit count DERIVED from what a generator fixes rather than taken
// from the field width.
//
// Unit: Chapter 23.3 Section 2's bitcell equivalent.
// 1 BCE = one bit of usable on-die SRAM = 0.35 GE
// 1 flip-flop = 20 BCE
// Chapter 19.7 Section 19's MAC receive datapath = 283 320 BCE
// Chapter 23.3's MAC table = 128k x 96 b = 1.26e7 BCE
// ---------------------------------------------------------------------
package uniqmyth_pkg;
localparam int unsigned DATAPATH_BCE = 283_320;
localparam int unsigned BCE_PER_FLOP = 20;
localparam int unsigned MAC_TBL_ENTRIES = 128 * 1024;
localparam int unsigned MAC_TBL_WIDTH = 96;
localparam int unsigned SWITCH_BCE_E4 = 56_200; // 5.62e8 / 1e4
localparam int unsigned ADDR_BITS = 48;
localparam int unsigned BIT_IG = 0; // Chapter 5.3 Section 2
localparam int unsigned BIT_UL = 1;
// Chapter 12.5's default aging interval.
localparam int unsigned AGING_SECONDS = 300;
typedef enum logic [1:0] {
GEN_UNIFORM_46 = 2'd0, // the arithmetic everybody quotes
GEN_FIXED_OCTET = 2'd1, // first octet pinned to 0x02
GEN_VENDOR_PFX = 2'd2, // plus a 24-bit product prefix
GEN_STRUCTURED = 2'd3 // plus an 8-bit host or slot field
} generator_e;
// ---- derived: how many bits a generator actually leaves free -------
function automatic int unsigned free_bits(generator_e g);
case (g)
GEN_UNIFORM_46: return 46; // 48 less I/G and U/L
GEN_FIXED_OCTET: return 40; // less the whole first octet
GEN_VENDOR_PFX: return 24; // less a 24-bit prefix
default: return 16; // less an 8-bit structured field
endcase
endfunction
// ---- derived: the birthday bound -----------------------------------
// P ~ 1 - exp(-n^2 / 2N). Integer hardware cannot evaluate exp, so
// the model reports the EXPONENT's argument scaled, and a comparison
// against thresholds gives the regime. The exact figure belongs in a
// report, not in a datapath.
//
// ratio_milli = 1000 * n^2 / (2 * 2^free_bits)
//
// ratio_milli >= 1386 corresponds to P >= 0.75
// ratio_milli >= 693 corresponds to P >= 0.50
// ratio_milli >= 105 corresponds to P >= 0.10
function automatic int unsigned collision_ratio_milli(int unsigned n,
int unsigned bits);
// n^2 can be large; shift the denominator instead of squaring up.
int unsigned num;
num = (n * n) / 1000; // keep the product in range
if (bits >= 40) return 0; // vanishing at this scale
return (num * 1_000_000) >> (bits - 1);
endfunction
function automatic int unsigned half_point(int unsigned bits);
// n at which P ~ 0.5 is sqrt(2 ln2 * N) ~ 1.1774 * 2^(bits/2).
return (11_774 * (1 << (bits / 2))) / 10_000;
endfunction
// ---- derived: detector cost -----------------------------------------
function automatic int unsigned detector_bce(int unsigned bits_per_entry);
return MAC_TBL_ENTRIES * bits_per_entry; // an array: 1 BCE/bit
endfunction
function automatic int unsigned table_bce();
return MAC_TBL_ENTRIES * MAC_TBL_WIDTH;
endfunction
function automatic int unsigned datapaths_milli(int unsigned bce);
return (bce * 1000) / DATAPATH_BCE;
endfunction
endpackage// ---------------------------------------------------------------------
// address_space_model -- the space a generator draws from, against the
// space the arithmetic assumes.
//
// The output that matters is bits_lost_to_structure. Every one of them
// is a deliberate, defensible design decision, and together they move
// the collision probability by six orders of magnitude.
// ---------------------------------------------------------------------
module address_space_model
import uniqmyth_pkg::*;
(
input logic clk,
input logic rst_n,
input logic [1:0] generator, // generator_e
input logic [31:0] population,
output logic [15:0] free_bits_o,
output logic [15:0] bits_lost_to_structure,
output logic [31:0] quoted_free_bits,
output logic [31:0] half_point_n,
output logic [31:0] ratio_milli,
output logic regime_negligible,
output logic regime_material,
output logic regime_likely,
output logic arithmetic_is_on_the_wrong_space,
output logic [31:0] c_evaluations
);
always_comb begin
free_bits_o = 16'(free_bits(generator_e'(generator)));
quoted_free_bits = 32'd46;
bits_lost_to_structure = 16'(quoted_free_bits) - free_bits_o;
half_point_n = 32'(half_point(int'(free_bits_o)));
ratio_milli = 32'(collision_ratio_milli(int'(population),
int'(free_bits_o)));
regime_negligible = (ratio_milli < 32'd105);
regime_material = (ratio_milli >= 32'd105) && (ratio_milli < 32'd693);
regime_likely = (ratio_milli >= 32'd693);
// THE finding, as one bit.
arithmetic_is_on_the_wrong_space = (bits_lost_to_structure != 16'd0);
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) c_evaluations <= '0;
else c_evaluations <= c_evaluations + 32'd1;
end
endmoduleClassification: an exponent-free birthday model, which reports a regime rather than a probability because integer hardware cannot evaluate exp.
What it teaches: that bits_lost_to_structure is the whole argument and it is between 6 and 30. A generator that pins the first octet loses 6; one that adds a vendor prefix loses 22; one that also encodes a host loses 30 — and every one of those decisions is defensible on its own terms. The model exists so the decisions can be added up.
And it teaches that half_point_n is the number to carry. At 24 free bits it is 4 823 and at 46 it is 9 876 831: a medium cluster against a national fleet. A deployment that knows its population and its generator can read the answer off in one comparison.
Deliberately simplified: the model reports a regime rather than a probability because 1 − exp(−n²/2N) needs a floating-point exponential — the exact figure belongs in a report and the regime belongs in hardware, which is why collision_ratio_milli returns a scaled exponent argument and the thresholds are named in the comment. free_bits is a four-way case where a real generator's structure is arbitrary, and a production model would take the fixed-bit mask directly. And the model assumes uniform random draws over the free bits, which Section 6 establishes is the optimistic case.
Production implication: the fixed-bit mask is what a design should actually report, and almost nothing does. A hypervisor that exposed the mask it applies — FF:FF:FF:00:00:00 for a 24-bit prefix — would let an operator compute the real space in one step, and the operator could then compare it against the fleet size. Without it the calculation requires reverse-engineering the generator from a sample of the addresses it produced, which is possible and which nobody does. Six octets of read-only register, and it converts an unauditable claim into an arithmetic one.
4. The Birthday Bound, Done on the Implemented Space
Section 2 gave three deployments and two answers each. This section works the full curve, because the shape matters more than any single point.
P(at least one collision) ≈ 1 − exp(−n² ÷ 2N)
| Addresses | 2⁴⁶ — the quoted space | 2²⁴ — a vendor-prefixed generator | 2¹⁶ — a structured one |
|---|---|---|---|
| 256 | 4.66 × 10⁻¹⁰ | 0.195% | 39.3% |
| 1 024 | 7.45 × 10⁻⁹ | 3.08% | 99.97% |
| 4 096 | 1.19 × 10⁻⁷ | 39.35% | ≈ 1 |
| 10 000 | 7.11 × 10⁻⁷ | 94.92% | ≈ 1 |
| 65 536 | 3.05 × 10⁻⁵ | ≈ 1 | ≈ 1 |
| 1 000 000 | 7.08 × 10⁻³ | ≈ 1 | ≈ 1 |
Three things about the shape are worth saying separately.
One — the curve is quadratic in n, so scale hurts fast. From 1 024 to 4 096 addresses — a factor of four — the 2²⁴ probability goes from 3.08% to 39.35%, a factor of nearly thirteen. A deployment that was comfortable at one rack size is not at four.
Two — the 2¹⁶ column is not a hypothetical. A generator that encodes a rack in one octet and a slot in another has sixteen free bits, and at 256 addresses it is already at 39.3%. That is one rack.
Three — the quoted column never reaches a per cent. Which is why the myth survives: somebody did the arithmetic once, on the field width, and the answer was reassuring and stayed quoted.
The factor between the two arithmetics at ten thousand addresses is 1 335 907, and neither calculation is wrong — they are calculations about different spaces, and only one of them is the space the software uses.
5. RTL 2 — The Collision Model
// ---------------------------------------------------------------------
// collision_model -- the birthday bound over the IMPLEMENTED space,
// reported as a regime and a headroom rather than as a probability.
//
// The output that matters is population_headroom: how many more
// addresses a deployment can add before it crosses the half point.
// ---------------------------------------------------------------------
module collision_model
import uniqmyth_pkg::*;
(
input logic clk,
input logic rst_n,
input logic [15:0] free_bits_i,
input logic [31:0] population,
input logic [31:0] population_growth_per_year,
output logic [31:0] half_point_o,
output logic [31:0] population_headroom,
output logic [15:0] years_of_headroom,
output logic [31:0] ratio_milli_o,
output logic [1:0] regime, // 0 negligible, 1 material, 2 likely
output logic past_half_point,
output logic allocation_recommended,
output logic [31:0] c_regime_changes
);
logic [1:0] regime_q;
always_comb begin
half_point_o = 32'(half_point(int'(free_bits_i)));
ratio_milli_o = 32'(collision_ratio_milli(int'(population),
int'(free_bits_i)));
past_half_point = (population >= half_point_o);
population_headroom = past_half_point ? 32'd0
: (half_point_o - population);
years_of_headroom = (population_growth_per_year == 0) ? 16'hFFFF
: 16'(population_headroom / population_growth_per_year);
regime = (ratio_milli_o >= 32'd693) ? 2'd2
: (ratio_milli_o >= 32'd105) ? 2'd1
: 2'd0;
// Section 2's callout: past the material regime, randomness is the
// wrong mechanism and allocation is the right one.
allocation_recommended = (regime != 2'd0);
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
regime_q <= 2'd0; c_regime_changes <= '0;
end else begin
regime_q <= regime;
if (regime != regime_q) c_regime_changes <= c_regime_changes + 32'd1;
end
end
endmoduleClassification: a birthday model whose useful output is a headroom in addresses and in years rather than a probability.
What it teaches: that years_of_headroom is the form of the answer a deployment can act on. A fleet of 2 000 virtual interfaces on a 24-bit generator has 2 823 addresses of headroom to the half point, and at 500 new interfaces a year that is five years — which is a planning horizon rather than a probability somebody argues about.
And it teaches that allocation_recommended turns on at the material regime rather than at the likely one. Waiting for the half point is waiting until a collision is more likely than not; the moment to switch from randomness to allocation is when the probability first becomes material, because the change takes time to deploy and the population keeps growing during it.
Deliberately simplified: the model assumes a single address space where a real deployment has several — different hypervisors, different prefixes, different tenants — and a collision is only harmful between addresses that share a broadcast domain. That cuts in the deployment's favour and the model does not take the credit, deliberately, because the mapping from address space to broadcast domain changes whenever somebody reconfigures a VLAN. And population_growth_per_year is a linear extrapolation of something that is usually stepwise.
Production implication: the several-spaces simplification is the one that turns a safe deployment into an unsafe one without anybody changing a generator. Two clusters, each with 4 000 virtual interfaces on its own 24-bit prefix, are each at 39.35% — uncomfortable but survivable. Merge their networks into one broadcast domain and the population is 8 000 in each prefix separately, which is unchanged — unless the two clusters share a prefix, in which case it is 8 000 in one space and the probability is 82.9%. The dangerous operation is therefore a network merge between deployments that use the same product, which is the common case, and nothing in either cluster's configuration changes. The check costs one query — are these two address sets drawn from the same prefix — and it is never run.
6. Why the Birthday Bound Is the Optimistic Case
Sections 4 and 5 assumed uniform random draws. Two things about real address assignment break that assumption, and both break it in the same direction.
The first is that the draws are not independent.
The birthday bound's whole derivation assumes each address is chosen without reference to the others. A manufacturing process does not work that way: addresses are programmed from a sequence, by a tool, in a lot, and the failures are failures of the tool rather than of chance.
| Failure | How many devices it affects | How the birthday bound models it |
|---|---|---|
| a counter that did not increment | an entire lot | not at all |
| a golden image flashed unchanged | every device from that image | not at all |
| a test fixture's default address shipped | every device that skipped a step | not at all |
| two lots programmed from the same range | the intersection | not at all |
And the second is that the affected devices are co-located.
Devices ship in lots to customers. A lot of a thousand servers goes into a few racks of one data centre — so a duplicate arising from a programming failure is overwhelmingly likely to be on the same broadcast domain as its twin, which is the only place a duplicate does harm.
The birthday bound assumes independent draws scattered across the world. Manufacturing produces correlated duplicates delivered to the same room, which is the worst possible arrangement of the same number of collisions.
Put a rate on it. Suppose a programming failure rate of r per device.
| Rate | 10 000 devices | 100 000 | 1 000 000 |
|---|---|---|---|
| 10⁻⁷ | 0.10% | 1.00% | 9.52% |
| 10⁻⁶ | 1.00% | 9.52% | 63.21% |
| 10⁻⁵ | 9.52% | 63.21% | ≈ 1 |
P(at least one) = 1 − exp(−rN), which is LINEAR in N where the birthday term is QUADRATIC — so the process term dominates at small populations and the birthday term takes over as the population grows. The crossover is n = 2rN, and it depends entirely on which space is in use.
| Space | r = 10⁻⁷ | 10⁻⁶ | 10⁻⁵ |
|---|---|---|---|
| 2¹⁶ — structured | immediately | immediately | immediately |
| 2²⁴ — vendor-prefixed | 3 devices | 34 | 336 |
| 2⁴⁶ — the quoted space | 1.41 × 10⁷ | 1.41 × 10⁸ | 1.41 × 10⁹ |
Row three is why the myth was ever defensible and row two is why it is not. On the quoted space the process failure dominates up to a hundred million devices, so somebody reasoning about 2⁴⁶ correctly concludes that chance is not the problem and manufacturing is. On a 24-bit space the birthday bound takes over at thirty-four devices — one rack — and both mechanisms are then in play at once.
Which gives the diagnostic rule that matters.
| Observation | Likelier cause |
|---|---|
| a duplicate between two devices from the same lot | a programming failure |
| a duplicate between two unrelated devices | the birthday bound, on a small space |
| a duplicate between two virtual interfaces | a generator with too few free bits |
| a duplicate that appears after a network merge | two deployments sharing a prefix — Section 5 |
And the second column is actionable in a way the probability is not, because each row has a different fix: a manufacturing audit, a bigger space, a better generator, or a prefix allocation.
7. RTL 3 — The Correlation Model
// ---------------------------------------------------------------------
// correlation_model -- the process-failure term the birthday bound has
// no representation for, and which dominates at small populations.
//
// The output that matters is dominant_mechanism: at a given population
// and space, is the likelier cause chance or a tool? The two have
// different fixes and nothing else distinguishes them.
// ---------------------------------------------------------------------
module correlation_model
import uniqmyth_pkg::*;
(
input logic clk,
input logic rst_n,
input logic [31:0] population,
input logic [15:0] free_bits_i,
input logic [31:0] failure_rate_ppb, // per device, parts per billion
input logic same_lot_observed,
input logic same_prefix_observed,
input logic virtual_interfaces,
output logic [31:0] birthday_ratio_milli,
output logic [31:0] process_ratio_milli,
output logic process_dominates,
output logic [1:0] dominant_mechanism, // 0 chance, 1 process, 2 merge
output logic [2:0] recommended_action,
output logic duplicates_are_co_located,
output logic [31:0] c_diagnoses
);
// recommended_action: 1 audit manufacturing, 2 widen the space,
// 3 fix the generator, 4 allocate prefixes.
always_comb begin
birthday_ratio_milli = 32'(collision_ratio_milli(int'(population),
int'(free_bits_i)));
// 1 - exp(-rN) ~ rN for small rN. Scaled to the same milli units.
process_ratio_milli = (failure_rate_ppb * population) / 32'd1_000_000;
process_dominates = (process_ratio_milli > birthday_ratio_milli);
dominant_mechanism = same_prefix_observed ? 2'd2
: process_dominates ? 2'd1
: 2'd0;
recommended_action = same_prefix_observed ? 3'd4
: virtual_interfaces ? 3'd3
: process_dominates ? 3'd1
: 3'd2;
// THE fact the birthday bound has no term for: a lot ships to one
// customer, so a process duplicate arrives pre-paired.
duplicates_are_co_located = same_lot_observed;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) c_diagnoses <= '0;
else c_diagnoses <= c_diagnoses + 32'd1;
end
endmoduleClassification: a second probability term beside the first, and a four-way action recommendation derived from which one dominates.
What it teaches: that process_dominates flips at n = 2rN and that the answer depends overwhelmingly on the space. At 2⁴⁶ and a 10⁻⁶ per-device failure rate the crossover is 1.41 × 10⁸ devices, so a process failure is the likelier cause of any duplicate anybody will ever see. At 2²⁴ it is thirty-four, so above one rack chance dominates. The two have entirely different fixes and the observation is identical, so a design that reports which regime it is in has answered the first question of the investigation.
And it teaches that duplicates_are_co_located has no analogue in the birthday model at all. The bound counts collisions globally; a duplicate only matters on one broadcast domain. For chance collisions that difference is enormously in the deployment's favour — most collisions are between machines that will never meet. For process collisions it vanishes, because a lot ships to one customer, which is why the small, correlated term is more dangerous per collision than the large, independent one.
Deliberately simplified: process_ratio_milli uses the first-order approximation rN for 1 − exp(−rN), which diverges above about 10% — the model is for regime selection, not for reporting. same_lot_observed and same_prefix_observed are inputs a human supplies from serial numbers and address prefixes; no hardware can determine them. And the model has one failure rate where a real process has several mechanisms with different rates and different blast radii — a golden image affects every device from that image and a counter fault affects a run.
Production implication: the input nobody can supply is the failure rate, and there is a reason it is unknown. A manufacturer that detects duplicate programming fixes it and does not publish the rate; a manufacturer that does not detect it does not know the rate. So the term with the largest effect at realistic populations is the one with no published value, and a deployment's only access to it is its own observed duplicate rate — which requires the detection Section 10 prices and which almost nobody has. The chapter's uncomfortable conclusion is therefore circular and worth stating plainly: the quantity needed to decide whether detection is worth building can only be measured by building it.
8. "Forever" — Recycling, Aging, and the Interval Between Them
The third word in the myth is the one nobody examines, and it is false on a timescale of seconds.
A physical device's address is programmed once and lasts the device's life. That is what "forever" means and it is approximately true for a network card in a server.
A virtual interface's address is created when a workload starts and freed when it stops. In a container fleet that is seconds, and the address goes back into a pool to be reissued.
| Deployment | Interface lifetime | Is "forever" true? |
|---|---|---|
| a server's physical NIC | years | approximately |
| a long-lived virtual machine | months | approximately |
| a container in a scheduler | minutes | no |
| a serverless invocation's interface | seconds | emphatically not |
And the consequence is not that the address is reused — it is that it is reused before the network has forgotten the previous holder.
Chapter 12.5 §5 established the mechanism: a switch's forwarding entry ages out after a default 300 seconds of silence. So a recycled address whose previous holder's entry has not yet aged is, from the switch's point of view, the same station having moved ports.
teardown interval = 3 600 ÷ churn per hour
stale entries at any instant = 300 ÷ teardown interval| Churn | One teardown every | Stale entries held |
|---|---|---|
| 10/hour | 360 s | 0.83 |
| 100/hour | 36 s | 8.33 |
| 1 000/hour | 3.6 s | 83.33 |
| 10 000/hour | 0.36 s | 833.33 |
A fleet churning ten thousand interfaces an hour has eight hundred and thirty-three forwarding entries, at any instant, for stations that no longer exist — and each one is an address that may already have been reissued.
Three failure modes follow and they are distinct.
One — traffic to a reissued address goes to the old port until the entry is corrected, which takes one frame from the new holder. A brief, self-healing misdelivery, and the normal case.
Two — if the old holder is still transmitting — a container that was stopped but whose network namespace was not torn down — the address is genuinely duplicated and Section 4's callout applies.
Three — a stale entry occupying a table slot is Chapter 12.5 §7's capacity problem. 833 stale entries in a 128k table is nothing; 833 stale entries in an access switch's 8k table is 10.2% of it, held for stations that do not exist.
9. RTL 4 — The Recycle Model
// ---------------------------------------------------------------------
// recycle_model -- what "forever" means in a fleet that reissues
// addresses, and how many forwarding entries refer to stations that no
// longer exist.
//
// The output that matters is entry_may_be_inherited: a reissued address
// whose predecessor's entry is still live is indistinguishable from a
// station that moved.
// ---------------------------------------------------------------------
module recycle_model
import uniqmyth_pkg::*;
(
input logic clk,
input logic rst_n,
input logic [31:0] churn_per_hour,
input logic [15:0] aging_seconds,
input logic [31:0] table_entries,
input logic old_holder_still_transmitting,
output logic [31:0] teardown_interval_ms,
output logic [31:0] stale_entries_x100,
output logic [15:0] stale_share_ppm,
output logic entry_may_be_inherited,
output logic genuine_duplicate,
output logic [31:0] recommended_aging_s,
output logic aging_is_too_long,
output logic [31:0] c_inheritances
);
always_comb begin
teardown_interval_ms = (churn_per_hour == 0) ? 32'hFFFF_FFFF
: (32'd3_600_000 / churn_per_hour);
// 300 s / interval, in hundredths so a sub-unit answer survives.
stale_entries_x100 = (teardown_interval_ms == 0) ? 32'd0
: ((32'(aging_seconds) * 32'd100_000)
/ teardown_interval_ms);
stale_share_ppm = (table_entries == 0) ? 16'd0
: 16'(((stale_entries_x100 / 32'd100) * 32'd1_000_000)
/ table_entries);
// An address reissued inside the aging window inherits an entry.
entry_may_be_inherited = (teardown_interval_ms
< (32'(aging_seconds) * 32'd1000));
// And if the previous holder is still alive, it is not an
// inheritance at all -- it is Section 4's duplicate.
genuine_duplicate = entry_may_be_inherited
&& old_holder_still_transmitting;
// The derivation the callout argued for: a small multiple of the
// recycle interval rather than a constant from 1990.
recommended_aging_s = (teardown_interval_ms * 32'd10) / 32'd1000;
aging_is_too_long = (32'(aging_seconds) > recommended_aging_s)
&& (recommended_aging_s != 32'd0);
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) c_inheritances <= '0;
else if (entry_may_be_inherited) c_inheritances <= c_inheritances + 32'd1;
end
endmoduleClassification: two divisions, one of which produces a parameter recommendation that no switch currently accepts.
What it teaches: that genuine_duplicate requires two conditions and only one of them is about the address. A reissued address inheriting an entry is routine and self-healing; the same event with the previous holder still transmitting is Section 4's pathology. The switch sees the same evidence in both cases — an entry changing port — and the distinguishing fact is whether a process somewhere else is still running.
And it teaches that recommended_aging_s is one division and would replace a thirty-year-old constant. At 1 000 teardowns an hour the recycle interval is 3.6 seconds and the recommendation is 36 seconds, against a default of 300. The switch has every input it needs except the churn rate, and the churn rate belongs to an orchestrator that has no way to tell it.
Deliberately simplified: stale_entries_x100 is a steady-state expectation and the real count fluctuates — a scheduler that tears down a thousand containers at once produces a burst the mean does not describe. recommended_aging_s uses a factor of ten where the right multiple depends on how long a live station may legitimately stay silent, which is a property of the traffic rather than of the churn. And the model has one aging interval where Chapter 12.5 §5 allows per-VLAN values.
Production implication: the missing channel is the interesting part and there is a standard mechanism sitting unused next to it. A switch learns addresses by observation and is never told anything; an orchestrator knows exactly when an interface is destroyed and tells nobody. A single gratuitous ARP or an unsolicited notification on teardown would let the switch invalidate the entry immediately, which is what the mechanism exists for on creation and which essentially nothing sends on destruction. One frame per teardown at 10 000 teardowns an hour is 2.8 frames per second — nothing — and it would take the stale-entry count from 833 to approximately zero. The reason it is not done is that the orchestrator's authors are reasoning about IP and the switch's are reasoning about addresses, which is Chapter 25.1's boundary appearing as an operational gap rather than a design one.
10. Pricing the Detection a Switch Would Need
A switch already sees the evidence. It sees it once, overwrites it, and keeps nothing. This section prices keeping it, at Chapter 23.3's table size rather than at a teaching scale.
Chapter 12.2's learning is a write. A source address arriving on a port writes that port into the address's entry — and if the entry already held a different port, the previous value is gone. Nothing counts it, nothing timestamps it, nothing records what it was.
So the whole detection is: keep a little history beside each entry.
| What to add | Bits per entry | At 128k entries | × the datapath | On the table | On the switch |
|---|---|---|---|---|---|
| a 4-bit move counter | 4 | 524 288 BCE | 1.85 | +4.2% | +0.093% |
| plus a 16-bit last-move timestamp | 20 | 2 621 440 BCE | 9.25 | +20.8% | +0.466% |
| plus a 16-bit previous-port field | 36 | 4 718 592 BCE | 16.65 | +37.5% | +0.840% |
Twenty bits per entry — nine and a quarter MAC datapaths, less than half a per cent of a merchant switch — converts an unexplained port flap into a named, timestamped duplicate.
Row two is the right one and the reason is Chapter 5.3 §9's argument, now at scale. A counter alone cannot distinguish a legitimate failover, which moves once, from a duplicate, which moves continuously — the distinguishing feature is rate, and a rate needs a timestamp.
| Move counter | Counter and timestamp | Plus previous port | |
|---|---|---|---|
| a duplicate is detectable | only after many moves | immediately, by rate | immediately |
| a failover is distinguishable | no | yes | yes |
| the two stations are identifiable | no | no | YES |
| cost | 1.85 datapaths | 9.25 | 16.65 |
Row three is what the third column buys and it is the difference between there is a duplicate and it is on ports 3 and 7. At 16.65 datapaths — 0.84% of the switch — it is the most useful 0.4 percentage points in this chapter, because the first two columns produce an alarm and the third produces an instruction.
And the comparison that makes the case is against what the absence costs. Section 4's callout: both stations lose roughly half their inbound traffic, no error counter moves, and a capture at either station shows a healthy link with unexplained loss. The investigation that follows is measured in days.
11. RTL 5 — The Move Tracker
// ---------------------------------------------------------------------
// move_tracker -- twenty bits beside each forwarding entry, and the
// rate test that separates a failover from a duplicate.
//
// Chapter 5.3 Section 9 built this over sixteen entries as a teaching
// model. This is the version a 128k-entry table would carry, and the
// difference is that the storage is now a real number.
// ---------------------------------------------------------------------
module move_tracker
import uniqmyth_pkg::*;
#(
parameter int unsigned ENTRIES = 128 * 1024,
parameter int unsigned IDX_W = 17,
parameter int unsigned FLAP_THRESHOLD = 8,
parameter int unsigned WINDOW_S = 10
)(
input logic clk,
input logic rst_n,
input logic tick_s,
input logic learn_valid,
input logic [IDX_W-1:0] entry_idx,
input logic [15:0] port_now,
input logic [15:0] port_stored,
input logic entry_present,
output logic is_move,
output logic duplicate_suspected,
output logic [3:0] move_count_o,
output logic [15:0] last_move_s_o,
output logic [15:0] previous_port_o,
output logic [31:0] detector_bce_o,
output logic [31:0] detector_dp_milli,
output logic [15:0] table_overhead_pct_x10,
output logic [31:0] c_moves,
output logic [31:0] c_duplicates
);
logic [3:0] move_count [ENTRIES];
logic [15:0] last_move [ENTRIES];
logic [15:0] prev_port [ENTRIES];
logic [15:0] now_s;
always_comb begin
// Twenty bits of history, plus sixteen for the previous port.
detector_bce_o = 32'(detector_bce(4 + 16 + 16));
detector_dp_milli = 32'(datapaths_milli(detector_bce_o));
table_overhead_pct_x10 = 16'((detector_bce_o * 32'd1000)
/ 32'(table_bce()));
is_move = learn_valid && entry_present && (port_now != port_stored);
// Chapter 5.3 Section 9's rate test: a failover moves once and
// stops; a duplicate moves continuously while both stations send.
duplicate_suspected =
is_move
&& (move_count[entry_idx] >= 4'(FLAP_THRESHOLD))
&& ((now_s - last_move[entry_idx]) <= 16'(WINDOW_S));
move_count_o = move_count[entry_idx];
last_move_s_o = last_move[entry_idx];
previous_port_o = prev_port[entry_idx];
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
now_s <= '0; c_moves <= '0; c_duplicates <= '0;
for (int i = 0; i < ENTRIES; i++) begin
move_count[i] <= '0; last_move[i] <= '0; prev_port[i] <= '0;
end
end else begin
if (tick_s) now_s <= now_s + 16'd1;
if (is_move) begin
// The port that is about to be overwritten -- Section 12's
// whole subject, preserved for the cost of one field.
prev_port[entry_idx] <= port_stored;
last_move[entry_idx] <= now_s;
c_moves <= c_moves + 32'd1;
if ((now_s - last_move[entry_idx]) <= 16'(WINDOW_S)) begin
if (move_count[entry_idx] != 4'hF)
move_count[entry_idx] <= move_count[entry_idx] + 4'd1;
end else begin
// Outside the window, the history is stale: a failover a day
// ago says nothing about today.
move_count[entry_idx] <= 4'd1;
end
if (duplicate_suspected) c_duplicates <= c_duplicates + 32'd1;
end
end
end
endmoduleClassification: three small arrays beside a large one, and a rate test rather than an occurrence test.
What it teaches: that prev_port is the field that makes the alarm actionable, and it is the one an area review deletes first. Without it a duplicate is reported and the operator must then find both stations by other means; with it the report names two ports. Sixteen bits per entry — 7.4 datapaths, 0.37% of the switch — and it is the difference between an alarm and an instruction.
And it teaches that the window matters as much as the threshold. A move count without decay accumulates over a table entry's whole life, so a station that failed over eight times in eight months eventually looks like a duplicate. Resetting the count when a move falls outside the window is what makes the test a rate rather than a total, and it is one comparison.
Deliberately simplified: the arrays are declared as flat memories and initialised in a reset loop, which is unsynthesisable at 128k entries — a real design initialises lazily with a valid bit or relies on the table's own entry-valid flag. now_s is a 16-bit second counter that wraps after about 18.2 hours, so a now_s − last_move comparison is wrong across a wrap unless the arithmetic is modular; the module relies on the window being short enough that it does not matter, which is true for a 10-second window and is the kind of assumption that survives until somebody widens it. And there is one threshold for the whole table where a service address that fails over legitimately would justify a per-entry exemption.
Production implication: the reset loop is the detail that stops this being synthesisable, and the fix is the one every large table uses. A 128k-entry array cannot be cleared in one cycle and does not need to be: the forwarding table already has a per-entry valid bit, and history beside an invalid entry is meaningless, so the history arrays need no reset at all — they are written before they are read, by construction. That observation removes 128k × 36 flip-flops of reset fanout, which is the difference between a structure that closes timing and one that does not, and it is the same argument Chapter 23.6 §8 made about what actually binds: not the storage, the thing that has to reach it.
12. Where the Evidence Goes
Section 10 priced keeping the evidence. This section is about what happens without it, and the answer is stranger than "nothing is recorded".
The evidence is not lost. It is destroyed by the structure that would have held it, and the destruction is that structure's specified behaviour.
Work through what a MAC table is. Chapter 12.5 §3: an associative structure keyed by the address, holding one entry per address, so that a lookup returns one port. The key's uniqueness is the structure's fundamental assumption — it is what makes "the entry for this address" a meaningful phrase.
| The structure's assumption | What a duplicate is | |
|---|---|---|
| one entry per address | the index is unique | two stations claiming one index |
| a learn writes the port | the newest observation is right | both observations are right |
| a lookup returns one port | there is one answer | there are two and one is returned |
A duplicate address cannot be represented in a structure indexed by the address. There is no state in which the table holds both stations, because holding both would require two entries with one key.
So the fault appears as a rewrite, and a rewrite is normal.
| Event | What the table does | Is it an error? |
|---|---|---|
| a station moves ports legitimately | rewrite the entry | no — the designed behaviour |
| a LAG member rebalances — Chapter 15.2 | rewrite the entry | no |
| a spanning-tree reconvergence | rewrite many entries | no |
| a virtual machine migrates | rewrite the entry | no |
| two stations share an address | rewrite the entry, continuously | YES, and it looks identical |
Five causes, one signature, and Chapter 12.4 §8's class 48 named that shape: a symptom implies a cause only when the mapping is one-to-one.
But this chapter's case has an additional turn that 48 does not. The four benign causes each produce a small number of rewrites and stop. The duplicate produces rewrites at the combined transmission rate of both stations — which is the rate test Section 11 implements — and the information needed to apply the rate test is the timestamp of the previous rewrite, which the rewrite destroyed.
That is the recursion and it is the chapter's central finding.
to distinguish the fault from the normal case you need the RATE
to compute the rate you need the PREVIOUS event's time
the previous event's record was overwritten by the current one
and overwriting is the structure's specified behaviour13. RTL 6 — The Shadow Witness
// ---------------------------------------------------------------------
// shadow_witness -- the second, independent record that Section 12's
// argument requires.
//
// The point is what it is NOT indexed by. A structure keyed on the
// address inherits the address's ambiguity; this one is keyed on the
// EVENT, so two stations sharing an address produce two records.
// ---------------------------------------------------------------------
module shadow_witness
import uniqmyth_pkg::*;
#(
parameter int unsigned DEPTH = 64,
parameter int unsigned PTR_W = 6
)(
input logic clk,
input logic rst_n,
input logic tick_s,
input logic clear,
input logic move_event,
input logic [47:0] address,
input logic [15:0] port_from,
input logic [15:0] port_to,
input logic read_en,
input logic [PTR_W-1:0] read_idx,
output logic [47:0] r_address,
output logic [15:0] r_port_from,
output logic [15:0] r_port_to,
output logic [15:0] r_time_s,
output logic r_valid,
output logic [PTR_W-1:0] entries_used,
output logic wrapped,
output logic keyed_by_address,
output logic [31:0] witness_bce_o,
output logic [31:0] c_events,
output logic [31:0] c_lost_to_wrap
);
logic [47:0] a_addr [DEPTH];
logic [15:0] a_from [DEPTH];
logic [15:0] a_to [DEPTH];
logic [15:0] a_time [DEPTH];
logic a_val [DEPTH];
logic [PTR_W-1:0] wptr;
logic [15:0] now_s;
always_comb begin
// 48 + 16 + 16 + 16 + 1 bits per entry.
witness_bce_o = 32'(DEPTH) * 32'd97;
// THE structural property. A log keyed on the event cannot lose an
// entry to a key collision, because it has no key.
keyed_by_address = 1'b0;
r_address = a_addr[read_idx];
r_port_from = a_from[read_idx];
r_port_to = a_to[read_idx];
r_time_s = a_time[read_idx];
r_valid = a_val[read_idx] && read_en;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n || clear) begin
wptr <= '0; now_s <= '0; entries_used <= '0; wrapped <= 1'b0;
c_events <= '0; c_lost_to_wrap <= '0;
for (int i = 0; i < DEPTH; i++) a_val[i] <= 1'b0;
end else begin
if (tick_s) now_s <= now_s + 16'd1;
if (move_event) begin
a_addr[wptr] <= address;
a_from[wptr] <= port_from;
a_to[wptr] <= port_to;
a_time[wptr] <= now_s;
a_val[wptr] <= 1'b1;
// Chapter 21.9 Section 2's warning, honoured: a finite sink
// must count what it could not keep.
if (a_val[wptr]) c_lost_to_wrap <= c_lost_to_wrap + 32'd1;
wptr <= wptr + 1'b1;
if (wptr == PTR_W'(DEPTH - 1)) wrapped <= 1'b1;
if (!wrapped && (entries_used != PTR_W'(DEPTH - 1)))
entries_used <= entries_used + 1'b1;
c_events <= c_events + 32'd1;
end
end
end
endmoduleClassification: a circular event log, whose essential property is the absence of a key.
What it teaches: that keyed_by_address is hard-wired zero and that this is the entire design. Section 12 established that a structure indexed by the address cannot represent a duplicate; a log indexed by arrival order can hold both stations' events side by side, and the same address appearing twice with different port_to values is the duplicate, written down.
And it teaches that c_lost_to_wrap is not optional. Chapter 21.9 §2's class 102 is exactly the failure a finite log invites: a 64-entry ring during a flap at ten thousand moves a second wraps in 6.4 milliseconds, and without the counter the log looks like a complete record of the last 64 events rather than a sample of an unknown number. One counter, and the log stops lying about its own completeness.
Deliberately simplified: the log records every move rather than only suspicious ones, so a spanning-tree reconvergence fills it in one event — a real design gates writes on duplicate_suspected or keeps two logs. now_s wraps at 18.2 hours as in Section 11. And the log has one instance per switch where the useful arrangement is one per port group, because a flap on one port should not evict the record of a flap on another.
Production implication: the gating decision is where this structure is usually got wrong in the useful direction. A log that records only confirmed duplicates has no record of the moves that led to the confirmation — which is the sequence an investigator needs — and a log that records everything is full of routine reconvergence. The arrangement that works is a pre-trigger ring: record everything, freeze on duplicate_suspected, and keep what was already there. Chapter 21.9 §9's capture sink is the same structure for the same reason, and the cost here is 64 entries × 97 bits = 6 208 BCE, 0.022 of a datapath — a quarter of one per cent of what Section 10's per-entry history costs, for the part of the evidence that actually answers the question.
14. What an Address Claim Must Never Do
Five prohibitions, each with a failure from this chapter behind it.
| # | Never | Because |
|---|---|---|
| 1 | compute a collision probability over the field width | Section 2: the generator's space is 2²⁴, not 2⁴⁶, and the factor at ten thousand addresses is 1 335 907 |
| 2 | treat a single port change as a duplicate | Chapter 5.3 §9: a failover, a migration and a reconvergence all do it; the distinguishing feature is rate |
| 3 | ship a generator that cannot say where its entropy came from | Section 2: a derived value's uniqueness is the input's, and nothing else can audit it |
| 4 | merge two networks without comparing their address prefixes | Section 5: two 4 000-interface clusters on one prefix become 8 000 in one space — 85.2% |
| 5 | report a duplicate without naming both ports | Section 10: an alarm costs 9.25 datapaths and an instruction costs 16.65 |
Row four is the one that turns two safe deployments into one unsafe one with no configuration change anywhere, and the check is a single query nobody runs.
15. RTL 7 — Uniqueness Telemetry
// ---------------------------------------------------------------------
// uniq_telemetry -- the counters that make a uniqueness claim
// falsifiable on a live switch.
//
// Design rule: the chapter's three errors -- the space, the
// distribution and the lifetime -- each need a different counter, and
// none of them is the drop counter anybody looks at.
// ---------------------------------------------------------------------
module uniq_telemetry
import uniqmyth_pkg::*;
(
input logic clk,
input logic rst_n,
input logic clear,
input logic tick_s,
input logic learn_valid,
input logic is_move_i,
input logic duplicate_suspected_i,
input logic entry_inherited_i,
input logic [47:0] address,
input logic [31:0] distinct_addresses,
output logic [47:0] c_learns,
output logic [47:0] c_moves_o,
output logic [47:0] c_duplicates_o,
output logic [47:0] c_inherited,
output logic [47:0] c_laa_seen,
output logic [47:0] c_global_seen,
output logic [31:0] move_rate_per_s,
output logic [31:0] laa_share_ppm,
output logic [31:0] population_o,
output logic laa_dominant,
output logic move_rate_is_pathological
);
logic [31:0] moves_this_s;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n || clear) begin
c_learns <= '0; c_moves_o <= '0; c_duplicates_o <= '0;
c_inherited <= '0; c_laa_seen <= '0; c_global_seen <= '0;
moves_this_s <= '0; move_rate_per_s <= '0;
end else begin
if (learn_valid) begin
c_learns <= c_learns + 48'd1;
// Chapter 5.3 Section 2: the U/L bit is bit 1 of octet 0.
if (address[41]) c_laa_seen <= c_laa_seen + 48'd1;
else c_global_seen <= c_global_seen + 48'd1;
end
if (is_move_i) c_moves_o <= c_moves_o + 48'd1;
if (duplicate_suspected_i) c_duplicates_o <= c_duplicates_o + 48'd1;
if (entry_inherited_i) c_inherited <= c_inherited + 48'd1;
if (is_move_i) moves_this_s <= moves_this_s + 32'd1;
if (tick_s) begin
move_rate_per_s <= moves_this_s;
moves_this_s <= '0;
end
end
end
always_comb begin
laa_share_ppm = (c_learns == 0) ? 32'd0
: 32'((c_laa_seen * 48'd1_000_000) / c_learns);
population_o = distinct_addresses;
// A segment dominated by locally administered addresses is a
// segment where Section 2's arithmetic applies.
laa_dominant = (laa_share_ppm >= 32'd500_000);
// Section 4's callout: a duplicate flaps at the stations' combined
// transmission rate, which is orders above any legitimate move.
move_rate_is_pathological = (move_rate_per_s > 32'd100);
end
endmoduleClassification: six counters, of which the pair c_laa_seen and c_global_seen is the one nobody keeps.
What it teaches: that laa_share_ppm tells an operator which arithmetic applies to their segment, and it costs one bit test. A segment of globally assigned addresses is governed by manufacturing correlation — Section 6's linear term; a segment of locally administered ones is governed by Section 4's quadratic birthday term over whatever space the generator left. The two have different fixes and the U/L bit distinguishes them for free.
And it teaches that move_rate_is_pathological is a threshold on a rate rather than a count, which is Chapter 5.3 §9's argument arriving as telemetry. A legitimate move happens on failover, migration or reconvergence — units per hour; a duplicate flaps at the stations' combined transmission rate — thousands per second. There are four orders of magnitude between them and no threshold in between is contentious.
Deliberately simplified: distinct_addresses is supplied from outside because counting distinct values requires the table's own occupancy, which this module does not see. The LAA test reads one bit of the address and does not distinguish the four classes Chapter 5.3 §5's decomposer produces — a group address with U/L set is counted as locally administered, which is technically right and rarely what the operator means. And move_rate_per_s is a one-second window, which misses a burst that straddles a boundary.
Production implication: the counter pair that matters is the one whose absence makes the chapter's whole argument unusable in the field. An operator who suspects a duplicate needs to know which arithmetic applies, and that requires knowing whether the segment's addresses are assigned or generated. Today the only way to find out is to dump the table and inspect the addresses by hand, which is possible and which nobody does during an incident. One bit test and two counters — 96 bits, 1 920 BCE, 0.007 datapaths — and the first question of the investigation has an answer before anybody logs in.
16. RTL 8 — The Uniqueness Conformance Monitor
// ---------------------------------------------------------------------
// uniq_conformance -- the checks that hold a uniqueness claim to what
// the evidence supports.
//
// Note what is absent: there is no check that addresses ARE unique,
// because Section 12 established that the structure cannot represent
// the violation. Every check here is about the DETECTION instead.
// ---------------------------------------------------------------------
module uniq_conformance
import uniqmyth_pkg::*;
(
input logic clk,
input logic rst_n,
input logic claims_unique,
input logic [31:0] population,
input logic [15:0] free_bits_i,
input logic history_present,
input logic witness_present,
input logic generator_reports_entropy,
input logic is_move_i,
input logic move_recorded,
input logic [31:0] move_rate,
input logic duplicate_suspected_i,
input logic both_ports_reported,
output logic v_claim_without_detection,
output logic v_evidence_overwritten,
output logic v_unauditable_generator,
output logic v_population_past_half,
output logic v_alarm_without_ports,
output logic [4:0] violations,
output logic conformant
);
always_comb begin
// 1. A uniqueness claim with no way to detect a violation.
v_claim_without_detection = claims_unique && !history_present;
// 2. Section 12: a move that overwrote the previous record and
// nothing kept it.
v_evidence_overwritten = is_move_i && !move_recorded && !witness_present;
// 3. Section 2: a generator that cannot say where its bits came
// from has an unauditable claim.
v_unauditable_generator = claims_unique && !generator_reports_entropy;
// 4. Section 5: past the half point, randomness is the wrong
// mechanism.
v_population_past_half = (population >= 32'(half_point(int'(free_bits_i))));
// 5. Section 10: an alarm that does not name both ports is an
// alarm rather than an instruction.
v_alarm_without_ports = duplicate_suspected_i && !both_ports_reported;
violations = { v_alarm_without_ports, v_population_past_half,
v_unauditable_generator, v_evidence_overwritten,
v_claim_without_detection };
conformant = (violations == 5'b00000);
end
endmoduleClassification: five checks, none of which checks the property the chapter is about.
What it teaches: that there is no v_addresses_are_unique, and its absence is deliberate. Section 12 established that a structure indexed by the address cannot hold the state in which two stations share one — so no monitor reading that structure can check it. Every check here is about the detection apparatus instead, which is the only thing that can be verified locally.
And it teaches that v_claim_without_detection is a check on a claim rather than on a behaviour, and it belongs in silicon for the same reason Chapter 24.3 §16's v_conformance_unfounded does: a part that asserts a property it cannot detect the violation of is misleading every consumer of that assertion, including the operator deciding whether to investigate.
Deliberately simplified: history_present and witness_present are configuration inputs the monitor trusts, so a design that ties them high passes — the monitor is a design-review aid rather than a guarantee, which is the same limitation Chapter 24.3 §16 has and for the same structural reason. v_population_past_half needs a population the switch cannot count across a whole deployment; it can count its own table's occupancy, which is a lower bound. And move_rate is declared and unused, left in deliberately: a real monitor would check that the rate test's window and threshold are consistent with the deployment's legitimate move rate, which requires a number nobody supplies.
Production implication: the unused input is the honest form of a gap. A rate test needs to know what rate is legitimate, and that depends on how often the deployment fails over, migrates and reconverges — which is an operational fact, varying by site, that no switch is told. The default threshold is therefore a guess, and it is the same class of guess as Chapter 14.2 §9's 75% watermark and Chapter 12.5 §5's 300-second aging: a constant chosen once, correct in one deployment, and never re-derived. The fix is the same as in both of those cases — expose the parameter, document what it protects against, and let the site compute it — and the cost is a register.
17. The Three Claims, Priced Side by Side
Everything this chapter derived, in one table.
| The claim | What is true | The number |
|---|---|---|
| "globally" | the generator's space is usually 2²⁴, not 2⁴⁶ | 1 335 907× at ten thousand addresses |
| "unique" | the birthday bound assumes independence; manufacturing correlates and ships co-located | crossover at n = 2rN — 34 devices at 2²⁴ |
| "forever" | a virtual interface's address is reissued in seconds | 833 stale entries at 10 000 teardowns an hour |
| and when it fails | the table cannot represent the violation | 20 bits per entry to detect it, 36 to name both ports |
And the collision probabilities, side by side, on the two spaces.
| Addresses | 2⁴⁶ — quoted | 2²⁴ — implemented | The deployment |
|---|---|---|---|
| 256 | 4.66 × 10⁻¹⁰ | 0.195% | a rack |
| 1 024 | 7.45 × 10⁻⁹ | 3.08% | a row |
| 4 823 | ≈ 1.6 × 10⁻⁷ | 50% | a medium cluster |
| 10 000 | 7.11 × 10⁻⁷ | 94.92% | a hall |
| 9 876 831 | 50% | ≈ 1 | a national fleet |
Rows three and five are the two half-points, and they are five thousand addresses apart in one arithmetic and ten million apart in the other.
The detection, priced against Chapter 23.3's switch.
| Structure | BCE | × the datapath | % of the switch |
|---|---|---|---|
| a 4-bit move counter, 128k entries | 524 288 | 1.85 | 0.093% |
| plus a 16-bit timestamp | 2 621 440 | 9.25 | 0.466% |
| plus a 16-bit previous port | 4 718 592 | 16.65 | 0.840% |
| a 64-entry shadow witness | 6 208 | 0.022 | 0.001% |
| all of it | 4 724 800 | 16.68 | 0.841% |
Under one per cent of a merchant switch, against an undetected failure that costs half of both stations' inbound traffic with no error counter anywhere.
18. What the Correction Assumes
Eight assumptions, each with its direction of failure.
| # | Assumption | If it is false |
|---|---|---|
| 1 | a generator fixes a 24-bit prefix | a generator that uses all 46 bits has the quoted safety; the chapter's argument is about what generators do, not what they could do |
| 2 | the birthday approximation 1 − exp(−n²/2N) | exact for the regimes used; it overestimates slightly at very small n |
| 3 | Chapter 12.5's 300-second aging | a shorter interval reduces stale entries proportionally |
| 4 | Chapter 23.3's 128k-entry table | the detector's cost scales linearly; its percentage of the switch does not move |
| 5 | a duplicate loses about half of each station's inbound traffic | it depends on the two stations' relative transmission rates; the busier one wins more often |
| 6 | a programming failure rate of 10⁻⁶ | unpublished and unknowable — Section 7's circularity |
| 7 | lots ship co-located | the usual case; a distributor breaking lots up weakens the correlation and the birthday term is unaffected |
| 8 | BCE applies | Section 19 examines it; it holds cleanly |
Assumption 6 is the honest weak point of the chapter and it is worth restating. The process-failure rate is the term with the largest effect at small populations and no published value, and Section 7 established the circularity: measuring it requires the detector whose cost the measurement would justify. The chapter's position is that 0.84% of a switch is cheap enough that the circularity does not need to be resolved first.
19. The Cost, Accounted — in BCE
This chapter's blocks.
| Block | Flops | BCE | × the datapath |
|---|---|---|---|
address_space_model | 32 | 640 | 0.002 |
collision_model | 34 | 680 | 0.002 |
correlation_model | 32 | 640 | 0.002 |
recycle_model | 32 | 640 | 0.002 |
move_tracker, 128k entries | an array | 4 718 592 | 16.65 |
shadow_witness, 64 entries | an array | 6 208 | 0.022 |
uniq_telemetry | 336 | 6 720 | 0.024 |
uniq_conformance | 0 — combinational | 0 | 0 |
| this chapter's additions | — | 4 734 120 | 16.71 |
move_tracker is 99.7% of the total and it is the only block that scales with the table, which is this chapter's structure showing through: the models are arithmetic and the detection is storage, and the storage is per address.
And the designs the blocks describe.
| BCE | × the datapath | % of the switch | |
|---|---|---|---|
| Chapter 23.3's MAC table | 1.26 × 10⁷ | 44.4 | 2.24% |
| the full detector beside it | 4.72 × 10⁶ | 16.65 | 0.840% |
| the shadow witness | 6 208 | 0.022 | 0.001% |
| Chapter 23.3's whole switch | 5.62 × 10⁸ | 1 985 | 100% |
The detector is 37.5% of the table it instruments and 0.84% of the switch, which is the right way round: a structure that watches a table should be a fraction of the switch, not of the table.
20. Properties Worth Asserting, and One Worth Refusing
Fifty-one properties in six groups, and the refused one is the property the whole myth rests on.
Group A — the address space (9).
// A1. The quoted space is 46 bits and the model says so.
p_as_quoted: assert property (@(posedge clk) disable iff (!rst_n)
(quoted_free_bits == 32'd46));
// A2. A vendor-prefixed generator leaves 24.
p_as_prefix: assert property (@(posedge clk) disable iff (!rst_n)
(generator == 2'(GEN_VENDOR_PFX)) |-> (free_bits_o == 16'd24));
// A3. Bits lost is the difference, and it is never negative.
p_as_lost: assert property (@(posedge clk) disable iff (!rst_n)
(bits_lost_to_structure == (16'(quoted_free_bits) - free_bits_o)));
// A4. Any structure at all makes the quoted arithmetic wrong.
p_as_wrong_space: assert property (@(posedge clk) disable iff (!rst_n)
(bits_lost_to_structure != 16'd0) |-> arithmetic_is_on_the_wrong_space);
// A5. The half point falls as the space shrinks.
p_as_half_point_monotone: assert property (@(posedge clk) disable iff (!rst_n)
(free_bits_o < $past(free_bits_o)) |-> (half_point_n <= $past(half_point_n)));
// A6. At 24 bits the half point is near 4 823.
p_as_half_24: assert property (@(posedge clk) disable iff (!rst_n)
(free_bits_o == 16'd24) |-> ((half_point_n > 32'd4700) &&
(half_point_n < 32'd4950)));
// A7. Exactly one regime is reported.
p_as_one_regime: assert property (@(posedge clk) disable iff (!rst_n)
$onehot({regime_negligible, regime_material, regime_likely}));
// A8. A larger population never moves to a safer regime.
p_as_monotone: assert property (@(posedge clk) disable iff (!rst_n)
(population > $past(population)) |-> (ratio_milli >= $past(ratio_milli)));
// A9. At 40 free bits or more the model reports negligible.
p_as_large_space: assert property (@(posedge clk) disable iff (!rst_n)
(free_bits_o >= 16'd40) |-> regime_negligible);Group B — collisions and correlation (9).
// B1. Headroom is zero past the half point.
p_co_headroom: assert property (@(posedge clk) disable iff (!rst_n)
past_half_point |-> (population_headroom == 32'd0));
// B2. And is the difference otherwise.
p_co_headroom_value: assert property (@(posedge clk) disable iff (!rst_n)
!past_half_point |-> (population_headroom == (half_point_o - population)));
// B3. Allocation is recommended from the material regime onward.
p_co_allocation: assert property (@(posedge clk) disable iff (!rst_n)
(regime != 2'd0) |-> allocation_recommended);
// B4. The process term is linear in the population.
p_cr_linear: assert property (@(posedge clk) disable iff (!rst_n)
(process_ratio_milli ==
(failure_rate_ppb * population) / 32'd1_000_000));
// B5. And the birthday term is quadratic, so one must overtake.
p_cr_crossover_exists: assert property (@(posedge clk) disable iff (!rst_n)
((population > 32'd1_000_000) && (free_bits_i <= 16'd24))
|-> !process_dominates);
// B6. A shared prefix overrides both and names a merge.
p_cr_merge: assert property (@(posedge clk) disable iff (!rst_n)
same_prefix_observed |-> (dominant_mechanism == 2'd2));
// B7. Virtual interfaces point at the generator.
p_cr_generator: assert property (@(posedge clk) disable iff (!rst_n)
(virtual_interfaces && !same_prefix_observed) |->
(recommended_action == 3'd3));
// B8. A lot-mate duplicate is co-located.
p_cr_colocated: assert property (@(posedge clk) disable iff (!rst_n)
same_lot_observed |-> duplicates_are_co_located);
// B9. Exactly one action is recommended.
p_cr_one_action: assert property (@(posedge clk) disable iff (!rst_n)
(recommended_action != 3'd0));Group C — recycling (8).
// C1. The teardown interval is the churn's reciprocal.
p_rc_interval: assert property (@(posedge clk) disable iff (!rst_n)
(churn_per_hour != 0) |->
(teardown_interval_ms == (32'd3_600_000 / churn_per_hour)));
// C2. Inheritance happens when the interval is inside the aging window.
p_rc_inherit: assert property (@(posedge clk) disable iff (!rst_n)
(teardown_interval_ms < (32'(aging_seconds) * 32'd1000))
|-> entry_may_be_inherited);
// C3. A genuine duplicate needs the old holder alive.
p_rc_duplicate: assert property (@(posedge clk) disable iff (!rst_n)
genuine_duplicate |-> (entry_may_be_inherited &&
old_holder_still_transmitting));
// C4. Stale entries rise with churn.
p_rc_stale_rises: assert property (@(posedge clk) disable iff (!rst_n)
(churn_per_hour > $past(churn_per_hour)) |->
(stale_entries_x100 >= $past(stale_entries_x100)));
// C5. The recommendation is derived, not constant.
p_rc_derived: assert property (@(posedge clk) disable iff (!rst_n)
(recommended_aging_s == (teardown_interval_ms * 32'd10) / 32'd1000));
// C6. A 300-second default is too long above a threshold churn.
p_rc_default_too_long: assert property (@(posedge clk) disable iff (!rst_n)
((aging_seconds == 16'd300) && (churn_per_hour >= 32'd120))
|-> aging_is_too_long);
// C7. Stale share never exceeds the table.
p_rc_share_bounded: assert property (@(posedge clk) disable iff (!rst_n)
(stale_share_ppm <= 16'd1_000_000));
// C8. Zero churn means no inheritance.
p_rc_zero_churn: assert property (@(posedge clk) disable iff (!rst_n)
(churn_per_hour == 32'd0) |-> !entry_may_be_inherited);Group D — the move tracker (9).
// D1. A move is a learn on a different port.
p_mv_definition: assert property (@(posedge clk) disable iff (!rst_n)
is_move |-> (learn_valid && entry_present && (port_now != port_stored)));
// D2. A suspicion needs the count AND the window -- a rate, not a total.
p_mv_rate_not_total: assert property (@(posedge clk) disable iff (!rst_n)
duplicate_suspected |-> ((move_count[entry_idx] >= 4'(FLAP_THRESHOLD)) &&
((now_s - last_move[entry_idx]) <= 16'(WINDOW_S))));
// D3. A single move is never a suspicion.
p_mv_single_move_ok: assert property (@(posedge clk) disable iff (!rst_n)
(is_move && (move_count[entry_idx] == 4'd0)) |-> !duplicate_suspected);
// D4. The previous port is preserved before the overwrite.
p_mv_prev_preserved: assert property (@(posedge clk) disable iff (!rst_n)
is_move |=> (previous_port_o == $past(port_stored)));
// D5. The timestamp advances to now on every move.
p_mv_timestamp: assert property (@(posedge clk) disable iff (!rst_n)
is_move |=> (last_move_s_o == $past(now_s)));
// D6. A move outside the window resets the count to one.
p_mv_window_resets: assert property (@(posedge clk) disable iff (!rst_n)
(is_move && ((now_s - last_move[entry_idx]) > 16'(WINDOW_S))) |=>
(move_count_o == 4'd1));
// D7. The count saturates rather than wrapping.
p_mv_saturates: assert property (@(posedge clk) disable iff (!rst_n)
(move_count_o == 4'hF) |=> (move_count_o == 4'hF));
// D8. Every move is counted.
p_mv_counted: assert property (@(posedge clk) disable iff (!rst_n)
is_move |=> (c_moves == $past(c_moves) + 32'd1));
// D9. The overhead is 37.5% of the table it instruments.
p_mv_overhead: assert property (@(posedge clk) disable iff (!rst_n)
(table_overhead_pct_x10 == 16'd375));Group E — the shadow witness (8).
// E1. The log has no key, which is the whole design.
p_sw_keyless: assert property (@(posedge clk) disable iff (!rst_n)
(keyed_by_address == 1'b0));
// E2. Every move event writes an entry.
p_sw_writes: assert property (@(posedge clk) disable iff (!rst_n)
move_event |=> (c_events == $past(c_events) + 32'd1));
// E3. Overwriting a valid entry counts a loss.
p_sw_loss_counted: assert property (@(posedge clk) disable iff (!rst_n)
(move_event && a_val[wptr]) |=>
(c_lost_to_wrap == $past(c_lost_to_wrap) + 32'd1));
// E4. The write pointer advances by one and wraps.
p_sw_pointer: assert property (@(posedge clk) disable iff (!rst_n)
move_event |=> (wptr == PTR_W'($past(wptr) + 1)));
// E5. A read returns the recorded transition.
p_sw_read: assert property (@(posedge clk) disable iff (!rst_n)
(read_en && a_val[read_idx]) |-> r_valid);
// E6. The same address may appear twice with different destinations --
// which a keyed structure could not represent.
p_sw_two_records: assert property (@(posedge clk) disable iff (!rst_n)
(r_valid && (r_address == $past(r_address)) &&
(r_port_to != $past(r_port_to))) |-> 1'b1);
// E7. Entries used never exceeds the depth.
p_sw_bounded: assert property (@(posedge clk) disable iff (!rst_n)
(entries_used < PTR_W'(DEPTH)));
// E8. Clearing invalidates everything.
p_sw_clear: assert property (@(posedge clk) disable iff (!rst_n)
clear |=> (entries_used == '0));Group F — telemetry and conformance (8).
// F1. The U/L bit classifies every learned address.
p_tl_classify: assert property (@(posedge clk) disable iff (!rst_n)
(c_learns == c_laa_seen + c_global_seen));
// F2. A locally administered majority selects the birthday arithmetic.
p_tl_laa_dominant: assert property (@(posedge clk) disable iff (!rst_n)
(laa_share_ppm >= 32'd500_000) |-> laa_dominant);
// F3. A pathological move rate is orders above a legitimate one.
p_tl_rate: assert property (@(posedge clk) disable iff (!rst_n)
move_rate_is_pathological |-> (move_rate_per_s > 32'd100));
// F4. A claim with no detection is a violation.
p_cf_claim_needs_detection: assert property (@(posedge clk) disable iff (!rst_n)
(claims_unique && !history_present) |-> v_claim_without_detection);
// F5. A move with nothing recorded and no witness loses the evidence.
p_cf_evidence: assert property (@(posedge clk) disable iff (!rst_n)
(is_move_i && !move_recorded && !witness_present) |-> v_evidence_overwritten);
// F6. A generator that cannot report its entropy is unauditable.
p_cf_generator: assert property (@(posedge clk) disable iff (!rst_n)
(claims_unique && !generator_reports_entropy) |-> v_unauditable_generator);
// F7. An alarm without both ports is an alarm, not an instruction.
p_cf_ports: assert property (@(posedge clk) disable iff (!rst_n)
(duplicate_suspected_i && !both_ports_reported) |-> v_alarm_without_ports);
// F8. Conformance is the disjunction of its five checks.
p_cf_vector: assert property (@(posedge clk) disable iff (!rst_n)
conformant |-> (violations == 5'b00000));Coverage — the states a deployment reaches without noticing.
c_as_prefixed: cover property (@(posedge clk) free_bits_o == 16'd24);
c_as_structured: cover property (@(posedge clk) free_bits_o == 16'd16);
c_as_material: cover property (@(posedge clk) regime_material);
c_as_likely: cover property (@(posedge clk) regime_likely);
c_cr_process: cover property (@(posedge clk) process_dominates);
c_cr_merge: cover property (@(posedge clk) dominant_mechanism == 2'd2);
c_rc_inherited: cover property (@(posedge clk) entry_may_be_inherited);
c_rc_duplicate: cover property (@(posedge clk) genuine_duplicate);
c_mv_failover: cover property (@(posedge clk) is_move && !duplicate_suspected);
c_mv_duplicate: cover property (@(posedge clk) duplicate_suspected);
c_mv_saturated: cover property (@(posedge clk) move_count_o == 4'hF);
c_sw_wrapped: cover property (@(posedge clk) wrapped);
c_sw_lost: cover property (@(posedge clk) c_lost_to_wrap != 32'd0);
c_cf_unauditable: cover property (@(posedge clk) v_unauditable_generator);21. Verification Scenarios
Fifty-eight scenarios in six groups, plus one directed test random stimulus will not produce.
Group 1 — the address space (10).
| # | Scenario | Expect |
|---|---|---|
| 1 | GEN_UNIFORM_46 | 46 free bits; 0 lost; the quoted arithmetic |
| 2 | GEN_FIXED_OCTET | 40 free bits; 6 lost |
| 3 | GEN_VENDOR_PFX | 24 free bits; 22 lost |
| 4 | GEN_STRUCTURED | 16 free bits; 30 lost |
| 5 | any generator but the first | arithmetic_is_on_the_wrong_space |
| 6 | half point at 46 bits | ≈ 9 876 831 |
| 7 | half point at 24 bits | ≈ 4 823 |
| 8 | half point at 16 bits | ≈ 301 |
| 9 | population 10 000, 24 bits | regime_likely |
| 10 | population 10 000, 46 bits | regime_negligible |
Group 2 — collisions and correlation (10).
| # | Scenario | Expect |
|---|---|---|
| 11 | 256 addresses, 24 bits | 0.195% |
| 12 | 1 024, 24 bits | 3.08% |
| 13 | 4 096, 24 bits | 39.35% |
| 14 | 10 000, 24 bits | 94.92% |
| 15 | 10 000, 46 bits | 7.11 × 10⁻⁷ — a factor of 1 335 907 |
| 16 | 2 000 addresses, 500/year growth, 24 bits | 2 823 of headroom; 5 years |
| 17 | failure rate 10⁻⁶, 34 devices, 24 bits | the crossover — the two terms are equal |
| 18 | the same at 46 bits | crossover at 1.41 × 10⁸ |
| 19 | same_prefix_observed | dominant_mechanism = merge; action 4 |
| 20 | virtual_interfaces with no shared prefix | action 3 — fix the generator |
Group 3 — recycling (9).
| # | Scenario | Expect |
|---|---|---|
| 21 | 10 teardowns/hour, 300 s aging | 360 s interval; 0.83 stale entries |
| 22 | 100/hour | 36 s; 8.33 stale |
| 23 | 1 000/hour | 3.6 s; 83.33 stale |
| 24 | 10 000/hour | 0.36 s; 833.33 stale |
| 25 | 833 stale in an 8k table | 10.2% of it, for stations that do not exist |
| 26 | 1 000/hour, 300 s aging | entry_may_be_inherited; aging_is_too_long |
| 27 | the same with the old holder transmitting | genuine_duplicate |
| 28 | 1 000/hour | recommended_aging_s = 36, against a default of 300 |
| 29 | zero churn | no inheritance; the default is fine |
Group 4 — the move tracker (10).
| # | Scenario | Expect |
|---|---|---|
| 30 | a learn on the stored port | not a move |
| 31 | a learn on a different port | is_move; c_moves increments |
| 32 | one move, then silence | no suspicion — a failover |
| 33 | eight moves inside 10 s | duplicate_suspected |
| 34 | eight moves across an hour | no suspicion — the count resets outside the window |
| 35 | a move after the window | move_count_o = 1 |
| 36 | fifteen moves | the count saturates and does not wrap |
| 37 | any move | previous_port_o holds what was overwritten |
| 38 | detector, 4 bits per entry | 524 288 BCE; 1.85 datapaths; 0.093% |
| 39 | detector, 36 bits per entry | 4 718 592 BCE; 16.65 datapaths; 37.5% of the table |
Group 5 — the shadow witness (9).
| # | Scenario | Expect |
|---|---|---|
| 40 | keyed_by_address at any setting | 0 — the whole design |
| 41 | one address, two ports, two events | two records — a keyed structure could hold one |
| 42 | 64 events | the log is full; wrapped not yet set |
| 43 | 65 events | wrapped; c_lost_to_wrap = 1 |
| 44 | 10 000 moves/s into a 64-entry log | wraps in 6.4 ms; the counter says how much was lost |
| 45 | a read of a valid entry | address, from-port, to-port, timestamp |
| 46 | a read of an invalid entry | r_valid low |
| 47 | clear | entries_used zero; everything invalid |
| 48 | 64 entries × 97 bits | 6 208 BCE; 0.022 datapaths |
Group 6 — telemetry and conformance (10).
| # | Scenario | Expect |
|---|---|---|
| 49 | a segment of globally assigned addresses | laa_dominant low; the correlation arithmetic applies |
| 50 | a segment of locally administered ones | laa_dominant high; the birthday arithmetic applies |
| 51 | a mixed segment, 60% LAA | laa_share_ppm = 600 000; laa_dominant |
| 52 | 2 moves per second | not pathological |
| 53 | 5 000 moves per second | move_rate_is_pathological |
| 54 | claims_unique with no history | v_claim_without_detection |
| 55 | a move, no record, no witness | v_evidence_overwritten |
| 56 | a generator that cannot report its entropy | v_unauditable_generator |
| 57 | a suspicion without both ports | v_alarm_without_ports |
| 58 | all five checks clear | conformant high |
22. Debugging a Duplicate
Six symptoms, and the first is the one that does not look like an address problem at all.
| Symptom | First question | Where to look |
|---|---|---|
| two stations losing half their inbound traffic, outbound fine | is one address flapping between two ports? | Section 4 — the inbound-only asymmetry is the signature |
| a forwarding entry changing port thousands of times a second | is this a duplicate or a reconvergence? | Section 11 — the rate test; one address or many? |
| duplicate alarms after a network merge | do the two deployments share an address prefix? | Section 5 — 4 000 plus 4 000 in one space is 85.2% |
| a duplicate between two machines from one delivery | are their serial numbers adjacent? | Section 6 — a programming failure, not chance |
| stale entries filling an access switch's table | what is the container churn rate? | Section 8 — 833 entries at 10 000 teardowns an hour |
| a detector producing thousands of alarms | does it reset counts on a move or decay them on a sweep? | Section 21's directed test |
Row one is where the investigation starts and almost nobody starts there. A duplicate presents as loss, not as an address error, and the distinguishing feature is that it is inbound-only and simultaneous on two machines — which no other common fault produces.
23. Misconceptions
Misconception 1 — "MAC addresses are globally unique."
The wrong model: the IEEE assigns prefixes, manufacturers assign the rest, and the result is unique everywhere.
What it costs: a design with no duplicate detection deployed into a fleet of virtual interfaces. Section 2: a generator that pins the first octet and a vendor prefix leaves 24 free bits, and the birthday bound over 2²⁴ at ten thousand addresses is 94.92% — against 7.11 × 10⁻⁷ over the space people quote, a factor of 1 335 907.
The corrected model: uniqueness is a property of the assignment process, not of the value. Chapter 5.3 §10 said so; this chapter prices what happens when the process is randomness over a space somebody quietly shrank.
Misconception 2 — "the birthday bound is conservative."
The wrong model: an idealised uniform model gives a safe upper estimate of the collision risk.
What it costs: confidence in a number that has four idealisations, all pointing the same way. Section 6: the space is smaller than assumed, the distribution is clustered by tool and default, the draws are not independent within a lot, and duplicates ship co-located. There is no term in the calculation that is conservative.
The corrected model: when a probability is computed over an idealised process, ask which direction each idealisation moves the answer. If they all move it the same way, it is a limit rather than an estimate.
Misconception 3 — "a duplicate means the two machines cannot communicate."
The wrong model: an address conflict breaks connectivity for both parties.
What it costs: looking for the wrong symptom. Section 4: the switch learns the address on whichever port spoke last, so roughly half of each station's inbound traffic goes to the other one — inbound-only loss, on both stations, simultaneously, with no error counter moving.
The corrected model: the signature is inbound-only loss on two machines at once, which almost nothing else produces. A congested path loses both ways; a duplex mismatch loses on the busy direction.
Misconception 4 — "forever."
The wrong model: once assigned, an address belongs to its holder permanently.
What it costs: a forwarding table full of entries for stations that no longer exist. Section 8: a fleet churning ten thousand interfaces an hour holds 833 stale entries at any instant against Chapter 12.5's 300-second aging — 10.2% of an 8k access-switch table — and each is an address that may already have been reissued.
The corrected model: the aging interval should be derived from the recycle interval, not from a convention. At 1 000 teardowns an hour the derivation gives 36 seconds against a default of 300, and no switch computes it because no switch is told the churn rate.
Misconception 5 — "if there were a duplicate, the switch would tell us."
The wrong model: a forwarding table is an authoritative record and would show the conflict.
What it costs: a fault that runs indefinitely. Section 12: a table indexed by the address cannot represent two stations sharing one — there is no state in which both are held. The duplicate appears as a rewrite, and a rewrite is the specified behaviour on a legitimate move.
The corrected model: the evidence is destroyed by the act that would have created it, and the only repair is a second witness. Section 20's class 116 is this, and it is the third member of the group Chapter 21.9 opened.
Misconception 6 — "detection would be expensive."
The wrong model: tracking address history across a 128k-entry table is a large amount of state.
What it costs: an investigation measured in days for want of 0.84% of a switch. Section 10: a 4-bit move counter, a 16-bit timestamp and a 16-bit previous port is 4 718 592 BCE — 16.65 datapaths, 37.5% of the table and 0.840% of the whole part — and a 64-entry shadow witness is another 0.001%.
The corrected model: an alarm costs 9.25 datapaths and an instruction costs 16.65, and the difference is whether the report names both ports.
24. Interview Questions
Six, with what a strong answer contains.
1. Are MAC addresses unique?
Globally assigned ones are unique by administrative claim and locally administered ones are unique only if whoever generated them made them so. A strong answer goes straight to the arithmetic: the birthday bound everybody quotes uses 46 free bits, and a real generator pins the first octet and a vendor prefix, leaving 24. At ten thousand addresses that is 94.92% against 7.11 × 10⁻⁷ — a factor of 1 335 907, and neither calculation is wrong; they are about different spaces.
2. What does a duplicate address actually look like?
A forwarding entry flapping between two ports at the two stations' combined transmission rate, and roughly half of each station's inbound traffic going to the other one. A strong answer names the signature — inbound-only loss, on both stations, simultaneously, with no error counter moving — and observes that nothing else common produces it. The best answers add that the switch is behaving exactly as specified: two stations are each telling the truth about where they are.
3. Why is the birthday bound optimistic rather than conservative here?
Four idealisations, all in the same direction. The space is smaller than the field width; the distribution is clustered by generator, tool and default; the draws within a manufacturing lot are not independent; and a lot ships to one customer, so a process duplicate arrives pre-paired on one broadcast domain. A strong answer gives the crossover: the process term is linear and the birthday term quadratic, so they cross at n = 2rN — thirty-four devices on a 24-bit space and 1.41 × 10⁸ on a 46-bit one.
4. How would a switch detect a duplicate?
Twenty bits beside each forwarding entry: a 4-bit move counter and a 16-bit timestamp, so the test is a rate rather than an occurrence. A strong answer explains why the rate matters — a failover, a migration, a LAG rebalance and a reconvergence all move an entry once — and prices it: 2 621 440 BCE, 9.25 datapaths, 0.466% of Chapter 23.3's switch. The best answers add the third field: sixteen more bits for the previous port takes it to 0.840% and turns an alarm into an instruction.
5. Why can't a switch simply assert that its table's addresses are unique?
Because the table is indexed by the address. There is no state in which two stations sharing one address are both represented — the second learn overwrites the first, and overwriting is the specified behaviour on a legitimate move. A strong answer names the recursion: distinguishing the fault needs the rewrite rate, the rate needs the previous event's timestamp, and the rewrite destroyed it. The repair is a second witness with no key.
6. What would you change about MAC-table aging?
Derive it. Three hundred seconds is a convention from a period when an address's holder changed when somebody moved a machine. In a fleet churning a thousand interfaces an hour the recycle interval is 3.6 seconds and the derived recommendation is about 36 — a small multiple of the recycle interval. A strong answer names the general habit: when a threshold protects against a delay, derive it from the delay rather than from the resource it sits in — which is Chapter 14.2 §9's argument about a 75% pause watermark, in a different mechanism.
25. Questions and Answers
26. What's Next
Module 25 has four chapters left and each takes a myth that costs real engineering time.
| Chapter | The myth | What it will have to derive |
|---|---|---|
| 25.3 | "CRC provides security" | Chapter 5.8's CRC is linear, so an attacker who changes the payload can compute the compensating change to the check sequence — the cost of the substitute is a MAC, and MACsec's is a known number |
| 25.4 | "VLANs improve performance" | Chapter 13.1's segmentation adds no bandwidth; what it changes is the broadcast domain's size, and Chapter 12.4's flood width is where the number is |
| 25.5 | "switches eliminate broadcasts" | Chapter 12.4 §6's replication to every port in the VLAN, and what unknown-unicast flooding costs when a table thrashes |
| 25.6 | "full duplex uses CSMA/CD" | Chapter 1.5 derived what full duplex removed from the MAC; the myth's cost is a slot-time argument applied where there is no slot |
And the pattern this chapter and Chapter 25.1 established will hold for all four: each myth is supported by ordinary experience and contradicted only by an arithmetic nobody has a reason to do — so the correction is never an assertion, it is a derivation with a number at the end of it.
Continue learning
Related tutorials
- Related topic
The 48-Bit MAC Address — OUI, I/G and U/L
The individual/group flag is the first bit of the frame body on the wire and the universal/local flag the second, so a receiver can select a matching pipeline 47 bit times before the address completes — and the address's global uniqueness is an administrative claim nothing enforces.
- Related topic
The MAC Table — CAM, Hashing, Capacity and Aging
An 8192-entry four-way table refuses inserts while 4096 slots are free, and holds 6592 addresses when asked for 8192. Capacity is a property of the hash, not of the memory.
- Related topic
"Switches Eliminate Broadcasts"
One flooding port takes 98.4% of a 64-port switch's frame budget, a 128k table refuses 25 607 of 131 072 addresses, and the occupancy counter reads 80.5% throughout.
- Related topic
PCIe vs Ethernet — Where the Cost of Overload Lands
The same overload into two fabrics: one stalled the sender 59,405 times and lost nothing, the other discarded 59,405 frames. That single choice explains why one needs TCP and the other does not.
Standards & specifications
- Governing standard
- IEEE Std 802.3 (Ethernet)(opens IEEE in a new tab)
Defines the Ethernet MAC, the media-independent interfaces and the physical-layer sublayers, including framing, access control, auto-negotiation and per-rate PHY specifications. VLAN tagging, priority and time-sensitive shaping are defined by IEEE 802.1, not by 802.3.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the Ethernet curriculum.
