Merge branch 'develop' into feature/bitcoin-indexer

This commit is contained in:
Gaze
2024-04-09 23:16:19 +07:00
18 changed files with 502 additions and 712 deletions

View File

@@ -6,7 +6,9 @@ type ErrorKind string
const (
// NotFound is returned when a requested item is not found.
NotFound = ErrorKind("Not Found")
NotFound = ErrorKind("Not Found")
OverflowUint64 = ErrorKind("overflow uint64")
OverflowUint128 = ErrorKind("overflow uint128")
)
// Error satisfies the error interface and prints human-readable errors.

1
go.mod
View File

@@ -8,6 +8,7 @@ require (
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
github.com/cockroachdb/errors v1.11.1
github.com/gaze-network/uint128 v1.2.0
github.com/jackc/pgx/v5 v5.5.5
github.com/samber/lo v1.39.0
github.com/stretchr/testify v1.8.1

2
go.sum
View File

@@ -47,6 +47,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gaze-network/uint128 v1.2.0 h1:LRruR+EvzNv/eJg8nk0hztMJ4tOpYKolFYlQZqNvWmo=
github.com/gaze-network/uint128 v1.2.0/go.mod h1:zAwwcnoRUNiiQj0vjLmHgNgJ+w2RUgzMAJgl8d7tRug=
github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0=
github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=

47
lib/leb128/leb128.go Normal file
View File

@@ -0,0 +1,47 @@
package leb128
import (
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/uint128"
)
const (
ErrEmpty = errs.ErrorKind("leb128: empty byte sequence")
ErrUnterminated = errs.ErrorKind("leb128: unterminated byte sequence")
)
func EncodeLEB128(input uint128.Uint128) []byte {
bytes := make([]byte, 0)
// for n >> 7 > 0
for !input.Rsh(7).IsZero() {
last_7_bits := input.And64(0b0111_1111).Uint8()
bytes = append(bytes, last_7_bits|0b1000_0000)
input = input.Rsh(7)
}
last_byte := input.Uint8()
bytes = append(bytes, last_byte)
return bytes
}
func DecodeLEB128(data []byte) (n uint128.Uint128, length int, err error) {
if len(data) == 0 {
return uint128.Uint128{}, 0, ErrEmpty
}
n = uint128.From64(0)
for i, b := range data {
if i > 18 {
return uint128.Uint128{}, 0, errs.OverflowUint128
}
value := uint128.New(uint64(b&0b0111_1111), 0)
if i == 18 && !value.And64(0b0111_1100).IsZero() {
return uint128.Uint128{}, 0, errs.OverflowUint128
}
n = n.Or(value.Lsh(uint(7 * i)))
// if the high bit is not set, then this is the last byte
if b&0b1000_0000 == 0 {
return n, i + 1, nil
}
}
return uint128.Uint128{}, 0, ErrUnterminated
}

83
lib/leb128/leb128_test.go Normal file
View File

@@ -0,0 +1,83 @@
package leb128
import (
"testing"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/uint128"
"github.com/stretchr/testify/assert"
)
func TestRoundTrip(t *testing.T) {
test := func(n uint128.Uint128) {
t.Run(n.String(), func(t *testing.T) {
t.Parallel()
encoded := EncodeLEB128(n)
decoded, length, err := DecodeLEB128(encoded)
assert.NoError(t, err)
assert.Equal(t, n, decoded)
assert.Equal(t, len(encoded), length)
})
}
test(uint128.Zero)
// powers of two
for i := 0; i < 128; i++ {
n := uint128.From64(1)
n = n.Lsh(uint(i))
test(n)
}
// alternating bits
n := uint128.Zero
for i := 0; i < 128; i++ {
n = n.Lsh(1).Or(uint128.From64(uint64(i % 2)))
test(n)
}
}
func TestDecodeError(t *testing.T) {
testError := func(name string, bytes []byte, expectedError error) {
t.Run(name, func(t *testing.T) {
t.Parallel()
_, _, err := DecodeLEB128(bytes)
if expectedError == nil {
assert.NoError(t, err)
} else {
assert.ErrorIs(t, err, expectedError)
}
})
}
testError("empty", []byte{}, ErrEmpty)
testError("unterminated", []byte{0b1000_0000}, ErrUnterminated)
// may not be longer than 19 bytes
testError("valid 18 bytes", []byte{
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 0,
}, nil)
testError("overflow 19 bytes", []byte{
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128,
128, 0,
}, errs.OverflowUint128)
// may not overflow uint128
testError("overflow 1", []byte{
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 64,
}, errs.OverflowUint128)
testError("overflow 2", []byte{
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 32,
}, errs.OverflowUint128)
testError("overflow 3", []byte{
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 16,
}, errs.OverflowUint128)
testError("overflow 4", []byte{
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 8,
}, errs.OverflowUint128)
testError("overflow 5", []byte{
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 4,
}, errs.OverflowUint128)
testError("not overflow", []byte{
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 2,
}, nil)
}

View File

@@ -1,9 +0,0 @@
package runes
import "math/big"
type Edict struct {
Amount big.Int
Id RuneId
Output int
}

View File

@@ -1,53 +0,0 @@
package runes
import (
"math/big"
"github.com/Cleverse/go-utilities/utils"
)
type Terms struct {
// Amount of the rune to be minted per transaction
Amount *big.Int
// Number of allowed mints
Cap *big.Int
// Block height at which the rune can start being minted. If both HeightStart and OffsetStart are set, use the higher value.
HeightStart uint64
// Block height at which the rune can no longer be minted. If both HeightEnd and OffsetEnd are set, use the lower value.
HeightEnd uint64
// Offset from etched block at which the rune can start being minted. If both HeightStart and OffsetStart are set, use the higher value.
OffsetStart uint64
// Offset from etched block at which the rune can no longer be minted. If both HeightEnd and OffsetEnd are set, use the lower value.
OffsetEnd uint64
}
type Etching struct {
// Number of runes to be minted during etching
Premine *big.Int
// Rune name
Rune Rune
// Minting terms. If not provided, the rune is not mintable.
Terms *Terms
// Bitmap of spacers to be displayed between each letter of the rune name
Spacers uint32
// Single Unicode codepoint to represent the rune
Symbol rune
// Number of decimals when displaying the rune
Divisibility uint8
}
const (
maxDivisibility uint8 = 38
maxSpacers uint32 = 0b00000111_11111111_11111111_11111111
)
func (e Etching) Supply() *big.Int {
terms := utils.Default(e.Terms, &Terms{})
amount := utils.Default(terms.Amount, big.NewInt(0))
cap := utils.Default(terms.Cap, big.NewInt(0))
premine := utils.Default(e.Premine, big.NewInt(0))
result := new(big.Int).Mul(amount, cap)
return result.Add(result, premine)
}

View File

@@ -1,75 +0,0 @@
package runes
import (
"fmt"
"math/big"
"testing"
"github.com/stretchr/testify/assert"
)
// TODO: add maxSpacers test
func TestSupply(t *testing.T) {
testNumber := 0
test := func(e Etching, expectedSupply *big.Int) {
t.Run(fmt.Sprintf("case_%d", testNumber), func(t *testing.T) {
t.Parallel()
actualSupply := e.Supply()
assert.Equal(t, expectedSupply, actualSupply)
})
testNumber++
}
test(Etching{}, big.NewInt(0))
test(Etching{
Premine: big.NewInt(0),
Terms: nil,
}, big.NewInt(0))
test(Etching{
Premine: big.NewInt(1),
Terms: nil,
}, big.NewInt(1))
test(Etching{
Premine: big.NewInt(1),
Terms: &Terms{
Amount: nil,
Cap: nil,
},
}, big.NewInt(1))
test(Etching{
Premine: big.NewInt(1000),
Terms: &Terms{
Amount: big.NewInt(100),
Cap: big.NewInt(10),
},
}, big.NewInt(2000))
test(Etching{
Premine: nil,
Terms: &Terms{
Amount: big.NewInt(100),
Cap: big.NewInt(10),
},
}, big.NewInt(1000))
test(Etching{
Premine: big.NewInt(1000),
Terms: &Terms{
Amount: big.NewInt(100),
Cap: nil,
},
}, big.NewInt(1000))
test(Etching{
Premine: big.NewInt(1000),
Terms: &Terms{
Amount: nil,
Cap: big.NewInt(10),
},
}, big.NewInt(1000))
}

View File

@@ -1,164 +0,0 @@
package runes
import (
"math/big"
"slices"
"github.com/Cleverse/go-utilities/utils"
"github.com/gaze-network/indexer-network/common"
"github.com/gaze-network/indexer-network/common/errs"
)
type Rune big.Int
func NewRune(value int64) *Rune {
return (*Rune)(big.NewInt(value))
}
func NewRuneFromBigInt(value *big.Int) *Rune {
return (*Rune)(new(big.Int).Set(value))
}
var ErrInvalidBase10 = errs.ErrorKind("invalid base-10 character: must be in the range [0-9]")
// NewRuneFromString creates a new Rune from a string of base-10 integer
func NewRuneFromString(value string) (*Rune, error) {
bi, ok := new(big.Int).SetString(value, 10)
if !ok {
return nil, ErrInvalidBase10
}
return (*Rune)(bi), nil
}
var ErrInvalidBase26 = errs.ErrorKind("invalid base-26 character: must be in the range [A-Z]")
// NewRuneFromBase26 creates a new Rune from a string of modified base-26 integer
func NewRuneFromBase26(value string) (*Rune, error) {
x := big.NewInt(0)
one := big.NewInt(1)
int26 := big.NewInt(26)
for i, char := range value {
if i > 0 {
x = x.Add(x, one)
}
x = x.Mul(x, int26)
if char < 'A' || char > 'Z' {
return nil, ErrInvalidBase26
}
x = x.Add(x, big.NewInt(int64(char-'A')))
}
return (*Rune)(x), nil
}
var firstReservedRune = utils.Must(NewRuneFromString("6402364363415443603228541259936211926"))
var unlockSteps = []*big.Int{
utils.Must(new(big.Int).SetString("0", 10)), // A
utils.Must(new(big.Int).SetString("26", 10)), // AA
utils.Must(new(big.Int).SetString("702", 10)), // AAA
utils.Must(new(big.Int).SetString("18278", 10)), // AAAA
utils.Must(new(big.Int).SetString("475254", 10)), // AAAAA
utils.Must(new(big.Int).SetString("12356630", 10)), // AAAAAA
utils.Must(new(big.Int).SetString("321272406", 10)), // AAAAAAA
utils.Must(new(big.Int).SetString("8353082582", 10)), // AAAAAAAA
utils.Must(new(big.Int).SetString("217180147158", 10)), // AAAAAAAAA
utils.Must(new(big.Int).SetString("5646683826134", 10)), // AAAAAAAAAA
utils.Must(new(big.Int).SetString("146813779479510", 10)), // AAAAAAAAAAA
utils.Must(new(big.Int).SetString("3817158266467286", 10)), // AAAAAAAAAAAA
utils.Must(new(big.Int).SetString("99246114928149462", 10)), // AAAAAAAAAAAAA
utils.Must(new(big.Int).SetString("2580398988131886038", 10)), // AAAAAAAAAAAAAA
utils.Must(new(big.Int).SetString("67090373691429037014", 10)), // AAAAAAAAAAAAAAA
utils.Must(new(big.Int).SetString("1744349715977154962390", 10)), // AAAAAAAAAAAAAAAA
utils.Must(new(big.Int).SetString("45353092615406029022166", 10)), // AAAAAAAAAAAAAAAAA
utils.Must(new(big.Int).SetString("1179180408000556754576342", 10)), // AAAAAAAAAAAAAAAAAA
utils.Must(new(big.Int).SetString("30658690608014475618984918", 10)), // AAAAAAAAAAAAAAAAAAA
utils.Must(new(big.Int).SetString("797125955808376366093607894", 10)), // AAAAAAAAAAAAAAAAAAAA
utils.Must(new(big.Int).SetString("20725274851017785518433805270", 10)), // AAAAAAAAAAAAAAAAAAAAA
utils.Must(new(big.Int).SetString("538857146126462423479278937046", 10)), // AAAAAAAAAAAAAAAAAAAAAA
utils.Must(new(big.Int).SetString("14010285799288023010461252363222", 10)), // AAAAAAAAAAAAAAAAAAAAAAA
utils.Must(new(big.Int).SetString("364267430781488598271992561443798", 10)), // AAAAAAAAAAAAAAAAAAAAAAAA
utils.Must(new(big.Int).SetString("9470953200318703555071806597538774", 10)), // AAAAAAAAAAAAAAAAAAAAAAAAA
utils.Must(new(big.Int).SetString("246244783208286292431866971536008150", 10)), // AAAAAAAAAAAAAAAAAAAAAAAAAA
utils.Must(new(big.Int).SetString("6402364363415443603228541259936211926", 10)), // AAAAAAAAAAAAAAAAAAAAAAAAAAA
utils.Must(new(big.Int).SetString("166461473448801533683942072758341510102", 10)), // AAAAAAAAAAAAAAAAAAAAAAAAAAAA
}
func (r Rune) IsReserved() bool {
return (*big.Int)(&r).Cmp((*big.Int)(firstReservedRune)) >= 0
}
func (r Rune) Add(value *big.Int) *Rune {
bi := (*big.Int)(&r)
return (*Rune)(bi.Add(bi, value))
}
// Commitment returns the commitment of the rune. The commitment is the little-endian encoding of the rune.
func (r Rune) Commitment() []byte {
bytes := (*big.Int)(&r).Bytes()
slices.Reverse(bytes)
return bytes
}
// String returns the string representation of the rune in modified base-26 integer
func (r Rune) String() string {
chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
// value = r + 1
value := new(big.Int).Add((*big.Int)(&r), big.NewInt(1))
var encoded []byte
for value.Sign() > 0 {
// idx = (value - 1) % 26
idx := new(big.Int).Mod(new(big.Int).Sub(value, big.NewInt(1)), big.NewInt(26)).Int64()
encoded = append(encoded, chars[idx])
// value = (value - 1) / 26
value = new(big.Int).Div(new(big.Int).Sub(value, big.NewInt(1)), big.NewInt(26))
}
slices.Reverse(encoded)
return string(encoded)
}
func FirstRuneHeight(network common.Network) uint64 {
switch network {
case common.NetworkMainnet:
return common.HalvingInterval * 4
case common.NetworkTestnet:
return common.HalvingInterval * 12
}
panic("invalid network")
}
func MinimumRuneAtHeight(network common.Network, height uint64) *Rune {
offset := height + 1
interval := common.HalvingInterval / 12
// runes are gradually unlocked from rune activation height until the next halving
start := FirstRuneHeight(network)
end := start + common.HalvingInterval
if offset < start {
return (*Rune)(unlockSteps[12])
}
if offset >= end {
return (*Rune)(unlockSteps[0])
}
progress := offset - start
length := 12 - progress/uint64(interval)
startRune := unlockSteps[length]
endRune := unlockSteps[length-1] // length cannot be 0 because we checked that offset < end
remainder := big.NewInt(int64(progress) % int64(interval))
runeRange := new(big.Int).Sub(startRune, endRune)
result := new(big.Int).Mul(runeRange, remainder)
result = result.Div(result, big.NewInt(int64(interval)))
result = result.Sub(startRune, result)
return (*Rune)(result)
}
func GetReservedRune(blockHeight uint64, txIndex uint64) *Rune {
// firstReservedRune + ((blockHeight << 32) | txIndex)
increment := big.NewInt(int64(blockHeight))
increment = increment.Lsh(increment, 32)
increment = increment.Or(increment, big.NewInt(int64(txIndex)))
return firstReservedRune.Add(increment)
}

View File

@@ -1,73 +0,0 @@
package runes
import (
"strconv"
"strings"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
)
type RuneId struct {
BlockHeight uint64
TxIndex uint64
}
func NewRuneId(blockHeight uint64, txIndex uint64) RuneId {
return RuneId{
BlockHeight: blockHeight,
TxIndex: txIndex,
}
}
var (
ErrInvalidSeparator = errs.ErrorKind("invalid rune id: must contain exactly one separator")
ErrCannotParseBlockHeight = errs.ErrorKind("invalid rune id: cannot parse block height")
ErrCannotParseTxIndex = errs.ErrorKind("invalid rune id: cannot parse tx index")
)
func NewRuneIdFromString(str string) (RuneId, error) {
strs := strings.Split(str, ":")
if len(strs) != 2 {
return RuneId{}, ErrInvalidSeparator
}
blockHeightStr, txIndexStr := strs[0], strs[1]
blockHeight, err := strconv.ParseUint(blockHeightStr, 10, 64)
if err != nil {
return RuneId{}, errors.WithStack(errors.Join(err, ErrCannotParseBlockHeight))
}
txIndex, err := strconv.ParseUint(txIndexStr, 10, 64)
if err != nil {
return RuneId{}, errors.WithStack(errors.Join(err, ErrCannotParseTxIndex))
}
return RuneId{
BlockHeight: blockHeight,
TxIndex: txIndex,
}, nil
}
// Delta calculates the delta encoding between two RuneIds. If the two RuneIds are in the same block, then the block delta is 0 and the tx index delta is the difference between the two tx indices.
// If the two RuneIds are in different blocks, then the block delta is the difference between the two block indices and the tx index delta is the tx index in the other block.
func (r RuneId) Delta(next RuneId) (uint64, uint64) {
blockDelta := next.BlockHeight - r.BlockHeight
// if the block is the same, then tx index is the difference between the two
if blockDelta == 0 {
return 0, next.TxIndex - r.TxIndex
}
// otherwise, tx index is the tx index in the next block
return blockDelta, next.TxIndex
}
// Next calculates the next RuneId given a block delta and tx index delta.
func (r RuneId) Next(blockDelta uint64, txIndexDelta uint64) RuneId {
if blockDelta == 0 {
return RuneId{
BlockHeight: r.BlockHeight,
TxIndex: r.TxIndex + txIndexDelta,
}
}
return RuneId{
BlockHeight: r.BlockHeight + blockDelta,
TxIndex: txIndexDelta,
}
}

View File

@@ -1,83 +0,0 @@
package runes
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewRuneIdFromString(t *testing.T) {
type testcase struct {
name string
input string
expectedOutput RuneId
shouldError bool
}
// TODO: test error instance match expected errors
testcases := []testcase{
{
name: "valid rune id",
input: "1:2",
expectedOutput: RuneId{
BlockHeight: 1,
TxIndex: 2,
},
shouldError: false,
},
{
name: "too many separators",
input: "1:2:3",
expectedOutput: RuneId{},
shouldError: true,
},
{
name: "too few separators",
input: "1",
expectedOutput: RuneId{},
shouldError: true,
},
{
name: "invalid tx index",
input: "1:a",
expectedOutput: RuneId{},
shouldError: true,
},
{
name: "invalid block",
input: "a:1",
expectedOutput: RuneId{},
shouldError: true,
},
{
name: "empty tx index",
input: "1:",
expectedOutput: RuneId{},
shouldError: true,
},
{
name: "empty block",
input: ":1",
expectedOutput: RuneId{},
shouldError: true,
},
{
name: "empty block and tx index",
input: ":",
expectedOutput: RuneId{},
shouldError: true,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
runeId, err := NewRuneIdFromString(tc.input)
if tc.shouldError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expectedOutput, runeId)
}
})
}
}

