Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions backend/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package backend

import (
"encoding/hex"
"errors"
"fmt"
"math"
"math/big"
"reflect"
"strconv"
"strings"

Expand Down Expand Up @@ -38,6 +40,33 @@ type ChainContext interface {
ScriptCbor(scriptHash common.Blake2b224) ([]byte, error)
}

// ValidateAdditionalUtxo verifies that a resolved UTxO has both pieces needed
// by backend evaluation APIs. TransactionInput and TransactionOutput are
// interfaces, so this also rejects typed nil pointers stored in either field.
func ValidateAdditionalUtxo(utxo common.Utxo) error {
if isNilInterface(utxo.Id) {
return errors.New("additional UTxO is missing transaction input")
}
if isNilInterface(utxo.Output) {
return errors.New("additional UTxO is missing transaction output")
}
return nil
}

func isNilInterface(value any) bool {
if value == nil {
return true
}

valueOf := reflect.ValueOf(value)
switch valueOf.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
return valueOf.IsNil()
default:
return false
}
}

// GenesisParameters holds Cardano genesis configuration values.
type GenesisParameters struct {
ActiveSlotsCoefficient float64 `json:"active_slots_coefficient"`
Expand Down
10 changes: 10 additions & 0 deletions backend/blockfrost/blockfrost.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,12 @@ var (
)

func (b *BlockFrostChainContext) EvaluateTx(txCbor []byte, additionalUtxos []common.Utxo) (map[common.RedeemerKey]common.ExUnits, error) {
for i, utxo := range additionalUtxos {
if err := backend.ValidateAdditionalUtxo(utxo); err != nil {
return nil, fmt.Errorf("invalid additional UTxO at index %d: %w", i, err)
}
}

// Prefer /utils/txs/evaluate (hex body). Apollo always passes spending
// inputs as additionalUtxos even when they are already on-chain; posting
// them to /utils/txs/evaluate/utxos re-encodes inline datums and reference
Expand Down Expand Up @@ -429,6 +435,10 @@ func buildEvalUtxosRequest(txCbor []byte, additionalUtxos []common.Utxo) ([]byte
// bfAdditionalUtxoItemFromUtxo builds a single [txIn, txOut] additional-UTxO
// entry from a resolved gouroboros UTxO.
func bfAdditionalUtxoItemFromUtxo(utxo common.Utxo) (bfAdditionalUtxoItem, error) {
if err := backend.ValidateAdditionalUtxo(utxo); err != nil {
return bfAdditionalUtxoItem{}, err
}

out := utxo.Output

txIn := bfTxIn{
Expand Down
84 changes: 84 additions & 0 deletions backend/blockfrost/blockfrost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,49 @@ func TestBuildEvalUtxosRequestShape(t *testing.T) {
}
}

func TestBfAdditionalUtxoItemFromUtxoRejectsMissingFields(t *testing.T) {
valid := sampleAdaOnlyUtxo(t)
var typedNilInput *shelley.ShelleyTransactionInput
var typedNilOutput *babbage.BabbageTransactionOutput

tests := []struct {
name string
utxo common.Utxo
wantErr string
}{
{
name: "nil transaction input",
utxo: common.Utxo{Output: valid.Output},
wantErr: "missing transaction input",
},
{
name: "typed nil transaction input",
utxo: common.Utxo{Id: typedNilInput, Output: valid.Output},
wantErr: "missing transaction input",
},
{
name: "nil transaction output",
utxo: common.Utxo{Id: valid.Id},
wantErr: "missing transaction output",
},
{
name: "typed nil transaction output",
utxo: common.Utxo{Id: valid.Id, Output: typedNilOutput},
wantErr: "missing transaction output",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if _, err := bfAdditionalUtxoItemFromUtxo(test.utxo); err == nil {
t.Fatal("expected malformed UTxO error")
} else if !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("error = %q, want substring %q", err, test.wantErr)
}
})
}
}

func TestBuildEvalUtxosRequestHashOnlyDatum(t *testing.T) {
var txId common.Blake2b256
for i := range txId {
Expand Down Expand Up @@ -758,6 +801,47 @@ func TestEvaluateTxPrefersSimpleEndpointWhenAdditionalUtxosProvided(t *testing.T
}
}

func TestEvaluateTxRejectsMalformedAdditionalUtxosBeforeRequest(t *testing.T) {
requests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()

valid := sampleAdaOnlyUtxo(t)
tests := []struct {
name string
utxo common.Utxo
wantErr string
}{
{
name: "missing transaction input",
utxo: common.Utxo{Output: valid.Output},
wantErr: "missing transaction input",
},
{
name: "missing transaction output",
utxo: common.Utxo{Id: valid.Id},
wantErr: "missing transaction output",
},
}

ctx := NewBlockFrostChainContext(server.URL, 0, "")
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if _, err := ctx.EvaluateTx([]byte{0x84}, []common.Utxo{test.utxo}); err == nil {
t.Fatal("expected malformed UTxO error")
} else if !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("error = %q, want substring %q", err, test.wantErr)
}
})
}
if requests != 0 {
t.Fatalf("requests = %d, want 0", requests)
}
}

