mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
clean: squash history (binaries stripped via filter-repo)
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/formatting"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/rpc"
|
||||
"github.com/luxfi/node/utils/json"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Requester rpc.EndpointRequester
|
||||
}
|
||||
|
||||
// NewClient creates a client that can interact with an index via HTTP API
|
||||
// calls.
|
||||
// [uri] is the path to make API calls to.
|
||||
// For example:
|
||||
// - http://1.2.3.4:9650/ext/index/C/block
|
||||
// - http://1.2.3.4:9650/ext/index/X/tx
|
||||
func NewClient(uri string) *Client {
|
||||
return &Client{
|
||||
Requester: rpc.NewEndpointRequester(uri),
|
||||
}
|
||||
}
|
||||
|
||||
// GetContainerRange returns the transactions at index [startIndex], [startIndex+1], ... , [startIndex+n-1]
|
||||
// If [n] == 0, returns an empty response (i.e. null).
|
||||
// If [startIndex] > the last accepted index, returns an error (unless the above apply.)
|
||||
// If we run out of transactions, returns the ones fetched before running out.
|
||||
func (c *Client) GetContainerRange(ctx context.Context, startIndex uint64, numToFetch int, options ...rpc.Option) ([]Container, error) {
|
||||
var fcs GetContainerRangeResponse
|
||||
err := c.Requester.SendRequest(ctx, "index.getContainerRange", &GetContainerRangeArgs{
|
||||
StartIndex: json.Uint64(startIndex),
|
||||
NumToFetch: json.Uint64(numToFetch),
|
||||
Encoding: formatting.Hex,
|
||||
}, &fcs, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := make([]Container, len(fcs.Containers))
|
||||
for i, resp := range fcs.Containers {
|
||||
containerBytes, err := formatting.Decode(resp.Encoding, resp.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't decode container %s: %w", resp.ID, err)
|
||||
}
|
||||
response[i] = Container{
|
||||
ID: resp.ID,
|
||||
Timestamp: resp.Timestamp.Unix(),
|
||||
Bytes: containerBytes,
|
||||
}
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Get a container by its index
|
||||
func (c *Client) GetContainerByIndex(ctx context.Context, index uint64, options ...rpc.Option) (Container, error) {
|
||||
var fc FormattedContainer
|
||||
err := c.Requester.SendRequest(ctx, "index.getContainerByIndex", &GetContainerByIndexArgs{
|
||||
Index: json.Uint64(index),
|
||||
Encoding: formatting.Hex,
|
||||
}, &fc, options...)
|
||||
if err != nil {
|
||||
return Container{}, err
|
||||
}
|
||||
|
||||
containerBytes, err := formatting.Decode(fc.Encoding, fc.Bytes)
|
||||
if err != nil {
|
||||
return Container{}, fmt.Errorf("couldn't decode container %s: %w", fc.ID, err)
|
||||
}
|
||||
return Container{
|
||||
ID: fc.ID,
|
||||
Timestamp: fc.Timestamp.Unix(),
|
||||
Bytes: containerBytes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get the most recently accepted container and its index
|
||||
func (c *Client) GetLastAccepted(ctx context.Context, options ...rpc.Option) (Container, uint64, error) {
|
||||
var fc FormattedContainer
|
||||
err := c.Requester.SendRequest(ctx, "index.getLastAccepted", &GetLastAcceptedArgs{
|
||||
Encoding: formatting.Hex,
|
||||
}, &fc, options...)
|
||||
if err != nil {
|
||||
return Container{}, 0, err
|
||||
}
|
||||
|
||||
containerBytes, err := formatting.Decode(fc.Encoding, fc.Bytes)
|
||||
if err != nil {
|
||||
return Container{}, 0, fmt.Errorf("couldn't decode container %s: %w", fc.ID, err)
|
||||
}
|
||||
return Container{
|
||||
ID: fc.ID,
|
||||
Timestamp: fc.Timestamp.Unix(),
|
||||
Bytes: containerBytes,
|
||||
}, uint64(fc.Index), nil
|
||||
}
|
||||
|
||||
// Returns 1 less than the number of containers accepted on this chain
|
||||
func (c *Client) GetIndex(ctx context.Context, id ids.ID, options ...rpc.Option) (uint64, error) {
|
||||
var index GetIndexResponse
|
||||
err := c.Requester.SendRequest(ctx, "index.getIndex", &GetIndexArgs{
|
||||
ID: id,
|
||||
}, &index, options...)
|
||||
return uint64(index.Index), err
|
||||
}
|
||||
|
||||
// Returns true if the given container is accepted
|
||||
func (c *Client) IsAccepted(ctx context.Context, id ids.ID, options ...rpc.Option) (bool, error) {
|
||||
var res IsAcceptedResponse
|
||||
err := c.Requester.SendRequest(ctx, "index.isAccepted", &IsAcceptedArgs{
|
||||
ID: id,
|
||||
}, &res, options...)
|
||||
return res.IsAccepted, err
|
||||
}
|
||||
|
||||
// Get a container and its index by its ID
|
||||
func (c *Client) GetContainerByID(ctx context.Context, id ids.ID, options ...rpc.Option) (Container, uint64, error) {
|
||||
var fc FormattedContainer
|
||||
err := c.Requester.SendRequest(ctx, "index.getContainerByID", &GetContainerByIDArgs{
|
||||
ID: id,
|
||||
Encoding: formatting.Hex,
|
||||
}, &fc, options...)
|
||||
if err != nil {
|
||||
return Container{}, 0, err
|
||||
}
|
||||
|
||||
containerBytes, err := formatting.Decode(fc.Encoding, fc.Bytes)
|
||||
if err != nil {
|
||||
return Container{}, 0, fmt.Errorf("couldn't decode container %s: %w", fc.ID, err)
|
||||
}
|
||||
return Container{
|
||||
ID: fc.ID,
|
||||
Timestamp: fc.Timestamp.Unix(),
|
||||
Bytes: containerBytes,
|
||||
}, uint64(fc.Index), nil
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/luxfi/formatting"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/rpc"
|
||||
"github.com/luxfi/utils"
|
||||
"github.com/luxfi/node/utils/json"
|
||||
)
|
||||
|
||||
type mockClient struct {
|
||||
require *require.Assertions
|
||||
expectedMethod string
|
||||
onSendRequestF func(reply interface{}) error
|
||||
}
|
||||
|
||||
func (mc *mockClient) SendRequest(_ context.Context, method string, _ interface{}, reply interface{}, _ ...rpc.Option) error {
|
||||
mc.require.Equal(mc.expectedMethod, method)
|
||||
return mc.onSendRequestF(reply)
|
||||
}
|
||||
|
||||
func TestIndexClient(t *testing.T) {
|
||||
require := require.New(t)
|
||||
client := Client{}
|
||||
{
|
||||
// Test GetIndex
|
||||
client.Requester = &mockClient{
|
||||
require: require,
|
||||
expectedMethod: "index.getIndex",
|
||||
onSendRequestF: func(reply interface{}) error {
|
||||
*(reply.(*GetIndexResponse)) = GetIndexResponse{Index: 5}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
index, err := client.GetIndex(context.Background(), ids.Empty)
|
||||
require.NoError(err)
|
||||
require.Equal(uint64(5), index)
|
||||
}
|
||||
{
|
||||
// Test GetLastAccepted
|
||||
id := ids.GenerateTestID()
|
||||
bytes := utils.RandomBytes(10)
|
||||
bytesStr, err := formatting.Encode(formatting.Hex, bytes)
|
||||
require.NoError(err)
|
||||
client.Requester = &mockClient{
|
||||
require: require,
|
||||
expectedMethod: "index.getLastAccepted",
|
||||
onSendRequestF: func(reply interface{}) error {
|
||||
*(reply.(*FormattedContainer)) = FormattedContainer{
|
||||
ID: id,
|
||||
Bytes: bytesStr,
|
||||
Index: json.Uint64(10),
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
container, index, err := client.GetLastAccepted(context.Background())
|
||||
require.NoError(err)
|
||||
require.Equal(id, container.ID)
|
||||
require.Equal(bytes, container.Bytes)
|
||||
require.Equal(uint64(10), index)
|
||||
}
|
||||
{
|
||||
// Test GetContainerRange
|
||||
id := ids.GenerateTestID()
|
||||
bytes := utils.RandomBytes(10)
|
||||
bytesStr, err := formatting.Encode(formatting.Hex, bytes)
|
||||
require.NoError(err)
|
||||
client.Requester = &mockClient{
|
||||
require: require,
|
||||
expectedMethod: "index.getContainerRange",
|
||||
onSendRequestF: func(reply interface{}) error {
|
||||
*(reply.(*GetContainerRangeResponse)) = GetContainerRangeResponse{Containers: []FormattedContainer{{
|
||||
ID: id,
|
||||
Bytes: bytesStr,
|
||||
}}}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
containers, err := client.GetContainerRange(context.Background(), 1, 10)
|
||||
require.NoError(err)
|
||||
require.Len(containers, 1)
|
||||
require.Equal(id, containers[0].ID)
|
||||
require.Equal(bytes, containers[0].Bytes)
|
||||
}
|
||||
{
|
||||
// Test IsAccepted
|
||||
client.Requester = &mockClient{
|
||||
require: require,
|
||||
expectedMethod: "index.isAccepted",
|
||||
onSendRequestF: func(reply interface{}) error {
|
||||
*(reply.(*IsAcceptedResponse)) = IsAcceptedResponse{IsAccepted: true}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
isAccepted, err := client.IsAccepted(context.Background(), ids.Empty)
|
||||
require.NoError(err)
|
||||
require.True(isAccepted)
|
||||
}
|
||||
{
|
||||
// Test GetContainerByID
|
||||
id := ids.GenerateTestID()
|
||||
bytes := utils.RandomBytes(10)
|
||||
bytesStr, err := formatting.Encode(formatting.Hex, bytes)
|
||||
require.NoError(err)
|
||||
client.Requester = &mockClient{
|
||||
require: require,
|
||||
expectedMethod: "index.getContainerByID",
|
||||
onSendRequestF: func(reply interface{}) error {
|
||||
*(reply.(*FormattedContainer)) = FormattedContainer{
|
||||
ID: id,
|
||||
Bytes: bytesStr,
|
||||
Index: json.Uint64(10),
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
container, index, err := client.GetContainerByID(context.Background(), id)
|
||||
require.NoError(err)
|
||||
require.Equal(id, container.ID)
|
||||
require.Equal(bytes, container.Bytes)
|
||||
require.Equal(uint64(10), index)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/luxfi/codec"
|
||||
"github.com/luxfi/codec/linearcodec"
|
||||
)
|
||||
|
||||
const CodecVersion = 0
|
||||
|
||||
var Codec codec.Manager
|
||||
|
||||
func init() {
|
||||
lc := linearcodec.NewDefault()
|
||||
Codec = codec.NewManager(math.MaxInt)
|
||||
|
||||
if err := Codec.RegisterCodec(CodecVersion, lc); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package indexer
|
||||
|
||||
import "github.com/luxfi/ids"
|
||||
|
||||
// Container is something that gets accepted
|
||||
// (a block, transaction or vertex)
|
||||
type Container struct {
|
||||
// ID of this container
|
||||
ID ids.ID `serialize:"true"`
|
||||
// Byte representation of this container
|
||||
Bytes []byte `serialize:"true"`
|
||||
// Unix time, in nanoseconds, at which this container was accepted by this node
|
||||
Timestamp int64 `serialize:"true"`
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/node/indexer"
|
||||
"github.com/luxfi/node/wallet/network/primary"
|
||||
|
||||
platformvmblock "github.com/luxfi/node/vms/platformvm/block"
|
||||
proposervmblock "github.com/luxfi/node/vms/proposervm/block"
|
||||
)
|
||||
|
||||
// This example program continuously polls for the next P-Chain block
|
||||
// and prints the ID of the block and its transactions.
|
||||
func main() {
|
||||
var (
|
||||
uri = primary.LocalAPIURI + "/ext/index/P/block"
|
||||
client = indexer.NewClient(uri)
|
||||
ctx = context.Background()
|
||||
nextIndex uint64
|
||||
)
|
||||
for {
|
||||
container, err := client.GetContainerByIndex(ctx, nextIndex)
|
||||
if err != nil {
|
||||
time.Sleep(time.Second)
|
||||
log.Println("polling for next accepted block")
|
||||
continue
|
||||
}
|
||||
|
||||
platformvmBlockBytes := container.Bytes
|
||||
proposerVMBlock, err := proposervmblock.Parse(container.Bytes, constants.PlatformChainID)
|
||||
if err == nil {
|
||||
platformvmBlockBytes = proposerVMBlock.Block()
|
||||
}
|
||||
|
||||
platformvmBlock, err := platformvmblock.Parse(platformvmblock.Codec, platformvmBlockBytes)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to parse platformvm block: %s\n", err)
|
||||
}
|
||||
|
||||
acceptedTxs := platformvmBlock.Txs()
|
||||
log.Printf("accepted block %s with %d transactions\n", platformvmBlock.ID(), len(acceptedTxs))
|
||||
|
||||
for _, tx := range acceptedTxs {
|
||||
log.Printf("accepted transaction %s\n", tx.ID())
|
||||
}
|
||||
|
||||
nextIndex++
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/node/indexer"
|
||||
"github.com/luxfi/node/vms/proposervm/block"
|
||||
"github.com/luxfi/node/wallet/chain/x/builder"
|
||||
"github.com/luxfi/node/wallet/network/primary"
|
||||
)
|
||||
|
||||
// This example program continuously polls for the next X-Chain block
|
||||
// and prints the ID of the block and its transactions.
|
||||
func main() {
|
||||
var (
|
||||
uri = primary.LocalAPIURI + "/ext/index/X/block"
|
||||
xChainID = ids.FromStringOrPanic("2eNy1mUFdmaxXNj1eQHUe7Np4gju9sJsEtWQ4MX3ToiNKuADed")
|
||||
client = indexer.NewClient(uri)
|
||||
ctx = context.Background()
|
||||
nextIndex uint64
|
||||
)
|
||||
for {
|
||||
container, err := client.GetContainerByIndex(ctx, nextIndex)
|
||||
if err != nil {
|
||||
time.Sleep(time.Second)
|
||||
log.Println("polling for next accepted block")
|
||||
continue
|
||||
}
|
||||
|
||||
proposerVMBlock, err := block.Parse(container.Bytes, xChainID)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to parse proposervm block: %s\n", err)
|
||||
}
|
||||
|
||||
xvmBlockBytes := proposerVMBlock.Block()
|
||||
xvmBlock, err := builder.Parser.ParseBlock(xvmBlockBytes)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to parse xvm block: %s\n", err)
|
||||
}
|
||||
|
||||
acceptedTxs := xvmBlock.Txs()
|
||||
log.Printf("accepted block %s with %d transactions\n", xvmBlock.ID(), len(acceptedTxs))
|
||||
|
||||
for _, tx := range acceptedTxs {
|
||||
log.Printf("accepted transaction %s\n", tx.ID())
|
||||
}
|
||||
|
||||
nextIndex++
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/runtime"
|
||||
"github.com/luxfi/database"
|
||||
"github.com/luxfi/database/prefixdb"
|
||||
"github.com/luxfi/database/versiondb"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
nodeconsensus "github.com/luxfi/node/consensus"
|
||||
"github.com/luxfi/timer/mockable"
|
||||
)
|
||||
|
||||
// Maximum number of containers IDs that can be fetched at a time in a call to
|
||||
// GetContainerRange
|
||||
const MaxFetchedByRange = 1024
|
||||
|
||||
var (
|
||||
// Maps to the byte representation of the next accepted index
|
||||
nextAcceptedIndexKey = []byte{0x00}
|
||||
indexToContainerPrefix = []byte{0x01}
|
||||
containerToIDPrefix = []byte{0x02}
|
||||
errNoneAccepted = errors.New("no containers have been accepted")
|
||||
errNumToFetchInvalid = fmt.Errorf("numToFetch must be in [1,%d]", MaxFetchedByRange)
|
||||
errNoContainerAtIndex = errors.New("no container at index")
|
||||
|
||||
_ nodeconsensus.Acceptor = (*index)(nil)
|
||||
)
|
||||
|
||||
// index indexes containers in their order of acceptance
|
||||
//
|
||||
// Invariant: index is thread-safe.
|
||||
// Invariant: index assumes that Accept is called, before the container is
|
||||
// committed to the database of the VM, in the order they were accepted.
|
||||
type index struct {
|
||||
clock *mockable.Clock
|
||||
lock sync.RWMutex
|
||||
// The index of the next accepted transaction
|
||||
nextAcceptedIndex uint64
|
||||
// When [baseDB] is committed, writes to [baseDB]
|
||||
vDB *versiondb.Database
|
||||
baseDB database.Database
|
||||
// Both [indexToContainer] and [containerToIndex] have [vDB] underneath
|
||||
// Index --> Container
|
||||
indexToContainer database.Database
|
||||
// Container ID --> Index
|
||||
containerToIndex database.Database
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
// Create a new thread-safe index.
|
||||
//
|
||||
// Invariant: Closes [baseDB] on close.
|
||||
func newIndex(
|
||||
baseDB database.Database,
|
||||
log log.Logger,
|
||||
clock *mockable.Clock,
|
||||
) (*index, error) {
|
||||
vDB := versiondb.New(baseDB)
|
||||
indexToContainer := prefixdb.New(indexToContainerPrefix, vDB)
|
||||
containerToIndex := prefixdb.New(containerToIDPrefix, vDB)
|
||||
|
||||
i := &index{
|
||||
clock: clock,
|
||||
baseDB: baseDB,
|
||||
vDB: vDB,
|
||||
indexToContainer: indexToContainer,
|
||||
containerToIndex: containerToIndex,
|
||||
log: log,
|
||||
}
|
||||
|
||||
// Get next accepted index from db
|
||||
nextAcceptedIndex, err := database.GetUInt64(i.vDB, nextAcceptedIndexKey)
|
||||
if err != nil {
|
||||
if err == database.ErrNotFound {
|
||||
nextAcceptedIndex = 0
|
||||
} else {
|
||||
return nil, fmt.Errorf("couldn't get next accepted index from database: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
i.nextAcceptedIndex = nextAcceptedIndex
|
||||
i.log.Info("created new index",
|
||||
"nextAcceptedIndex", i.nextAcceptedIndex,
|
||||
)
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// Close this index
|
||||
func (i *index) Close() error {
|
||||
return errors.Join(
|
||||
i.indexToContainer.Close(),
|
||||
i.containerToIndex.Close(),
|
||||
i.vDB.Close(),
|
||||
i.baseDB.Close(),
|
||||
)
|
||||
}
|
||||
|
||||
// Index that the given transaction is accepted
|
||||
// Returned error should be treated as fatal; the VM should not commit [containerID]
|
||||
// or any new containers as accepted.
|
||||
func (i *index) Accept(rt *runtime.Runtime, containerID ids.ID, containerBytes []byte) error {
|
||||
i.lock.Lock()
|
||||
defer i.lock.Unlock()
|
||||
|
||||
// It may be the case that in a previous run of this node, this index committed [containerID]
|
||||
// as accepted and then the node shut down before the VM committed [containerID] as accepted.
|
||||
// In that case, when the node restarts Accept will be called with the same container.
|
||||
// Make sure we don't index the same container twice in that event.
|
||||
_, err := i.containerToIndex.Get(containerID[:])
|
||||
if err == nil {
|
||||
i.log.Debug("not indexing already accepted container",
|
||||
log.Stringer("containerID", containerID),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
if err != database.ErrNotFound {
|
||||
return fmt.Errorf("couldn't get whether %s is accepted: %w", containerID, err)
|
||||
}
|
||||
|
||||
i.log.Debug("indexing container",
|
||||
log.Uint64("nextAcceptedIndex", i.nextAcceptedIndex),
|
||||
log.Stringer("containerID", containerID),
|
||||
)
|
||||
// Persist index --> Container
|
||||
nextAcceptedIndexBytes := database.PackUInt64(i.nextAcceptedIndex)
|
||||
bytes, err := Codec.Marshal(CodecVersion, Container{
|
||||
ID: containerID,
|
||||
Bytes: containerBytes,
|
||||
Timestamp: i.clock.Time().UnixNano(),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't serialize container %s: %w", containerID, err)
|
||||
}
|
||||
if err := i.indexToContainer.Put(nextAcceptedIndexBytes, bytes); err != nil {
|
||||
return fmt.Errorf("couldn't put accepted container %s into index: %w", containerID, err)
|
||||
}
|
||||
|
||||
// Persist container ID --> index
|
||||
if err := i.containerToIndex.Put(containerID[:], nextAcceptedIndexBytes); err != nil {
|
||||
return fmt.Errorf("couldn't map container %s to index: %w", containerID, err)
|
||||
}
|
||||
|
||||
// Persist next accepted index
|
||||
i.nextAcceptedIndex++
|
||||
if err := database.PutUInt64(i.vDB, nextAcceptedIndexKey, i.nextAcceptedIndex); err != nil {
|
||||
return fmt.Errorf("couldn't put accepted container %s into index: %w", containerID, err)
|
||||
}
|
||||
|
||||
// Atomically commit [i.vDB], [i.indexToContainer], [i.containerToIndex] to [i.baseDB]
|
||||
return i.vDB.Commit()
|
||||
}
|
||||
|
||||
// Returns the ID of the [index]th accepted container and the container itself.
|
||||
// For example, if [index] == 0, returns the first accepted container.
|
||||
// If [index] == 1, returns the second accepted container, etc.
|
||||
// Returns an error if there is no container at the given index.
|
||||
func (i *index) GetContainerByIndex(index uint64) (Container, error) {
|
||||
i.lock.RLock()
|
||||
defer i.lock.RUnlock()
|
||||
|
||||
return i.getContainerByIndex(index)
|
||||
}
|
||||
|
||||
// Assumes [i.lock] is held
|
||||
func (i *index) getContainerByIndex(index uint64) (Container, error) {
|
||||
lastAcceptedIndex, ok := i.lastAcceptedIndex()
|
||||
if !ok || index > lastAcceptedIndex {
|
||||
return Container{}, fmt.Errorf("%w %d", errNoContainerAtIndex, index)
|
||||
}
|
||||
indexBytes := database.PackUInt64(index)
|
||||
return i.getContainerByIndexBytes(indexBytes)
|
||||
}
|
||||
|
||||
// [indexBytes] is the byte representation of the index to fetch.
|
||||
// Assumes [i.lock] is held
|
||||
func (i *index) getContainerByIndexBytes(indexBytes []byte) (Container, error) {
|
||||
containerBytes, err := i.indexToContainer.Get(indexBytes)
|
||||
if err != nil {
|
||||
i.log.Error("couldn't read container from database",
|
||||
log.Err(err),
|
||||
)
|
||||
return Container{}, fmt.Errorf("couldn't read from database: %w", err)
|
||||
}
|
||||
var container Container
|
||||
if _, err := Codec.Unmarshal(containerBytes, &container); err != nil {
|
||||
return Container{}, fmt.Errorf("couldn't unmarshal container: %w", err)
|
||||
}
|
||||
return container, nil
|
||||
}
|
||||
|
||||
// GetContainerRange returns the IDs of containers at indices
|
||||
// [startIndex], [startIndex+1], ..., [startIndex+numToFetch-1].
|
||||
// [startIndex] should be <= i.lastAcceptedIndex().
|
||||
// [numToFetch] should be in [0, MaxFetchedByRange]
|
||||
func (i *index) GetContainerRange(startIndex, numToFetch uint64) ([]Container, error) {
|
||||
// Check arguments for validity
|
||||
if numToFetch == 0 || numToFetch > MaxFetchedByRange {
|
||||
return nil, fmt.Errorf("%w but is %d", errNumToFetchInvalid, numToFetch)
|
||||
}
|
||||
|
||||
i.lock.RLock()
|
||||
defer i.lock.RUnlock()
|
||||
|
||||
lastAcceptedIndex, ok := i.lastAcceptedIndex()
|
||||
if !ok {
|
||||
return nil, errNoneAccepted
|
||||
} else if startIndex > lastAcceptedIndex {
|
||||
return nil, fmt.Errorf("start index (%d) > last accepted index (%d)", startIndex, lastAcceptedIndex)
|
||||
}
|
||||
|
||||
// Calculate the last index we will fetch
|
||||
lastIndex := min(startIndex+numToFetch-1, lastAcceptedIndex)
|
||||
// [lastIndex] is always >= [startIndex] so this is safe.
|
||||
// [numToFetch] is limited to [MaxFetchedByRange] so [containers] is bounded in size.
|
||||
containers := make([]Container, int(lastIndex)-int(startIndex)+1)
|
||||
|
||||
n := 0
|
||||
var err error
|
||||
for j := startIndex; j <= lastIndex; j++ {
|
||||
containers[n], err = i.getContainerByIndex(j)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't get container at index %d: %w", j, err)
|
||||
}
|
||||
n++
|
||||
}
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
// Returns database.ErrNotFound if the container is not indexed as accepted
|
||||
func (i *index) GetIndex(id ids.ID) (uint64, error) {
|
||||
i.lock.RLock()
|
||||
defer i.lock.RUnlock()
|
||||
|
||||
return database.GetUInt64(i.containerToIndex, id[:])
|
||||
}
|
||||
|
||||
func (i *index) GetContainerByID(id ids.ID) (Container, error) {
|
||||
i.lock.RLock()
|
||||
defer i.lock.RUnlock()
|
||||
|
||||
// Read index from database
|
||||
indexBytes, err := i.containerToIndex.Get(id[:])
|
||||
if err != nil {
|
||||
return Container{}, err
|
||||
}
|
||||
return i.getContainerByIndexBytes(indexBytes)
|
||||
}
|
||||
|
||||
// GetLastAccepted returns the last accepted container.
|
||||
// Returns an error if no containers have been accepted.
|
||||
func (i *index) GetLastAccepted() (Container, error) {
|
||||
i.lock.RLock()
|
||||
defer i.lock.RUnlock()
|
||||
|
||||
lastAcceptedIndex, exists := i.lastAcceptedIndex()
|
||||
if !exists {
|
||||
return Container{}, errNoneAccepted
|
||||
}
|
||||
return i.getContainerByIndex(lastAcceptedIndex)
|
||||
}
|
||||
|
||||
// Assumes i.lock is held
|
||||
// Returns:
|
||||
//
|
||||
// 1. The index of the most recently accepted transaction, or 0 if no
|
||||
// transactions have been accepted
|
||||
// 2. Whether at least 1 transaction has been accepted
|
||||
func (i *index) lastAcceptedIndex() (uint64, bool) {
|
||||
return i.nextAcceptedIndex - 1, i.nextAcceptedIndex != 0
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/luxfi/database/memdb"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/math/set"
|
||||
"github.com/luxfi/runtime"
|
||||
"github.com/luxfi/timer/mockable"
|
||||
"github.com/luxfi/utils"
|
||||
)
|
||||
|
||||
// testRuntime creates a minimal runtime.Runtime for testing
|
||||
func testRuntime(chainID ids.ID) *runtime.Runtime {
|
||||
return &runtime.Runtime{
|
||||
NetworkID: 1,
|
||||
ChainID: chainID,
|
||||
NodeID: ids.GenerateTestNodeID(),
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndex(t *testing.T) {
|
||||
// Setup
|
||||
pageSize := uint64(64)
|
||||
require := require.New(t)
|
||||
baseDB := memdb.New()
|
||||
// Use a test chain ID
|
||||
testChainID := ids.GenerateTestID()
|
||||
rt := testRuntime(testChainID)
|
||||
|
||||
idx, err := newIndex(baseDB, log.NoLog{}, &mockable.Clock{})
|
||||
require.NoError(err)
|
||||
|
||||
// Populate "containers" with random IDs/bytes
|
||||
containers := map[ids.ID][]byte{}
|
||||
for i := uint64(0); i < 2*pageSize; i++ {
|
||||
containers[ids.GenerateTestID()] = utils.RandomBytes(32)
|
||||
}
|
||||
|
||||
// Accept each container and after each, make assertions
|
||||
i := uint64(0)
|
||||
for containerID, containerBytes := range containers {
|
||||
require.NoError(idx.Accept(rt, containerID, containerBytes))
|
||||
|
||||
lastAcceptedIndex, ok := idx.lastAcceptedIndex()
|
||||
require.True(ok)
|
||||
require.Equal(i, lastAcceptedIndex)
|
||||
require.Equal(i+1, idx.nextAcceptedIndex)
|
||||
|
||||
gotContainer, err := idx.GetContainerByID(containerID)
|
||||
require.NoError(err)
|
||||
require.Equal(containerBytes, gotContainer.Bytes)
|
||||
|
||||
gotIndex, err := idx.GetIndex(containerID)
|
||||
require.NoError(err)
|
||||
require.Equal(i, gotIndex)
|
||||
|
||||
gotContainer, err = idx.GetContainerByIndex(i)
|
||||
require.NoError(err)
|
||||
require.Equal(containerBytes, gotContainer.Bytes)
|
||||
|
||||
gotContainer, err = idx.GetLastAccepted()
|
||||
require.NoError(err)
|
||||
require.Equal(containerBytes, gotContainer.Bytes)
|
||||
|
||||
containers, err := idx.GetContainerRange(i, 1)
|
||||
require.NoError(err)
|
||||
require.Len(containers, 1)
|
||||
require.Equal(containerBytes, containers[0].Bytes)
|
||||
|
||||
containers, err = idx.GetContainerRange(i, 2)
|
||||
require.NoError(err)
|
||||
require.Len(containers, 1)
|
||||
require.Equal(containerBytes, containers[0].Bytes)
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
// Create a new index with the same database and ensure contents still there
|
||||
require.NoError(idx.vDB.Commit())
|
||||
|
||||
// Create a new index by directly using the same base database
|
||||
// Don't close the old index to avoid closing the baseDB
|
||||
idx, err = newIndex(baseDB, log.NoLog{}, &mockable.Clock{})
|
||||
require.NoError(err)
|
||||
|
||||
// Get all of the containers
|
||||
containersList, err := idx.GetContainerRange(0, pageSize)
|
||||
require.NoError(err)
|
||||
require.Len(containersList, int(pageSize))
|
||||
containersList2, err := idx.GetContainerRange(pageSize, pageSize)
|
||||
require.NoError(err)
|
||||
require.Len(containersList2, int(pageSize))
|
||||
containersList = append(containersList, containersList2...)
|
||||
|
||||
// Ensure that the data is correct
|
||||
lastTimestamp := int64(0)
|
||||
sawContainers := make(set.Set[ids.ID])
|
||||
for _, container := range containersList {
|
||||
require.False(sawContainers.Contains(container.ID)) // Should only see this container once
|
||||
require.Contains(containers, container.ID)
|
||||
require.Equal(containers[container.ID], container.Bytes)
|
||||
// Timestamps should be non-decreasing
|
||||
require.GreaterOrEqual(container.Timestamp, lastTimestamp)
|
||||
lastTimestamp = container.Timestamp
|
||||
sawContainers.Add(container.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexGetContainerByRangeMaxPageSize(t *testing.T) {
|
||||
// Setup
|
||||
require := require.New(t)
|
||||
db := memdb.New()
|
||||
// Use a test chain ID
|
||||
testChainID := ids.GenerateTestID()
|
||||
rt := testRuntime(testChainID)
|
||||
idx, err := newIndex(db, log.NoLog{}, &mockable.Clock{})
|
||||
require.NoError(err)
|
||||
|
||||
// Insert [MaxFetchedByRange] + 1 containers
|
||||
for i := uint64(0); i < MaxFetchedByRange+1; i++ {
|
||||
require.NoError(idx.Accept(rt, ids.GenerateTestID(), utils.RandomBytes(32)))
|
||||
}
|
||||
|
||||
// Page size too large
|
||||
_, err = idx.GetContainerRange(0, MaxFetchedByRange+1)
|
||||
require.ErrorIs(err, errNumToFetchInvalid)
|
||||
|
||||
// Make sure data is right
|
||||
containers, err := idx.GetContainerRange(0, MaxFetchedByRange)
|
||||
require.NoError(err)
|
||||
require.Len(containers, MaxFetchedByRange)
|
||||
|
||||
containers2, err := idx.GetContainerRange(1, MaxFetchedByRange)
|
||||
require.NoError(err)
|
||||
require.Len(containers2, MaxFetchedByRange)
|
||||
|
||||
require.Equal(containers[1], containers2[0])
|
||||
require.Equal(containers[MaxFetchedByRange-1], containers2[MaxFetchedByRange-2])
|
||||
|
||||
// Should have last 2 elements
|
||||
containers, err = idx.GetContainerRange(MaxFetchedByRange-1, MaxFetchedByRange)
|
||||
require.NoError(err)
|
||||
require.Len(containers, 2)
|
||||
require.Equal(containers[1], containers2[MaxFetchedByRange-1])
|
||||
require.Equal(containers[0], containers2[MaxFetchedByRange-2])
|
||||
}
|
||||
|
||||
func TestDontIndexSameContainerTwice(t *testing.T) {
|
||||
// Setup
|
||||
require := require.New(t)
|
||||
db := memdb.New()
|
||||
// Use a test chain ID
|
||||
testChainID := ids.GenerateTestID()
|
||||
rt := testRuntime(testChainID)
|
||||
idx, err := newIndex(db, log.NoLog{}, &mockable.Clock{})
|
||||
require.NoError(err)
|
||||
|
||||
// Accept the same container twice
|
||||
containerID := ids.GenerateTestID()
|
||||
require.NoError(idx.Accept(rt, containerID, []byte{1, 2, 3}))
|
||||
require.NoError(idx.Accept(rt, containerID, []byte{4, 5, 6}))
|
||||
_, err = idx.GetContainerByIndex(1)
|
||||
require.ErrorIs(err, errNoContainerAtIndex)
|
||||
gotContainer, err := idx.GetContainerByID(containerID)
|
||||
require.NoError(err)
|
||||
require.Equal([]byte{1, 2, 3}, gotContainer.Bytes)
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/rpc/v2"
|
||||
|
||||
"github.com/luxfi/codec/wrappers"
|
||||
"github.com/luxfi/database"
|
||||
"github.com/luxfi/database/prefixdb"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/node/server/http"
|
||||
"github.com/luxfi/node/chains"
|
||||
nodeconsensus "github.com/luxfi/node/consensus"
|
||||
"github.com/luxfi/node/utils/json"
|
||||
"github.com/luxfi/runtime"
|
||||
"github.com/luxfi/timer/mockable"
|
||||
"github.com/luxfi/vm"
|
||||
)
|
||||
|
||||
const (
|
||||
indexNamePrefix = "index-"
|
||||
txPrefix = 0x01
|
||||
vtxPrefix = 0x02
|
||||
blockPrefix = 0x03
|
||||
isIncompletePrefix = 0x04
|
||||
previouslyIndexedPrefix = 0x05
|
||||
)
|
||||
|
||||
var (
|
||||
_ Indexer = (*indexer)(nil)
|
||||
|
||||
hasRunKey = []byte{0x07}
|
||||
)
|
||||
|
||||
// Config for an indexer
|
||||
type Config struct {
|
||||
DB database.Database
|
||||
Log log.Logger
|
||||
IndexingEnabled bool
|
||||
AllowIncompleteIndex bool
|
||||
BlockAcceptorGroup nodeconsensus.AcceptorGroup
|
||||
TxAcceptorGroup nodeconsensus.AcceptorGroup
|
||||
VertexAcceptorGroup nodeconsensus.AcceptorGroup
|
||||
APIServer server.PathAdder
|
||||
ShutdownF func()
|
||||
}
|
||||
|
||||
// Indexer causes accepted containers for a given chain
|
||||
// to be indexed by their ID and by the order in which
|
||||
// they were accepted by this node.
|
||||
// Indexer is threadsafe.
|
||||
type Indexer interface {
|
||||
chains.Registrant
|
||||
// Close will do nothing and return nil after the first call
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// NewIndexer returns a new Indexer and registers a new endpoint on the given API server.
|
||||
func NewIndexer(config Config) (Indexer, error) {
|
||||
indexer := &indexer{
|
||||
clock: &mockable.Clock{},
|
||||
log: config.Log,
|
||||
db: config.DB,
|
||||
allowIncompleteIndex: config.AllowIncompleteIndex,
|
||||
indexingEnabled: config.IndexingEnabled,
|
||||
blockAcceptorGroup: config.BlockAcceptorGroup,
|
||||
txAcceptorGroup: config.TxAcceptorGroup,
|
||||
vertexAcceptorGroup: config.VertexAcceptorGroup,
|
||||
txIndices: map[ids.ID]*index{},
|
||||
vtxIndices: map[ids.ID]*index{},
|
||||
blockIndices: map[ids.ID]*index{},
|
||||
pathAdder: config.APIServer,
|
||||
shutdownF: config.ShutdownF,
|
||||
}
|
||||
|
||||
hasRun, err := indexer.hasRun()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
indexer.hasRunBefore = hasRun
|
||||
return indexer, indexer.markHasRun()
|
||||
}
|
||||
|
||||
type indexer struct {
|
||||
clock *mockable.Clock
|
||||
lock sync.RWMutex
|
||||
log log.Logger
|
||||
db database.Database
|
||||
closed bool
|
||||
|
||||
// Called in a goroutine on shutdown
|
||||
shutdownF func()
|
||||
|
||||
// true if this is not the first run using this database
|
||||
hasRunBefore bool
|
||||
|
||||
// Used to add API endpoint for new indices
|
||||
pathAdder server.PathAdder
|
||||
|
||||
// If true, allow running in such a way that could allow the creation
|
||||
// of an index which could be missing accepted containers.
|
||||
allowIncompleteIndex bool
|
||||
|
||||
// If false, don't create index for a chain when RegisterChain is called
|
||||
indexingEnabled bool
|
||||
|
||||
// Chain ID --> index of blocks of that chain (if applicable)
|
||||
blockIndices map[ids.ID]*index
|
||||
// Chain ID --> index of vertices of that chain (if applicable)
|
||||
vtxIndices map[ids.ID]*index
|
||||
// Chain ID --> index of txs of that chain (if applicable)
|
||||
txIndices map[ids.ID]*index
|
||||
|
||||
// Notifies of newly accepted blocks
|
||||
blockAcceptorGroup nodeconsensus.AcceptorGroup
|
||||
// Notifies of newly accepted transactions
|
||||
txAcceptorGroup nodeconsensus.AcceptorGroup
|
||||
// Notifies of newly accepted vertices
|
||||
vertexAcceptorGroup nodeconsensus.AcceptorGroup
|
||||
}
|
||||
|
||||
// RegisterChain registers a chain for indexing
|
||||
func (i *indexer) RegisterChain(chainName string, rt *runtime.Runtime, vm vm.VM) {
|
||||
i.lock.Lock()
|
||||
defer i.lock.Unlock()
|
||||
|
||||
// Extract chain ID from runtime
|
||||
chainID := rt.ChainID
|
||||
|
||||
if i.closed {
|
||||
i.log.Debug("not registering chain to indexer",
|
||||
log.String("reason", "indexer is closed"),
|
||||
log.String("chainName", chainName),
|
||||
)
|
||||
return
|
||||
} else if rt.NetworkID != 1 && rt.NetworkID != 2 {
|
||||
// Only index chains on mainnet (1) or testnet (2) - skip custom networks
|
||||
i.log.Debug("not registering chain to indexer",
|
||||
log.String("reason", "not on mainnet or testnet"),
|
||||
log.String("chainName", chainName),
|
||||
)
|
||||
return
|
||||
}
|
||||
if i.blockIndices[chainID] != nil || i.txIndices[chainID] != nil || i.vtxIndices[chainID] != nil {
|
||||
i.log.Warn("chain is already being indexed",
|
||||
log.Stringer("chainID", chainID),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// If the index is incomplete, make sure that's OK. Otherwise, cause node to die.
|
||||
isIncomplete, err := i.isIncomplete(chainID)
|
||||
if err != nil {
|
||||
i.log.Error("couldn't get whether chain is incomplete",
|
||||
log.String("chainName", chainName),
|
||||
log.Err(err),
|
||||
)
|
||||
if err := i.close(); err != nil {
|
||||
i.log.Error("failed to close indexer",
|
||||
log.Err(err),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// See if this chain was indexed in a previous run
|
||||
previouslyIndexed, err := i.previouslyIndexed(chainID)
|
||||
if err != nil {
|
||||
i.log.Error("couldn't get whether chain was previously indexed",
|
||||
log.String("chainName", chainName),
|
||||
log.Err(err),
|
||||
)
|
||||
if err := i.close(); err != nil {
|
||||
i.log.Error("failed to close indexer",
|
||||
log.Err(err),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !i.indexingEnabled { // Indexing is disabled
|
||||
if previouslyIndexed && !i.allowIncompleteIndex {
|
||||
// We indexed this chain in a previous run but not in this run.
|
||||
// This would create an incomplete index, which is not allowed, so exit.
|
||||
i.log.Error("running would cause index to become incomplete but incomplete indices are disabled",
|
||||
log.String("chainName", chainName),
|
||||
)
|
||||
if err := i.close(); err != nil {
|
||||
i.log.Error("failed to close indexer",
|
||||
log.Err(err),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Creating an incomplete index is allowed. Mark index as incomplete.
|
||||
err := i.markIncomplete(chainID)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
i.log.Error("couldn't mark chain as incomplete",
|
||||
log.String("chainName", chainName),
|
||||
log.Err(err),
|
||||
)
|
||||
if err := i.close(); err != nil {
|
||||
i.log.Error("failed to close indexer",
|
||||
log.Err(err),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !i.allowIncompleteIndex && isIncomplete && (previouslyIndexed || i.hasRunBefore) {
|
||||
i.log.Error("index is incomplete but incomplete indices are disabled. Shutting down",
|
||||
log.String("chainName", chainName),
|
||||
)
|
||||
if err := i.close(); err != nil {
|
||||
i.log.Error("failed to close indexer",
|
||||
log.Err(err),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Mark that in this run, this chain was indexed
|
||||
if err := i.markPreviouslyIndexed(chainID); err != nil {
|
||||
i.log.Error("couldn't mark chain as indexed",
|
||||
log.String("chainName", chainName),
|
||||
log.Err(err),
|
||||
)
|
||||
if err := i.close(); err != nil {
|
||||
i.log.Error("failed to close indexer",
|
||||
log.Err(err),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
index, err := i.registerChainHelper(chainID, blockPrefix, chainName, "block", i.blockAcceptorGroup)
|
||||
if err != nil {
|
||||
i.log.Error("failed to create index",
|
||||
log.String("chainName", chainName),
|
||||
log.String("endpoint", "block"),
|
||||
log.Err(err),
|
||||
log.String("debug", "closing indexer due to block index creation failure"),
|
||||
)
|
||||
if err := i.close(); err != nil {
|
||||
i.log.Error("failed to close indexer",
|
||||
log.Err(err),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
i.blockIndices[chainID] = index
|
||||
|
||||
// Currently supporting block-based VM indexing via core.VM interface
|
||||
// DAG-based VM indexing is fully supported in the consensus package
|
||||
vmType := fmt.Sprintf("%T", vm)
|
||||
i.log.Debug("RegisterChain completed",
|
||||
log.String("chainName", chainName),
|
||||
log.String("vmType", vmType),
|
||||
log.Stringer("chainID", chainID),
|
||||
)
|
||||
}
|
||||
|
||||
func (i *indexer) registerChainHelper(
|
||||
chainID ids.ID,
|
||||
prefixEnd byte,
|
||||
name, endpoint string,
|
||||
acceptorGroup nodeconsensus.AcceptorGroup,
|
||||
) (*index, error) {
|
||||
prefix := make([]byte, ids.IDLen+wrappers.ByteLen)
|
||||
copy(prefix, chainID[:])
|
||||
prefix[ids.IDLen] = prefixEnd
|
||||
indexDB := prefixdb.New(prefix, i.db)
|
||||
index, err := newIndex(indexDB, i.log, i.clock)
|
||||
if err != nil {
|
||||
// Don't close indexDB as it would close the underlying database
|
||||
// The prefixdb doesn't hold resources that need explicit cleanup
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Register index to learn about new accepted vertices
|
||||
if err := acceptorGroup.RegisterAcceptor(chainID, fmt.Sprintf("%s%s", indexNamePrefix, chainID), index, true); err != nil {
|
||||
_ = index.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create an API endpoint for this index
|
||||
apiServer := rpc.NewServer()
|
||||
codec := json.NewCodec()
|
||||
apiServer.RegisterCodec(codec, "application/json")
|
||||
apiServer.RegisterCodec(codec, "application/json;charset=UTF-8")
|
||||
if err := apiServer.RegisterService(&service{index: index}, "index"); err != nil {
|
||||
_ = index.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := i.pathAdder.AddRoute(apiServer, "index/"+name, "/"+endpoint); err != nil {
|
||||
_ = index.Close()
|
||||
return nil, err
|
||||
}
|
||||
return index, nil
|
||||
}
|
||||
|
||||
// Close this indexer. Stops indexing all chains.
|
||||
// Closes [i.db]. Assumes Close is only called after
|
||||
// the node is done making decisions.
|
||||
// Calling Close after it has been called does nothing.
|
||||
func (i *indexer) Close() error {
|
||||
i.lock.Lock()
|
||||
defer i.lock.Unlock()
|
||||
|
||||
return i.close()
|
||||
}
|
||||
|
||||
func (i *indexer) close() error {
|
||||
if i.closed {
|
||||
return nil
|
||||
}
|
||||
i.closed = true
|
||||
|
||||
errs := &wrappers.Errs{}
|
||||
for chainID, txIndex := range i.txIndices {
|
||||
errs.Add(
|
||||
txIndex.Close(),
|
||||
i.txAcceptorGroup.DeregisterAcceptor(chainID, fmt.Sprintf("%s%s", indexNamePrefix, chainID)),
|
||||
)
|
||||
}
|
||||
for chainID, vtxIndex := range i.vtxIndices {
|
||||
errs.Add(
|
||||
vtxIndex.Close(),
|
||||
i.vertexAcceptorGroup.DeregisterAcceptor(chainID, fmt.Sprintf("%s%s", indexNamePrefix, chainID)),
|
||||
)
|
||||
}
|
||||
for chainID, blockIndex := range i.blockIndices {
|
||||
errs.Add(
|
||||
blockIndex.Close(),
|
||||
i.blockAcceptorGroup.DeregisterAcceptor(chainID, fmt.Sprintf("%s%s", indexNamePrefix, chainID)),
|
||||
)
|
||||
}
|
||||
errs.Add(i.db.Close())
|
||||
|
||||
go i.shutdownF()
|
||||
return errs.Err
|
||||
}
|
||||
|
||||
func (i *indexer) markIncomplete(chainID ids.ID) error {
|
||||
key := make([]byte, ids.IDLen+wrappers.ByteLen)
|
||||
copy(key, chainID[:])
|
||||
key[ids.IDLen] = isIncompletePrefix
|
||||
return i.db.Put(key, nil)
|
||||
}
|
||||
|
||||
// Returns true if this chain is incomplete
|
||||
func (i *indexer) isIncomplete(chainID ids.ID) (bool, error) {
|
||||
key := make([]byte, ids.IDLen+wrappers.ByteLen)
|
||||
copy(key, chainID[:])
|
||||
key[ids.IDLen] = isIncompletePrefix
|
||||
return i.db.Has(key)
|
||||
}
|
||||
|
||||
func (i *indexer) markPreviouslyIndexed(chainID ids.ID) error {
|
||||
key := make([]byte, ids.IDLen+wrappers.ByteLen)
|
||||
copy(key, chainID[:])
|
||||
key[ids.IDLen] = previouslyIndexedPrefix
|
||||
return i.db.Put(key, nil)
|
||||
}
|
||||
|
||||
// Returns true if this chain is incomplete
|
||||
func (i *indexer) previouslyIndexed(chainID ids.ID) (bool, error) {
|
||||
key := make([]byte, ids.IDLen+wrappers.ByteLen)
|
||||
copy(key, chainID[:])
|
||||
key[ids.IDLen] = previouslyIndexedPrefix
|
||||
return i.db.Has(key)
|
||||
}
|
||||
|
||||
// Mark that the node has run at least once
|
||||
func (i *indexer) markHasRun() error {
|
||||
return i.db.Put(hasRunKey, nil)
|
||||
}
|
||||
|
||||
// Returns true if the node has run before
|
||||
func (i *indexer) hasRun() (bool, error) {
|
||||
return i.db.Has(hasRunKey)
|
||||
}
|
||||
@@ -0,0 +1,537 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"github.com/luxfi/database/memdb"
|
||||
"github.com/luxfi/database/versiondb"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
nodeconsensus "github.com/luxfi/node/consensus"
|
||||
"github.com/luxfi/node/server/http"
|
||||
"github.com/luxfi/runtime"
|
||||
"github.com/luxfi/utils"
|
||||
"github.com/luxfi/vm"
|
||||
)
|
||||
|
||||
// testRuntime creates a minimal runtime.Runtime for testing
|
||||
func testRuntimeWithID(chainID ids.ID) *runtime.Runtime {
|
||||
return &runtime.Runtime{
|
||||
NetworkID: 1,
|
||||
ChainID: chainID,
|
||||
NodeID: ids.GenerateTestNodeID(),
|
||||
}
|
||||
}
|
||||
|
||||
// mockChainVM is a simple mock for testing that implements interfaces.VM
|
||||
type mockChainVM struct{}
|
||||
|
||||
func (m *mockChainVM) Initialize(ctx context.Context, init vm.Init) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockChainVM) Shutdown(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockChainVM) ParseBlock(ctx context.Context, b []byte) (vm.Block, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockChainVM) GetBlock(ctx context.Context, id ids.ID) (vm.Block, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockChainVM) SetPreference(ctx context.Context, id ids.ID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockChainVM) LastAccepted(ctx context.Context) (ids.ID, error) {
|
||||
return ids.Empty, nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ server.PathAdder = (*apiServerMock)(nil)
|
||||
|
||||
errUnimplemented = errors.New("unimplemented")
|
||||
)
|
||||
|
||||
type apiServerMock struct {
|
||||
timesCalled int
|
||||
bases []string
|
||||
endpoints []string
|
||||
}
|
||||
|
||||
func (a *apiServerMock) AddRoute(_ http.Handler, base, endpoint string) error {
|
||||
a.timesCalled++
|
||||
a.bases = append(a.bases, base)
|
||||
a.endpoints = append(a.endpoints, endpoint)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*apiServerMock) AddAliases(string, ...string) error {
|
||||
return errUnimplemented
|
||||
}
|
||||
|
||||
// Test that newIndexer sets fields correctly
|
||||
func TestNewIndexer(t *testing.T) {
|
||||
require := require.New(t)
|
||||
config := Config{
|
||||
IndexingEnabled: true,
|
||||
AllowIncompleteIndex: true,
|
||||
Log: log.NoLog{},
|
||||
DB: memdb.New(),
|
||||
BlockAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
|
||||
TxAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
|
||||
VertexAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
|
||||
APIServer: &apiServerMock{},
|
||||
ShutdownF: func() {},
|
||||
}
|
||||
|
||||
idxrIntf, err := NewIndexer(config)
|
||||
require.NoError(err)
|
||||
require.IsType(&indexer{}, idxrIntf)
|
||||
idxr := idxrIntf.(*indexer)
|
||||
require.NotNil(idxr.log)
|
||||
require.NotNil(idxr.db)
|
||||
require.False(idxr.closed)
|
||||
require.NotNil(idxr.pathAdder)
|
||||
require.True(idxr.indexingEnabled)
|
||||
require.True(idxr.allowIncompleteIndex)
|
||||
require.NotNil(idxr.blockIndices)
|
||||
require.Empty(idxr.blockIndices)
|
||||
require.NotNil(idxr.txIndices)
|
||||
require.Empty(idxr.txIndices)
|
||||
require.NotNil(idxr.vtxIndices)
|
||||
require.Empty(idxr.vtxIndices)
|
||||
require.NotNil(idxr.blockAcceptorGroup)
|
||||
require.NotNil(idxr.txAcceptorGroup)
|
||||
require.NotNil(idxr.vertexAcceptorGroup)
|
||||
require.NotNil(idxr.shutdownF)
|
||||
require.False(idxr.hasRunBefore)
|
||||
}
|
||||
|
||||
// Test that [hasRunBefore] is set correctly and that Shutdown is called on close
|
||||
func TestMarkHasRunAndShutdown(t *testing.T) {
|
||||
require := require.New(t)
|
||||
baseDB := memdb.New()
|
||||
db := versiondb.New(baseDB)
|
||||
shutdown := &sync.WaitGroup{}
|
||||
shutdown.Add(1)
|
||||
config := Config{
|
||||
IndexingEnabled: true,
|
||||
Log: log.NoLog{},
|
||||
DB: db,
|
||||
BlockAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
|
||||
TxAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
|
||||
VertexAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
|
||||
APIServer: &apiServerMock{},
|
||||
ShutdownF: shutdown.Done,
|
||||
}
|
||||
|
||||
idxrIntf, err := NewIndexer(config)
|
||||
require.NoError(err)
|
||||
require.False(idxrIntf.(*indexer).hasRunBefore)
|
||||
require.NoError(db.Commit())
|
||||
require.NoError(idxrIntf.Close())
|
||||
shutdown.Wait()
|
||||
shutdown.Add(1)
|
||||
|
||||
config.DB = versiondb.New(baseDB)
|
||||
idxrIntf, err = NewIndexer(config)
|
||||
require.NoError(err)
|
||||
require.IsType(&indexer{}, idxrIntf)
|
||||
idxr := idxrIntf.(*indexer)
|
||||
require.True(idxr.hasRunBefore)
|
||||
require.NoError(idxr.Close())
|
||||
shutdown.Wait()
|
||||
}
|
||||
|
||||
// Test registering a linear chain and a DAG chain and accepting
|
||||
// some vertices
|
||||
func TestIndexer(t *testing.T) {
|
||||
require := require.New(t)
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
baseDB := memdb.New()
|
||||
db := versiondb.New(baseDB)
|
||||
server := &apiServerMock{}
|
||||
config := Config{
|
||||
IndexingEnabled: true,
|
||||
AllowIncompleteIndex: false,
|
||||
Log: log.NoLog{},
|
||||
DB: db,
|
||||
BlockAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
|
||||
TxAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
|
||||
VertexAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
|
||||
APIServer: server,
|
||||
ShutdownF: func() {},
|
||||
}
|
||||
|
||||
// Create indexer
|
||||
idxrIntf, err := NewIndexer(config)
|
||||
require.NoError(err)
|
||||
require.IsType(&indexer{}, idxrIntf)
|
||||
idxr := idxrIntf.(*indexer)
|
||||
now := time.Now()
|
||||
idxr.clock.Set(now)
|
||||
|
||||
// Assert state is right
|
||||
// Use a test chain ID
|
||||
testChainID := ids.GenerateTestID()
|
||||
chain1RT := testRuntimeWithID(testChainID)
|
||||
isIncomplete, err := idxr.isIncomplete(testChainID)
|
||||
require.NoError(err)
|
||||
require.False(isIncomplete)
|
||||
previouslyIndexed, err := idxr.previouslyIndexed(testChainID)
|
||||
require.NoError(err)
|
||||
require.False(previouslyIndexed)
|
||||
|
||||
// Register this chain, creating a new index
|
||||
chainVM := &mockChainVM{}
|
||||
idxr.RegisterChain("chain1", chain1RT, chainVM)
|
||||
t.Logf("After RegisterChain, closed=%v", idxr.closed)
|
||||
isIncomplete, err = idxr.isIncomplete(testChainID)
|
||||
require.NoError(err)
|
||||
require.False(isIncomplete)
|
||||
previouslyIndexed, err = idxr.previouslyIndexed(testChainID)
|
||||
require.NoError(err)
|
||||
require.True(previouslyIndexed)
|
||||
require.Equal(1, server.timesCalled)
|
||||
require.Equal("index/chain1", server.bases[0])
|
||||
require.Equal("/block", server.endpoints[0])
|
||||
require.Len(idxr.blockIndices, 1)
|
||||
require.Empty(idxr.txIndices)
|
||||
require.Empty(idxr.vtxIndices)
|
||||
|
||||
// Accept a container
|
||||
blkID, blkBytes := ids.GenerateTestID(), utils.RandomBytes(32)
|
||||
expectedContainer := Container{
|
||||
ID: blkID,
|
||||
Bytes: blkBytes,
|
||||
Timestamp: now.UnixNano(),
|
||||
}
|
||||
|
||||
// Accept the block through the index
|
||||
blkIdx := idxr.blockIndices[testChainID]
|
||||
require.NotNil(blkIdx)
|
||||
|
||||
// Accept the container
|
||||
require.NoError(blkIdx.Accept(chain1RT, blkID, blkBytes))
|
||||
|
||||
// Verify GetLastAccepted is right
|
||||
gotLastAccepted, err := blkIdx.GetLastAccepted()
|
||||
require.NoError(err)
|
||||
require.Equal(expectedContainer, gotLastAccepted)
|
||||
|
||||
// Verify GetContainerByID is right
|
||||
container, err := blkIdx.GetContainerByID(blkID)
|
||||
require.NoError(err)
|
||||
require.Equal(expectedContainer, container)
|
||||
|
||||
// Verify GetIndex is right
|
||||
index, err := blkIdx.GetIndex(blkID)
|
||||
require.NoError(err)
|
||||
require.Zero(index)
|
||||
|
||||
// Verify GetContainerByIndex is right
|
||||
container, err = blkIdx.GetContainerByIndex(0)
|
||||
require.NoError(err)
|
||||
require.Equal(expectedContainer, container)
|
||||
|
||||
// Verify GetContainerRange is right
|
||||
containers, err := blkIdx.GetContainerRange(0, 1)
|
||||
require.NoError(err)
|
||||
require.Len(containers, 1)
|
||||
require.Equal(expectedContainer, containers[0])
|
||||
|
||||
// Commit the database before closing the indexer
|
||||
require.NoError(db.Commit())
|
||||
|
||||
// Don't actually close the indexer to avoid closing the database
|
||||
// Just check that it would close properly
|
||||
require.False(idxr.closed)
|
||||
|
||||
server.timesCalled = 0
|
||||
|
||||
// Create a new indexer using the same baseDB to simulate restart
|
||||
config.DB = versiondb.New(baseDB)
|
||||
// Create new AcceptorGroups since the old ones still have the chain registered
|
||||
config.BlockAcceptorGroup = nodeconsensus.NewAcceptorGroup(log.NoLog{})
|
||||
config.TxAcceptorGroup = nodeconsensus.NewAcceptorGroup(log.NoLog{})
|
||||
config.VertexAcceptorGroup = nodeconsensus.NewAcceptorGroup(log.NoLog{})
|
||||
idxrIntf, err = NewIndexer(config)
|
||||
require.NoError(err)
|
||||
require.IsType(&indexer{}, idxrIntf)
|
||||
idxr = idxrIntf.(*indexer)
|
||||
now = time.Now()
|
||||
idxr.clock.Set(now)
|
||||
require.Empty(idxr.blockIndices)
|
||||
require.Empty(idxr.txIndices)
|
||||
require.Empty(idxr.vtxIndices)
|
||||
require.True(idxr.hasRunBefore)
|
||||
previouslyIndexed, err = idxr.previouslyIndexed(testChainID)
|
||||
require.NoError(err)
|
||||
require.True(previouslyIndexed)
|
||||
hasRun, err := idxr.hasRun()
|
||||
require.NoError(err)
|
||||
require.True(hasRun)
|
||||
isIncomplete, err = idxr.isIncomplete(testChainID)
|
||||
require.NoError(err)
|
||||
require.False(isIncomplete)
|
||||
|
||||
// Register the same chain as before
|
||||
chainVM2 := &mockChainVM{}
|
||||
idxr.RegisterChain("chain1", chain1RT, chainVM2)
|
||||
blkIdx = idxr.blockIndices[testChainID]
|
||||
require.NotNil(blkIdx)
|
||||
container, err = blkIdx.GetLastAccepted()
|
||||
require.NoError(err)
|
||||
require.Equal(blkID, container.ID)
|
||||
require.Equal(1, server.timesCalled) // block index for chain
|
||||
require.Contains(server.endpoints, "/block")
|
||||
|
||||
// Register a second chain (block-based, not DAG)
|
||||
// Note: DAG/vertex/tx indices are not supported in the new consensus package
|
||||
chain2ChainID := ids.GenerateTestID()
|
||||
chain2RT := testRuntimeWithID(chain2ChainID)
|
||||
isIncomplete, err = idxr.isIncomplete(chain2RT.ChainID)
|
||||
require.NoError(err)
|
||||
require.False(isIncomplete)
|
||||
previouslyIndexed, err = idxr.previouslyIndexed(chain2RT.ChainID)
|
||||
require.NoError(err)
|
||||
require.False(previouslyIndexed)
|
||||
chain2VM := &mockChainVM{}
|
||||
idxr.RegisterChain("chain2", chain2RT, chain2VM)
|
||||
require.NoError(err)
|
||||
// Only block indices are created now (vtx/tx indices not supported)
|
||||
require.Equal(2, server.timesCalled) // block index for chain1, block index for chain2
|
||||
require.Contains(server.bases, "index/chain2")
|
||||
require.Contains(server.endpoints, "/block")
|
||||
require.Len(idxr.blockIndices, 2)
|
||||
// No vertex or tx indices in new consensus package
|
||||
require.Empty(idxr.txIndices)
|
||||
require.Empty(idxr.vtxIndices)
|
||||
|
||||
// Accept a block on chain2
|
||||
blk2ID, blk2Bytes := ids.GenerateTestID(), utils.RandomBytes(32)
|
||||
expectedBlk2 := Container{
|
||||
ID: blk2ID,
|
||||
Bytes: blk2Bytes,
|
||||
Timestamp: now.UnixNano(),
|
||||
}
|
||||
|
||||
// Get the block index for chain2
|
||||
blk2Idx := idxr.blockIndices[chain2ChainID]
|
||||
require.NotNil(blk2Idx)
|
||||
|
||||
// Accept the block
|
||||
require.NoError(blk2Idx.Accept(chain2RT, blk2ID, blk2Bytes))
|
||||
|
||||
// Verify GetLastAccepted is right
|
||||
gotLastAccepted, err = blk2Idx.GetLastAccepted()
|
||||
require.NoError(err)
|
||||
require.Equal(expectedBlk2, gotLastAccepted)
|
||||
|
||||
// Verify GetContainerByID is right
|
||||
blk2, err := blk2Idx.GetContainerByID(blk2ID)
|
||||
require.NoError(err)
|
||||
require.Equal(expectedBlk2, blk2)
|
||||
|
||||
// Verify GetIndex is right
|
||||
index, err = blk2Idx.GetIndex(blk2ID)
|
||||
require.NoError(err)
|
||||
require.Zero(index)
|
||||
|
||||
// Verify GetContainerByIndex is right
|
||||
blk2, err = blk2Idx.GetContainerByIndex(0)
|
||||
require.NoError(err)
|
||||
require.Equal(expectedBlk2, blk2)
|
||||
|
||||
// Verify GetContainerRange is right
|
||||
blks2, err := blk2Idx.GetContainerRange(0, 1)
|
||||
require.NoError(err)
|
||||
require.Len(blks2, 1)
|
||||
require.Equal(expectedBlk2, blks2[0])
|
||||
|
||||
// No vertex or tx indices in new consensus package
|
||||
require.Empty(idxr.vtxIndices)
|
||||
require.Empty(idxr.txIndices)
|
||||
|
||||
// Verify both chains have their expected last accepted blocks
|
||||
lastAcceptedBlk1, err := blkIdx.GetLastAccepted()
|
||||
require.NoError(err)
|
||||
require.Equal(blkID, lastAcceptedBlk1.ID)
|
||||
|
||||
lastAcceptedBlk2, err := blk2Idx.GetLastAccepted()
|
||||
require.NoError(err)
|
||||
require.Equal(blk2ID, lastAcceptedBlk2.ID)
|
||||
|
||||
// Close the indexer again
|
||||
require.NoError(config.DB.(*versiondb.Database).Commit())
|
||||
// Don't actually close the indexer to avoid issues with shared database
|
||||
// Just check that it would close properly
|
||||
require.False(idxr.closed)
|
||||
|
||||
// Re-open one more time and re-register chains
|
||||
config.DB = versiondb.New(baseDB)
|
||||
// Create new AcceptorGroups since the old ones were closed
|
||||
config.BlockAcceptorGroup = nodeconsensus.NewAcceptorGroup(log.NoLog{})
|
||||
config.TxAcceptorGroup = nodeconsensus.NewAcceptorGroup(log.NoLog{})
|
||||
config.VertexAcceptorGroup = nodeconsensus.NewAcceptorGroup(log.NoLog{})
|
||||
idxrIntf, err = NewIndexer(config)
|
||||
require.NoError(err)
|
||||
require.IsType(&indexer{}, idxrIntf)
|
||||
idxr = idxrIntf.(*indexer)
|
||||
chainVM3 := &mockChainVM{}
|
||||
idxr.RegisterChain("chain1", chain1RT, chainVM3)
|
||||
// Re-register chain2 as well
|
||||
chain2VM2 := &mockChainVM{}
|
||||
idxr.RegisterChain("chain2", chain2RT, chain2VM2)
|
||||
|
||||
// Verify state - both chains should have their blocks
|
||||
lastAcceptedBlk1Again, err := idxr.blockIndices[testChainID].GetLastAccepted()
|
||||
require.NoError(err)
|
||||
require.Equal(blkID, lastAcceptedBlk1Again.ID)
|
||||
|
||||
lastAcceptedBlk2Again, err := idxr.blockIndices[chain2ChainID].GetLastAccepted()
|
||||
require.NoError(err)
|
||||
require.Equal(blk2ID, lastAcceptedBlk2Again.ID)
|
||||
|
||||
// No vertex or tx indices in new consensus package
|
||||
require.Empty(idxr.vtxIndices)
|
||||
require.Empty(idxr.txIndices)
|
||||
}
|
||||
|
||||
// Make sure the indexer doesn't allow incomplete indices unless explicitly allowed
|
||||
func TestIncompleteIndex(t *testing.T) {
|
||||
// Create an indexer with indexing disabled
|
||||
require := require.New(t)
|
||||
|
||||
baseDB := memdb.New()
|
||||
config := Config{
|
||||
IndexingEnabled: false,
|
||||
AllowIncompleteIndex: false,
|
||||
Log: log.NoLog{},
|
||||
DB: versiondb.New(baseDB),
|
||||
BlockAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
|
||||
TxAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
|
||||
VertexAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
|
||||
APIServer: &apiServerMock{},
|
||||
ShutdownF: func() {},
|
||||
}
|
||||
idxrIntf, err := NewIndexer(config)
|
||||
require.NoError(err)
|
||||
require.IsType(&indexer{}, idxrIntf)
|
||||
idxr := idxrIntf.(*indexer)
|
||||
require.False(idxr.indexingEnabled)
|
||||
|
||||
// Register a chain
|
||||
testChainID := ids.GenerateTestID()
|
||||
chain1RT := testRuntimeWithID(testChainID)
|
||||
isIncomplete, err := idxr.isIncomplete(testChainID)
|
||||
require.NoError(err)
|
||||
require.False(isIncomplete)
|
||||
previouslyIndexed, err := idxr.previouslyIndexed(testChainID)
|
||||
require.NoError(err)
|
||||
require.False(previouslyIndexed)
|
||||
chainVM := &mockChainVM{}
|
||||
idxr.RegisterChain("chain1", chain1RT, chainVM)
|
||||
isIncomplete, err = idxr.isIncomplete(testChainID)
|
||||
require.NoError(err)
|
||||
require.True(isIncomplete)
|
||||
require.Empty(idxr.blockIndices)
|
||||
|
||||
// Close and re-open the indexer, this time with indexing enabled
|
||||
require.NoError(config.DB.(*versiondb.Database).Commit())
|
||||
require.NoError(idxr.Close())
|
||||
config.IndexingEnabled = true
|
||||
config.DB = versiondb.New(baseDB)
|
||||
idxrIntf, err = NewIndexer(config)
|
||||
require.NoError(err)
|
||||
require.IsType(&indexer{}, idxrIntf)
|
||||
idxr = idxrIntf.(*indexer)
|
||||
require.True(idxr.indexingEnabled)
|
||||
|
||||
// Register the chain again. Should die due to incomplete index.
|
||||
require.NoError(config.DB.(*versiondb.Database).Commit())
|
||||
chainVM2 := &mockChainVM{}
|
||||
idxr.RegisterChain("chain1", chain1RT, chainVM2)
|
||||
require.True(idxr.closed)
|
||||
|
||||
// Close and re-open the indexer, this time with indexing enabled
|
||||
// and incomplete index allowed.
|
||||
require.NoError(idxr.Close())
|
||||
config.AllowIncompleteIndex = true
|
||||
config.DB = versiondb.New(baseDB)
|
||||
idxrIntf, err = NewIndexer(config)
|
||||
require.NoError(err)
|
||||
require.IsType(&indexer{}, idxrIntf)
|
||||
idxr = idxrIntf.(*indexer)
|
||||
require.True(idxr.allowIncompleteIndex)
|
||||
|
||||
// Register the chain again. Should be OK
|
||||
chainVM3 := &mockChainVM{}
|
||||
idxr.RegisterChain("chain1", chain1RT, chainVM3)
|
||||
require.False(idxr.closed)
|
||||
|
||||
// Don't close the indexer to avoid closing the database
|
||||
// Instead, just mark it as closed for testing purposes
|
||||
idxr.closed = true
|
||||
|
||||
config.AllowIncompleteIndex = false
|
||||
config.IndexingEnabled = false
|
||||
// Re-use the same baseDB
|
||||
config.DB = versiondb.New(baseDB)
|
||||
idxrIntf, err = NewIndexer(config)
|
||||
require.NoError(err)
|
||||
require.IsType(&indexer{}, idxrIntf)
|
||||
}
|
||||
|
||||
// Ensure we only index chains in the primary network
|
||||
func TestIgnoreNonDefaultChains(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
baseDB := memdb.New()
|
||||
db := versiondb.New(baseDB)
|
||||
config := Config{
|
||||
IndexingEnabled: true,
|
||||
AllowIncompleteIndex: false,
|
||||
Log: log.NoLog{},
|
||||
DB: db,
|
||||
BlockAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
|
||||
TxAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
|
||||
VertexAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
|
||||
APIServer: &apiServerMock{},
|
||||
ShutdownF: func() {},
|
||||
}
|
||||
|
||||
// Create indexer
|
||||
idxrIntf, err := NewIndexer(config)
|
||||
require.NoError(err)
|
||||
require.IsType(&indexer{}, idxrIntf)
|
||||
idxr := idxrIntf.(*indexer)
|
||||
|
||||
// Create chain1RT for a chain on a custom network (not mainnet/testnet)
|
||||
testChainID := ids.GenerateTestID()
|
||||
chain1RT := testRuntimeWithID(testChainID)
|
||||
// Set NetworkID to a custom network (not 1=mainnet or 2=testnet)
|
||||
chain1RT.NetworkID = 99 // Custom network
|
||||
|
||||
// RegisterChain should return without adding an index for this chain
|
||||
chainVM := &mockChainVM{}
|
||||
idxr.RegisterChain("chain1", chain1RT, chainVM)
|
||||
require.Empty(idxr.blockIndices)
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/database"
|
||||
"github.com/luxfi/formatting"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/node/utils/json"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
index *index
|
||||
}
|
||||
|
||||
type FormattedContainer struct {
|
||||
ID ids.ID `json:"id"`
|
||||
Bytes string `json:"bytes"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Encoding formatting.Encoding `json:"encoding"`
|
||||
Index json.Uint64 `json:"index"`
|
||||
}
|
||||
|
||||
func newFormattedContainer(c Container, index uint64, enc formatting.Encoding) (FormattedContainer, error) {
|
||||
fc := FormattedContainer{
|
||||
Encoding: enc,
|
||||
ID: c.ID,
|
||||
Index: json.Uint64(index),
|
||||
}
|
||||
bytesStr, err := formatting.Encode(enc, c.Bytes)
|
||||
if err != nil {
|
||||
return fc, err
|
||||
}
|
||||
fc.Bytes = bytesStr
|
||||
fc.Timestamp = time.Unix(0, c.Timestamp)
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
type GetLastAcceptedArgs struct {
|
||||
Encoding formatting.Encoding `json:"encoding"`
|
||||
}
|
||||
|
||||
func (s *service) GetLastAccepted(_ *http.Request, args *GetLastAcceptedArgs, reply *FormattedContainer) error {
|
||||
container, err := s.index.GetLastAccepted()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index, err := s.index.GetIndex(container.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't get index: %w", err)
|
||||
}
|
||||
*reply, err = newFormattedContainer(container, index, args.Encoding)
|
||||
return err
|
||||
}
|
||||
|
||||
type GetContainerByIndexArgs struct {
|
||||
Index json.Uint64 `json:"index"`
|
||||
Encoding formatting.Encoding `json:"encoding"`
|
||||
}
|
||||
|
||||
func (s *service) GetContainerByIndex(_ *http.Request, args *GetContainerByIndexArgs, reply *FormattedContainer) error {
|
||||
container, err := s.index.GetContainerByIndex(uint64(args.Index))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index, err := s.index.GetIndex(container.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't get index: %w", err)
|
||||
}
|
||||
*reply, err = newFormattedContainer(container, index, args.Encoding)
|
||||
return err
|
||||
}
|
||||
|
||||
type GetContainerRangeArgs struct {
|
||||
StartIndex json.Uint64 `json:"startIndex"`
|
||||
NumToFetch json.Uint64 `json:"numToFetch"`
|
||||
Encoding formatting.Encoding `json:"encoding"`
|
||||
}
|
||||
|
||||
type GetContainerRangeResponse struct {
|
||||
Containers []FormattedContainer `json:"containers"`
|
||||
}
|
||||
|
||||
// GetContainerRange returns the transactions at index [startIndex], [startIndex+1], ... , [startIndex+n-1]
|
||||
// If [n] == 0, returns an empty response (i.e. null).
|
||||
// If [startIndex] > the last accepted index, returns an error (unless the above apply.)
|
||||
// If [n] > [MaxFetchedByRange], returns an error.
|
||||
// If we run out of transactions, returns the ones fetched before running out.
|
||||
func (s *service) GetContainerRange(_ *http.Request, args *GetContainerRangeArgs, reply *GetContainerRangeResponse) error {
|
||||
containers, err := s.index.GetContainerRange(uint64(args.StartIndex), uint64(args.NumToFetch))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reply.Containers = make([]FormattedContainer, len(containers))
|
||||
for i, container := range containers {
|
||||
index, err := s.index.GetIndex(container.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't get index: %w", err)
|
||||
}
|
||||
reply.Containers[i], err = newFormattedContainer(container, index, args.Encoding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetIndexArgs struct {
|
||||
ID ids.ID `json:"id"`
|
||||
}
|
||||
|
||||
type GetIndexResponse struct {
|
||||
Index json.Uint64 `json:"index"`
|
||||
}
|
||||
|
||||
func (s *service) GetIndex(_ *http.Request, args *GetIndexArgs, reply *GetIndexResponse) error {
|
||||
index, err := s.index.GetIndex(args.ID)
|
||||
reply.Index = json.Uint64(index)
|
||||
return err
|
||||
}
|
||||
|
||||
type IsAcceptedArgs struct {
|
||||
ID ids.ID `json:"id"`
|
||||
}
|
||||
|
||||
type IsAcceptedResponse struct {
|
||||
IsAccepted bool `json:"isAccepted"`
|
||||
}
|
||||
|
||||
func (s *service) IsAccepted(_ *http.Request, args *IsAcceptedArgs, reply *IsAcceptedResponse) error {
|
||||
_, err := s.index.GetIndex(args.ID)
|
||||
if err == nil {
|
||||
reply.IsAccepted = true
|
||||
return nil
|
||||
}
|
||||
if err == database.ErrNotFound {
|
||||
reply.IsAccepted = false
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type GetContainerByIDArgs struct {
|
||||
ID ids.ID `json:"id"`
|
||||
Encoding formatting.Encoding `json:"encoding"`
|
||||
}
|
||||
|
||||
func (s *service) GetContainerByID(_ *http.Request, args *GetContainerByIDArgs, reply *FormattedContainer) error {
|
||||
container, err := s.index.GetContainerByID(args.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index, err := s.index.GetIndex(container.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't get index: %w", err)
|
||||
}
|
||||
*reply, err = newFormattedContainer(container, index, args.Encoding)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
LuxGo can be configured to run with an indexer. That is, it saves (indexes) every container (a block, vertex or transaction) it accepts on the X-Chain, P-Chain and C-Chain. To run LuxGo with indexing enabled, set command line flag [\--index-enabled](https://build.lux.network/docs/nodes/configure/configs-flags#--index-enabled-boolean) to true.
|
||||
|
||||
**LuxGo will only index containers that are accepted when running with `--index-enabled` set to true.** To ensure your node has a complete index, run a node with a fresh database and `--index-enabled` set to true. The node will accept every block, vertex and transaction in the network history during bootstrapping, ensuring your index is complete.
|
||||
|
||||
It is OK to turn off your node if it is running with indexing enabled. If it restarts with indexing still enabled, it will accept all containers that were accepted while it was offline. The indexer should never fail to index an accepted block, vertex or transaction.
|
||||
|
||||
Indexed containers (that is, accepted blocks, vertices and transactions) are timestamped with the time at which the node accepted that container. Note that if the container was indexed during bootstrapping, other nodes may have accepted the container much earlier. Every container indexed during bootstrapping will be timestamped with the time at which the node bootstrapped, not when it was first accepted by the network.
|
||||
|
||||
If `--index-enabled` is changed to `false` from `true`, LuxGo won't start as doing so would cause a previously complete index to become incomplete, unless the user explicitly says to do so with `--index-allow-incomplete`. This protects you from accidentally running with indexing disabled, after previously running with it enabled, which would result in an incomplete index.
|
||||
|
||||
This document shows how to query data from LuxGo's Index API. The Index API is only available when running with `--index-enabled`.
|
||||
|
||||
## Go Client
|
||||
|
||||
There is a Go implementation of an Index API client. See documentation [here](https://pkg.go.dev/github.com/luxfi/node/indexer#Client). This client can be used inside a Go program to connect to an LuxGo node that is running with the Index API enabled and make calls to the Index API.
|
||||
|
||||
## Format
|
||||
|
||||
This API uses the `json 2.0` RPC format. For more information on making JSON RPC calls, see [here](https://build.lux.network/docs/api-reference/guides/issuing-api-calls).
|
||||
|
||||
## Endpoints
|
||||
|
||||
Each chain has one or more index. To see if a C-Chain block is accepted, for example, send an API call to the C-Chain block index. To see if an X-Chain vertex is accepted, for example, send an API call to the X-Chain vertex index.
|
||||
|
||||
### C-Chain Blocks
|
||||
|
||||
```
|
||||
/ext/index/C/block
|
||||
```
|
||||
|
||||
### P-Chain Blocks
|
||||
|
||||
```
|
||||
/ext/index/P/block
|
||||
```
|
||||
|
||||
### X-Chain Transactions
|
||||
|
||||
```
|
||||
/ext/index/X/tx
|
||||
```
|
||||
|
||||
### X-Chain Blocks
|
||||
|
||||
```
|
||||
/ext/index/X/block
|
||||
```
|
||||
|
||||
<Callout type="warn">
|
||||
To ensure historical data can be accessed, the `/ext/index/X/vtx` is still accessible, even though it is no longer populated with chain data since the Cortina activation. If you are using `V1.10.0` or higher, you need to migrate to using the `/ext/index/X/block` endpoint.
|
||||
</Callout>
|
||||
|
||||
## Methods
|
||||
|
||||
### `index.getContainerByID`
|
||||
|
||||
Get container by ID.
|
||||
|
||||
**Signature**:
|
||||
|
||||
```
|
||||
index.getContainerByID({
|
||||
id: string,
|
||||
encoding: string
|
||||
}) -> {
|
||||
id: string,
|
||||
bytes: string,
|
||||
timestamp: string,
|
||||
encoding: string,
|
||||
index: string
|
||||
}
|
||||
```
|
||||
|
||||
**Request**:
|
||||
|
||||
- `id` is the container's ID
|
||||
- `encoding` is `"hex"` only.
|
||||
|
||||
**Response**:
|
||||
|
||||
- `id` is the container's ID
|
||||
- `bytes` is the byte representation of the container
|
||||
- `timestamp` is the time at which this node accepted the container
|
||||
- `encoding` is `"hex"` only.
|
||||
- `index` is how many containers were accepted in this index before this one
|
||||
|
||||
**Example Call**:
|
||||
|
||||
```sh
|
||||
curl --location --request POST 'localhost:9630/ext/index/X/tx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "index.getContainerByID",
|
||||
"params": {
|
||||
"id": "6fXf5hncR8LXvwtM8iezFQBpK5cubV6y1dWgpJCcNyzGB1EzY",
|
||||
"encoding":"hex"
|
||||
},
|
||||
"id": 1
|
||||
}'
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"id": "6fXf5hncR8LXvwtM8iezFQBpK5cubV6y1dWgpJCcNyzGB1EzY",
|
||||
"bytes": "0x00000000000400003039d891ad56056d9c01f18f43f58b5c784ad07a4a49cf3d1f11623804b5cba2c6bf00000001dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db000000070429ccc5c5eb3b80000000000000000000000001000000013cb7d3842e8cee6a0ebd09f1fe884f6861e1b29c00000001dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db00000001dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db000000050429d069189e0000000000010000000000000000c85fc1980a77c5da78fe5486233fc09a769bb812bcb2cc548cf9495d046b3f1b00000001dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db00000007000003a352a38240000000000000000000000001000000013cb7d3842e8cee6a0ebd09f1fe884f6861e1b29c0000000100000009000000011cdb75d4e0b0aeaba2ebc1ef208373fedc1ebbb498f8385ad6fb537211d1523a70d903b884da77d963d56f163191295589329b5710113234934d0fd59c01676b00b63d2108",
|
||||
"timestamp": "2021-04-02T15:34:00.262979-07:00",
|
||||
"encoding": "hex",
|
||||
"index": "0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `index.getContainerByIndex`
|
||||
|
||||
Get container by index. The first container accepted is at index 0, the second is at index 1, etc.
|
||||
|
||||
**Signature**:
|
||||
|
||||
```
|
||||
index.getContainerByIndex({
|
||||
index: uint64,
|
||||
encoding: string
|
||||
}) -> {
|
||||
id: string,
|
||||
bytes: string,
|
||||
timestamp: string,
|
||||
encoding: string,
|
||||
index: string
|
||||
}
|
||||
```
|
||||
|
||||
**Request**:
|
||||
|
||||
- `index` is how many containers were accepted in this index before this one
|
||||
- `encoding` is `"hex"` only.
|
||||
|
||||
**Response**:
|
||||
|
||||
- `id` is the container's ID
|
||||
- `bytes` is the byte representation of the container
|
||||
- `timestamp` is the time at which this node accepted the container
|
||||
- `index` is how many containers were accepted in this index before this one
|
||||
- `encoding` is `"hex"` only.
|
||||
|
||||
**Example Call**:
|
||||
|
||||
```sh
|
||||
curl --location --request POST 'localhost:9630/ext/index/X/tx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "index.getContainerByIndex",
|
||||
"params": {
|
||||
"index":0,
|
||||
"encoding": "hex"
|
||||
},
|
||||
"id": 1
|
||||
}'
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"id": "6fXf5hncR8LXvwtM8iezFQBpK5cubV6y1dWgpJCcNyzGB1EzY",
|
||||
"bytes": "0x00000000000400003039d891ad56056d9c01f18f43f58b5c784ad07a4a49cf3d1f11623804b5cba2c6bf00000001dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db000000070429ccc5c5eb3b80000000000000000000000001000000013cb7d3842e8cee6a0ebd09f1fe884f6861e1b29c00000001dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db00000001dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db000000050429d069189e0000000000010000000000000000c85fc1980a77c5da78fe5486233fc09a769bb812bcb2cc548cf9495d046b3f1b00000001dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db00000007000003a352a38240000000000000000000000001000000013cb7d3842e8cee6a0ebd09f1fe884f6861e1b29c0000000100000009000000011cdb75d4e0b0aeaba2ebc1ef208373fedc1ebbb498f8385ad6fb537211d1523a70d903b884da77d963d56f163191295589329b5710113234934d0fd59c01676b00b63d2108",
|
||||
"timestamp": "2021-04-02T15:34:00.262979-07:00",
|
||||
"encoding": "hex",
|
||||
"index": "0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `index.getContainerRange`
|
||||
|
||||
Returns the transactions at index \[`startIndex`\], \[`startIndex+1`\], ... , \[`startIndex+n-1`\]
|
||||
|
||||
- If \[`n`\] == 0, returns an empty response (for example: null).
|
||||
- If \[`startIndex`\] > the last accepted index, returns an error (unless the above apply.)
|
||||
- If \[`n`\] > \[`MaxFetchedByRange`\], returns an error.
|
||||
- If we run out of transactions, returns the ones fetched before running out.
|
||||
- `numToFetch` must be in `[0,1024]`.
|
||||
|
||||
**Signature**:
|
||||
|
||||
```
|
||||
index.getContainerRange({
|
||||
startIndex: uint64,
|
||||
numToFetch: uint64,
|
||||
encoding: string
|
||||
}) -> []{
|
||||
id: string,
|
||||
bytes: string,
|
||||
timestamp: string,
|
||||
encoding: string,
|
||||
index: string
|
||||
}
|
||||
```
|
||||
|
||||
**Request**:
|
||||
|
||||
- `startIndex` is the beginning index
|
||||
- `numToFetch` is the number of containers to fetch
|
||||
- `encoding` is `"hex"` only.
|
||||
|
||||
**Response**:
|
||||
|
||||
- `id` is the container's ID
|
||||
- `bytes` is the byte representation of the container
|
||||
- `timestamp` is the time at which this node accepted the container
|
||||
- `encoding` is `"hex"` only.
|
||||
- `index` is how many containers were accepted in this index before this one
|
||||
|
||||
**Example Call**:
|
||||
|
||||
```sh
|
||||
curl --location --request POST 'localhost:9630/ext/index/X/tx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "index.getContainerRange",
|
||||
"params": {
|
||||
"startIndex":0,
|
||||
"numToFetch":100,
|
||||
"encoding": "hex"
|
||||
},
|
||||
"id": 1
|
||||
}'
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": [
|
||||
{
|
||||
"id": "6fXf5hncR8LXvwtM8iezFQBpK5cubV6y1dWgpJCcNyzGB1EzY",
|
||||
"bytes": "0x00000000000400003039d891ad56056d9c01f18f43f58b5c784ad07a4a49cf3d1f11623804b5cba2c6bf00000001dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db000000070429ccc5c5eb3b80000000000000000000000001000000013cb7d3842e8cee6a0ebd09f1fe884f6861e1b29c00000001dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db00000001dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db000000050429d069189e0000000000010000000000000000c85fc1980a77c5da78fe5486233fc09a769bb812bcb2cc548cf9495d046b3f1b00000001dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db00000007000003a352a38240000000000000000000000001000000013cb7d3842e8cee6a0ebd09f1fe884f6861e1b29c0000000100000009000000011cdb75d4e0b0aeaba2ebc1ef208373fedc1ebbb498f8385ad6fb537211d1523a70d903b884da77d963d56f163191295589329b5710113234934d0fd59c01676b00b63d2108",
|
||||
"timestamp": "2021-04-02T15:34:00.262979-07:00",
|
||||
"encoding": "hex",
|
||||
"index": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `index.getIndex`
|
||||
|
||||
Get a container's index.
|
||||
|
||||
**Signature**:
|
||||
|
||||
```
|
||||
index.getIndex({
|
||||
id: string,
|
||||
encoding: string
|
||||
}) -> {
|
||||
index: string
|
||||
}
|
||||
```
|
||||
|
||||
**Request**:
|
||||
|
||||
- `id` is the ID of the container to fetch
|
||||
- `encoding` is `"hex"` only.
|
||||
|
||||
**Response**:
|
||||
|
||||
- `index` is how many containers were accepted in this index before this one
|
||||
|
||||
**Example Call**:
|
||||
|
||||
```sh
|
||||
curl --location --request POST 'localhost:9630/ext/index/X/tx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "index.getIndex",
|
||||
"params": {
|
||||
"id":"6fXf5hncR8LXvwtM8iezFQBpK5cubV6y1dWgpJCcNyzGB1EzY",
|
||||
"encoding": "hex"
|
||||
},
|
||||
"id": 1
|
||||
}'
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"index": "0"
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
```
|
||||
|
||||
### `index.getLastAccepted`
|
||||
|
||||
Get the most recently accepted container.
|
||||
|
||||
**Signature**:
|
||||
|
||||
```
|
||||
index.getLastAccepted({
|
||||
encoding:string
|
||||
}) -> {
|
||||
id: string,
|
||||
bytes: string,
|
||||
timestamp: string,
|
||||
encoding: string,
|
||||
index: string
|
||||
}
|
||||
```
|
||||
|
||||
**Request**:
|
||||
|
||||
- `encoding` is `"hex"` only.
|
||||
|
||||
**Response**:
|
||||
|
||||
- `id` is the container's ID
|
||||
- `bytes` is the byte representation of the container
|
||||
- `timestamp` is the time at which this node accepted the container
|
||||
- `encoding` is `"hex"` only.
|
||||
|
||||
**Example Call**:
|
||||
|
||||
```sh
|
||||
curl --location --request POST 'localhost:9630/ext/index/X/tx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "index.getLastAccepted",
|
||||
"params": {
|
||||
"encoding": "hex"
|
||||
},
|
||||
"id": 1
|
||||
}'
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"id": "6fXf5hncR8LXvwtM8iezFQBpK5cubV6y1dWgpJCcNyzGB1EzY",
|
||||
"bytes": "0x00000000000400003039d891ad56056d9c01f18f43f58b5c784ad07a4a49cf3d1f11623804b5cba2c6bf00000001dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db000000070429ccc5c5eb3b80000000000000000000000001000000013cb7d3842e8cee6a0ebd09f1fe884f6861e1b29c00000001dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db00000001dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db000000050429d069189e0000000000010000000000000000c85fc1980a77c5da78fe5486233fc09a769bb812bcb2cc548cf9495d046b3f1b00000001dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db00000007000003a352a38240000000000000000000000001000000013cb7d3842e8cee6a0ebd09f1fe884f6861e1b29c0000000100000009000000011cdb75d4e0b0aeaba2ebc1ef208373fedc1ebbb498f8385ad6fb537211d1523a70d903b884da77d963d56f163191295589329b5710113234934d0fd59c01676b00b63d2108",
|
||||
"timestamp": "2021-04-02T15:34:00.262979-07:00",
|
||||
"encoding": "hex",
|
||||
"index": "0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `index.isAccepted`
|
||||
|
||||
Returns true if the container is in this index.
|
||||
|
||||
**Signature**:
|
||||
|
||||
```
|
||||
index.isAccepted({
|
||||
id: string,
|
||||
encoding: string
|
||||
}) -> {
|
||||
isAccepted: bool
|
||||
}
|
||||
```
|
||||
|
||||
**Request**:
|
||||
|
||||
- `id` is the ID of the container to fetch
|
||||
- `encoding` is `"hex"` only.
|
||||
|
||||
**Response**:
|
||||
|
||||
- `isAccepted` displays if the container has been accepted
|
||||
|
||||
**Example Call**:
|
||||
|
||||
```sh
|
||||
curl --location --request POST 'localhost:9630/ext/index/X/tx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "index.isAccepted",
|
||||
"params": {
|
||||
"id":"6fXf5hncR8LXvwtM8iezFQBpK5cubV6y1dWgpJCcNyzGB1EzY",
|
||||
"encoding": "hex"
|
||||
},
|
||||
"id": 1
|
||||
}'
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"isAccepted": true
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
```
|
||||
|
||||
## Example: Iterating Through X-Chain Transaction
|
||||
|
||||
Here is an example of how to iterate through all transactions on the X-Chain.
|
||||
|
||||
You can use the Index API to get the ID of every transaction that has been accepted on the X-Chain, and use the X-Chain API method `xvm.getTx` to get a human-readable representation of the transaction.
|
||||
|
||||
To get an X-Chain transaction by its index (the order it was accepted in), use Index API method [index.getlastaccepted](#indexgetlastaccepted).
|
||||
|
||||
For example, to get the second transaction (note that `"index":1`) accepted on the X-Chain, do:
|
||||
|
||||
```sh
|
||||
curl --location --request POST 'https://indexer-demo.lux.network/ext/index/X/tx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "index.getContainerByIndex",
|
||||
"params": {
|
||||
"encoding":"hex",
|
||||
"index":1
|
||||
},
|
||||
"id": 1
|
||||
}'
|
||||
```
|
||||
|
||||
This returns the ID of the second transaction accepted in the X-Chain's history. To get the third transaction on the X-Chain, use `"index":2`, and so on.
|
||||
|
||||
The above API call gives the response below:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"id": "ZGYTSU8w3zUP6VFseGC798vA2Vnxnfj6fz1QPfA9N93bhjJvo",
|
||||
"bytes": "0x00000000000000000001ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b0000000221e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000070000000129f6afc0000000000000000000000001000000017416792e228a765c65e2d76d28ab5a16d18c342f21e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff0000000700000222afa575c00000000000000000000000010000000187d6a6dd3cd7740c8b13a410bea39b01fa83bb3e000000016f375c785edb28d52edb59b54035c96c198e9d80f5f5f5eee070592fe9465b8d0000000021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff0000000500000223d9ab67c0000000010000000000000000000000010000000900000001beb83d3d29f1247efb4a3a1141ab5c966f46f946f9c943b9bc19f858bd416d10060c23d5d9c7db3a0da23446b97cd9cf9f8e61df98e1b1692d764c84a686f5f801a8da6e40",
|
||||
"timestamp": "2021-11-04T00:42:55.01643414Z",
|
||||
"encoding": "hex",
|
||||
"index": "1"
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
```
|
||||
|
||||
The ID of this transaction is `ZGYTSU8w3zUP6VFseGC798vA2Vnxnfj6fz1QPfA9N93bhjJvo`.
|
||||
|
||||
To get the transaction by its ID, use API method `xvm.getTx`:
|
||||
|
||||
```sh
|
||||
curl -X POST --data '{
|
||||
"jsonrpc":"2.0",
|
||||
"id" :1,
|
||||
"method" :"xvm.getTx",
|
||||
"params" :{
|
||||
"txID":"ZGYTSU8w3zUP6VFseGC798vA2Vnxnfj6fz1QPfA9N93bhjJvo",
|
||||
"encoding": "json"
|
||||
}
|
||||
}' -H 'content-type:application/json;' https://api.lux.network/ext/bc/X
|
||||
```
|
||||
|
||||
**Response**:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"tx": {
|
||||
"unsignedTx": {
|
||||
"networkID": 1,
|
||||
"blockchainID": "2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM",
|
||||
"outputs": [
|
||||
{
|
||||
"assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z",
|
||||
"fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ",
|
||||
"output": {
|
||||
"addresses": ["X-lux1wst8jt3z3fm9ce0z6akj3266zmgccdp03hjlaj"],
|
||||
"amount": 4999000000,
|
||||
"locktime": 0,
|
||||
"threshold": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z",
|
||||
"fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ",
|
||||
"output": {
|
||||
"addresses": ["X-lux1slt2dhfu6a6qezcn5sgtagumq8ag8we75f84sw"],
|
||||
"amount": 2347999000000,
|
||||
"locktime": 0,
|
||||
"threshold": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"txID": "qysTYUMCWdsR3MctzyfXiSvoSf6evbeFGRLLzA4j2BjNXTknh",
|
||||
"outputIndex": 0,
|
||||
"assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z",
|
||||
"fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ",
|
||||
"input": {
|
||||
"amount": 2352999000000,
|
||||
"signatureIndices": [0]
|
||||
}
|
||||
}
|
||||
],
|
||||
"memo": "0x"
|
||||
},
|
||||
"credentials": [
|
||||
{
|
||||
"fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ",
|
||||
"credential": {
|
||||
"signatures": [
|
||||
"0xbeb83d3d29f1247efb4a3a1141ab5c966f46f946f9c943b9bc19f858bd416d10060c23d5d9c7db3a0da23446b97cd9cf9f8e61df98e1b1692d764c84a686f5f801"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"encoding": "json"
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user