View File

@@ -1,254 +0,0 @@
package runes
import (
"fmt"
"math"
"math/big"
"strings"
"testing"
"github.com/gaze-network/indexer-network/common"
"github.com/stretchr/testify/assert"
)
func TestString(t *testing.T) {
test := func(rune *Rune, encoded string) {
t.Run(encoded, func(t *testing.T) {
t.Parallel()
actualEncoded := rune.String()
assert.Equal(t, encoded, actualEncoded)
actualRune, err := NewRuneFromBase26(encoded)
assert.NoError(t, err)
assert.Equal(t, rune, actualRune)
})
}
test(NewRune(0), "A")
test(NewRune(1), "B")
test(NewRune(2), "C")
test(NewRune(3), "D")
test(NewRune(4), "E")
test(NewRune(5), "F")
test(NewRune(6), "G")
test(NewRune(7), "H")
test(NewRune(8), "I")
test(NewRune(9), "J")
test(NewRune(10), "K")
test(NewRune(11), "L")
test(NewRune(12), "M")
test(NewRune(13), "N")
test(NewRune(14), "O")
test(NewRune(15), "P")
test(NewRune(16), "Q")
test(NewRune(17), "R")
test(NewRune(18), "S")
test(NewRune(19), "T")
test(NewRune(20), "U")
test(NewRune(21), "V")
test(NewRune(22), "W")
test(NewRune(23), "X")
test(NewRune(24), "Y")
test(NewRune(25), "Z")
test(NewRune(26), "AA")
test(NewRune(27), "AB")
test(NewRune(51), "AZ")
test(NewRune(52), "BA")
test(NewRune(53), "BB")
maxUint128 := new(big.Int).Lsh(big.NewInt(1), 128)
maxUint128 = maxUint128.Sub(maxUint128, big.NewInt(1))
test(NewRuneFromBigInt(new(big.Int).Sub(maxUint128, big.NewInt(2))), "BCGDENLQRQWDSLRUGSNLBTMFIJAT")
test(NewRuneFromBigInt(new(big.Int).Sub(maxUint128, big.NewInt(1))), "BCGDENLQRQWDSLRUGSNLBTMFIJAU")
test(NewRuneFromBigInt(maxUint128), "BCGDENLQRQWDSLRUGSNLBTMFIJAV")
}
func TestNewRuneFromBase26Error(t *testing.T) {
_, err := NewRuneFromBase26("?")
assert.ErrorIs(t, err, ErrInvalidBase26)
}
func TestFirstRuneHeight(t *testing.T) {
test := func(network common.Network, expected uint64) {
t.Run(network.String(), func(t *testing.T) {
t.Parallel()
actual := FirstRuneHeight(network)
assert.Equal(t, expected, actual)
})
}
test(common.NetworkMainnet, 840_000)
test(common.NetworkTestnet, 2_520_000)
}
func TestMinimumRuneAtHeightMainnet(t *testing.T) {
test := func(height uint64, encoded string) {
t.Run(fmt.Sprintf("%d", height), func(t *testing.T) {
t.Parallel()
rune, err := NewRuneFromBase26(encoded)
assert.NoError(t, err)
actual := MinimumRuneAtHeight(common.NetworkMainnet, height)
assert.Equal(t, (*big.Int)(rune).String(), (*big.Int)(actual).String())
})
}
start := FirstRuneHeight(common.NetworkMainnet)
end := start + common.HalvingInterval
interval := uint64(common.HalvingInterval / 12)
test(0, "AAAAAAAAAAAAA")
test(start/2, "AAAAAAAAAAAAA")
test(start, "ZZYZXBRKWXVA")
test(start+1, "ZZXZUDIVTVQA")
test(end-1, "A")
test(end, "A")
test(end+1, "A")
test(math.MaxUint32, "A")
test(start+interval*0-1, "AAAAAAAAAAAAA")
test(start+interval*0, "ZZYZXBRKWXVA")
test(start+interval*0+1, "ZZXZUDIVTVQA")
test(start+interval*1-1, "AAAAAAAAAAAA")
test(start+interval*1, "ZZYZXBRKWXV")
test(start+interval*1+1, "ZZXZUDIVTVQ")
test(start+interval*2-1, "AAAAAAAAAAA")
test(start+interval*2, "ZZYZXBRKWY")
test(start+interval*2+1, "ZZXZUDIVTW")
test(start+interval*3-1, "AAAAAAAAAA")
test(start+interval*3, "ZZYZXBRKX")
test(start+interval*3+1, "ZZXZUDIVU")
test(start+interval*4-1, "AAAAAAAAA")
test(start+interval*4, "ZZYZXBRL")
test(start+interval*4+1, "ZZXZUDIW")
test(start+interval*5-1, "AAAAAAAA")
test(start+interval*5, "ZZYZXBS")
test(start+interval*5+1, "ZZXZUDJ")
test(start+interval*6-1, "AAAAAAA")
test(start+interval*6, "ZZYZXC")
test(start+interval*6+1, "ZZXZUE")
test(start+interval*7-1, "AAAAAA")
test(start+interval*7, "ZZYZY")
test(start+interval*7+1, "ZZXZV")
test(start+interval*8-1, "AAAAA")
test(start+interval*8, "ZZZA")
test(start+interval*8+1, "ZZYA")
test(start+interval*9-1, "AAAA")
test(start+interval*9, "ZZZ")
test(start+interval*9+1, "ZZY")
test(start+interval*10-2, "AAC")
test(start+interval*10-1, "AAA")
test(start+interval*10, "AAA")
test(start+interval*10+1, "AAA")
test(start+interval*10+interval/2, "NA")
test(start+interval*11-2, "AB")
test(start+interval*11-1, "AA")
test(start+interval*11, "AA")
test(start+interval*11+1, "AA")
test(start+interval*11+interval/2, "N")
test(start+interval*12-2, "B")
test(start+interval*12-1, "A")
test(start+interval*12, "A")
test(start+interval*12+1, "A")
}
func TestMinimumRuneAtHeightTestnet(t *testing.T) {
test := func(height uint64, runeStr string) {
t.Run(fmt.Sprintf("%d", height), func(t *testing.T) {
t.Parallel()
rune, err := NewRuneFromBase26(runeStr)
assert.NoError(t, err)
actual := MinimumRuneAtHeight(common.NetworkTestnet, height)
assert.Equal(t, rune, actual)
})
}
start := FirstRuneHeight(common.NetworkTestnet)
test(start-1, "AAAAAAAAAAAAA")
test(start, "ZZYZXBRKWXVA")
test(start+1, "ZZXZUDIVTVQA")
}
func TestIsReserved(t *testing.T) {
test := func(runeStr string, expected bool) {
t.Run(runeStr, func(t *testing.T) {
t.Parallel()
rune, err := NewRuneFromBase26(runeStr)
assert.NoError(t, err)
actual := rune.IsReserved()
assert.Equal(t, expected, actual)
})
}
test("A", false)
test("B", false)
test("ZZZZZZZZZZZZZZZZZZZZZZZZZZ", false)
test("AAAAAAAAAAAAAAAAAAAAAAAAAAA", true)
test("AAAAAAAAAAAAAAAAAAAAAAAAAAB", true)
test("BCGDENLQRQWDSLRUGSNLBTMFIJAV", true)
}
func TestGetReservedRune(t *testing.T) {
test := func(blockHeight uint64, txIndex uint64, expected *Rune) {
t.Run(fmt.Sprintf("blockHeight_%d_txIndex_%d", blockHeight, txIndex), func(t *testing.T) {
t.Parallel()
rune := GetReservedRune(blockHeight, txIndex)
assert.Equal(t, expected, rune)
})
}
test(0, 0, firstReservedRune)
test(0, 1, firstReservedRune.Add(big.NewInt(1)))
test(0, 2, firstReservedRune.Add(big.NewInt(2)))
test(1, 0, firstReservedRune.Add(new(big.Int).Lsh(big.NewInt(1), 32)))
test(1, 1, firstReservedRune.Add(new(big.Int).Lsh(big.NewInt(1), 32)).Add(big.NewInt(1)))
test(1, 2, firstReservedRune.Add(new(big.Int).Lsh(big.NewInt(1), 32)).Add(big.NewInt(2)))
test(2, 0, firstReservedRune.Add(new(big.Int).Lsh(big.NewInt(2), 32)))
test(2, 1, firstReservedRune.Add(new(big.Int).Lsh(big.NewInt(2), 32)).Add(big.NewInt(1)))
test(2, 2, firstReservedRune.Add(new(big.Int).Lsh(big.NewInt(2), 32)).Add(big.NewInt(2)))
test(math.MaxInt64, 0, firstReservedRune.Add(new(big.Int).Lsh(big.NewInt(math.MaxInt64), 32)))
test(math.MaxInt64, math.MaxInt64, firstReservedRune.Add(new(big.Int).Lsh(big.NewInt(math.MaxInt64), 32)).Add(big.NewInt(math.MaxInt64)))
}
func TestUnlockSteps(t *testing.T) {
for i := 0; i < len(unlockSteps); i++ {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
t.Parallel()
encoded := (*Rune)(unlockSteps[i]).String()
expected := strings.Repeat("A", i+1)
assert.Equal(t, expected, encoded)
})
}
}
func TestCommitment(t *testing.T) {
test := func(rune *Rune, expected []byte) {
t.Run((*big.Int)(rune).String(), func(t *testing.T) {
t.Parallel()
actual := rune.Commitment()
assert.Equal(t, expected, actual)
})
}
test(NewRune(0), []byte{})
test(NewRune(1), []byte{1})
test(NewRune(2), []byte{2})
test(NewRune(255), []byte{255})
test(NewRune(256), []byte{0, 1})
test(NewRune(257), []byte{1, 1})
test(NewRune(65535), []byte{255, 255})
test(NewRune(65536), []byte{0, 0, 1})
}

