Skip to content
Open
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
27 changes: 24 additions & 3 deletions ApolloBuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,14 +550,35 @@ func (b *Apollo) scriptDataHash() (*serialization.ScriptDataHash, error) {
datums := b.datums
usedCms := map[any]cbor.Marshaler{}
if len(redeemers) > 0 {
// Prefer the network cost models from the protocol parameters so the
// language views match the current on-chain values; fall back to the
// hardcoded constants when the chain context can't supply them. Using
// stale hardcoded cost models produces a script_data_hash the node
// rejects with PPViewHashesDontMatch.
var costModelsRaw map[string][]int64
if pp, ppErr := b.Context.GetProtocolParams(); ppErr == nil {
costModelsRaw = pp.CostModelsRaw
}
if len(PV1Scripts) > 0 {
usedCms[serialization.CustomBytes{Value: "00"}] = PlutusData.PLUTUSV1COSTMODEL
if raw := costModelsRaw["PlutusV1"]; len(raw) > 0 {
usedCms[serialization.CustomBytes{Value: "00"}] = PlutusData.CostModelV1Raw(raw)
} else {
usedCms[serialization.CustomBytes{Value: "00"}] = PlutusData.PLUTUSV1COSTMODEL
}
}
if len(PV2Scripts) > 0 {
usedCms[1] = PlutusData.PLUTUSV2COSTMODEL
if raw := costModelsRaw["PlutusV2"]; len(raw) > 0 {
usedCms[1] = PlutusData.NewCostModelArray(raw)
} else {
usedCms[1] = PlutusData.PLUTUSV2COSTMODEL
}
}
if len(PV3Scripts) > 0 || len(b.referenceInputs) > 0 {
usedCms[2] = PlutusData.PLUTUSV3COSTMODEL
if raw := costModelsRaw["PlutusV3"]; len(raw) > 0 {
usedCms[2] = PlutusData.NewCostModelArray(raw)
} else {
usedCms[2] = PlutusData.PLUTUSV3COSTMODEL
}
}

}
Expand Down
103 changes: 103 additions & 0 deletions scriptdatahash_repro_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package apollo

import (
"encoding/hex"
"encoding/json"
"os"
"testing"

"github.com/Salvionied/apollo/serialization"
"github.com/Salvionied/apollo/serialization/PlutusData"
"github.com/fxamacker/cbor/v2"
)

// sdhFixture mirrors the JSON dumped by dumpScriptDataHash from a real mainnet
// emission tx that the node rejected with PPViewHashesDontMatch.
type sdhFixture struct {
RedeemerBytesHex string `json:"redeemer_bytes_hex"`
DatumBytesHex string `json:"datum_bytes_hex"`
CostModelBytesHex string `json:"cost_model_bytes_hex"`
ScriptDataHashHex string `json:"script_data_hash_hex"`
CostModelsRaw map[string][]int64 `json:"cost_models_raw"`
ReferenceInputs int `json:"reference_inputs"`
}

// The hash the node computed from the *current* mainnet cost models (the
// "expected" SafeHash in the ConwayUtxowFailure PPViewHashesDontMatch error).
const expectedNodeScriptDataHash = "a48ad51eccebc251615e8c69b834750146d4cf166f2f44c1dddc9b38d294941b"

func loadSDHFixture(t *testing.T) sdhFixture {
t.Helper()
data, err := os.ReadFile("testdata/sdh_repro.json")
if err != nil {
t.Skipf("fixture missing: %v", err)
}
var f sdhFixture
if err := json.Unmarshal(data, &f); err != nil {
t.Fatalf("parse fixture: %v", err)
}
return f
}

func hashSDH(t *testing.T, redeemerHex, datumHex string, costModelBytes []byte) string {
t.Helper()
redeemer, err := hex.DecodeString(redeemerHex)
if err != nil {
t.Fatal(err)
}
datum, err := hex.DecodeString(datumHex)
if err != nil {
t.Fatal(err)
}
total := append([]byte{}, redeemer...)
total = append(total, datum...)
total = append(total, costModelBytes...)
h, err := serialization.Blake2bHash(total)
if err != nil {
t.Fatal(err)
}
return hex.EncodeToString(h)
}

// TestSDH_HardcodedReproducesRejectedHash is a sanity check: feeding the
// hardcoded PLUTUSV3COSTMODEL through the same encoding path reproduces the
// exact script_data_hash the fork produced (and the node rejected). This proves
// the test harness faithfully mirrors apollo's scriptDataHash().
func TestSDH_HardcodedReproducesRejectedHash(t *testing.T) {
f := loadSDHFixture(t)
if f.ReferenceInputs == 0 {
t.Fatalf("fixture expected reference inputs (V3 path), got 0")
}
usedCms := map[any]cbor.Marshaler{2: PlutusData.PLUTUSV3COSTMODEL}
cmBytes, err := cbor.Marshal(usedCms)
if err != nil {
t.Fatal(err)
}
got := hashSDH(t, f.RedeemerBytesHex, f.DatumBytesHex, cmBytes)
if got != f.ScriptDataHashHex {
t.Fatalf("harness mismatch:\n got %s\n want %s (fork-produced)", got, f.ScriptDataHashHex)
}
t.Logf("hardcoded path reproduces fork hash %s", got)
}

// TestSDH_NetworkCostModelsMatchNode verifies that building the V3 cost-model
// CBOR from the live mainnet CostModelsRaw (instead of the stale hardcoded
// constant) yields the script_data_hash the node expects. This is the target
// behaviour for the fix.
func TestSDH_NetworkCostModelsMatchNode(t *testing.T) {
f := loadSDHFixture(t)
v3 := f.CostModelsRaw["PlutusV3"]
if len(v3) == 0 {
t.Fatal("fixture missing CostModelsRaw[PlutusV3]")
}
usedCms := map[any]cbor.Marshaler{2: PlutusData.NewCostModelArray(v3)}
cmBytes, err := cbor.Marshal(usedCms)
if err != nil {
t.Fatal(err)
}
got := hashSDH(t, f.RedeemerBytesHex, f.DatumBytesHex, cmBytes)
if got != expectedNodeScriptDataHash {
t.Fatalf("network cost-model hash mismatch:\n got %s\n want %s (node-expected)", got, expectedNodeScriptDataHash)
}
t.Logf("network cost-model path matches node-expected hash %s", got)
}
45 changes: 45 additions & 0 deletions serialization/PlutusData/costmodel_raw.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package PlutusData

import "github.com/fxamacker/cbor/v2"

// This file adds helpers to build language views (cost models) for the Conway
// script_data_hash from the *network* cost-model values supplied by the chain
// context (ProtocolParameters.CostModelsRaw), rather than from the hardcoded
// PLUTUSV1/V2/V3COSTMODEL constants. The hardcoded constants go stale whenever
// the protocol updates its cost models, which makes the on-chain
// script_data_hash mismatch (ConwayUtxowFailure PPViewHashesDontMatch).
//
// CostModelsRaw values are already in canonical ledger order, so they map
// directly onto apollo's existing encodings: V2/V3 as a plain definite array,
// V1 with the special bytestring-wrapped indefinite-array encoding.

// NewCostModelArray builds the plain-array cost model (PlutusV2/V3 encoding)
// from raw int64 values in canonical order.
func NewCostModelArray(raw []int64) CostModelArray {
out := make(CostModelArray, len(raw))
for i, v := range raw {
out[i] = int32(v)
}
return out
}

// CostModelV1Raw encodes PlutusV1 cost models from raw values in canonical
// order, mirroring CM.MarshalCBOR's special encoding: the integer array is
// serialized as an indefinite-length array and then wrapped in a CBOR
// bytestring. (CM.MarshalCBOR derives the order by sorting parameter names; the
// raw values are already in that order, so the resulting bytes are identical.)
type CostModelV1Raw []int64

func (c CostModelV1Raw) MarshalCBOR() ([]byte, error) {
res := make([]int, len(c))
for i, v := range c {
res[i] = int(v)
}
partial, err := cbor.Marshal(res)
if err != nil {
return nil, err
}
partial[1] = 0x9f
partial = append(partial, 0xff)
return cbor.Marshal(partial[1:])
}
Loading