mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
luxd serves chain routes under /v1/chain, and the node, cli and sdk releases already ship it. The deployed gateways still answer the old prefix, so remote reads break until they are redeployed; the estate moves together rather than sitting half converted. Lines that record an observation - a probe, a status code, a past release - keep the prefix they were written against, as do URLs on hosts that are not ours. Avalanche endpoints move to /ext/bc, which is the path that host serves. Co-authored-by: Hanzo Dev <[email protected]>
215 lines
7.6 KiB
Go
215 lines
7.6 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package zkvm
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/luxfi/database"
|
|
"github.com/luxfi/ids"
|
|
)
|
|
|
|
// call drives one endpoint at the path the node mounts it on. The node matches
|
|
// /v1/chain/<chainID>+key EXACTLY and hands the handler the full path, so a
|
|
// handler that dispatches on r.URL.Path recognises nothing — which is what a
|
|
// mux behind a single key does.
|
|
func call(t *testing.T, vm *VM, verb, route, query string, body string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
h, ok := vm.endpoints()[route]
|
|
require.True(t, ok, "no endpoint at %s", route)
|
|
|
|
url := "/v1/chain/24C9zm36x43T7LqcaKF1ikHxSeuQeTXnstzi5Gwh2apo18rXNE" + route
|
|
if query != "" {
|
|
url += "?" + query
|
|
}
|
|
r := httptest.NewRequest(verb, url, strings.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, r)
|
|
return rec
|
|
}
|
|
|
|
func TestEveryEndpointAnswersAtItsMountedPath(t *testing.T) {
|
|
vm := newVM(t)
|
|
for route, h := range vm.endpoints() {
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/v1/chain/chain"+route, nil))
|
|
require.NotEqual(t, http.StatusNotFound, rec.Code,
|
|
"GET %s: the handler dispatches on the request path instead of answering at its mount", route)
|
|
}
|
|
require.Len(t, vm.endpoints(), 9)
|
|
}
|
|
|
|
func TestSendTransactionEndpoint(t *testing.T) {
|
|
vm := newVM(t)
|
|
|
|
require.Equal(t, http.StatusBadRequest, call(t, vm, http.MethodPost, "/sendTransaction", "", "not json").Code)
|
|
|
|
tx := spendTx(nullifier(1))
|
|
tx.Fee = 1 << 40
|
|
tx.ID = ids.GenerateTestID() // the sender's choice, and it is ignored
|
|
body, err := json.Marshal(tx)
|
|
require.NoError(t, err)
|
|
|
|
rec := call(t, vm, http.MethodPost, "/sendTransaction", "", string(body))
|
|
require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
|
|
require.Contains(t, rec.Body.String(), tx.ComputeID().String())
|
|
require.Equal(t, 1, vm.mempool.Size())
|
|
|
|
// The same transaction again is the transaction the pool already holds.
|
|
require.Equal(t, http.StatusOK, call(t, vm, http.MethodPost, "/sendTransaction", "", string(body)).Code)
|
|
require.Equal(t, 1, vm.mempool.Size())
|
|
|
|
// A DIFFERENT transaction spending the same note is refused.
|
|
rival := spendTx(nullifier(1))
|
|
rival.Fee = 1 << 40
|
|
rival.Memo = []byte("different")
|
|
rivalBody, err := json.Marshal(rival)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusBadRequest, call(t, vm, http.MethodPost, "/sendTransaction", "", string(rivalBody)).Code)
|
|
}
|
|
|
|
func TestGetTransactionEndpoint(t *testing.T) {
|
|
vm := newVM(t)
|
|
|
|
require.Equal(t, http.StatusBadRequest, call(t, vm, http.MethodGet, "/getTransaction", "txID=nonsense", "").Code)
|
|
require.Equal(t, http.StatusNotFound,
|
|
call(t, vm, http.MethodGet, "/getTransaction", "txID="+ids.GenerateTestID().String(), "").Code)
|
|
|
|
tx := spendTx(nullifier(1))
|
|
require.NoError(t, vm.mempool.AddTransaction(tx))
|
|
rec := call(t, vm, http.MethodGet, "/getTransaction", "txID="+tx.ID.String(), "")
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
require.Contains(t, rec.Body.String(), "pending")
|
|
}
|
|
|
|
func TestBlockEndpoints(t *testing.T) {
|
|
ctx := context.Background()
|
|
vm := newVM(t)
|
|
|
|
require.Equal(t, http.StatusBadRequest, call(t, vm, http.MethodGet, "/getBlock", "blockID=nonsense", "").Code)
|
|
require.Equal(t, http.StatusNotFound,
|
|
call(t, vm, http.MethodGet, "/getBlock", "blockID="+ids.GenerateTestID().String(), "").Code)
|
|
|
|
blk := build(t, vm, spendTx(nullifier(1)))
|
|
require.NoError(t, blk.Verify(ctx))
|
|
require.NoError(t, blk.Accept(ctx))
|
|
|
|
rec := call(t, vm, http.MethodGet, "/getBlock", "blockID="+blk.ID().String(), "")
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
var summary BlockSummary
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &summary))
|
|
require.Equal(t, blk.ID(), summary.ID)
|
|
require.EqualValues(t, 1, summary.Height)
|
|
|
|
rec = call(t, vm, http.MethodGet, "/getLatestBlock", "", "")
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
require.Contains(t, rec.Body.String(), blk.ID().String())
|
|
}
|
|
|
|
// The latest block is whatever the tip names. A tip that names nothing this
|
|
// node holds is a failure to report, not a block to invent.
|
|
func TestLatestBlockReportsAnUnresolvableTip(t *testing.T) {
|
|
ctx := context.Background()
|
|
vm := newVM(t)
|
|
|
|
tx := spendTx(nullifier(1))
|
|
acceptProofs(vm, tx)
|
|
require.NoError(t, vm.mempool.AddTransaction(tx))
|
|
built, err := vm.BuildVertex(ctx)
|
|
require.NoError(t, err)
|
|
require.NoError(t, built.Verify(ctx))
|
|
require.NoError(t, built.Accept(ctx))
|
|
|
|
// The tip is a vertex, which is a decision this chain makes but not a
|
|
// block the caller can be handed.
|
|
require.Equal(t, http.StatusNotFound, call(t, vm, http.MethodGet, "/getLatestBlock", "", "").Code)
|
|
}
|
|
|
|
func TestUTXOEndpoints(t *testing.T) {
|
|
vm := newVM(t)
|
|
|
|
require.Equal(t, http.StatusBadRequest, call(t, vm, http.MethodGet, "/getUTXO", "", "").Code)
|
|
require.Equal(t, http.StatusNotFound, call(t, vm, http.MethodGet, "/getUTXO", "commitment=absent", "").Code)
|
|
|
|
require.NoError(t, vm.utxoDB.AddUTXO(&UTXO{
|
|
TxID: ids.ID{1}, Commitment: []byte("c"), Ciphertext: []byte("note"), Height: 3,
|
|
}))
|
|
|
|
rec := call(t, vm, http.MethodGet, "/getUTXO", "commitment=c", "")
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
require.Contains(t, rec.Body.String(), "bm90ZQ==") // base64("note")
|
|
|
|
rec = call(t, vm, http.MethodGet, "/getUTXOCount", "", "")
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
require.Contains(t, rec.Body.String(), `"count":1`)
|
|
}
|
|
|
|
func TestNullifierEndpointReportsAFailedRead(t *testing.T) {
|
|
vm := newVM(t)
|
|
|
|
require.Equal(t, http.StatusBadRequest, call(t, vm, http.MethodGet, "/isNullifierSpent", "", "").Code)
|
|
|
|
rec := call(t, vm, http.MethodGet, "/isNullifierSpent", "nullifier=n", "")
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
require.Contains(t, rec.Body.String(), `"isSpent":false`)
|
|
|
|
require.NoError(t, vm.nullifierDB.MarkNullifierSpent([]byte("n"), 7))
|
|
rec = call(t, vm, http.MethodGet, "/isNullifierSpent", "nullifier=n", "")
|
|
require.Contains(t, rec.Body.String(), `"isSpent":true`)
|
|
require.Contains(t, rec.Body.String(), `"height":7`)
|
|
|
|
// A set that cannot be READ is a failure, not an unspent note.
|
|
vm.nullifierDB.spent = map[string]uint64{}
|
|
vm.nullifierDB.db = &brokenDB{Database: vm.nullifierDB.db, err: errors.New("disk gone")}
|
|
require.Equal(t, http.StatusInternalServerError,
|
|
call(t, vm, http.MethodGet, "/isNullifierSpent", "nullifier=n", "").Code)
|
|
}
|
|
|
|
func TestStatusEndpoints(t *testing.T) {
|
|
vm := newVM(t)
|
|
|
|
rec := call(t, vm, http.MethodGet, "/getStatus", "", "")
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
require.Contains(t, rec.Body.String(), "utxoCount")
|
|
|
|
rec = call(t, vm, http.MethodGet, "/getProofStats", "", "")
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
require.Contains(t, rec.Body.String(), "cacheHits")
|
|
}
|
|
|
|
func TestEndpointsRefuseTheWrongMethod(t *testing.T) {
|
|
vm := newVM(t)
|
|
require.Equal(t, http.StatusMethodNotAllowed,
|
|
call(t, vm, http.MethodGet, "/sendTransaction", "", "").Code)
|
|
require.Equal(t, http.StatusMethodNotAllowed,
|
|
call(t, vm, http.MethodPost, "/getStatus", "", "").Code)
|
|
}
|
|
|
|
// The same routes are reachable by path through NewHTTPHandler.
|
|
func TestHTTPHandlerMountsEveryRoute(t *testing.T) {
|
|
vm := newVM(t)
|
|
mux, err := vm.NewHTTPHandler(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
for route := range vm.endpoints() {
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, route, nil))
|
|
require.NotEqual(t, http.StatusNotFound, rec.Code, route)
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/elsewhere", nil))
|
|
require.Equal(t, http.StatusNotFound, rec.Code)
|
|
}
|
|
|
|
var _ database.Database = (*brokenDB)(nil)
|