4.3.2. PoENF Consensus Algorithm

Given current transactions pool *TX*, the validator *vi* ∈ *D* chooses all ENF-proof transactions generated by committee members to construct an ENF-proof transactions list *TXENF* = {*tx*1, *tx*2, ..., *txK*}, where *K* is the committee size. An ENF proof is a vector *E* = {*<sup>e</sup>*1,*e*2, ...,*ed*}, where *ei* ∈ R is the ENF sample value, and *d* is the samples' size. By using *swarm*\_*hash* that is stored in the *data* parameter of *txk*, the *Ek* sent by *vk* can be fetched from off-chain storage. Thus, each *vi* can locally maintain a set of collected ENF proof vectors *Gi* = {*<sup>E</sup>*1, *E*2, ..., *Ek*}, where *k* ≤ *K*.

In order to become a slot leader and propose a new block in the current block proposal round, *vi* must show that its ENF proof *Ei* can solve a PoENF puzzle problem. Intuitively, the goal of PoENF puzzle problem is to choose the *Ek* that deviates the least from all ENF proofs in *Gi* based on their relative distances, which are computed with the Euclidean norm. However, a single Byzantine validator can force the PoENF algorithm to choose any arbitrary ENF proof by sending a poisoned *Eb* that is too far away from other ENF proofs. Therefore, our PoENF algorithm adopts the Krum aggregation rule to provide the (*<sup>α</sup>*, *f*)-Byzantine resilience property [39].

For each *vi* ∈ *D*, let *Gi* = {*<sup>E</sup>*1, *E*2, ..., *En*} include *n* ≥ 2 *f* + 3 collected ENF proofs from PoENF committee members, and at most only *f* is sent by Byzantine nodes. For any *i* = *j*, let *i* → *j* denote the fact that *Ej* belongs to the *n* − *f* − 2 closest ENF proofs to *Ei*. Then, we define the ENF score for *vi*.

$$s(i) = \sum\_{i \to j} ||E\_i - E\_j||^2. \tag{1}$$

Equation (1) calculates ENF scores (*s*(1), ...,*<sup>s</sup>*(*n*)) associated with validators *v*1 to *vn*, respectively, and applies the Krum rule to select the minimum ENF score as follows.

$$s^\* = \underset{i \in \{1, \ldots, n\}}{\text{argmin}} \left( s(i) \right). \tag{2}$$

Finally, the PoENF puzzle problem is formally defined as the following.

**Definition 4.** *Proof-of-ENF: Given Gi* = {*<sup>E</sup>*1, *E*2, ..., *En*} *collected by validator vi* ∈ *D, the process of PoENF verifies whether a valid ENF proof Ej can meet the condition s*(*j*) ≤ *<sup>s</sup>*<sup>∗</sup>*. If it does, vj wins the leader election and is qualified to propose a block; otherwise, the blocks generated by any vj are rejected.*

Given the above definitions, the PoENF-enabled block generation procedures are presented in Algorithm 1. During the current block generation round slot *slt*, each validator *vi* ∈ *D* executes the *generate*\_*block*() function to propose a candidate block according to its collected ENF proofs in the transactions pool. If the ENF score *si* is not greater than the target value *<sup>s</sup>*<sup>∗</sup>, then *vi* can create a new block and broadcast it to the network. Otherwise, they are only allowed to verify blocks from other validators until the current round finished. The closer *si* is to *<sup>s</sup>*<sup>∗</sup>, the higher probability that *vi* can propose a new block.

### **Algorithm 1** The PoENF-based block generation procedures.

```
1: procedure: generate_block(vi)
 2: hc ← H(head(C))
 3: height ← head(C).height + 1
 4: mt
          _
            root ← MTree(vi.TX)
 5: [E1, E2, ..., En] ← ENF_vect(vi.TX)
 6: enf _score ← []
 7: for Ei in [E1, E2, ..., En] do
 8: si ← ∑i→j Ei − Ej2
 9: enf _score.append(si)
10: end for
11: s∗ ← Min(enf _score)
12: if si ≤ s∗ then
13: new_block ← (hc||mt_root||vi.TX||vi.pk||sl.t||height)
14: σi ← Sign(new_block, vi.sk)
15: return (new_block||σi)
16: end if
17: procedure: verify_block(new_block, σj)
18: if Verify_Sign(new_block, σj) = True OR
19: Verify_TX(new_block) = True then
20: return False
21: end if
22: hc ← H(head(C))
23: if new
              _
                block.height = head(C).height + 1 OR
24: new
               _
                block.hc = hc then
25: return False
26: end if
27: [E1, E2, ..., En] ← ENF_vect(new_block.tx_list)
28: enf _score ← []
29: for Ei in [E1, E2, ..., En] do
30: si ← ∑i→j Ei − Ej2
31: enf _score.append(si)
32: end for
33: s∗ ← Min(enf _score)
34: if sj > s∗ then
35: return False
36: end if
37: return True
```
In block verification process, each validator calls the *veri f y*\_*block*() function to determine whether or not the received *new* \_ *block* can be accepted and appended to chain. The *Veri f y*\_*Sign*() checks if a *new*\_*block* sent by *vj* has a valid signature *<sup>σ</sup>j*, while *Veri f y*\_*TX*() validates that all transactions recorded in *new* \_ *block* are sent from valid nodes and have the same Merkle tree root as *new* \_ *block*.*mt* \_ *root*. After validating that *new*\_*block* is generated in the current round slot *slt* with the correct chain header, a PoENF algorithm verifies if *vj* has a valid ENF proof with minimum score. If all conditions are satisfied, the *new*\_*block* is accepted into confirmed status, and *vi* updates the head of local chain as *head*(C) = *Bi*+<sup>1</sup> accordingly. Otherwise, the *new*\_*block* is rejected and discarded.