func TestEvaluateTxFallsBackToUtxosOnMissingInputs(t *testing.T) {
prevRetries, prevWait := evaluateSimpleRetries, evaluateSimpleRetryWait
evaluateSimpleRetries, evaluateSimpleRetryWait = 2, 0
Expand Down
4 changes: 4 additions & 0 deletions backend/ogmios/ogmios.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ func commonUtxosToShared(utxos []common.Utxo) ([]shared.Utxo, error) {
// "ada" (with inner key "lovelace") for the coin, and the policy ID hex (with
// inner asset-name hex) for native assets.
func commonUtxoToShared(utxo common.Utxo) (shared.Utxo, error) {
if err := backend.ValidateAdditionalUtxo(utxo); err != nil {
return shared.Utxo{}, err
}

out := utxo.Output

coin, err := bigIntToNum(out.Amount())
Expand Down
74 changes: 74 additions & 0 deletions backend/ogmios/ogmios_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,80 @@ func TestCommonUtxoToShared(t *testing.T) {
}
}

func TestCommonUtxoToSharedRejectsMissingFields(t *testing.T) {
valid := sampleCommonUtxo(t)
var typedNilInput *shelley.ShelleyTransactionInput
var typedNilOutput *babbage.BabbageTransactionOutput

tests := []struct {
name string
utxo common.Utxo
wantErr string
}{
{
name: "nil transaction input",
utxo: common.Utxo{Output: valid.Output},
wantErr: "missing transaction input",
},
{
name: "typed nil transaction input",
utxo: common.Utxo{Id: typedNilInput, Output: valid.Output},
wantErr: "missing transaction input",
},
{
name: "nil transaction output",
utxo: common.Utxo{Id: valid.Id},
wantErr: "missing transaction output",
},
{
name: "typed nil transaction output",
utxo: common.Utxo{Id: valid.Id, Output: typedNilOutput},
wantErr: "missing transaction output",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if _, err := commonUtxoToShared(test.utxo); err == nil {
t.Fatal("expected malformed UTxO error")
} else if !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("error = %q, want substring %q", err, test.wantErr)
}
})
}
}

func TestEvaluateTxRejectsMalformedAdditionalUtxos(t *testing.T) {
valid := sampleCommonUtxo(t)
tests := []struct {
name string
utxo common.Utxo
wantErr string
}{
{
name: "missing transaction input",
utxo: common.Utxo{Output: valid.Output},
wantErr: "missing transaction input",
},
{
name: "missing transaction output",
utxo: common.Utxo{Id: valid.Id},
wantErr: "missing transaction output",
},
}

ctx := NewOgmiosChainContext(ogmigo.New(), nil, 0)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if _, err := ctx.EvaluateTx([]byte{0x84}, []common.Utxo{test.utxo}); err == nil {
t.Fatal("expected malformed UTxO error")
} else if !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("error = %q, want substring %q", err, test.wantErr)
}
})
}
}

func TestCommonUtxoToSharedInlineDatum(t *testing.T) {
var txId common.Blake2b256
for i := range txId {
Expand Down