From 0eed0cdbde67fe51dfedc6ad3b6efdcf91614fd5 Mon Sep 17 00:00:00 2001 From: Chaitu-Tatipamula <107246959+Chaitu-Tatipamula@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:02:34 +0530 Subject: [PATCH 1/7] chore: add initial PDPVerifierLayout.sol --- src/PDPVerifierLayout.sol | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/PDPVerifierLayout.sol diff --git a/src/PDPVerifierLayout.sol b/src/PDPVerifierLayout.sol new file mode 100644 index 0000000..5c2f22a --- /dev/null +++ b/src/PDPVerifierLayout.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +pragma solidity ^0.8.20; + +// Code generated - DO NOT EDIT. +// This file is a generated binding and any changes will be lost. +// Generated with tools/generate_storage_layout.sh + +bytes32 constant CHALLENGE_FINALITY_SLOT = bytes32(uint256(0)); +bytes32 constant NEXT_DATA_SET_ID_SLOT = bytes32(uint256(1)); +bytes32 constant PIECE_CIDS_SLOT = bytes32(uint256(2)); +bytes32 constant PIECE_LEAF_COUNTS_SLOT = bytes32(uint256(3)); +bytes32 constant SUM_TREE_COUNTS_SLOT = bytes32(uint256(4)); +bytes32 constant NEXT_PIECE_ID_SLOT = bytes32(uint256(5)); +bytes32 constant DATA_SET_LEAF_COUNT_SLOT = bytes32(uint256(6)); +bytes32 constant NEXT_CHALLENGE_EPOCH_SLOT = bytes32(uint256(7)); +bytes32 constant DATA_SET_LISTENER_SLOT = bytes32(uint256(8)); +bytes32 constant CHALLENGE_RANGE_SLOT = bytes32(uint256(9)); +bytes32 constant SCHEDULED_REMOVALS_SLOT = bytes32(uint256(10)); +bytes32 constant SCHEDULED_REMOVALS_BITMAP_SLOT = bytes32(uint256(11)); +bytes32 constant STORAGE_PROVIDER_SLOT = bytes32(uint256(12)); +bytes32 constant DATA_SET_PROPOSED_STORAGE_PROVIDER_SLOT = bytes32(uint256(13)); +bytes32 constant DATA_SET_LAST_PROVEN_EPOCH_SLOT = bytes32(uint256(14)); +bytes32 constant FEE_STATUS_SLOT = bytes32(uint256(15)); +bytes32 constant NEXT_UPGRADE_SLOT = bytes32(uint256(16)); + From 33bfd134744e2bf097cc1f60cc911a62a66d1c26 Mon Sep 17 00:00:00 2001 From: Chaitu-Tatipamula <107246959+Chaitu-Tatipamula@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:07:35 +0530 Subject: [PATCH 2/7] chore: add storage layout checking tools and CI --- .github/workflows/makefile.yml | 5 + Makefile | 3 + tools/check_storage_layout.sh | 209 +++++++++++++++++++++++++++++++ tools/generate_storage_layout.sh | 17 +++ 4 files changed, 234 insertions(+) create mode 100755 tools/check_storage_layout.sh create mode 100755 tools/generate_storage_layout.sh diff --git a/.github/workflows/makefile.yml b/.github/workflows/makefile.yml index 837cd50..983fb24 100644 --- a/.github/workflows/makefile.yml +++ b/.github/workflows/makefile.yml @@ -34,3 +34,8 @@ jobs: run: | export PATH="/home/runner/.config/.foundry/bin:$PATH"; make test; + + - name: Check storage layout + run: | + export PATH="/home/runner/.config/.foundry/bin:$PATH"; + make check-layout; diff --git a/Makefile b/Makefile index 6b41476..82403b0 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,9 @@ RPC_URL ?= KEYSTORE ?= PASSWORD ?= +# Generated files +LAYOUT=src/PDPVerifierLayout.sol + # Default target .PHONY: default default: build test diff --git a/tools/check_storage_layout.sh b/tools/check_storage_layout.sh new file mode 100755 index 0000000..15e4248 --- /dev/null +++ b/tools/check_storage_layout.sh @@ -0,0 +1,209 @@ +#!/usr/bin/env bash +# Check that storage layout changes are additive only. +# Prevents destructive changes to upgradeable contract storage: +# - Removing existing storage slots +# - Changing the slot number of an existing variable +# - Inserting new slots in the middle (shifting existing slots) +# Allowed: Appending new slots at the end (highest slot numbers) +# +# Usage: check_storage_layout.sh [ ] +# No args: compares HEAD to working tree +# Two args: compares base_layout.sol to new_layout.sol + +set -euo pipefail + +# Clean up temp files on exit +TEMP_FILES=() +cleanup() { rm -f "${TEMP_FILES[@]:-}" 2>/dev/null || true; } +trap cleanup EXIT + +# Extract slot definitions from layout file (format: "NAME NUMBER" per line) +extract_slots() { + local file="$1" + grep -E 'bytes32 constant [A-Z0-9_]+_SLOT = bytes32\(uint256\([0-9]+\)\);' "$file" 2>/dev/null | \ + sed -E 's/.*constant ([A-Z0-9_]+_SLOT).*uint256\(([0-9]+)\).*/\1 \2/' | \ + sort -k2 -n +} + +# Function to validate a single layout file +validate_layout_format() { + local file="$1" + if [ ! -f "$file" ]; then + echo "Error: Layout file not found: $file" >&2 + return 1 + fi + + local slot_count=$(extract_slots "$file" | wc -l) + if [ "$slot_count" -eq 0 ]; then + echo "Error: No slot definitions found in: $file" >&2 + return 1 + fi + + # Check for gaps or duplicate slot numbers + local prev_num=-1 + local duplicate_count=0 + local gap_count=0 + + while IFS=' ' read -r name num; do + if [ "$num" -eq "$prev_num" ]; then + echo "Error: Duplicate slot number $num for $name" >&2 + duplicate_count=$((duplicate_count + 1)) + elif [ "$num" -gt "$((prev_num + 1))" ]; then + echo "Warning: Gap detected: slot $prev_num followed by $num" >&2 + gap_count=$((gap_count + 1)) + fi + prev_num="$num" + done < <(extract_slots "$file") + + if [ "$duplicate_count" -gt 0 ]; then + return 1 + fi + + return 0 +} + +# Function to compare two layouts and detect destructive changes +compare_layouts() { + local base_file="$1" + local new_file="$2" + + # Extract slots + local base_slots_file=$(mktemp) + local new_slots_file=$(mktemp) + TEMP_FILES+=("$base_slots_file" "$new_slots_file") + + extract_slots "$base_file" > "$base_slots_file" + extract_slots "$new_file" > "$new_slots_file" + + # Find max slot number in base + local max_base_slot=$(tail -1 "$base_slots_file" | awk '{print $2}') + max_base_slot=${max_base_slot:-"-1"} + + local errors=0 + local warnings=0 + + echo "Comparing storage layouts..." + echo " Base: $base_file (max slot: $max_base_slot)" + echo " New: $new_file" + + # Check 1: No existing slots removed or modified + while IFS=' ' read -r name num; do + if ! grep -q "^${name} ${num}$" "$new_slots_file"; then + if grep -q "^${name} " "$new_slots_file"; then + local new_num=$(grep "^${name} " "$new_slots_file" | awk '{print $2}') + echo " Destructive: ${name} moved from slot ${num} to ${new_num}" >&2 + else + echo " Destructive: ${name} (slot ${num}) was removed" >&2 + fi + errors=$((errors + 1)) + fi + done < "$base_slots_file" + + # Check 2: New slots must be appended (slot numbers > max_base_slot) + while IFS=' ' read -r name num; do + if ! grep -q "^${name} " "$base_slots_file"; then + if [ "$num" -le "$max_base_slot" ]; then + echo " Destructive: New slot ${name} inserted at ${num} (must be > ${max_base_slot})" >&2 + errors=$((errors + 1)) + else + echo " Added: ${name} at slot ${num}" + fi + fi + done < "$new_slots_file" + + # Report results + local base_count=$(wc -l < "$base_slots_file") + local new_count=$(wc -l < "$new_slots_file") + local added=$((new_count - base_count)) + + echo "" + if [ "$errors" -eq 0 ]; then + echo "Storage layout check passed" + echo " Slots: ${base_count} → ${new_count} (+${added} added)" + return 0 + else + echo "Storage layout check failed (${errors} destructive change(s) detected)" >&2 + return 1 + fi +} + +case $# in + 0) + # No arguments: compare HEAD to working tree + LAYOUT_FILE="src/PDPVerifierLayout.sol" + + if [ ! -f "$LAYOUT_FILE" ]; then + echo "Error: Layout file not found: $LAYOUT_FILE" >&2 + exit 1 + fi + + # Get the base commit (HEAD for regular check, or base branch for PRs) + if [ -n "${GITHUB_BASE_REF:-}" ]; then + BASE_REF="origin/$GITHUB_BASE_REF" + elif git rev-parse --quiet --verify HEAD~1 >/dev/null 2>&1; then + BASE_REF="HEAD~1" + else + echo "Warning: No base commit found, assuming initial commit" + BASE_REF="" + fi + + if [ -z "$BASE_REF" ]; then + # Initial commit - just validate format + echo "Initial layout detected, validating format only..." + if validate_layout_format "$LAYOUT_FILE"; then + echo "Storage layout format validated" + exit 0 + else + exit 1 + fi + fi + + # Get base version (must use repository-root relative path for git show) + GIT_PREFIX=$(git rev-parse --show-prefix) + FULL_LAYOUT_FILE="${GIT_PREFIX}${LAYOUT_FILE}" + + TEMP_BASE_LAYOUT=$(mktemp) + TEMP_FILES+=("$TEMP_BASE_LAYOUT") + + if ! git show "$BASE_REF:$FULL_LAYOUT_FILE" > "$TEMP_BASE_LAYOUT" 2>/dev/null; then + echo "Warning: Could not retrieve base layout, assuming new file" + if validate_layout_format "$LAYOUT_FILE"; then + echo "Storage layout format validated" + exit 0 + else + exit 1 + fi + fi + + # Validate both layouts before comparison + if ! validate_layout_format "$TEMP_BASE_LAYOUT"; then + echo "Error: Base layout validation failed" >&2 + exit 1 + fi + if ! validate_layout_format "$LAYOUT_FILE"; then + echo "Error: New layout validation failed" >&2 + exit 1 + fi + + compare_layouts "$TEMP_BASE_LAYOUT" "$LAYOUT_FILE" + ;; + + 2) + # Two arguments: compare base to new + if ! validate_layout_format "$1"; then + exit 1 + fi + if ! validate_layout_format "$2"; then + exit 1 + fi + compare_layouts "$1" "$2" + ;; + + *) + echo "Usage: $0 [ ]" >&2 + echo "" + echo " With no args: Compares HEAD to working tree" >&2 + echo " With two args: Compares base_layout.sol to new_layout.sol" >&2 + exit 1 + ;; +esac \ No newline at end of file diff --git a/tools/generate_storage_layout.sh b/tools/generate_storage_layout.sh new file mode 100755 index 0000000..cb21974 --- /dev/null +++ b/tools/generate_storage_layout.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +echo // SPDX-License-Identifier: Apache-2.0 OR MIT +echo pragma solidity ^0.8.20\; +echo +echo // Code generated - DO NOT EDIT. +echo // This file is a generated binding and any changes will be lost. +echo // Generated with tools/generate_storage_layout.sh +echo + +forge inspect --json $1 storageLayout \ + | jq -rM 'reduce .storage.[] as {$label,$slot} (null; . += "bytes32 constant " + ( + $label + | [scan("[A-Z]+(?=[A-Z][a-z]|$)|[A-Z]?[a-z0-9]+")] + | map(ascii_upcase) + | join("_") + ) + "_SLOT = bytes32(uint256(" + $slot + "));\n")' \ No newline at end of file From e7ab38fce3d83903fec8aec9732d8d6caf131a2c Mon Sep 17 00:00:00 2001 From: Chaitu-Tatipamula <107246959+Chaitu-Tatipamula@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:14:57 +0530 Subject: [PATCH 3/7] fix: update MakeFile to explicitly call script with PDPVerifier contract --- Makefile | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 82403b0..3aada00 100644 --- a/Makefile +++ b/Makefile @@ -57,4 +57,37 @@ extract-abis: .PHONY: contract-size-check contract-size-check: @echo "Checking contract sizes..." - bash tools/check-contract-size.sh \ No newline at end of file + bash tools/check-contract-size.sh + +# Storage layout generation +$(LAYOUT): tools/generate_storage_layout.sh src/PDPVerifier.sol + bash tools/generate_storage_layout.sh src/PDPVerifier.sol:PDPVerifier | forge fmt -r - > $@ + +# Main code generation target +.PHONY: gen +gen: check-tools $(LAYOUT) + @echo "Code generation complete" + +# Force regeneration - useful when things are broken +.PHONY: force-gen +force-gen: clean-gen gen + @echo "Force regeneration complete" + +# Clean generated files only +.PHONY: clean-gen +clean-gen: + @echo "Removing generated files..." + @rm -f $(LAYOUT) + @echo "Generated files removed" + +# Check required tools +.PHONY: check-tools +check-tools: + @which jq >/dev/null 2>&1 || (echo "Error: jq is required but not installed" && exit 1) + @which forge >/dev/null 2>&1 || (echo "Error: forge is required but not installed" && exit 1) + +# Storage layout validation +.PHONY: check-layout +check-layout: + @echo "Checking storage layout for destructive changes..." + @bash tools/check_storage_layout.sh \ No newline at end of file From 9964d1f48c77804c740bcb0bedfb47273df79d51 Mon Sep 17 00:00:00 2001 From: Chaitu-Tatipamula <107246959+Chaitu-Tatipamula@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:11:50 +0530 Subject: [PATCH 4/7] fix: format Layout file --- src/PDPVerifierLayout.sol | 1 - tools/generate_storage_layout.sh | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/PDPVerifierLayout.sol b/src/PDPVerifierLayout.sol index 5c2f22a..fc20391 100644 --- a/src/PDPVerifierLayout.sol +++ b/src/PDPVerifierLayout.sol @@ -22,4 +22,3 @@ bytes32 constant DATA_SET_PROPOSED_STORAGE_PROVIDER_SLOT = bytes32(uint256(13)); bytes32 constant DATA_SET_LAST_PROVEN_EPOCH_SLOT = bytes32(uint256(14)); bytes32 constant FEE_STATUS_SLOT = bytes32(uint256(15)); bytes32 constant NEXT_UPGRADE_SLOT = bytes32(uint256(16)); - diff --git a/tools/generate_storage_layout.sh b/tools/generate_storage_layout.sh index cb21974..fb9f5c7 100755 --- a/tools/generate_storage_layout.sh +++ b/tools/generate_storage_layout.sh @@ -9,9 +9,9 @@ echo // Generated with tools/generate_storage_layout.sh echo forge inspect --json $1 storageLayout \ - | jq -rM 'reduce .storage.[] as {$label,$slot} (null; . += "bytes32 constant " + ( - $label + | jq -rM '.storage | map("bytes32 constant " + ( + .label | [scan("[A-Z]+(?=[A-Z][a-z]|$)|[A-Z]?[a-z0-9]+")] | map(ascii_upcase) | join("_") - ) + "_SLOT = bytes32(uint256(" + $slot + "));\n")' \ No newline at end of file + ) + "_SLOT = bytes32(uint256(" + .slot + "));") | join("\n")' \ No newline at end of file From f903628d063e44bcf35d7830d83298f3f0886dda Mon Sep 17 00:00:00 2001 From: Chaitu-Tatipamula <107246959+Chaitu-Tatipamula@users.noreply.github.com> Date: Mon, 13 Apr 2026 23:27:37 +0530 Subject: [PATCH 5/7] fix(ci): address PR review feedback on storage layout checks - Update Makefile 'check-layout' to regenerate layouts and enforce git diff check - Restructure check_storage_layout.sh CI fallback to strictly fail - Transition script to use precise JSON metadata layout validation - Ensure 'fetch-depth: 0' in Github Actions for full historical diffing - Auto-generate PDPVerifierLayout.json snapshot --- .github/workflows/makefile.yml | 2 + Makefile | 13 +- src/PDPVerifierLayout.json | 104 +++++++++++++++ tools/check_storage_layout.sh | 235 +++++++++++++++++++-------------- 4 files changed, 252 insertions(+), 102 deletions(-) create mode 100644 src/PDPVerifierLayout.json diff --git a/.github/workflows/makefile.yml b/.github/workflows/makefile.yml index 983fb24..17d3f56 100644 --- a/.github/workflows/makefile.yml +++ b/.github/workflows/makefile.yml @@ -12,6 +12,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: configure run: ./configure diff --git a/Makefile b/Makefile index 3aada00..c58c5ac 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ PASSWORD ?= # Generated files LAYOUT=src/PDPVerifierLayout.sol +LAYOUT_JSON=src/PDPVerifierLayout.json # Default target .PHONY: default @@ -63,9 +64,13 @@ contract-size-check: $(LAYOUT): tools/generate_storage_layout.sh src/PDPVerifier.sol bash tools/generate_storage_layout.sh src/PDPVerifier.sol:PDPVerifier | forge fmt -r - > $@ +# Storage layout JSON (full metadata for upgrade safety checks) +$(LAYOUT_JSON): src/PDPVerifier.sol + forge inspect --json src/PDPVerifier.sol:PDPVerifier storageLayout | jq '[.storage[] | {label, slot, offset, type}]' > $@ + # Main code generation target .PHONY: gen -gen: check-tools $(LAYOUT) +gen: check-tools $(LAYOUT) $(LAYOUT_JSON) @echo "Code generation complete" # Force regeneration - useful when things are broken @@ -77,7 +82,7 @@ force-gen: clean-gen gen .PHONY: clean-gen clean-gen: @echo "Removing generated files..." - @rm -f $(LAYOUT) + @rm -f $(LAYOUT) $(LAYOUT_JSON) @echo "Generated files removed" # Check required tools @@ -88,6 +93,8 @@ check-tools: # Storage layout validation .PHONY: check-layout -check-layout: +check-layout: force-gen + @echo "Checking if layout files are up to date..." + @git diff --exit-code $(LAYOUT) $(LAYOUT_JSON) || (echo "Error: Layout files are stale. Please commit the generated changes." && exit 1) @echo "Checking storage layout for destructive changes..." @bash tools/check_storage_layout.sh \ No newline at end of file diff --git a/src/PDPVerifierLayout.json b/src/PDPVerifierLayout.json new file mode 100644 index 0000000..0f579ba --- /dev/null +++ b/src/PDPVerifierLayout.json @@ -0,0 +1,104 @@ +[ + { + "label": "challengeFinality", + "slot": "0", + "offset": 0, + "type": "uint256" + }, + { + "label": "nextDataSetId", + "slot": "1", + "offset": 0, + "type": "uint64" + }, + { + "label": "pieceCids", + "slot": "2", + "offset": 0, + "type": "mapping(uint256 => mapping(uint256 => struct Cids.Cid))" + }, + { + "label": "pieceLeafCounts", + "slot": "3", + "offset": 0, + "type": "mapping(uint256 => mapping(uint256 => uint256))" + }, + { + "label": "sumTreeCounts", + "slot": "4", + "offset": 0, + "type": "mapping(uint256 => mapping(uint256 => uint256))" + }, + { + "label": "nextPieceId", + "slot": "5", + "offset": 0, + "type": "mapping(uint256 => uint256)" + }, + { + "label": "dataSetLeafCount", + "slot": "6", + "offset": 0, + "type": "mapping(uint256 => uint256)" + }, + { + "label": "nextChallengeEpoch", + "slot": "7", + "offset": 0, + "type": "mapping(uint256 => uint256)" + }, + { + "label": "dataSetListener", + "slot": "8", + "offset": 0, + "type": "mapping(uint256 => address)" + }, + { + "label": "challengeRange", + "slot": "9", + "offset": 0, + "type": "mapping(uint256 => uint256)" + }, + { + "label": "scheduledRemovals", + "slot": "10", + "offset": 0, + "type": "mapping(uint256 => uint256[])" + }, + { + "label": "scheduledRemovalsBitmap", + "slot": "11", + "offset": 0, + "type": "mapping(uint256 => mapping(uint256 => uint256))" + }, + { + "label": "storageProvider", + "slot": "12", + "offset": 0, + "type": "mapping(uint256 => address)" + }, + { + "label": "dataSetProposedStorageProvider", + "slot": "13", + "offset": 0, + "type": "mapping(uint256 => address)" + }, + { + "label": "dataSetLastProvenEpoch", + "slot": "14", + "offset": 0, + "type": "mapping(uint256 => uint256)" + }, + { + "label": "feeStatus", + "slot": "15", + "offset": 0, + "type": "struct PDPVerifier.FeeStatus" + }, + { + "label": "nextUpgrade", + "slot": "16", + "offset": 0, + "type": "struct PDPVerifier.PlannedUpgrade" + } +] diff --git a/tools/check_storage_layout.sh b/tools/check_storage_layout.sh index 15e4248..5835c52 100755 --- a/tools/check_storage_layout.sh +++ b/tools/check_storage_layout.sh @@ -1,14 +1,15 @@ #!/usr/bin/env bash # Check that storage layout changes are additive only. # Prevents destructive changes to upgradeable contract storage: -# - Removing existing storage slots -# - Changing the slot number of an existing variable +# - Removing existing storage slots/variables +# - Changing the type of an existing variable +# - Changing the offset of an existing variable # - Inserting new slots in the middle (shifting existing slots) # Allowed: Appending new slots at the end (highest slot numbers) # -# Usage: check_storage_layout.sh [ ] -# No args: compares HEAD to working tree -# Two args: compares base_layout.sol to new_layout.sol +# Usage: check_storage_layout.sh [ ] +# No args: compares base branch/history to working tree +# Two args: compares base_layout.json to new_layout.json set -euo pipefail @@ -17,45 +18,39 @@ TEMP_FILES=() cleanup() { rm -f "${TEMP_FILES[@]:-}" 2>/dev/null || true; } trap cleanup EXIT -# Extract slot definitions from layout file (format: "NAME NUMBER" per line) -extract_slots() { - local file="$1" - grep -E 'bytes32 constant [A-Z0-9_]+_SLOT = bytes32\(uint256\([0-9]+\)\);' "$file" 2>/dev/null | \ - sed -E 's/.*constant ([A-Z0-9_]+_SLOT).*uint256\(([0-9]+)\).*/\1 \2/' | \ - sort -k2 -n -} +LAYOUT_JSON="src/PDPVerifierLayout.json" -# Function to validate a single layout file -validate_layout_format() { +# Function to validate a single layout JSON file +validate_layout_json() { local file="$1" if [ ! -f "$file" ]; then echo "Error: Layout file not found: $file" >&2 return 1 fi - local slot_count=$(extract_slots "$file" | wc -l) - if [ "$slot_count" -eq 0 ]; then - echo "Error: No slot definitions found in: $file" >&2 + # Check if it's a valid JSON array + if ! jq -e 'type == "array"' "$file" >/dev/null 2>&1; then + echo "Error: Invalid JSON layout file (must be an array): $file" >&2 return 1 fi - # Check for gaps or duplicate slot numbers - local prev_num=-1 - local duplicate_count=0 - local gap_count=0 - - while IFS=' ' read -r name num; do - if [ "$num" -eq "$prev_num" ]; then - echo "Error: Duplicate slot number $num for $name" >&2 - duplicate_count=$((duplicate_count + 1)) - elif [ "$num" -gt "$((prev_num + 1))" ]; then - echo "Warning: Gap detected: slot $prev_num followed by $num" >&2 - gap_count=$((gap_count + 1)) - fi - prev_num="$num" - done < <(extract_slots "$file") + local entry_count=$(jq 'length' "$file") + if [ "$entry_count" -eq 0 ]; then + echo "Error: No storage entries found in: $file" >&2 + return 1 + fi - if [ "$duplicate_count" -gt 0 ]; then + # Check that all entries have required fields + local missing=$(jq '[.[] | select(.label == null or .slot == null or .offset == null or .type == null)] | length' "$file") + if [ "$missing" -gt 0 ]; then + echo "Error: Some entries in $file are missing required fields (label, slot, offset, type)" >&2 + return 1 + fi + + # Check for duplicate slot+offset combinations + local dupes=$(jq '[group_by(.slot + ":" + (.offset | tostring)) | .[] | select(length > 1)] | length' "$file") + if [ "$dupes" -gt 0 ]; then + echo "Error: Duplicate slot/offset combinations detected in $file" >&2 return 1 fi @@ -66,91 +61,118 @@ validate_layout_format() { compare_layouts() { local base_file="$1" local new_file="$2" + local errors=0 - # Extract slots - local base_slots_file=$(mktemp) - local new_slots_file=$(mktemp) - TEMP_FILES+=("$base_slots_file" "$new_slots_file") + # Find the highest slot number currently in use in the base branch + local max_base_slot=$(jq '[.[].slot | tonumber] | max // -1' "$base_file") - extract_slots "$base_file" > "$base_slots_file" - extract_slots "$new_file" > "$new_slots_file" + local base_count=$(jq 'length' "$base_file") + local new_count=$(jq 'length' "$new_file") - # Find max slot number in base - local max_base_slot=$(tail -1 "$base_slots_file" | awk '{print $2}') - max_base_slot=${max_base_slot:-"-1"} + echo "Comparing storage layouts..." + echo " Base: $base_file ($base_count entries, max slot: $max_base_slot)" + echo " New: $new_file ($new_count entries)" + + # Check 1: No existing entries removed or modified (type/slot/offset) + while IFS= read -r entry; do + # Extract fields from the base entry + local label=$(echo "$entry" | jq -r '.label') + local slot=$(echo "$entry" | jq -r '.slot') + local offset=$(echo "$entry" | jq -r '.offset') + local type=$(echo "$entry" | jq -r '.type') + + # Try to find an entry with the exact same name (label) in the new file + local new_entry=$(jq -c --arg l "$label" '.[] | select(.label == $l)' "$new_file") + + if [ -z "$new_entry" ]; then + echo " DESTRUCTIVE: Variable '$label' (slot $slot, offset $offset) was removed" >&2 + errors=$((errors + 1)) + continue + fi - local errors=0 - local warnings=0 + # Extract fields from the new entry + local new_slot=$(echo "$new_entry" | jq -r '.slot') + local new_offset=$(echo "$new_entry" | jq -r '.offset') + local new_type=$(echo "$new_entry" | jq -r '.type') - echo "Comparing storage layouts..." - echo " Base: $base_file (max slot: $max_base_slot)" - echo " New: $new_file" - - # Check 1: No existing slots removed or modified - while IFS=' ' read -r name num; do - if ! grep -q "^${name} ${num}$" "$new_slots_file"; then - if grep -q "^${name} " "$new_slots_file"; then - local new_num=$(grep "^${name} " "$new_slots_file" | awk '{print $2}') - echo " Destructive: ${name} moved from slot ${num} to ${new_num}" >&2 - else - echo " Destructive: ${name} (slot ${num}) was removed" >&2 - fi + # Compare fields + if [ "$slot" != "$new_slot" ]; then + echo " DESTRUCTIVE: Variable '$label' slot changed from $slot to $new_slot" >&2 + errors=$((errors + 1)) + fi + + if [ "$offset" != "$new_offset" ]; then + echo " DESTRUCTIVE: Variable '$label' offset changed from $offset to $new_offset (slot $slot)" >&2 + errors=$((errors + 1)) + fi + + if [ "$type" != "$new_type" ]; then + echo " DESTRUCTIVE: Variable '$label' type changed from '$type' to '$new_type' (slot $slot)" >&2 errors=$((errors + 1)) fi - done < "$base_slots_file" + done < <(jq -c '.[]' "$base_file") + + # Check 2: New entries must be appended (slot numbers > max_base_slot) + while IFS= read -r entry; do + local label=$(echo "$entry" | jq -r '.label') + local slot=$(echo "$entry" | jq -r '.slot') + local offset=$(echo "$entry" | jq -r '.offset') + local type=$(echo "$entry" | jq -r '.type') + + # Check if this is a newly added variable + local base_match=$(jq -c --arg l "$label" '.[] | select(.label == $l)' "$base_file") - # Check 2: New slots must be appended (slot numbers > max_base_slot) - while IFS=' ' read -r name num; do - if ! grep -q "^${name} " "$base_slots_file"; then - if [ "$num" -le "$max_base_slot" ]; then - echo " Destructive: New slot ${name} inserted at ${num} (must be > ${max_base_slot})" >&2 + if [ -z "$base_match" ]; then + if [ "$slot" -le "$max_base_slot" ]; then + echo " DESTRUCTIVE: New variable '$label' inserted at slot $slot (must be > $max_base_slot)" >&2 errors=$((errors + 1)) else - echo " Added: ${name} at slot ${num}" + echo " Added: '$label' at slot $slot (offset $offset, type $type)" fi fi - done < "$new_slots_file" + done < <(jq -c '.[]' "$new_file") # Report results - local base_count=$(wc -l < "$base_slots_file") - local new_count=$(wc -l < "$new_slots_file") local added=$((new_count - base_count)) - echo "" if [ "$errors" -eq 0 ]; then echo "Storage layout check passed" - echo " Slots: ${base_count} → ${new_count} (+${added} added)" + echo " Entries: ${base_count} → ${new_count} (+${added} added)" return 0 else - echo "Storage layout check failed (${errors} destructive change(s) detected)" >&2 + echo "Storage layout check FAILED (${errors} destructive change(s) detected)" >&2 return 1 fi } case $# in 0) - # No arguments: compare HEAD to working tree - LAYOUT_FILE="src/PDPVerifierLayout.sol" - - if [ ! -f "$LAYOUT_FILE" ]; then - echo "Error: Layout file not found: $LAYOUT_FILE" >&2 + # No arguments: compare base history to working tree JSON + if [ ! -f "$LAYOUT_JSON" ]; then + echo "Error: Layout API JSON not found locally: $LAYOUT_JSON" >&2 exit 1 fi + IS_CI=${GITHUB_ACTIONS:-false} + # Get the base commit (HEAD for regular check, or base branch for PRs) if [ -n "${GITHUB_BASE_REF:-}" ]; then BASE_REF="origin/$GITHUB_BASE_REF" elif git rev-parse --quiet --verify HEAD~1 >/dev/null 2>&1; then BASE_REF="HEAD~1" else - echo "Warning: No base commit found, assuming initial commit" + if [ "$IS_CI" = "true" ]; then + echo "Error: Running in CI but neither GITHUB_BASE_REF nor HEAD~1 could be resolved." >&2 + exit 1 + fi + echo "Warning: No base commit found, assuming initial repository commit" BASE_REF="" fi if [ -z "$BASE_REF" ]; then - # Initial commit - just validate format - echo "Initial layout detected, validating format only..." - if validate_layout_format "$LAYOUT_FILE"; then + # Genuine initial commit without base ref + echo "Initial commit detected, validating format only..." + if validate_layout_json "$LAYOUT_JSON"; then echo "Storage layout format validated" exit 0 else @@ -158,52 +180,67 @@ case $# in fi fi + # Ensure base ref actually exists in our git tree + if ! git rev-parse --quiet --verify "$BASE_REF" >/dev/null 2>&1; then + if [ "$IS_CI" = "true" ]; then + echo "Error: CI base ref '$BASE_REF' could not be resolved! Please ensure fetch-depth: 0 is set in the workflow." >&2 + exit 1 + else + echo "Error: Base ref '$BASE_REF' could not be resolved." >&2 + exit 1 + fi + fi + # Get base version (must use repository-root relative path for git show) GIT_PREFIX=$(git rev-parse --show-prefix) - FULL_LAYOUT_FILE="${GIT_PREFIX}${LAYOUT_FILE}" + FULL_LAYOUT_JSON="${GIT_PREFIX}${LAYOUT_JSON}" - TEMP_BASE_LAYOUT=$(mktemp) - TEMP_FILES+=("$TEMP_BASE_LAYOUT") + # Check if the file ACTUALLY exists in the base branch tree + if git cat-file -e "$BASE_REF:$FULL_LAYOUT_JSON" 2>/dev/null; then + TEMP_BASE_LAYOUT=$(mktemp) + TEMP_FILES+=("$TEMP_BASE_LAYOUT") - if ! git show "$BASE_REF:$FULL_LAYOUT_FILE" > "$TEMP_BASE_LAYOUT" 2>/dev/null; then - echo "Warning: Could not retrieve base layout, assuming new file" - if validate_layout_format "$LAYOUT_FILE"; then + if ! git show "$BASE_REF:$FULL_LAYOUT_JSON" > "$TEMP_BASE_LAYOUT" 2>/dev/null; then + echo "Error: Layout file exists in base branch ($BASE_REF) but could not be retrieved via git show." >&2 + exit 1 + fi + else + # The file truly doesn't exist in the base ref + echo "Initial layout detected (file does not exist in $BASE_REF), validating format only..." + if validate_layout_json "$LAYOUT_JSON"; then echo "Storage layout format validated" exit 0 else + echo "Error: New layout validation failed" >&2 exit 1 fi fi # Validate both layouts before comparison - if ! validate_layout_format "$TEMP_BASE_LAYOUT"; then - echo "Error: Base layout validation failed" >&2 + if ! validate_layout_json "$TEMP_BASE_LAYOUT"; then + echo "Error: Base layout validation failed on file $TEMP_BASE_LAYOUT" >&2 exit 1 fi - if ! validate_layout_format "$LAYOUT_FILE"; then + if ! validate_layout_json "$LAYOUT_JSON"; then echo "Error: New layout validation failed" >&2 exit 1 fi - compare_layouts "$TEMP_BASE_LAYOUT" "$LAYOUT_FILE" + compare_layouts "$TEMP_BASE_LAYOUT" "$LAYOUT_JSON" ;; 2) - # Two arguments: compare base to new - if ! validate_layout_format "$1"; then - exit 1 - fi - if ! validate_layout_format "$2"; then - exit 1 - fi + # Two arguments: compare base JSON to new JSON explicitly + if ! validate_layout_json "$1"; then exit 1; fi + if ! validate_layout_json "$2"; then exit 1; fi compare_layouts "$1" "$2" ;; *) - echo "Usage: $0 [ ]" >&2 + echo "Usage: $0 [ ]" >&2 echo "" - echo " With no args: Compares HEAD to working tree" >&2 - echo " With two args: Compares base_layout.sol to new_layout.sol" >&2 + echo " With no args: Compares base branch to working tree" >&2 + echo " With two args: Compares base_layout.json to new_layout.json" >&2 exit 1 ;; esac \ No newline at end of file From 13a0e0279dc1ccff8bcef6878c7adf55f52b21fc Mon Sep 17 00:00:00 2001 From: Chaitu-Tatipamula <107246959+Chaitu-Tatipamula@users.noreply.github.com> Date: Mon, 13 Apr 2026 23:29:39 +0530 Subject: [PATCH 6/7] fix(ci): canonicalize layout json types resolving against dictionary --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c58c5ac..91c199f 100644 --- a/Makefile +++ b/Makefile @@ -66,7 +66,7 @@ $(LAYOUT): tools/generate_storage_layout.sh src/PDPVerifier.sol # Storage layout JSON (full metadata for upgrade safety checks) $(LAYOUT_JSON): src/PDPVerifier.sol - forge inspect --json src/PDPVerifier.sol:PDPVerifier storageLayout | jq '[.storage[] | {label, slot, offset, type}]' > $@ + forge inspect --json src/PDPVerifier.sol:PDPVerifier storageLayout | jq '[.types as $$types | .storage[] | {label, slot, offset, type: ($$types[.type].label // .type)}]' > $@ # Main code generation target .PHONY: gen From 38f3a764b62d2758d55d93478b03a52a99190993 Mon Sep 17 00:00:00 2001 From: Phi Date: Tue, 14 Apr 2026 10:07:16 +0200 Subject: [PATCH 7/7] fix(ci): preserve nested storage layout metadata --- Makefile | 4 +- src/PDPVerifierLayout.json | 377 +++++++++++++++++++++++++++++++--- tools/check_storage_layout.sh | 10 +- 3 files changed, 350 insertions(+), 41 deletions(-) diff --git a/Makefile b/Makefile index 91c199f..5da2657 100644 --- a/Makefile +++ b/Makefile @@ -66,7 +66,7 @@ $(LAYOUT): tools/generate_storage_layout.sh src/PDPVerifier.sol # Storage layout JSON (full metadata for upgrade safety checks) $(LAYOUT_JSON): src/PDPVerifier.sol - forge inspect --json src/PDPVerifier.sol:PDPVerifier storageLayout | jq '[.types as $$types | .storage[] | {label, slot, offset, type: ($$types[.type].label // .type)}]' > $@ + forge inspect --json src/PDPVerifier.sol:PDPVerifier storageLayout | jq -S '. as $$root | def normalize_type($$id): ($$root.types[$$id]) as $$type | {label: $$type.label, encoding: $$type.encoding, numberOfBytes: $$type.numberOfBytes} + (if $$type.key then {key: normalize_type($$type.key)} else {} end) + (if $$type.value then {value: normalize_type($$type.value)} else {} end) + (if $$type.base then {base: normalize_type($$type.base)} else {} end) + (if $$type.members then {members: [$$type.members[] | {label, slot, offset, type: normalize_type(.type)}]} else {} end); [.storage[] | {label, slot, offset, type: normalize_type(.type)}]' > $@ # Main code generation target .PHONY: gen @@ -97,4 +97,4 @@ check-layout: force-gen @echo "Checking if layout files are up to date..." @git diff --exit-code $(LAYOUT) $(LAYOUT_JSON) || (echo "Error: Layout files are stale. Please commit the generated changes." && exit 1) @echo "Checking storage layout for destructive changes..." - @bash tools/check_storage_layout.sh \ No newline at end of file + @bash tools/check_storage_layout.sh diff --git a/src/PDPVerifierLayout.json b/src/PDPVerifierLayout.json index 0f579ba..a6ec5e6 100644 --- a/src/PDPVerifierLayout.json +++ b/src/PDPVerifierLayout.json @@ -1,104 +1,413 @@ [ { "label": "challengeFinality", - "slot": "0", "offset": 0, - "type": "uint256" + "slot": "0", + "type": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } }, { "label": "nextDataSetId", - "slot": "1", "offset": 0, - "type": "uint64" + "slot": "1", + "type": { + "encoding": "inplace", + "label": "uint64", + "numberOfBytes": "8" + } }, { "label": "pieceCids", - "slot": "2", "offset": 0, - "type": "mapping(uint256 => mapping(uint256 => struct Cids.Cid))" + "slot": "2", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => mapping(uint256 => struct Cids.Cid))", + "numberOfBytes": "32", + "value": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => struct Cids.Cid)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "struct Cids.Cid", + "members": [ + { + "label": "data", + "offset": 0, + "slot": "0", + "type": { + "encoding": "bytes", + "label": "bytes", + "numberOfBytes": "32" + } + } + ], + "numberOfBytes": "32" + } + } + } }, { "label": "pieceLeafCounts", - "slot": "3", "offset": 0, - "type": "mapping(uint256 => mapping(uint256 => uint256))" + "slot": "3", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => mapping(uint256 => uint256))", + "numberOfBytes": "32", + "value": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } + } }, { "label": "sumTreeCounts", - "slot": "4", "offset": 0, - "type": "mapping(uint256 => mapping(uint256 => uint256))" + "slot": "4", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => mapping(uint256 => uint256))", + "numberOfBytes": "32", + "value": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } + } }, { "label": "nextPieceId", - "slot": "5", "offset": 0, - "type": "mapping(uint256 => uint256)" + "slot": "5", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } }, { "label": "dataSetLeafCount", - "slot": "6", "offset": 0, - "type": "mapping(uint256 => uint256)" + "slot": "6", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } }, { "label": "nextChallengeEpoch", - "slot": "7", "offset": 0, - "type": "mapping(uint256 => uint256)" + "slot": "7", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } }, { "label": "dataSetListener", - "slot": "8", "offset": 0, - "type": "mapping(uint256 => address)" + "slot": "8", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => address)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } + } }, { "label": "challengeRange", - "slot": "9", "offset": 0, - "type": "mapping(uint256 => uint256)" + "slot": "9", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } }, { "label": "scheduledRemovals", - "slot": "10", "offset": 0, - "type": "mapping(uint256 => uint256[])" + "slot": "10", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => uint256[])", + "numberOfBytes": "32", + "value": { + "base": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "encoding": "dynamic_array", + "label": "uint256[]", + "numberOfBytes": "32" + } + } }, { "label": "scheduledRemovalsBitmap", - "slot": "11", "offset": 0, - "type": "mapping(uint256 => mapping(uint256 => uint256))" + "slot": "11", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => mapping(uint256 => uint256))", + "numberOfBytes": "32", + "value": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } + } }, { "label": "storageProvider", - "slot": "12", "offset": 0, - "type": "mapping(uint256 => address)" + "slot": "12", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => address)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } + } }, { "label": "dataSetProposedStorageProvider", - "slot": "13", "offset": 0, - "type": "mapping(uint256 => address)" + "slot": "13", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => address)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } + } }, { "label": "dataSetLastProvenEpoch", - "slot": "14", "offset": 0, - "type": "mapping(uint256 => uint256)" + "slot": "14", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } }, { "label": "feeStatus", - "slot": "15", "offset": 0, - "type": "struct PDPVerifier.FeeStatus" + "slot": "15", + "type": { + "encoding": "inplace", + "label": "struct PDPVerifier.FeeStatus", + "members": [ + { + "label": "currentFeePerTiB", + "offset": 0, + "slot": "0", + "type": { + "encoding": "inplace", + "label": "uint96", + "numberOfBytes": "12" + } + }, + { + "label": "nextFeePerTiB", + "offset": 12, + "slot": "0", + "type": { + "encoding": "inplace", + "label": "uint96", + "numberOfBytes": "12" + } + }, + { + "label": "transitionTime", + "offset": 24, + "slot": "0", + "type": { + "encoding": "inplace", + "label": "uint64", + "numberOfBytes": "8" + } + } + ], + "numberOfBytes": "32" + } }, { "label": "nextUpgrade", - "slot": "16", "offset": 0, - "type": "struct PDPVerifier.PlannedUpgrade" + "slot": "16", + "type": { + "encoding": "inplace", + "label": "struct PDPVerifier.PlannedUpgrade", + "members": [ + { + "label": "nextImplementation", + "offset": 0, + "slot": "0", + "type": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } + }, + { + "label": "afterEpoch", + "offset": 20, + "slot": "0", + "type": { + "encoding": "inplace", + "label": "uint96", + "numberOfBytes": "12" + } + } + ], + "numberOfBytes": "32" + } } ] diff --git a/tools/check_storage_layout.sh b/tools/check_storage_layout.sh index 5835c52..8def622 100755 --- a/tools/check_storage_layout.sh +++ b/tools/check_storage_layout.sh @@ -79,7 +79,7 @@ compare_layouts() { local label=$(echo "$entry" | jq -r '.label') local slot=$(echo "$entry" | jq -r '.slot') local offset=$(echo "$entry" | jq -r '.offset') - local type=$(echo "$entry" | jq -r '.type') + local type=$(echo "$entry" | jq -cS '.type') # Try to find an entry with the exact same name (label) in the new file local new_entry=$(jq -c --arg l "$label" '.[] | select(.label == $l)' "$new_file") @@ -93,7 +93,7 @@ compare_layouts() { # Extract fields from the new entry local new_slot=$(echo "$new_entry" | jq -r '.slot') local new_offset=$(echo "$new_entry" | jq -r '.offset') - local new_type=$(echo "$new_entry" | jq -r '.type') + local new_type=$(echo "$new_entry" | jq -cS '.type') # Compare fields if [ "$slot" != "$new_slot" ]; then @@ -117,7 +117,7 @@ compare_layouts() { local label=$(echo "$entry" | jq -r '.label') local slot=$(echo "$entry" | jq -r '.slot') local offset=$(echo "$entry" | jq -r '.offset') - local type=$(echo "$entry" | jq -r '.type') + local type_label=$(echo "$entry" | jq -r '.type.label // .type') # Check if this is a newly added variable local base_match=$(jq -c --arg l "$label" '.[] | select(.label == $l)' "$base_file") @@ -127,7 +127,7 @@ compare_layouts() { echo " DESTRUCTIVE: New variable '$label' inserted at slot $slot (must be > $max_base_slot)" >&2 errors=$((errors + 1)) else - echo " Added: '$label' at slot $slot (offset $offset, type $type)" + echo " Added: '$label' at slot $slot (offset $offset, type $type_label)" fi fi done < <(jq -c '.[]' "$new_file") @@ -243,4 +243,4 @@ case $# in echo " With two args: Compares base_layout.json to new_layout.json" >&2 exit 1 ;; -esac \ No newline at end of file +esac