Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
57 changes: 9 additions & 48 deletions ledger/common/script/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@ import (
"github.com/blinklabs-io/plutigo/data"
)

// eraIdConway is the first era with strict validity upper bounds. Keep this
// numeric boundary here to avoid importing era packages into common/script.
const eraIdConway = 6

type ScriptContext interface {
isScriptContext()
ToPlutusData() data.PlutusData
Expand Down Expand Up @@ -198,11 +194,8 @@ func (t TxInfoV1) ToPlutusData() data.PlutusData {
)
}

// NewTxInfoV1FromTransaction builds a Plutus V1 TxInfo. strictValidityUpperBound
// selects the era-dependent encoding of a finite validity-interval upper bound:
// pass true in the Conway era or later (EXCLUSIVE upper bound in all cases) and
// false in Alonzo/Babbage (CLOSED upper bound for an upper-only interval). See
// the TimeRange.strictUpperBound documentation and cardano-ledger#3043.
// NewTxInfoV1FromTransaction builds a Plutus V1 TxInfo. The upper validity
// bound is always exclusive, matching cardano-ledger's strictUpperBound.
func NewTxInfoV1FromTransaction(
slotState lcommon.SlotState,
tx lcommon.Transaction,
Expand Down Expand Up @@ -257,7 +250,8 @@ func NewTxInfoV1FromTransaction(
// StrictValidityUpperBoundForTransaction reports whether the transaction's
// era uses an exclusive upper validity bound in Plutus script contexts.
func StrictValidityUpperBoundForTransaction(tx lcommon.Transaction) bool {
return tx.Type() >= eraIdConway
_ = tx
return true
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
}

type TxInfoV2 struct {
Expand Down Expand Up @@ -323,11 +317,8 @@ func (t TxInfoV2) ToPlutusData() data.PlutusData {
)
}

// NewTxInfoV2FromTransaction builds a Plutus V2 TxInfo. strictValidityUpperBound
// selects the era-dependent encoding of a finite validity-interval upper bound:
// pass true in the Conway era or later (EXCLUSIVE upper bound in all cases) and
// false in Babbage (CLOSED upper bound for an upper-only interval). See the
// TimeRange.strictUpperBound documentation and cardano-ledger#3043.
// NewTxInfoV2FromTransaction builds a Plutus V2 TxInfo. The upper validity
// bound is always exclusive, matching cardano-ledger's strictUpperBound.
func NewTxInfoV2FromTransaction(
slotState lcommon.SlotState,
tx lcommon.Transaction,
Expand Down Expand Up @@ -497,26 +488,6 @@ type TimeRange struct {
upperBound uint64
lowerBoundPresent bool
upperBoundPresent bool
// strictUpperBound selects cardano-ledger's ERA-DEPENDENT encoding of a
// finite validity-interval upper bound (invalidHereafter).
//
// Conway and later eras (Conway.transValidityInterval, cardano-ledger#3043)
// always use `strictUpperBound` — an EXCLUSIVE upper bound — for a finite
// upper bound, whether or not a lower bound is present:
// UpperBound (Finite t) False
//
// Pre-Conway eras (Alonzo/Babbage transVITime) use `PV1.to` for an
// upper-only interval, which is a CLOSED/INCLUSIVE upper bound
// UpperBound (Finite t) True
// but already use `strictUpperBound` (exclusive) when BOTH bounds are
// present. cardano-ledger#3043 could not change this pre-Conway behavior
// because it would alter historical on-chain script validation, so the
// corrected exclusive bound was gated to the Conway era.
//
// Set strictUpperBound = true when building a Plutus context in the Conway
// era or later, false for Alonzo/Babbage. Plutus V3 only exists in Conway
// and later, so V3 contexts always set this true.
strictUpperBound bool
}

func (t TimeRange) ToPlutusData() data.PlutusData {
Expand Down Expand Up @@ -562,17 +533,8 @@ func (t TimeRange) ToPlutusData() data.PlutusData {
t.upperBound,
t.upperBoundPresent,
false,
// Closure of a finite upper bound, matching cardano-ledger's
// ERA-DEPENDENT translation (see the strictUpperBound field):
// - both bounds present (lowerBoundPresent): EXCLUSIVE (false)
// in every era (transVITime / transValidityInterval both use
// strictUpperBound for a two-sided interval).
// - upper-only interval (no lower bound): EXCLUSIVE (false) in
// Conway and later (Conway.transValidityInterval,
// cardano-ledger#3043), INCLUSIVE (true) in Alonzo/Babbage
// (transVITime uses PV1.to).
// i.e. closed iff it is an upper-only, pre-Conway interval.
!t.lowerBoundPresent && !t.strictUpperBound,
// cardano-ledger's strictUpperBound is always exclusive.
false,
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
),
)
}
Expand Down Expand Up @@ -650,10 +612,9 @@ func sortedRedeemerKeys(
func validityRangeInfo(
slotState lcommon.SlotState,
tx lcommon.Transaction,
strictValidityUpperBound bool,
_ bool,
) (TimeRange, error) {
var ret TimeRange
ret.strictUpperBound = strictValidityUpperBound
startSlot := tx.ValidityIntervalStart()
endSlot, upperBoundPresent := lcommon.TransactionValidityIntervalUpperBound(
tx,
Expand Down
48 changes: 10 additions & 38 deletions ledger/common/script/context_timerange_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,34 +56,19 @@ func wholeRange(lower, upper data.PlutusData) data.PlutusData {
return data.NewConstr(0, lower, upper)
}

// TestTimeRangeToPlutusDataUpperBoundEraDependent pins cardano-ledger's
// ERA-DEPENDENT encoding of a finite validity-interval upper bound
// (invalidHereafter):
//
// - Conway and later (strictUpperBound == true): the upper bound is EXCLUSIVE
// in every case (Conway.transValidityInterval / cardano-ledger#3043).
// - Alonzo/Babbage (strictUpperBound == false): an upper-only interval uses
// PV1.to, a CLOSED/INCLUSIVE upper bound; a two-sided interval already uses
// strictUpperBound (EXCLUSIVE).
//
// The bug that motivated this test: gouroboros previously emitted
// `!lowerBoundPresent` for every era, giving Conway-era TTL-only transactions
// an INCLUSIVE upper bound and mis-computing script execution units.
func TestTimeRangeToPlutusDataUpperBoundEraDependent(t *testing.T) {
// TestTimeRangeToPlutusDataUpperBound pins cardano-ledger's strictUpperBound
// encoding: every finite upper bound is exclusive, including TTL-only ranges.
func TestTimeRangeToPlutusDataUpperBound(t *testing.T) {
tests := []struct {
name string
tr TimeRange
want data.PlutusData
}{
// --- Conway and later: upper bound always EXCLUSIVE ---
{
// invalidHereafter set, no invalidBefore, Conway+. This is
// the case the bug affected. Upper must be EXCLUSIVE.
name: "conway ttl only (upper present, lower absent)",
name: "ttl only (upper present, lower absent)",
tr: TimeRange{
upperBound: 1000,
upperBoundPresent: true,
strictUpperBound: true,
},
want: wholeRange(
infBound(false), // NegInf
Expand All @@ -97,41 +82,30 @@ func TestTimeRangeToPlutusDataUpperBoundEraDependent(t *testing.T) {
upperBound: 1000,
lowerBoundPresent: true,
upperBoundPresent: true,
strictUpperBound: true,
},
want: wholeRange(
finiteBound(500, true), // Finite 500, INCLUSIVE
finiteBound(1000, false), // Finite 1000, EXCLUSIVE
),
},
// --- Alonzo/Babbage: upper-only is INCLUSIVE, two-sided EXCLUSIVE ---
{
// invalidHereafter set, no invalidBefore, pre-Conway. Upper
// must be INCLUSIVE (PV1.to). A version/language-only gate
// that keyed off "V1/V2" would get this right but would then
// wrongly apply it to Conway-era V1/V2 as well — hence the
// gate is on the ERA, not the Plutus version.
name: "preconway ttl only (upper present, lower absent)",
name: "ttl only remains exclusive",
tr: TimeRange{
upperBound: 1000,
upperBoundPresent: true,
strictUpperBound: false,
},
want: wholeRange(
infBound(false), // NegInf
finiteBound(1000, true), // Finite 1000, INCLUSIVE
infBound(false), // NegInf
finiteBound(1000, false), // Finite 1000, EXCLUSIVE
),
},
{
// Two-sided interval is EXCLUSIVE-upper even pre-Conway
// (transVITime uses strictUpperBound when both bounds exist).
name: "preconway both bounds present",
name: "two-sided remains exclusive",
tr: TimeRange{
lowerBound: 500,
upperBound: 1000,
lowerBoundPresent: true,
upperBoundPresent: true,
strictUpperBound: false,
},
want: wholeRange(
finiteBound(500, true), // Finite 500, INCLUSIVE
Expand All @@ -144,7 +118,6 @@ func TestTimeRangeToPlutusDataUpperBoundEraDependent(t *testing.T) {
tr: TimeRange{
lowerBound: 500,
lowerBoundPresent: true,
strictUpperBound: true,
},
want: wholeRange(finiteBound(500, true), infBound(true)),
},
Expand All @@ -153,18 +126,17 @@ func TestTimeRangeToPlutusDataUpperBoundEraDependent(t *testing.T) {
tr: TimeRange{
lowerBound: 500,
lowerBoundPresent: true,
strictUpperBound: false,
},
want: wholeRange(finiteBound(500, true), infBound(true)),
},
{
name: "unbounded - conway",
tr: TimeRange{strictUpperBound: true},
tr: TimeRange{},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two cases are now byte-identical — same TimeRange{}, same want — and the same is true of "lower only (upper absent) - conway" and "- preconway" just above, because strictUpperBound was dropped from both Conway variants.

Each pair reads as covering two eras and covers one twice. That is a little worse than it looks: strictUpperBound is what makes these Conway cases Conway, so if the era-invariant shapes ever stop being era-invariant, nothing here would notice.

Either restoring strictUpperBound: true on the two Conway variants, or collapsing each pair into one case named for the shape rather than the era, would fix it. I would lean toward restoring the field, since these are exactly the cases that prove the gate does not affect the era-invariant shapes.

want: wholeRange(infBound(false), infBound(true)),
},
{
name: "unbounded - preconway",
tr: TimeRange{strictUpperBound: false},
tr: TimeRange{},
want: wholeRange(infBound(false), infBound(true)),
},
}
Expand Down
26 changes: 8 additions & 18 deletions ledger/common/script/context_validity_era_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,8 @@ type validRangeBuilder func(
[]lcommon.Utxo,
) (data.PlutusData, error)

// TestValidityRangeEraIdsUnchanged pins the era-numbering assumption behind
// the unexported eraIdConway gate in validityRangeInfo. That constant cannot
// reference the era packages (they import ledger/common/script), so a
// renumbering here is the signal to update it.
// TestValidityRangeEraIdsUnchanged pins the transaction type values used by
// the shared validity fixtures.
func TestValidityRangeEraIdsUnchanged(t *testing.T) {
require.Equal(t, 4, alonzo.TxTypeAlonzo)
require.Equal(t, 5, babbage.TxTypeBabbage)
Expand All @@ -89,13 +87,11 @@ func TestValidityRangeEraIdsUnchanged(t *testing.T) {
// on.
func TestValidityRangeUpperBoundByEra(t *testing.T) {
for _, era := range []struct {
name string
upperBoundOnlyIsClosed bool
tx eraTxBuilder
name string
Comment thread
chrisguiney marked this conversation as resolved.
tx eraTxBuilder
}{
{
name: "Alonzo",
upperBoundOnlyIsClosed: true,
name: "Alonzo",
tx: func(
t *testing.T,
f mockledger.ValidityIntervalFixture,
Expand All @@ -107,8 +103,7 @@ func TestValidityRangeUpperBoundByEra(t *testing.T) {
},
},
{
name: "Babbage",
upperBoundOnlyIsClosed: true,
name: "Babbage",
tx: func(
t *testing.T,
f mockledger.ValidityIntervalFixture,
Expand All @@ -120,8 +115,7 @@ func TestValidityRangeUpperBoundByEra(t *testing.T) {
},
},
{
name: "Conway",
upperBoundOnlyIsClosed: false,
name: "Conway",
tx: func(
t *testing.T,
f mockledger.ValidityIntervalFixture,
Expand All @@ -132,8 +126,7 @@ func TestValidityRangeUpperBoundByEra(t *testing.T) {
},
},
{
name: "Dijkstra",
upperBoundOnlyIsClosed: false,
name: "Dijkstra",
tx: func(
t *testing.T,
f mockledger.ValidityIntervalFixture,
Expand Down Expand Up @@ -207,12 +200,9 @@ func TestValidityRangeUpperBoundByEra(t *testing.T) {
nil,
)
require.NoError(t, err)
strictUpperBound := build.name == "V3" ||
!era.upperBoundOnlyIsClosed
expected := expectedValidityRange(
fixture.StartSlot,
fixture.EndSlot,
!strictUpperBound,
)
require.True(
t,
Expand Down
16 changes: 4 additions & 12 deletions ledger/common/script/context_validity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ func boolTag(value bool) uint64 {
func expectedValidityRange(
start *uint64,
end *uint64,
upperBoundOnlyIsClosed bool,
) data.PlutusData {
startPresent := start != nil
endPresent := end != nil
Expand All @@ -84,21 +83,14 @@ func expectedValidityRange(
if endPresent {
endValue = *end
}
// These fixtures build V1/V2 TxInfo from Alonzo/Babbage transactions, i.e.
// the PRE-CONWAY eras. cardano-ledger's transVITime translates an upper-only
// interval there with PV1.to, a CLOSED (inclusive) upper bound, while a
// two-sided interval uses strictUpperBound (exclusive). So the upper-bound
// closure is inclusive iff there is no lower bound (upper-only). The Conway
// era corrects the upper-only case to exclusive (cardano-ledger#3043); that
// is covered separately in the strictUpperBound=true tests.
return data.NewConstr(
0,
validityBound(startPresent, startValue, true, true),
validityBound(
endPresent,
endValue,
false,
!startPresent && upperBoundOnlyIsClosed,
false,
),
)
}
Expand All @@ -109,7 +101,7 @@ func requireValidityRange(
actual data.PlutusData,
) {
t.Helper()
expected := expectedValidityRange(fixture.StartSlot, fixture.EndSlot, true)
expected := expectedValidityRange(fixture.StartSlot, fixture.EndSlot)
require.True(
t,
expected.Equal(actual),
Expand All @@ -129,7 +121,7 @@ func TestValidityRangeMatchesCardanoLedger(t *testing.T) {
validitySlotState{},
tx,
nil,
false, // Alonzo era: pre-Conway, closed upper-only bound
false,
)
require.NoError(t, err)
requireValidityRange(
Expand All @@ -149,7 +141,7 @@ func TestValidityRangeMatchesCardanoLedger(t *testing.T) {
validitySlotState{},
tx,
nil,
false, // Babbage era: pre-Conway, closed upper-only bound
false,
)
require.NoError(t, err)
requireValidityRange(
Expand Down