-
Notifications
You must be signed in to change notification settings - Fork 33
forwarding ability: introduce db helpers #240
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package chanevents | ||
|
|
||
| import ( | ||
| "errors" | ||
| "sort" | ||
|
|
||
| "golang.org/x/exp/constraints" | ||
| ) | ||
|
|
||
| // number is the type constraint Quantile accepts: any sortable numeric type. | ||
| type number interface { | ||
| constraints.Integer | constraints.Float | ||
| } | ||
|
|
||
| // Quantile computes the q-quantile of a slice of comparable values. This can be | ||
| // used to compute the median (q=0.5) or the min (q=0) or max (q=1). | ||
| func Quantile[T number](xs []T, q float64) (float64, error) { | ||
| if q < 0 || q > 1 { | ||
| return 0, errors.New("quantile must be between 0 and 1") | ||
| } | ||
|
|
||
| if len(xs) == 0 { | ||
| return 0, errors.New("cannot compute quantile of empty slice") | ||
| } | ||
|
|
||
| if len(xs) == 1 { | ||
| return float64(xs[0]), nil | ||
| } | ||
|
|
||
| // Create a copy of the slice to avoid mutating the original. | ||
| ys := make([]T, len(xs)) | ||
| copy(ys, xs) | ||
|
|
||
| sort.Slice(ys, func(i, j int) bool { | ||
| return ys[i] < ys[j] | ||
| }) | ||
|
|
||
| // Compute fractional index of q-quantile. | ||
| if q == 1.0 { | ||
| return float64(ys[len(ys)-1]), nil | ||
| } | ||
| i := q * float64(len(ys)-1) | ||
|
|
||
| // Interpolate between the two consecutive values, depending on the | ||
| // fractional index position in between. | ||
| lowerIdx := int(i) | ||
| upperIdx := lowerIdx + 1 | ||
|
|
||
| lowerVal := float64(ys[lowerIdx]) | ||
| upperVal := float64(ys[upperIdx]) | ||
|
|
||
| indexDiff := i - float64(lowerIdx) | ||
|
|
||
| return lowerVal + (upperVal-lowerVal)*indexDiff, nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| package chanevents | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| // TestQuantile pins the interpolation contract and the error paths Quantile | ||
| // surfaces to callers. | ||
| func TestQuantile(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| q float64 | ||
| xs []float64 | ||
| want float64 | ||
| expectErr bool | ||
| }{ | ||
| { | ||
| name: "empty slice", | ||
| xs: []float64{}, | ||
| expectErr: true, | ||
| }, | ||
| { | ||
| name: "single value", | ||
| xs: []float64{ | ||
| 1, | ||
| }, | ||
| want: 1.0, | ||
| }, | ||
| { | ||
| name: "single value median", | ||
| xs: []float64{ | ||
| 1, | ||
| }, | ||
| q: 0.5, | ||
| want: 1.0, | ||
| }, | ||
| { | ||
| name: "quantile out of bound below", | ||
| xs: []float64{}, | ||
| q: -0.1, | ||
| expectErr: true, | ||
| }, | ||
| { | ||
| name: "quantile out of bound above", | ||
| xs: []float64{}, | ||
| q: 1.1, | ||
| expectErr: true, | ||
| }, | ||
| { | ||
| name: "median odd values", | ||
| q: 0.5, | ||
| xs: []float64{ | ||
| 1, | ||
| 2, | ||
| 3, | ||
| 4, | ||
| 5, | ||
| }, | ||
| want: 3.0, | ||
| }, | ||
| { | ||
| name: "median even values", | ||
| q: 0.5, | ||
| xs: []float64{ | ||
| 1, | ||
| 2, | ||
| 3, | ||
| 4, | ||
| }, | ||
| want: 2.5, | ||
| }, | ||
| { | ||
| name: "median unsorted", | ||
| q: 0.5, | ||
| xs: []float64{ | ||
| 1, | ||
| 3, | ||
| 2, | ||
| 4, | ||
| }, | ||
| want: 2.5, | ||
| }, | ||
| { | ||
| name: "0 percentile", | ||
| q: 0, | ||
| xs: []float64{ | ||
| 1, | ||
| 2, | ||
| 3, | ||
| 4, | ||
| 5, | ||
| }, | ||
| want: 1.0, | ||
| }, | ||
| { | ||
| name: "25 percentile", | ||
| q: 0.25, | ||
| xs: []float64{ | ||
| 1, | ||
| 2, | ||
| 3, | ||
| 4, | ||
| 5, | ||
| }, | ||
| want: 2.0, | ||
| }, | ||
| { | ||
| name: "75 percentile", | ||
| q: 0.75, | ||
| xs: []float64{ | ||
| 1, | ||
| 2, | ||
| 3, | ||
| 4, | ||
| 5, | ||
| }, | ||
| want: 4.0, | ||
| }, | ||
| { | ||
| name: "0.875 percentile", | ||
| q: 0.875, | ||
| xs: []float64{ | ||
| 1, | ||
| 2, | ||
| 3, | ||
| 4, | ||
| 5, | ||
| }, | ||
| want: 4.5, | ||
| }, | ||
| { | ||
| name: "100 percentile", | ||
| q: 1.0, | ||
| xs: []float64{ | ||
| 1, | ||
| 2, | ||
| 3, | ||
| 4, | ||
| 5, | ||
| }, | ||
| want: 5.0, | ||
| }, | ||
| } | ||
|
|
||
| for _, tc := range tests { | ||
| t.Run(tc.name, func(tt *testing.T) { | ||
| tt.Parallel() | ||
|
|
||
| got, err := Quantile(tc.xs, tc.q) | ||
| if tc.expectErr { | ||
| require.Error(tt, err) | ||
| return | ||
| } | ||
|
|
||
| require.InDelta(tt, tc.want, got, 1e-6) | ||
| }) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,6 +43,14 @@ type Queries interface { | |
|
|
||
| GetChannelEvents(ctx context.Context, | ||
| arg sqlc.GetChannelEventsParams) ([]sqlc.ChannelEvent, error) | ||
|
|
||
| GetLatestChannelEventBefore(ctx context.Context, | ||
| arg sqlc.GetLatestChannelEventBeforeParams) ( | ||
| sqlc.ChannelEvent, | ||
| error, | ||
| ) | ||
|
|
||
| GetChannels(ctx context.Context) ([]sqlc.GetChannelsRow, error) | ||
| } | ||
|
|
||
| // Store provides access to the db for channel events. | ||
|
|
@@ -184,6 +192,30 @@ func (s *Store) GetChannel(ctx context.Context, channelPoint string) (*Channel, | |
| }, nil | ||
| } | ||
|
|
||
| // GetChannelByShortChanID retrieves a channel by its short channel ID, | ||
| // returning ErrUnknownChannel if no row matches. | ||
| func (s *Store) GetChannelByShortChanID(ctx context.Context, | ||
| shortChannelID uint64) (*Channel, error) { | ||
|
|
||
| dbChannel, err := s.db.GetChannelByShortChanID( | ||
| ctx, scidToInt64(shortChannelID), | ||
| ) | ||
| if err != nil { | ||
| if errors.Is(err, sql.ErrNoRows) { | ||
| return nil, ErrUnknownChannel | ||
| } | ||
|
|
||
| return nil, err | ||
| } | ||
|
|
||
| return &Channel{ | ||
| ID: dbChannel.ID, | ||
| ChannelPoint: dbChannel.ChannelPoint, | ||
| ShortChannelID: int64ToSCID(dbChannel.ShortChannelID), | ||
| PeerID: dbChannel.PeerID, | ||
| }, nil | ||
| } | ||
|
|
||
| // AddChannelEvent adds a new channel event. | ||
| func (s *Store) AddChannelEvent(ctx context.Context, | ||
| event *ChannelEvent) error { | ||
|
|
@@ -257,6 +289,54 @@ func (s *Store) GetChannelEvents(ctx context.Context, channelID, afterID int64, | |
| return events, nil | ||
| } | ||
|
|
||
| // ScidToPeerMap returns the historic scid→peer index, including channels that | ||
| // have since closed. Unconfirmed channels (scid still zero) are not part of | ||
| // the contract. | ||
| func (s *Store) ScidToPeerMap(ctx context.Context) (map[uint64]string, error) { | ||
| dbChannels, err := s.db.GetChannels(ctx) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| scidToPeer := make(map[uint64]string, len(dbChannels)) | ||
| for _, dbChannel := range dbChannels { | ||
| // The short channel ID can be zero if it's not known yet. We | ||
| // should just ignore those. | ||
| if dbChannel.ShortChannelID == 0 { | ||
| continue | ||
| } | ||
|
|
||
| scidToPeer[uint64(dbChannel.ShortChannelID)] = dbChannel.Pubkey | ||
| } | ||
|
|
||
| return scidToPeer, nil | ||
| } | ||
|
|
||
| // GetLatestChannelUpdateBefore returns the latest channel event before a given | ||
| // time (exclusive). If no event is found, it returns (nil, nil). | ||
| func (s *Store) GetLatestChannelUpdateBefore(ctx context.Context, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could potentially make it optional to specify which event type this function uses (and then rename the function)?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I skipped this as there's only a single caller right now. |
||
| channelID int64, before time.Time) (*ChannelEvent, error) { | ||
|
|
||
| dbEvent, err := s.db.GetLatestChannelEventBefore( | ||
| ctx, sqlc.GetLatestChannelEventBeforeParams{ | ||
| ChannelID: channelID, | ||
| Timestamp: before.UTC(), | ||
| EventType: int16(EventTypeUpdate), | ||
| }, | ||
| ) | ||
| if err != nil { | ||
| // If there are no events before the start time, we return (nil, | ||
| // nil). | ||
| if errors.Is(err, sql.ErrNoRows) { | ||
| return nil, nil | ||
| } | ||
|
|
||
| return nil, err | ||
| } | ||
|
|
||
| return marshalChannelEvent(dbEvent), nil | ||
| } | ||
|
|
||
| // marshalChannelEvent converts a db channel event into our internal type. | ||
| func marshalChannelEvent(dbEvent sqlc.ChannelEvent) *ChannelEvent { | ||
| var localBalance fn.Option[btcutil.Amount] | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.