14
pkg/logger/attrs_keys.go Normal file
View File

@@ -0,0 +1,14 @@
package logger
import "log/slog"
// Keys for log attributes.
const (
TimeKey = slog.TimeKey
LevelKey = slog.LevelKey
MessageKey = slog.MessageKey
SourceKey = slog.SourceKey
ErrorKey = "error"
ErrorVerboseKey = "error_verbose"
ErrorStackTraceKey = "error_stacktrace"
)

View File

@@ -0,0 +1,49 @@
package logger
import (
"context"
"log/slog"
)
type (
handleFunc func(context.Context, slog.Record) error
middleware func(handleFunc) handleFunc
)
type chainHandlers struct {
h slog.Handler
middlewares []middleware
}
func newChainHandlers(handler slog.Handler, middlewares ...middleware) *chainHandlers {
return &chainHandlers{
h: handler,
middlewares: middlewares,
}
}
func (c *chainHandlers) Enabled(ctx context.Context, lvl slog.Level) bool {
return c.h.Enabled(ctx, lvl)
}
func (c *chainHandlers) Handle(ctx context.Context, rec slog.Record) error {
h := c.h.Handle
for i := len(c.middlewares) - 1; i >= 0; i-- {
h = c.middlewares[i](h)
}
return h(ctx, rec)
}
func (c *chainHandlers) WithGroup(group string) slog.Handler {
return &chainHandlers{
middlewares: c.middlewares,
h: c.h.WithGroup(group),
}
}
func (c *chainHandlers) WithAttrs(attrs []slog.Attr) slog.Handler {
return &chainHandlers{
middlewares: c.middlewares,
h: c.h.WithAttrs(attrs),
}
}

