clean: squash history (binaries stripped via filter-repo)

This commit is contained in:
Hanzo AI
2026-04-13 03:45:21 -07:00
commit f6e63d60b2
2029 changed files with 465257 additions and 0 deletions
+143
View File
@@ -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
}
+132
View File
@@ -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)
}
}
+24
View File
@@ -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)
}
}
+17
View File
@@ -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"`
}
+56
View File
@@ -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++
}
}
+56
View File
@@ -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++
}
}
+278
View File
@@ -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
}
+175
View File
@@ -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)
}
+392
View File
@@ -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)
}
+537
View File
@@ -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)
}
+165
View File
@@ -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
}
+541
View File
@@ -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
}
```