Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
baked_codec.hpp
Go to the documentation of this file.
1// pulp/host/baked_codec.hpp — the .pulpbake plan payload codec (internal).
2//
3// A BakedPlan is the serializable form of a lowerable frozen graph: the plan
4// INPUTS (bus arity + node records + connections) that prepare() recompiles into
5// the snapshot deterministically. This header owns only the PLAN PAYLOAD codec —
6// binary serialize + a bounded, capped, NON-public parse. It does NOT verify a
7// signature and MUST NOT be handed untrusted bytes on its own: the public,
8// signature-verifying load path (baked_graph_processor's load_baked) parses the
9// plan through parse_plan_bounded ONLY after the Ed25519 envelope has been
10// verified. Custom node process code is never in the plan — a Custom record carries
11// only (type_id, version, state); the code is re-resolved from the host registry at
12// load, exactly like a node pack.
13
14#pragma once
15
17#include <pulp/host/signal_graph.hpp> // NodeId, NodeType
18
19#include <cstddef>
20#include <cstdint>
21#include <optional>
22#include <span>
23#include <string>
24#include <vector>
25
26namespace pulp::host {
27
28// v1 plan caps. These match the routed executor's plan-builder limits (the tightest
29// real constraint), NOT SignalGraph::GraphLimits — a plan the executor cannot build
30// is worthless, and the tighter caps shrink the untrusted-parse surface.
31inline constexpr std::size_t kBakedMaxNodes = 512;
32inline constexpr std::size_t kBakedMaxConnections = 2048;
33inline constexpr int kBakedMaxPortsPerNode = 64;
34inline constexpr std::size_t kBakedMaxTotalPorts = 4096;
35inline constexpr int kBakedMaxBlock = 16384;
36inline constexpr std::size_t kBakedMaxCustomTypeId = 256; // bytes
37inline constexpr std::size_t kBakedMaxCustomState = 65536; // bytes per node
38inline constexpr std::size_t kBakedMaxPlanBytes = 4u * 1024u * 1024u; // whole plan
39// Version one is the legacy payload. Keep its value explicit so the default
40// constructed plan and every v1 caller retain the original wire bytes while
41// the public format constant now describes the largest format this codec can
42// admit.
43inline constexpr int kBakedPlanV1FormatVersion = 1;
44inline constexpr int kBakedMaxSupportedFormatVersion = 2;
46
47inline constexpr std::size_t kBakedMaxRegions = 16;
48inline constexpr std::size_t kBakedMaxRegionMembers = 64;
49inline constexpr std::size_t kBakedMaxRegionBoundaries = 8;
50inline constexpr std::size_t kBakedMaxRegionParameters = 16;
51inline constexpr std::size_t kBakedMaxRegionString = 1024;
52
53// The serializable frozen-graph plan. No Plugin nodes (bake refuses them); no
54// std::function anywhere (Custom code is re-resolved from the registry).
55struct BakedPlan {
59
60 struct Node {
61 NodeId id = 0;
65 float gain = 1.0f; // Gain nodes
66 std::string custom_type_id; // Custom nodes
67 int custom_version = 0; // Custom nodes
68 std::vector<std::uint8_t> custom_state; // Custom nodes
69 bool operator==(const Node&) const = default;
70 };
71 struct Conn {
73 int src_port = 0;
75 int dst_port = 0;
76 bool feedback = false;
77 bool operator==(const Conn&) const = default;
78 };
79 std::vector<Node> nodes;
80 std::vector<Conn> connections;
81 // v2 only. These are authored, callback-free records; executable sample
82 // kernels are resolved from the paired registration supplied to the v2
83 // loader. The writer/parser require canonical region/member/parameter
84 // ordering and refuse a non-canonical signed payload.
85 std::vector<SampleRegionDefinition> sample_regions;
86
87 bool operator==(const BakedPlan& other) const {
88 if (format_version != other.format_version || input_channels != other.input_channels ||
89 output_channels != other.output_channels || nodes != other.nodes ||
90 connections != other.connections ||
91 sample_regions.size() != other.sample_regions.size())
92 return false;
93
94 const auto equal_config = [](const SampleKernelConfig& lhs, const SampleKernelConfig& rhs) {
95 return lhs.kind == rhs.kind &&
96 lhs.boundary_index_or_parameter_id == rhs.boundary_index_or_parameter_id &&
97 lhs.constant == rhs.constant;
98 };
99 const auto equal_member = [&](const SampleRegionKernelNode& lhs,
100 const SampleRegionKernelNode& rhs) {
101 return lhs.node == rhs.node && lhs.type_id == rhs.type_id &&
102 lhs.version == rhs.version && equal_config(lhs.config, rhs.config);
103 };
104 const auto equal_limits = [](const SampleRegionLimits& lhs, const SampleRegionLimits& rhs) {
105 return lhs.max_member_nodes == rhs.max_member_nodes &&
106 lhs.max_internal_connections == rhs.max_internal_connections &&
107 lhs.max_input_boundaries == rhs.max_input_boundaries &&
108 lhs.max_output_boundaries == rhs.max_output_boundaries &&
109 lhs.max_delay_nodes == rhs.max_delay_nodes &&
110 lhs.max_promoted_parameters == rhs.max_promoted_parameters &&
111 lhs.max_state_bytes == rhs.max_state_bytes &&
112 lhs.max_logical_boundary_bytes == rhs.max_logical_boundary_bytes &&
113 lhs.max_work_per_frame == rhs.max_work_per_frame &&
114 lhs.max_work_per_block == rhs.max_work_per_block;
115 };
116 const auto equal_range = [](const state::ParamRange& lhs, const state::ParamRange& rhs) {
117 return lhs.min == rhs.min && lhs.max == rhs.max &&
118 lhs.default_value == rhs.default_value && lhs.step == rhs.step &&
119 lhs.skew == rhs.skew && lhs.symmetric_skew == rhs.symmetric_skew;
120 };
121 const auto equal_parameter = [&](const SampleRegionPromotedParameter& lhs,
123 return lhs.param_id == rhs.param_id && lhs.key == rhs.key && lhs.name == rhs.name &&
124 lhs.unit == rhs.unit && equal_range(lhs.range, rhs.range) &&
125 lhs.rate == rhs.rate &&
126 lhs.smoothing_ramp_seconds == rhs.smoothing_ramp_seconds &&
127 lhs.bound_node_id == rhs.bound_node_id && lhs.bound_port == rhs.bound_port;
128 };
129 const auto equal_region = [&](const SampleRegionDefinition& lhs,
130 const SampleRegionDefinition& rhs) {
131 if (lhs.region_id != rhs.region_id || !equal_limits(lhs.limits, rhs.limits) ||
132 lhs.members.size() != rhs.members.size() ||
133 lhs.input_boundaries != rhs.input_boundaries ||
134 lhs.output_boundaries != rhs.output_boundaries ||
135 lhs.promoted_parameters.size() != rhs.promoted_parameters.size())
136 return false;
137 for (std::size_t i = 0; i < lhs.members.size(); ++i)
138 if (!equal_member(lhs.members[i], rhs.members[i]))
139 return false;
140 for (std::size_t i = 0; i < lhs.promoted_parameters.size(); ++i)
141 if (!equal_parameter(lhs.promoted_parameters[i], rhs.promoted_parameters[i]))
142 return false;
143 return true;
144 };
145
146 for (std::size_t i = 0; i < sample_regions.size(); ++i)
147 if (!equal_region(sample_regions[i], other.sample_regions[i]))
148 return false;
149 return true;
150 }
151};
152
153// The raw plan payload codec. These are in `detail` — NOT the public surface —
154// precisely because parse_plan_bounded is unsafe on unverified bytes: its contract
155// is the INVERSE of the public verify_and_extract_plan (verification must precede
156// it). Only verify_and_extract_plan (after the Ed25519 envelope check) and the codec
157// tests may call them.
158namespace detail {
159
160// Serialize a plan to its canonical little-endian binary form (the bytes the
161// Ed25519 signature covers, minus the domain-separated header the signer prepends).
162// Deterministic: the same plan always yields the same bytes.
163std::vector<std::uint8_t> serialize_plan(const BakedPlan& plan);
164
165// Parse plan bytes into a BakedPlan, enforcing every cap DURING the streaming read
166// (before any per-element allocation grows unbounded) with checked arithmetic and
167// port-range validation. Returns std::nullopt on ANY violation: truncation,
168// unknown/future format_version, a cap exceeded, a port out of range, or trailing
169// garbage. Pure — resolves no custom type and calls no load_state (that happens on
170// the verified plan in the public loader). Safe to call on attacker bytes ONLY
171// after the signature envelope has been verified.
172std::optional<BakedPlan> parse_plan_bounded(std::span<const std::uint8_t> bytes);
173
174} // namespace detail
175
176// ── signed .pulpbake envelope ────────────────────────────────────────────
177// File layout: magic "PULPBAKE"(8) | manifest_len u32 | plan_len u32 | manifest |
178// plan. The manifest is fixed-size v1 { format_version, schema_version, ext_len,
179// plan_len, plan_sha256(32), signer_public_key(32), signature(64) }. The signature
180// covers a DOMAIN-SEPARATED canonical message binding the tag + versions + ext_len
181// + plan_len + plan_sha256 (which in turn covers the plan bytes) — so a tampered
182// plan, hash, version, or length all fail verification. There is NO unsigned load.
183inline constexpr int kBakedManifestSchemaVersion = 1;
184inline constexpr std::size_t kBakedManifestMaxBytes = 4096;
185
186// The set of publisher public keys a loader accepts (mirror NodePackTrust). Revoke
187// a key by dropping it. A plan signed by a key not in this set is rejected.
189 std::vector<std::vector<std::uint8_t>> trusted_public_keys; // each 32 bytes
190};
191
192// Serialize + sign a plan into a distributable .pulpbake artifact. `private_key_64`
193// is the NaCl-form Ed25519 secret (seed || public_key); the public key is derived
194// from it and embedded. Returns empty on a signing/size failure.
195std::vector<std::uint8_t> write_baked_signed(const BakedPlan& plan,
196 std::span<const std::uint8_t> private_key_64);
197
198// Verify a .pulpbake artifact and return its plan — or std::nullopt on ANY failure:
199// bad magic, over-cap length, malformed manifest, unknown/future version, untrusted
200// signer, bad signature, plan-hash mismatch, or a plan that fails parse_plan_bounded.
201// Nothing below the signature check touches the plan bytes. This is the ONLY entry
202// point for untrusted bytes.
203std::optional<BakedPlan> verify_and_extract_plan(std::span<const std::uint8_t> bytes,
204 const BakedTrust& trust);
205
206} // namespace pulp::host
std::uint32_t NodeId
Definition graph_types.hpp:7
Definition baked_codec.hpp:158
std::vector< std::uint8_t > serialize_plan(const BakedPlan &plan)
std::optional< BakedPlan > parse_plan_bounded(std::span< const std::uint8_t > bytes)
Bounded event contracts shared by host ingress and graph-authored nodes.
Definition anticipation_eligibility.hpp:9
constexpr std::size_t kBakedMaxRegions
Definition baked_codec.hpp:47
constexpr std::size_t kBakedMaxRegionMembers
Definition baked_codec.hpp:48
constexpr std::size_t kBakedMaxNodes
Definition baked_codec.hpp:31
constexpr std::size_t kBakedMaxPlanBytes
Definition baked_codec.hpp:38
constexpr int kBakedPlanFormatVersion
Definition baked_codec.hpp:45
constexpr std::size_t kBakedMaxRegionParameters
Definition baked_codec.hpp:50
constexpr std::size_t kBakedMaxRegionBoundaries
Definition baked_codec.hpp:49
constexpr int kBakedPlanV1FormatVersion
Definition baked_codec.hpp:43
constexpr int kBakedMaxPortsPerNode
Definition baked_codec.hpp:33
constexpr std::size_t kBakedMaxRegionString
Definition baked_codec.hpp:51
constexpr int kBakedMaxSupportedFormatVersion
Definition baked_codec.hpp:44
constexpr std::size_t kBakedMaxTotalPorts
Definition baked_codec.hpp:34
std::optional< BakedPlan > verify_and_extract_plan(std::span< const std::uint8_t > bytes, const BakedTrust &trust)
constexpr int kBakedMaxBlock
Definition baked_codec.hpp:35
constexpr std::size_t kBakedMaxConnections
Definition baked_codec.hpp:32
NodeType
Definition signal_graph_node.hpp:21
@ Gain
Definition signal_graph_node.hpp:25
constexpr int kBakedManifestSchemaVersion
Definition baked_codec.hpp:183
std::vector< std::uint8_t > write_baked_signed(const BakedPlan &plan, std::span< const std::uint8_t > private_key_64)
constexpr std::size_t kBakedMaxCustomTypeId
Definition baked_codec.hpp:36
constexpr std::size_t kBakedMaxCustomState
Definition baked_codec.hpp:37
constexpr std::size_t kBakedManifestMaxBytes
Definition baked_codec.hpp:184
Definition baked_codec.hpp:71
NodeId dst_node
Definition baked_codec.hpp:74
int dst_port
Definition baked_codec.hpp:75
int src_port
Definition baked_codec.hpp:73
NodeId src_node
Definition baked_codec.hpp:72
bool feedback
Definition baked_codec.hpp:76
bool operator==(const Conn &) const =default
Definition baked_codec.hpp:60
int custom_version
Definition baked_codec.hpp:67
bool operator==(const Node &) const =default
int num_input_ports
Definition baked_codec.hpp:63
std::vector< std::uint8_t > custom_state
Definition baked_codec.hpp:68
NodeType type
Definition baked_codec.hpp:62
int num_output_ports
Definition baked_codec.hpp:64
float gain
Definition baked_codec.hpp:65
std::string custom_type_id
Definition baked_codec.hpp:66
Definition baked_codec.hpp:55
std::vector< Conn > connections
Definition baked_codec.hpp:80
int input_channels
Definition baked_codec.hpp:57
int format_version
Definition baked_codec.hpp:56
std::vector< Node > nodes
Definition baked_codec.hpp:79
bool operator==(const BakedPlan &other) const
Definition baked_codec.hpp:87
std::vector< SampleRegionDefinition > sample_regions
Definition baked_codec.hpp:85
int output_channels
Definition baked_codec.hpp:58
Definition baked_codec.hpp:188
std::vector< std::vector< std::uint8_t > > trusted_public_keys
Definition baked_codec.hpp:189
Definition custom_node_type.hpp:332
float constant
Definition custom_node_type.hpp:335
SampleKernelConfigKind kind
Definition custom_node_type.hpp:333
std::uint32_t boundary_index_or_parameter_id
Definition custom_node_type.hpp:334
Definition sample_region_authoring.hpp:24
Definition sample_region_proof.hpp:35
Definition sample_region_proof.hpp:58
Definition sample_region_authoring.hpp:9
Defines the numeric range of a parameter, including normalization.
Definition parameter.hpp:131