71
pkg/logger/error.go Normal file
View File

@@ -0,0 +1,71 @@
package logger
import (
"context"
"fmt"
"log/slog"
"runtime"
"strings"
"github.com/cockroachdb/errors/errbase"
)
// AttrError returns an attribute with error key.
func AttrError(err error) slog.Attr {
if err == nil {
return slog.Attr{}
}
return slog.Any(ErrorKey, err)
}
func middlewareError() middleware {
return func(next handleFunc) handleFunc {
return func(ctx context.Context, rec slog.Record) error {
rec.Attrs(func(attr slog.Attr) bool {
if attr.Key == ErrorKey || attr.Key == "err" {
err := attr.Value.Any()
if err, ok := err.(error); ok && err != nil {
rec.AddAttrs(slog.String("error_verbose", fmt.Sprintf("%+v", err)))
if x, ok := err.(errbase.StackTraceProvider); ok {
rec.AddAttrs(slog.Any("stack_trace", traceLines(x.StackTrace())))
}
}
}
return false
})
return next(ctx, rec)
}
}
}
func traceLines(frames errbase.StackTrace) []string {
traceLines := make([]string, 0, len(frames))
// Iterate in reverse to skip uninteresting, consecutive runtime frames at
// the bottom of the trace.
skipping := true
for i := len(frames) - 1; i >= 0; i-- {
// Adapted from errors.Frame.MarshalText(), but avoiding repeated
// calls to FuncForPC and FileLine.
pc := uintptr(frames[i]) - 1
fn := runtime.FuncForPC(pc)
if fn == nil {
traceLines = append(traceLines, "unknown")
skipping = false
continue
}
name := fn.Name()
if skipping && strings.HasPrefix(name, "runtime.") {
continue
} else {
skipping = false
}
filename, lineNr := fn.FileLine(pc)
traceLines = append(traceLines, fmt.Sprintf("%s %s:%d", name, filename, lineNr))
}
return traceLines[:len(traceLines):len(traceLines)]
}

