diff --git a/ledger/alonzo/alonzo.go b/ledger/alonzo/alonzo.go index 1fc8a4e3..a2c0eb4e 100644 --- a/ledger/alonzo/alonzo.go +++ b/ledger/alonzo/alonzo.go @@ -17,7 +17,9 @@ package alonzo import ( "encoding/json" "fmt" + "iter" "math/big" + "slices" "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/gouroboros/ledger/common" @@ -453,6 +455,40 @@ type AlonzoRedeemer struct { type AlonzoRedeemers []AlonzoRedeemer +func (r AlonzoRedeemers) Iter() iter.Seq2[common.RedeemerKey, common.RedeemerValue] { + return func(yield func(common.RedeemerKey, common.RedeemerValue) bool) { + // Sort redeemers + sorted := make([]AlonzoRedeemer, len(r)) + copy(sorted, r) + slices.SortFunc( + sorted, + func(a, b AlonzoRedeemer) int { + if a.Tag < b.Tag || (a.Tag == b.Tag && a.Index < b.Index) { + return -1 + } + if a.Tag > b.Tag || (a.Tag == b.Tag && a.Index > b.Index) { + return 1 + } + return 0 + }, + ) + // Yield keys + for _, redeemer := range sorted { + tmpKey := common.RedeemerKey{ + Tag: redeemer.Tag, + Index: redeemer.Index, + } + tmpVal := common.RedeemerValue{ + Data: redeemer.Data, + ExUnits: redeemer.ExUnits, + } + if !yield(tmpKey, tmpVal) { + return + } + } + } +} + func (r AlonzoRedeemers) Indexes(tag common.RedeemerTag) []uint { ret := []uint{} for _, redeemer := range r { @@ -466,13 +502,16 @@ func (r AlonzoRedeemers) Indexes(tag common.RedeemerTag) []uint { func (r AlonzoRedeemers) Value( index uint, tag common.RedeemerTag, -) (cbor.LazyValue, common.ExUnits) { +) common.RedeemerValue { for _, redeemer := range r { if redeemer.Tag == tag && uint(redeemer.Index) == index { - return redeemer.Data, redeemer.ExUnits + return common.RedeemerValue{ + Data: redeemer.Data, + ExUnits: redeemer.ExUnits, + } } } - return cbor.LazyValue{}, common.ExUnits{} + return common.RedeemerValue{} } type AlonzoTransactionWitnessSet struct { diff --git a/ledger/alonzo/alonzo_test.go b/ledger/alonzo/alonzo_test.go index 8314dd12..12e276e5 100644 --- a/ledger/alonzo/alonzo_test.go +++ b/ledger/alonzo/alonzo_test.go @@ -164,3 +164,84 @@ func TestAlonzoTransactionOutputToPlutusDataCoinAssets(t *testing.T) { t.Fatalf("did not get expected PlutusData\n got: %#v\n wanted: %#v", tmpData, expectedData) } } + +func TestAlonzoRedeemersIter(t *testing.T) { + testRedeemers := AlonzoRedeemers{ + { + Tag: common.RedeemerTagMint, + Index: 2, + ExUnits: common.ExUnits{ + Memory: 1111, + Steps: 2222, + }, + }, + { + Tag: common.RedeemerTagMint, + Index: 0, + ExUnits: common.ExUnits{ + Memory: 1111, + Steps: 0, + }, + }, + { + Tag: common.RedeemerTagSpend, + Index: 4, + ExUnits: common.ExUnits{ + Memory: 0, + Steps: 4444, + }, + }, + } + expectedOrder := []struct { + Key common.RedeemerKey + Value common.RedeemerValue + }{ + { + Key: common.RedeemerKey{ + Tag: common.RedeemerTagSpend, + Index: 4, + }, + Value: common.RedeemerValue{ + ExUnits: common.ExUnits{ + Memory: 0, + Steps: 4444, + }, + }, + }, + { + Key: common.RedeemerKey{ + Tag: common.RedeemerTagMint, + Index: 0, + }, + Value: common.RedeemerValue{ + ExUnits: common.ExUnits{ + Memory: 1111, + Steps: 0, + }, + }, + }, + { + Key: common.RedeemerKey{ + Tag: common.RedeemerTagMint, + Index: 2, + }, + Value: common.RedeemerValue{ + ExUnits: common.ExUnits{ + Memory: 1111, + Steps: 2222, + }, + }, + }, + } + iterIdx := 0 + for key, val := range testRedeemers.Iter() { + expected := expectedOrder[iterIdx] + if !reflect.DeepEqual(key, expected.Key) { + t.Fatalf("did not get expected key: got %#v, wanted %#v", key, expected.Key) + } + if !reflect.DeepEqual(val, expected.Value) { + t.Fatalf("did not get expected value: got %#v, wanted %#v", val, expected.Value) + } + iterIdx++ + } +} diff --git a/ledger/common/redeemer.go b/ledger/common/redeemer.go new file mode 100644 index 00000000..d4ea8be0 --- /dev/null +++ b/ledger/common/redeemer.go @@ -0,0 +1,42 @@ +// Copyright 2025 Blink Labs Software +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package common + +import ( + "github.com/blinklabs-io/gouroboros/cbor" +) + +type RedeemerTag uint8 + +const ( + RedeemerTagSpend RedeemerTag = 0 + RedeemerTagMint RedeemerTag = 1 + RedeemerTagCert RedeemerTag = 2 + RedeemerTagReward RedeemerTag = 3 + RedeemerTagVoting RedeemerTag = 4 + RedeemerTagProposing RedeemerTag = 5 +) + +type RedeemerKey struct { + cbor.StructAsArray + Tag RedeemerTag + Index uint32 +} + +type RedeemerValue struct { + cbor.StructAsArray + Data cbor.LazyValue + ExUnits ExUnits +} diff --git a/ledger/common/tx.go b/ledger/common/tx.go index b788ed6f..e3a2ae12 100644 --- a/ledger/common/tx.go +++ b/ledger/common/tx.go @@ -15,6 +15,8 @@ package common import ( + "iter" + "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/plutigo/data" utxorpc "github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano" @@ -90,7 +92,8 @@ type TransactionWitnessSet interface { type TransactionWitnessRedeemers interface { Indexes(RedeemerTag) []uint - Value(uint, RedeemerTag) (cbor.LazyValue, ExUnits) + Value(uint, RedeemerTag) RedeemerValue + Iter() iter.Seq2[RedeemerKey, RedeemerValue] } type Utxo struct { diff --git a/ledger/common/witness.go b/ledger/common/witness.go index 8fe93a7e..4ca664ce 100644 --- a/ledger/common/witness.go +++ b/ledger/common/witness.go @@ -18,17 +18,6 @@ import ( "github.com/blinklabs-io/gouroboros/cbor" ) -type RedeemerTag uint8 - -const ( - RedeemerTagSpend RedeemerTag = 0 - RedeemerTagMint RedeemerTag = 1 - RedeemerTagCert RedeemerTag = 2 - RedeemerTagReward RedeemerTag = 3 - RedeemerTagVoting RedeemerTag = 4 - RedeemerTagProposing RedeemerTag = 5 -) - type VkeyWitness struct { cbor.StructAsArray Vkey []byte diff --git a/ledger/conway/conway.go b/ledger/conway/conway.go index ad918ca8..28d972f5 100644 --- a/ledger/conway/conway.go +++ b/ledger/conway/conway.go @@ -17,6 +17,9 @@ package conway import ( "errors" "fmt" + "iter" + "maps" + "slices" "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/gouroboros/ledger/alonzo" @@ -154,20 +157,8 @@ func (h *ConwayBlockHeader) Era() common.Era { return EraConway } -type ConwayRedeemerKey struct { - cbor.StructAsArray - Tag common.RedeemerTag - Index uint32 -} - -type ConwayRedeemerValue struct { - cbor.StructAsArray - Data cbor.LazyValue - ExUnits common.ExUnits -} - type ConwayRedeemers struct { - Redeemers map[ConwayRedeemerKey]ConwayRedeemerValue + Redeemers map[common.RedeemerKey]common.RedeemerValue legacyRedeemers alonzo.AlonzoRedeemers legacy bool } @@ -190,6 +181,32 @@ func (r *ConwayRedeemers) MarshalCBOR() ([]byte, error) { return cbor.Encode(r.Redeemers) } +func (r ConwayRedeemers) Iter() iter.Seq2[common.RedeemerKey, common.RedeemerValue] { + return func(yield func(common.RedeemerKey, common.RedeemerValue) bool) { + // Sort redeemers + sorted := slices.Collect(maps.Keys(r.Redeemers)) + slices.SortFunc( + sorted, + func(a, b common.RedeemerKey) int { + if a.Tag < b.Tag || (a.Tag == b.Tag && a.Index < b.Index) { + return -1 + } + if a.Tag > b.Tag || (a.Tag == b.Tag && a.Index > b.Index) { + return 1 + } + return 0 + }, + ) + // Yield keys + for _, redeemerKey := range sorted { + tmpVal := r.Redeemers[redeemerKey] + if !yield(redeemerKey, tmpVal) { + return + } + } + } +} + func (r ConwayRedeemers) Indexes(tag common.RedeemerTag) []uint { if r.legacy { return r.legacyRedeemers.Indexes(tag) @@ -206,18 +223,18 @@ func (r ConwayRedeemers) Indexes(tag common.RedeemerTag) []uint { func (r ConwayRedeemers) Value( index uint, tag common.RedeemerTag, -) (cbor.LazyValue, common.ExUnits) { +) common.RedeemerValue { if r.legacy { return r.legacyRedeemers.Value(index, tag) } - redeemer, ok := r.Redeemers[ConwayRedeemerKey{ + redeemerVal, ok := r.Redeemers[common.RedeemerKey{ Tag: tag, Index: uint32(index), // #nosec G115 }] if ok { - return redeemer.Data, redeemer.ExUnits + return redeemerVal } - return cbor.LazyValue{}, common.ExUnits{} + return common.RedeemerValue{} } type ConwayTransactionWitnessSet struct { diff --git a/ledger/conway/conway_test.go b/ledger/conway/conway_test.go new file mode 100644 index 00000000..8a287f02 --- /dev/null +++ b/ledger/conway/conway_test.go @@ -0,0 +1,108 @@ +// Copyright 2025 Blink Labs Software +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package conway + +import ( + "reflect" + "testing" + + "github.com/blinklabs-io/gouroboros/ledger/common" +) + +func TestConwayRedeemersIter(t *testing.T) { + testRedeemers := ConwayRedeemers{ + Redeemers: map[common.RedeemerKey]common.RedeemerValue{ + { + Tag: common.RedeemerTagMint, + Index: 2, + }: { + ExUnits: common.ExUnits{ + Memory: 1111, + Steps: 2222, + }, + }, + { + Tag: common.RedeemerTagMint, + Index: 0, + }: { + ExUnits: common.ExUnits{ + Memory: 1111, + Steps: 0, + }, + }, + { + Tag: common.RedeemerTagSpend, + Index: 4, + }: { + ExUnits: common.ExUnits{ + Memory: 0, + Steps: 4444, + }, + }, + }, + } + expectedOrder := []struct { + Key common.RedeemerKey + Value common.RedeemerValue + }{ + { + Key: common.RedeemerKey{ + Tag: common.RedeemerTagSpend, + Index: 4, + }, + Value: common.RedeemerValue{ + ExUnits: common.ExUnits{ + Memory: 0, + Steps: 4444, + }, + }, + }, + { + Key: common.RedeemerKey{ + Tag: common.RedeemerTagMint, + Index: 0, + }, + Value: common.RedeemerValue{ + ExUnits: common.ExUnits{ + Memory: 1111, + Steps: 0, + }, + }, + }, + { + Key: common.RedeemerKey{ + Tag: common.RedeemerTagMint, + Index: 2, + }, + Value: common.RedeemerValue{ + ExUnits: common.ExUnits{ + Memory: 1111, + Steps: 2222, + }, + }, + }, + } + iterIdx := 0 + for key, val := range testRedeemers.Iter() { + expected := expectedOrder[iterIdx] + if !reflect.DeepEqual(key, expected.Key) { + t.Fatalf("did not get expected key: got %#v, wanted %#v", key, expected.Key) + } + if !reflect.DeepEqual(val, expected.Value) { + t.Fatalf("did not get expected value: got %#v, wanted %#v", val, expected.Value) + } + iterIdx++ + } +} diff --git a/ledger/conway/rules_test.go b/ledger/conway/rules_test.go index 147e1d39..377767a0 100644 --- a/ledger/conway/rules_test.go +++ b/ledger/conway/rules_test.go @@ -967,7 +967,7 @@ func TestUtxoValidateInsufficientCollateral(t *testing.T) { }, WitnessSet: conway.ConwayTransactionWitnessSet{ WsRedeemers: conway.ConwayRedeemers{ - Redeemers: map[conway.ConwayRedeemerKey]conway.ConwayRedeemerValue{ + Redeemers: map[common.RedeemerKey]common.RedeemerValue{ // Placeholder entry {}: {}, }, @@ -1063,7 +1063,7 @@ func TestUtxoValidateCollateralContainsNonAda(t *testing.T) { }, WitnessSet: conway.ConwayTransactionWitnessSet{ WsRedeemers: conway.ConwayRedeemers{ - Redeemers: map[conway.ConwayRedeemerKey]conway.ConwayRedeemerValue{ + Redeemers: map[common.RedeemerKey]common.RedeemerValue{ // Placeholder entry {}: {}, }, @@ -1241,7 +1241,7 @@ func TestUtxoValidateNoCollateralInputs(t *testing.T) { Body: conway.ConwayTransactionBody{}, WitnessSet: conway.ConwayTransactionWitnessSet{ WsRedeemers: conway.ConwayRedeemers{ - Redeemers: map[conway.ConwayRedeemerKey]conway.ConwayRedeemerValue{ + Redeemers: map[common.RedeemerKey]common.RedeemerValue{ // Placeholder entry {}: {}, }, @@ -1314,13 +1314,13 @@ func TestUtxoValidateNoCollateralInputs(t *testing.T) { } func TestUtxoValidateExUnitsTooBigUtxo(t *testing.T) { - testRedeemerSmall := conway.ConwayRedeemerValue{ + testRedeemerSmall := common.RedeemerValue{ ExUnits: common.ExUnits{ Memory: 1_000_000, Steps: 2_000, }, } - testRedeemerLarge := conway.ConwayRedeemerValue{ + testRedeemerLarge := common.RedeemerValue{ ExUnits: common.ExUnits{ Memory: 1_000_000_000, Steps: 2_000_000, @@ -1342,7 +1342,7 @@ func TestUtxoValidateExUnitsTooBigUtxo(t *testing.T) { "ExUnits too large", func(t *testing.T) { testTx.WitnessSet.WsRedeemers = conway.ConwayRedeemers{ - Redeemers: map[conway.ConwayRedeemerKey]conway.ConwayRedeemerValue{ + Redeemers: map[common.RedeemerKey]common.RedeemerValue{ {}: testRedeemerLarge, }, } @@ -1374,7 +1374,7 @@ func TestUtxoValidateExUnitsTooBigUtxo(t *testing.T) { "ExUnits under limit", func(t *testing.T) { testTx.WitnessSet.WsRedeemers = conway.ConwayRedeemers{ - Redeemers: map[conway.ConwayRedeemerKey]conway.ConwayRedeemerValue{ + Redeemers: map[common.RedeemerKey]common.RedeemerValue{ {}: testRedeemerSmall, }, }