use alloy::primitives::{Address, B256, U256};
use alloy::sol_types::SolValue;
use eyre::Result;
use serde::{Deserialize, Serialize};
use crate::{
common::policy_runtime::PolicyRuntime,
generated::newton_policy_client::{INewtonPolicy::PolicyConfig, INewtonPolicyClient::PolicySpec},
};
pub use newton_rego_kernel::{MAX_POLICIES, MAX_POLICY_FIELD_BYTES, MAX_RESPONSE_POLICY_BYTES, MAX_TASK_POLICY_BYTES};
pub const POLICY_SET_DOMAIN: B256 = B256::new([
0x67, 0x1c, 0xdd, 0x56, 0x63, 0xce, 0xa1, 0xdd, 0x5f, 0x0b, 0x42, 0x27, 0x8c, 0xe6, 0x55, 0x70, 0x19, 0x4b, 0xc5,
0x4e, 0x20, 0x44, 0x97, 0x31, 0xd2, 0xae, 0x86, 0x71, 0x36, 0x89, 0xde, 0x91,
]);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedPolicySet {
pub policy_client: Address,
pub policy_id: B256,
pub revision: u64,
pub policies: Vec<PolicyRuntime>,
}
pub fn check_policy_data_bounds<'a>(fields: impl IntoIterator<Item = (usize, &'a str, usize)>) -> Result<()> {
check_policy_data_bounds_with_cap(fields, MAX_TASK_POLICY_BYTES)
}
pub fn check_policy_data_bounds_with_cap<'a>(
fields: impl IntoIterator<Item = (usize, &'a str, usize)>,
total_cap: usize,
) -> Result<()> {
let mut total = 0usize;
for (index, field, len) in fields {
eyre::ensure!(
len <= MAX_POLICY_FIELD_BYTES,
"policy {index} {field} is {len} bytes, over the {MAX_POLICY_FIELD_BYTES} byte limit"
);
total += len;
}
eyre::ensure!(
total <= total_cap,
"task policy data is {total} bytes, over the {total_cap} byte limit"
);
Ok(())
}
impl ResolvedPolicySet {
pub fn compute_policy_id(&self, chain_id: u64) -> B256 {
let specs: Vec<PolicySpec> = self
.policies
.iter()
.map(|p| PolicySpec {
policy: p.policy_address,
config: PolicyConfig {
policyParams: p.policy_config.policyParams.clone(),
expireAfter: p.policy_config.expireAfter,
},
})
.collect();
let encoded = (
POLICY_SET_DOMAIN,
U256::from(chain_id),
self.policy_client,
self.revision,
specs,
)
.abi_encode_params();
alloy::primitives::keccak256(&encoded)
}
pub fn verify_policy_id(&self, chain_id: u64) -> Result<()> {
let computed = self.compute_policy_id(chain_id);
eyre::ensure!(
computed == self.policy_id,
"policy ID mismatch: stored={}, computed={}",
self.policy_id,
computed
);
Ok(())
}
pub fn requires_privacy(&self) -> bool {
self.policies
.iter()
.any(|p| crate::common::privacy_detection::requires_privacy(&String::from_utf8_lossy(&p.rego)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy::primitives::{b256, Bytes};
const CONSTANTS_SOL: &str = include_str!("../../../../contracts/src/libraries/PolicyConstants.sol");
#[test]
fn max_policies_matches_solidity() {
let declared = CONSTANTS_SOL
.lines()
.find_map(|l| l.split_once("uint256 constant MAX_POLICIES ="))
.map(|(_, rhs)| rhs.trim().trim_end_matches(';').parse::<usize>().unwrap())
.expect("MAX_POLICIES not found in PolicyConstants.sol");
assert_eq!(MAX_POLICIES, declared);
}
#[test]
fn policy_set_domain_matches_solidity_preimage() {
let preimage = CONSTANTS_SOL
.lines()
.find_map(|l| l.split_once("POLICY_SET_DOMAIN = keccak256(\""))
.and_then(|(_, rhs)| rhs.split_once('"'))
.map(|(s, _)| s.to_string())
.expect("POLICY_SET_DOMAIN not found in PolicyConstants.sol");
assert_eq!(preimage, "newton.policy.set");
assert_eq!(POLICY_SET_DOMAIN, alloy::primitives::keccak256(&preimage));
}
fn sample_policy(policy_address: Address, policy_params: &[u8], expire_after: u32) -> PolicyRuntime {
PolicyRuntime {
chain_id: 1,
policy_client: Address::repeat_byte(0x11),
policy_id: B256::ZERO,
policy_address,
policy_config: crate::newton_policy::INewtonPolicy::PolicyConfig {
policyParams: Bytes::copy_from_slice(policy_params),
expireAfter: expire_after,
},
entrypoint: String::new(),
schema: serde_json::Value::Null,
policy_cid: String::new(),
policy_code_hash: B256::ZERO,
current_block: 0,
expire_block: 0,
wasm_cid: String::new(),
secrets_schema_cid: String::new(),
rego: Bytes::new(),
}
}
fn sample() -> ResolvedPolicySet {
ResolvedPolicySet {
policy_client: Address::repeat_byte(0x11),
policy_id: b256!("6da916bc40a763e80c09dbfd714f41fe08003f62765cb6c2fc881510f6c0cf7c"),
revision: 7,
policies: vec![sample_policy(
Address::repeat_byte(0x22),
&[0xde, 0xad, 0xbe, 0xef],
100,
)],
}
}
#[test]
fn policy_data_bounds_reject_an_oversize_field() {
let err = check_policy_data_bounds([(0, "params", MAX_POLICY_FIELD_BYTES + 1)]).unwrap_err();
assert!(err.to_string().contains("policy 0 params"));
}
#[test]
fn policy_data_bounds_reject_an_oversize_aggregate() {
let fields: Vec<_> = (0..MAX_POLICIES).map(|i| (i, "rego", MAX_POLICY_FIELD_BYTES)).collect();
let err = check_policy_data_bounds(fields).unwrap_err();
assert!(err.to_string().contains("task policy data"));
}
#[test]
fn policy_data_bounds_accept_a_set_at_the_aggregate_limit() {
let count = MAX_TASK_POLICY_BYTES / MAX_POLICY_FIELD_BYTES;
let fields: Vec<_> = (0..count).map(|i| (i, "rego", MAX_POLICY_FIELD_BYTES)).collect();
check_policy_data_bounds(fields).unwrap();
}
#[test]
fn policy_id_matches_onchain_encoding() {
sample().verify_policy_id(1).unwrap();
}
#[test]
fn policy_id_is_bound_to_every_field() {
let mut wrong_chain = sample();
assert!(wrong_chain.verify_policy_id(2).is_err());
wrong_chain.revision += 1;
assert!(wrong_chain.verify_policy_id(1).is_err());
let mut wrong_params = sample();
wrong_params.policies[0].policy_config.policyParams = Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef, 0x00]);
assert!(wrong_params.verify_policy_id(1).is_err());
let mut wrong_expiry = sample();
wrong_expiry.policies[0].policy_config.expireAfter = 101;
assert!(wrong_expiry.verify_policy_id(1).is_err());
let mut wrong_policy = sample();
wrong_policy.policies[0].policy_address = Address::repeat_byte(0x23);
assert!(wrong_policy.verify_policy_id(1).is_err());
}
}