40
pkg/logger/gcp.go Normal file
View File

@@ -0,0 +1,40 @@
package logger
import "log/slog"
// GCPAttrReplacer replaces the default attribute keys with the GCP logging attribute keys.
func GCPAttrReplacer(groups []string, attr slog.Attr) slog.Attr {
switch attr.Key {
case MessageKey:
attr.Key = "message"
case SourceKey:
attr.Key = "logging.googleapis.com/sourceLocation"
case LevelKey:
attr.Key = "severity"
lvl, ok := attr.Value.Any().(slog.Level)
if ok {
attr.Value = slog.StringValue(gcpSeverityMapping(lvl))
}
}
return attr
}
// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity
func gcpSeverityMapping(lvl slog.Level) string {
switch {
case lvl < slog.LevelInfo:
return "DEBUG"
case lvl < slog.LevelWarn:
return "INFO"
case lvl < slog.LevelError:
return "WARNING"
case lvl < LevelCritical:
return "ERROR"
case lvl < LevelPanic:
return "CRITICAL"
case lvl < LevelFatal:
return "ALERT"
default:
return "EMERGENCY"
}
}

37
pkg/logger/level.go Normal file
View File

@@ -0,0 +1,37 @@
package logger
import (
"fmt"
"log/slog"
)
const (
LevelCritical = slog.Level(12)
LevelPanic = slog.Level(14)
LevelFatal = slog.Level(16)
)
func levelAttrReplacer(groups []string, attr slog.Attr) slog.Attr {
if len(groups) == 0 && attr.Key == "level" {
str := func(base string, val slog.Level) string {
if val == 0 {
return base
}
return fmt.Sprintf("%s%+d", base, val)
}
if l, ok := attr.Value.Any().(slog.Level); ok {
switch {
case l < LevelCritical:
return attr
case l < LevelPanic:
return slog.Attr{Key: attr.Key, Value: slog.StringValue(str("CRITICAL", l-LevelCritical))}
case l < LevelFatal:
return slog.Attr{Key: attr.Key, Value: slog.StringValue(str("PANIC", l-LevelPanic))}
default:
return slog.Attr{Key: attr.Key, Value: slog.StringValue(str("FATAL", l-LevelFatal))}
}
}
}
return attr
}

