Skip to content

Commit

Permalink
Rename the term Ticket Quality to Ticket Rank for better readability
Browse files Browse the repository at this point in the history
The Converge ticket selection mechanism previously used the term
"quality" to determine which ticket is better where the lower the
quality value the better the ticket.

Additionally, the term "quality" is already used in GPBFT for `QUALITY`
phase, which is a totally different thing.

To avoid terminology confusion, and for a more intuitive notion in the
context of ticket selection use the term "rank" instead, where instead
one would say: whichever ticket ranks first is better, i.e. the smaller
the value of rank the better the ticket.

While at it, update the docs on the compute function.
  • Loading branch information
masih committed Sep 5, 2024
1 parent ff0b6f5 commit 795542e
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 44 deletions.
21 changes: 10 additions & 11 deletions gpbft/gpbft.go
Original file line number Diff line number Diff line change
Expand Up @@ -1287,13 +1287,12 @@ type convergeState struct {
type ConvergeValue struct {
Chain ECChain
Justification *Justification

Quality float64
Rank float64
}

// IsOtherBetter returns true if the argument is better than self
func (cv *ConvergeValue) IsOtherBetter(cv2 ConvergeValue) bool {
return !cv.IsValid() || cv2.Quality < cv.Quality
func (cv *ConvergeValue) IsOtherBetter(other ConvergeValue) bool {
return !cv.IsValid() || other.Rank < cv.Rank
}

func (cv *ConvergeValue) IsValid() bool {
Expand All @@ -1318,7 +1317,7 @@ func (c *convergeState) SetSelfValue(value ECChain, justification *Justification
c.values[key] = ConvergeValue{
Chain: value,
Justification: justification,
Quality: math.Inf(1), // +Inf because any real ConvergeValue is better than self-value
Rank: math.Inf(1), // +Inf because any real ConvergeValue is better than self-value
}
}
}
Expand All @@ -1340,18 +1339,18 @@ func (c *convergeState) Receive(sender ActorID, table PowerTable, value ECChain,
senderPower, _ := table.Get(sender)

key := value.Key()
// Keep only the first justification and best ticket
// Keep only the first justification and best ticket.
if v, found := c.values[key]; !found {
c.values[key] = ConvergeValue{
Chain: value,
Justification: justification,
Quality: ComputeTicketQuality(ticket, senderPower),
Rank: ComputeTicketRank(ticket, senderPower),
}
} else {
newQual := ComputeTicketQuality(ticket, senderPower)
// best ticket is lowest
if newQual < v.Quality {
v.Quality = newQual
// The best ticket is the one that ranks first, i.e. smallest rank value.
rank := ComputeTicketRank(ticket, senderPower)
if rank < v.Rank {
v.Rank = rank
c.values[key] = v
}
}
Expand Down
37 changes: 22 additions & 15 deletions gpbft/ticket_quality.go → gpbft/ticket_rank.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,32 @@ import (
"golang.org/x/crypto/blake2b"
)

// ComputeTicketQuality computes the quality of the ticket.
// The lower the resulting quality the better.
// We take the ticket, hash it using Blake2b256, take the low 128 bits, interpret them as a Q.128
// fixed point number in range of [0, 1). Then we convert this uniform distribution into exponential one,
// using -log(x) inverse distribution function.
// The exponential distribution has a property where minimum of two exponentially distributed random
// variables is itself a exponentially distributed.
// This allows us to use the rate parameter to weight across different participants according to there power.
// This ends up being `-log(ticket) / power` where ticket is [0, 1).
// We additionally use log-base-2 instead of natural logarithm as it is easier to implement,
// and it is just a linear factor on all tickets, meaning it does not influence their ordering.
func ComputeTicketQuality(ticket []byte, power int64) float64 {
if power <= 0 {
// ComputeTicketRank computes a rank for the given ticket, weighted by a
// participant's power. A lower rank value indicates a better ranking. The
// process involves the following steps:
//
// 1. Hash the ticket using the Blake2b256 hash function.
// 2. Extract the low 128 bits from the hash and interpret them as a Q.128
// fixed-point number in the range [0, 1).
// 3. Convert this uniform distribution to an exponential distribution using
// the inverse distribution function -log(x).
//
// The exponential distribution has the property that the minimum of two
// exponentially distributed random variables is also exponentially distributed.
// This allows us to weight ranks according to the participant's power by using
// the formula: `-log(ticket) / power`, where `ticket` is in the range [0, 1).
//
// We use a base-2 logarithm instead of a natural logarithm for ease of
// implementation. The choice of logarithm base only affects all ranks linearly
// and does not alter their order.
func ComputeTicketRank(ticket Ticket, scaledPower int64) float64 {
if scaledPower <= 0 {
return math.Inf(1)
}
// we could use Blake2b-128 but 256 is more common and more widely supported
ticketHash := blake2b.Sum256(ticket)
quality := linearToExpDist(ticketHash[:16])
return quality / float64(power)
rank := linearToExpDist(ticketHash[:16])
return rank / float64(scaledPower)
}

// ticket should be 16 bytes
Expand Down
36 changes: 18 additions & 18 deletions gpbft/ticket_quality_test.go → gpbft/ticket_rank_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"golang.org/x/exp/rand"
)

func TestTQ_BigLog2_Table(t *testing.T) {
func TestTicketRank_BigLog2(t *testing.T) {
tests := []struct {
name string
input string
Expand Down Expand Up @@ -65,48 +65,48 @@ func FuzzTQ_LinearToExp(f *testing.F) {
})
}

func TestComputeTicketQuality(t *testing.T) {
func TestComputeTicketRank(t *testing.T) {
t.Run("Non-zero for non-zero power", func(t *testing.T) {
ticket := generateTicket(t)
power := int64(10)
quality := ComputeTicketQuality(ticket, power)
require.Greater(t, quality, 0.0, "Expected positive quality value, got %f", quality)
rank := ComputeTicketRank(ticket, power)
require.Greater(t, rank, 0.0, "Expected positive rank value, got %f", rank)
})

t.Run("Weighed by power", func(t *testing.T) {
ticket := generateTicket(t)
quality1 := ComputeTicketQuality(ticket, 10)
quality2 := ComputeTicketQuality(ticket, 11)
require.Less(t, quality2, quality1, "Expected quality2 to be less than quality1 due to weight by power, got quality1=%f, quality2=%f", quality1, quality2)
rank1 := ComputeTicketRank(ticket, 10)
rank2 := ComputeTicketRank(ticket, 11)
require.Less(t, rank2, rank1, "Expected rank2 to be less than rank1 due to weight by power, got rank1=%f, rank2=%f", rank1, rank2)
})

t.Run("Zero power is handled gracefully", func(t *testing.T) {
ticket := generateTicket(t)
quality := ComputeTicketQuality(ticket, 0)
require.True(t, math.IsInf(quality, 1), "Expected quality to be infinity with power 0, got %f", quality)
rank := ComputeTicketRank(ticket, 0)
require.True(t, math.IsInf(rank, 1), "Expected rank to be infinity with power 0, got %f", rank)
})

t.Run("Negative power is handled gracefully", func(t *testing.T) {
ticket := generateTicket(t)
quality := ComputeTicketQuality(ticket, -5)
require.True(t, math.IsInf(quality, 1), "Expected quality to be infinity for negative power, got %f", quality)
rank := ComputeTicketRank(ticket, -5)
require.True(t, math.IsInf(rank, 1), "Expected rank to be infinity for negative power, got %f", rank)
})

t.Run("Different tickets should have different qualities", func(t *testing.T) {
quality1 := ComputeTicketQuality(generateTicket(t), 1413)
quality2 := ComputeTicketQuality(generateTicket(t), 1413)
require.NotEqual(t, quality1, quality2, "Expected different qualities for different tickets, got quality1=%f, quality2=%f", quality1, quality2)
rank1 := ComputeTicketRank(generateTicket(t), 1413)
rank2 := ComputeTicketRank(generateTicket(t), 1413)
require.NotEqual(t, rank1, rank2, "Expected different qualities for different tickets, got rank1=%f, rank2=%f", rank1, rank2)
})

t.Run("Tickets with same 16 byte prefix should different quality", func(t *testing.T) {
t.Run("Tickets with same 16 byte prefix should different rank", func(t *testing.T) {
prefix := generateTicket(t)
ticket1 := append(prefix, 14)
ticket2 := append(prefix, 13)
require.NotEqual(t, ticket1, ticket2)

quality1 := ComputeTicketQuality(ticket1, 1413)
quality2 := ComputeTicketQuality(ticket2, 1413)
require.NotEqual(t, quality1, quality2, "Expected different qualities for different tickets with the same 16 byte prefix, got quality1=%f, quality2=%f", quality1, quality2)
rank1 := ComputeTicketRank(ticket1, 1413)
rank2 := ComputeTicketRank(ticket2, 1413)
require.NotEqual(t, rank1, rank2, "Expected different qualities for different tickets with the same 16 byte prefix, got rank1=%f, rank2=%f", rank1, rank2)
})
}

Expand Down

0 comments on commit 795542e

Please sign in to comment.