diff --git a/ledger/common/rules.go b/ledger/common/rules.go new file mode 100644 index 00000000..a900fea1 --- /dev/null +++ b/ledger/common/rules.go @@ -0,0 +1,17 @@ +// 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 + +type UtxoValidationRuleFunc func(Transaction, LedgerState, TipState, ProtocolParameters) error diff --git a/ledger/common/state.go b/ledger/common/state.go new file mode 100644 index 00000000..d46dc059 --- /dev/null +++ b/ledger/common/state.go @@ -0,0 +1,38 @@ +// 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 ( + pcommon "github.com/blinklabs-io/gouroboros/protocol/common" +) + +// UtxoState defines the interface for querying the UTxO state +type UtxoState interface { + UtxosById([]TransactionInput) ([]Utxo, error) +} + +// CertState defines the interface for querying the certificate state +type CertState interface{} + +// LedgerState defines the interface for querying the ledger +type LedgerState interface { + UtxoState + CertState +} + +// TipState defines the interface for querying the current tip +type TipState interface { + Tip() (pcommon.Tip, error) +} diff --git a/ledger/shelley/errors.go b/ledger/shelley/errors.go new file mode 100644 index 00000000..60e92c6c --- /dev/null +++ b/ledger/shelley/errors.go @@ -0,0 +1,30 @@ +// 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 shelley + +import "fmt" + +type ExpiredUtxoError struct { + Ttl uint64 + Slot uint64 +} + +func (e ExpiredUtxoError) Error() string { + return fmt.Sprintf( + "expired UTxO: TTL %d, slot %d", + e.Ttl, + e.Slot, + ) +} diff --git a/ledger/shelley/rules.go b/ledger/shelley/rules.go new file mode 100644 index 00000000..0f268e8e --- /dev/null +++ b/ledger/shelley/rules.go @@ -0,0 +1,39 @@ +// 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 shelley + +import ( + common "github.com/blinklabs-io/gouroboros/ledger/common" +) + +var UtxoValidationRules = []common.UtxoValidationRuleFunc{ + UtxoValidateTimeToLive, +} + +// UtxoValidateTimeToLive ensures that the current tip slot is not after the specified TTL value +func UtxoValidateTimeToLive(tx common.Transaction, ls common.LedgerState, ts common.TipState, pp common.ProtocolParameters) error { + tip, err := ts.Tip() + if err != nil { + return err + } + ttl := tx.TTL() + if ttl == 0 || ttl >= tip.Point.Slot { + return nil + } + return ExpiredUtxoError{ + Ttl: ttl, + Slot: tip.Point.Slot, + } +} diff --git a/ledger/shelley/rules_test.go b/ledger/shelley/rules_test.go new file mode 100644 index 00000000..191babb9 --- /dev/null +++ b/ledger/shelley/rules_test.go @@ -0,0 +1,149 @@ +// 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 shelley_test + +import ( + "testing" + + "github.com/blinklabs-io/gouroboros/ledger/common" + "github.com/blinklabs-io/gouroboros/ledger/shelley" + pcommon "github.com/blinklabs-io/gouroboros/protocol/common" + + "github.com/stretchr/testify/assert" +) + +type testLedgerState struct { + utxos []common.Utxo +} + +func (ls testLedgerState) UtxosById(_ []common.TransactionInput) ([]common.Utxo, error) { + return ls.utxos, nil +} + +type testTipState struct { + tip pcommon.Tip +} + +func (ts testTipState) Tip() (pcommon.Tip, error) { + return ts.tip, nil +} + +func TestUtxoValidateTimeToLive(t *testing.T) { + var testSlot uint64 = 555666777 + var testZeroSlot uint64 = 0 + testTx := &shelley.ShelleyTransaction{ + Body: shelley.ShelleyTransactionBody{ + Ttl: testSlot, + }, + } + testLedgerState := testLedgerState{} + testProtocolParams := &shelley.ShelleyProtocolParameters{} + var testBeforeSlot uint64 = 555666700 + var testAfterSlot uint64 = 555666799 + // Test helper function + testRun := func(t *testing.T, name string, testTipSlot uint64, validateFunc func(*testing.T, error)) { + t.Run( + name, + func(t *testing.T) { + testTipState := testTipState{ + tip: pcommon.Tip{ + Point: pcommon.Point{ + Slot: testTipSlot, + }, + }, + } + err := shelley.UtxoValidateTimeToLive( + testTx, + testLedgerState, + testTipState, + testProtocolParams, + ) + validateFunc(t, err) + }, + ) + } + // Slot before TTL + testRun( + t, + "slot before TTL", + testBeforeSlot, + func(t *testing.T, err error) { + if err != nil { + t.Errorf( + "UtxoValidateTimeToLive should succeed when provided a tip slot (%d) before the specified TTL (%d)\n got error: %v", + testBeforeSlot, + testTx.TTL(), + err, + ) + } + }, + ) + // Slot equal to TTL + testRun( + t, + "slot equal to TTL", + testSlot, + func(t *testing.T, err error) { + if err != nil { + t.Errorf( + "UtxoValidateTimeToLive should succeed when provided a tip slot (%d) equal to the specified TTL (%d)\n got error: %v", + testSlot, + testTx.TTL(), + err, + ) + } + }, + ) + // Slot after TTL + testRun( + t, + "slot after TTL", + testAfterSlot, + func(t *testing.T, err error) { + if err == nil { + t.Errorf( + "UtxoValidateTimeToLive should fail when provided a tip slot (%d) after the specified TTL (%d)", + testAfterSlot, + testTx.TTL(), + ) + return + } + testErrType := shelley.ExpiredUtxoError{} + assert.IsType( + t, + testErrType, + err, + "did not get expected error type: got %T, wanted %T", + err, + testErrType, + ) + }, + ) + // Zero TTL + testTx.Body.Ttl = testZeroSlot + testRun( + t, + "zero TTL", + testZeroSlot, + func(t *testing.T, err error) { + if err != nil { + t.Errorf( + "UtxoValidateTimeToLive should succeed when provided a zero TTL\n got error: %v", + err, + ) + } + }, + ) +} diff --git a/protocol/chainsync/messages.go b/protocol/chainsync/messages.go index 761cf5cd..9ca5d34e 100644 --- a/protocol/chainsync/messages.go +++ b/protocol/chainsync/messages.go @@ -270,9 +270,5 @@ func NewMsgDone() *MsgDone { return m } -type Tip struct { - // Tells the CBOR decoder to convert to/from a struct and a CBOR array - _ struct{} `cbor:",toarray"` - Point common.Point - BlockNumber uint64 -} +// Tip is an alias to keep historical code from breaking after moving this elsewhere +type Tip = common.Tip diff --git a/protocol/common/types.go b/protocol/common/types.go index 9c5be214..0b92cb49 100644 --- a/protocol/common/types.go +++ b/protocol/common/types.go @@ -66,3 +66,10 @@ func (p *Point) MarshalCBOR() ([]byte, error) { } return cbor.Encode(data) } + +// Tip represents a Point combined with a block number +type Tip struct { + cbor.StructAsArray + Point Point + BlockNumber uint64 +}