155
pkg/logger/logger.go Normal file
View File

@@ -0,0 +1,155 @@
// nolint: sloglint
package logger
import (
"context"
"log/slog"
"os"
"strings"
)
const (
// DefaultLevel is the default minimum reporting level for the logger
DefaultLevel = slog.LevelDebug
// logLevel set `log` output level to `DEBUG`.
// `log` is allowed for debugging purposes only.
//
// NOTE: Please use `slog` for logging instead of `log`, and
// do not use `log` for production code.
logLevel = slog.LevelDebug
)
var (
// minimum reporting level for the logger
lvl = new(slog.LevelVar)
// top-level logger
logger *slog.Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: lvl,
ReplaceAttr: levelAttrReplacer,
}))
)
// Set default slog logger
func init() {
lvl.Set(DefaultLevel)
slog.SetDefault(logger)
}
// Set `log` output level
func init() {
slog.SetLogLoggerLevel(logLevel)
}
// SetLevel sets the minimum reporting level for the logger
func SetLevel(level slog.Level) (old slog.Level) {
old = lvl.Level()
lvl.Set(level)
return old
}
// Debug calls [Logger.Debug] on the default logger.
func With(args ...any) *slog.Logger {
return logger.With(args...)
}
// Debug calls [Logger.Debug] on the default logger.
func Debug(msg string, args ...any) {
logger.Debug(msg, args...)
}
// Info calls [Logger.Info] on the default logger.
func Info(msg string, args ...any) {
logger.Info(msg, args...)
}
// Warn calls [Logger.Warn] on the default logger.
func Warn(msg string, args ...any) {
logger.Warn(msg, args...)
}
// Error calls [Logger.Error] on the default logger.
// TODO: support stack trace for error
func Error(msg string, err error, args ...any) {
logger.Error(msg, append(args, AttrError(err))...)
}
// Panic calls [Logger.Log] with PANIC level on the default logger and then panic.
func Panic(msg string, args ...any) {
logger.Log(context.Background(), LevelPanic, msg, args...)
panic(msg)
}
// Log calls [Logger.Log] on the default logger.
func Log(level slog.Level, msg string, args ...any) {
logger.Log(context.Background(), level, msg, args...)
}
// LogAttrs calls [Logger.LogAttrs] on the default logger.
func LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) {
logger.LogAttrs(ctx, level, msg, attrs...)
}
// Config is the logger configuration.
type Config struct {
// Env is the logger environment.
// - PRODUCTION, PROD: use JSON format, log level: INFO
// - Default: use Text format, log level: DEBUG
Env string `env:"ENV,expand" envDefault:"${ENV}"`
Platform string `env:"PLATFORM" envDefault:"none"`
}
// Init initializes global logger and slog logger with given configuration.
func Init(cfg Config) error {
replacers := []func([]string, slog.Attr) slog.Attr{}
// Platform specific attr replacer
switch strings.ToLower(cfg.Platform) {
case "gcp":
replacers = append(replacers, GCPAttrReplacer)
}
// Default attr replacer
replacers = append(replacers,
levelAttrReplacer,
)
var (
handler slog.Handler
level = new(slog.LevelVar)
options = &slog.HandlerOptions{
AddSource: true,
Level: level,
ReplaceAttr: attrReplacerChain(replacers...),
}
)
switch strings.ToLower(cfg.Env) {
case "production", "prod":
level.Set(slog.LevelInfo)
handler = slog.NewJSONHandler(os.Stdout, options)
default:
level.Set(DefaultLevel)
handler = slog.NewTextHandler(os.Stdout, options)
}
logger = slog.New(newChainHandlers(handler, middlewareError()))
lvl = level
slog.SetDefault(logger)
logger.Info("logger initialized", slog.String("environment", cfg.Env))
return nil
}
// attrReplacerChain returns a function that applies a chain of replacers to an attribute.
func attrReplacerChain(replacers ...func([]string, slog.Attr) slog.Attr) func([]string, slog.Attr) slog.Attr {
return func(groups []string, attr slog.Attr) slog.Attr {
for _, replacer := range replacers {
attr = replacer(groups, attr)
}
return attr
}
}