use super::source::{OracleSrc, Signer};
use super::*;
use lyquor_primitives::oracle::{OracleConfigDelta as OracleConfigDeltaWire, ValidatePreimage, eth};
use lyquor_primitives::{Address, Bytes, CallParams, Cipher, Hash, HashBytes, InputABI};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct CertifiedCallParams {
pub origin: Address,
pub method: String,
pub input: Bytes,
pub target: OracleTarget,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ValidateRequest {
pub header: OracleHeader,
pub params: CallParams,
pub extra: Bytes,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ValidateResponse {
pub approval: bool,
pub sig: Bytes,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ProposeRequest {
pub init: Bytes,
pub nonce: HashBytes,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ProposeResponse {
pub input: Bytes,
pub sig: Bytes,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct ProposalInput {
pub from: NodeID,
pub input: Bytes,
pub sig: Bytes,
}
impl ProposalInput {
fn verify(
&self, lyquid_id: LyquidID, topic: &str, group: &str, proposer: NodeID, init: Bytes, nonce: HashBytes,
config: &OracleConfig, seen: &mut HashSet<NodeID>,
) -> LyquidResult<bool> {
if !seen.insert(self.from) {
return Ok(false);
}
let signer = match config.committee.get(&self.from) {
Some(s) => s,
None => return Ok(false),
};
let key = signer.get_verifying_key(Cipher::Ed25519);
ProposePreimage {
lyquid_id,
topic: topic.into(),
group: group.into(),
proposer,
init,
nonce,
input: self.input.clone(),
}
.verify(self.sig.clone(), key)
}
}
#[derive(Serialize, Deserialize)]
struct ProposePreimage {
lyquid_id: LyquidID,
topic: String,
group: String,
proposer: NodeID,
init: Bytes,
nonce: HashBytes,
input: Bytes,
}
impl ProposePreimage {
const PREFIX: &'static [u8] = b"lyquor_propose_preimage_v1\0";
fn to_preimage(&self) -> Vec<u8> {
lyquor_primitives::encode_object_with_prefix(Self::PREFIX, self)
}
fn verify(&self, sig: Bytes, pk: Bytes) -> LyquidResult<bool> {
let m = self.to_preimage();
lyquor_api::verify(m.into(), Cipher::Ed25519, sig, pk)
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Proposal {
nonce: HashBytes,
inputs: Vec<ProposalInput>,
output: CertifiedCallParams,
}
fn two_phase_cert_nonce(proposal_nonce: HashBytes) -> HashBytes {
blake3::hash(&<[u8; 32]>::from(proposal_nonce)).into()
}
fn group_from_chunks(prefix: &str, suffix: &[Option<&str>]) -> String {
let mut group = prefix.to_string();
for s in suffix {
if let Some(s) = s {
group.push_str("::");
group.push_str(s);
}
}
group
}
fn random_cert_nonce() -> Option<HashBytes> {
let bytes = lyquor_api::random_bytes(32).ok()?;
let hash = Hash::from_slice(&bytes).ok()?;
Some(hash.into())
}
fn validate_phase(
lyquid_id: LyquidID, proposer: NodeID, group_suffix: &str, target: OracleTarget, epoch: u32,
config_hash: HashBytes, mut params: CallParams, extra: Bytes, config: OracleConfig,
nonce_fn: impl FnOnce() -> Option<HashBytes>, timeout_ms: Option<u64>,
) -> LyquidResult<Option<CallParams>> {
let nonce = nonce_fn().ok_or_else(|| LyquidError::LyquidRuntime("NEAT: failed to generate nonce.".into()))?;
let callee = config.committee.keys().cloned().collect::<Vec<_>>();
let input_raw = params.input.clone();
let header = OracleHeader {
proposer,
target,
config_hash,
epoch,
nonce,
};
let yea = ValidatePreimage {
header,
params: params.clone(),
approval: true,
};
let nay = ValidatePreimage {
header,
params: params.clone(),
approval: false,
};
let (yea_msg, nay_msg) = match header.target.target {
OracleServiceTarget::EVM { .. } => (
eth::ValidatePreimage::try_from(yea).unwrap().to_preimage(),
eth::ValidatePreimage::try_from(nay).unwrap().to_preimage(),
),
OracleServiceTarget::LVM(_) => (yea.to_preimage(), nay.to_preimage()),
};
lyquor_api::universal_procedural_call(
lyquid_id,
Some(group_from_chunks("oracle", &[Some("single_phase"), Some(group_suffix)])),
"validate".into(),
encode_by_fields!(msg: ValidateRequest = ValidateRequest {
header,
params: params.clone(),
extra,
}),
Some(
encode_by_fields!(
callee: Vec<NodeID> = callee,
header: OracleHeader = header,
yea_msg: Bytes = yea_msg.into(),
nay_msg: Bytes = nay_msg.into(),
config: OracleConfig = config
)
.into(),
),
timeout_ms,
)
.and_then(|r| lyquor_primitives::decode_object(&r).ok_or(LyquidError::LyquorOutput))
.map(|cert: Option<OracleCert>| {
cert.map(move |cert| {
params.input = encode_by_fields!(cert: OracleCert = cert, input_raw: Bytes = input_raw).into();
params
})
})
}
fn certify<S: crate::runtime::internal::StateAccessor, I: crate::runtime::internal::StateAccessor>(
ctx: &mut crate::runtime::InstanceContextImpl<S, I>, topic: &str, params: CertifiedCallParams, extra: Bytes,
group_suffix: Option<&'static str>, timeout_ms: Option<u64>,
) -> LyquidResult<Option<CallParams>> {
let lyquid = ctx.lyquid_id;
let Some(source) = crate::runtime::internal::builtin_network_state()
.oracle_src(topic)
.and_then(|oracle| oracle.source_state(params.target))
else {
return Ok(None);
};
let config = source.current_config().clone();
if !config.is_valid() {
return Ok(None);
}
let group = group_from_chunks(topic, &[group_suffix]);
let call_params = CallParams {
origin: params.origin,
caller: lyquid.into(),
group: group.clone(),
method: params.method,
input: params.input,
abi: match params.target.target {
OracleServiceTarget::EVM { .. } => InputABI::Eth,
OracleServiceTarget::LVM(_) => InputABI::Lyquor,
},
};
validate_phase(
lyquid,
ctx.node_id,
&group,
params.target,
config.epoch,
source.current_config_hash().into(),
call_params,
extra,
config,
random_cert_nonce,
timeout_ms,
)
}
fn propose_and_certify<S: crate::runtime::internal::StateAccessor, I: crate::runtime::internal::StateAccessor>(
ctx: &mut crate::runtime::InstanceContextImpl<S, I>, topic: &str, target: OracleTarget, init: Bytes,
group_suffix: Option<&'static str>, timeout_ms: Option<u64>,
) -> LyquidResult<Option<CallParams>> {
let lyquid = ctx.lyquid_id;
let Some(source) = crate::runtime::internal::builtin_network_state()
.oracle_src(topic)
.and_then(|oracle| oracle.source_state(target))
else {
return Ok(None);
};
let config = source.current_config().clone();
if !config.is_valid() {
return Ok(None);
}
let callee = config.committee.keys().cloned().collect::<Vec<_>>();
let nonce = Hash::from_slice(&lyquor_api::random_bytes(32)?)
.map_err(|_| LyquidError::LyquidRuntime("NEAT: failed to obtain proposal nonce.".into()))?
.into();
let proposal: Option<Proposal> = lyquor_api::universal_procedural_call(
lyquid,
Some(group_from_chunks(
"oracle",
&[Some("two_phase"), Some(topic), group_suffix],
)),
"propose".into(),
encode_by_fields!(msg: ProposeRequest = ProposeRequest {
init: init.clone(),
nonce,
}),
Some(
encode_by_fields!(
callee: Vec<NodeID> = callee,
init: Bytes = init.clone(),
nonce: HashBytes = nonce,
vote_config: OracleConfig = config.clone()
)
.into(),
),
timeout_ms,
)
.and_then(|r| lyquor_primitives::decode_object(&r).ok_or(LyquidError::LyquorOutput))?;
let Proposal {
nonce: proposal_nonce,
inputs,
output,
} = match proposal {
Some(p) => p,
None => return Ok(None),
};
let cert_nonce = two_phase_cert_nonce(proposal_nonce);
let group = group_from_chunks(topic, &[Some("two_phase"), group_suffix]);
let call_params = CallParams {
origin: output.origin,
caller: lyquid.into(),
group: group.clone(),
method: output.method,
input: output.input,
abi: match output.target.target {
OracleServiceTarget::EVM { .. } => InputABI::Eth,
OracleServiceTarget::LVM(_) => InputABI::Lyquor,
},
};
validate_phase(
lyquid,
ctx.node_id,
&group,
output.target,
config.epoch,
source.current_config_hash().into(),
call_params,
encode_by_fields!(
init: Bytes = init,
nonce: HashBytes = proposal_nonce,
inputs: Vec<ProposalInput> = inputs
)
.into(),
config,
|| Some(cert_nonce),
timeout_ms,
)
}
fn advance_epoch<S: crate::runtime::internal::StateAccessor, I: crate::runtime::internal::StateAccessor>(
ctx: &mut crate::runtime::InstanceContextImpl<S, I>, topic: &str, target: OracleTarget,
) -> LyquidResult<Option<CallParams>> {
let Some(source) = crate::runtime::internal::builtin_network_state().oracle_src(topic) else {
return Ok(None);
};
let (epoch, config, delta, config_hash, change_count) = match source.propose_advance_epoch(target) {
Some(d) => d,
None => return Ok(None),
};
let config = config.clone();
let group = group_from_chunks(topic, &[Some(EPOCH_GROUP_SUFFIX)]);
let params = CallParams {
origin: Address::ZERO,
caller: Address::from(ctx.lyquid_id),
group: "oracle::internal".to_string(),
method: ADVANCE_EPOCH_METHOD.into(),
input: encode_by_fields!(
topic: String = topic.to_string(),
config_delta: OracleConfigDeltaWire = delta,
change_count: u32 = change_count
)
.into(),
abi: InputABI::Lyquor,
};
validate_phase(
ctx.lyquid_id,
ctx.node_id,
&group,
target,
epoch,
config_hash.into(),
params,
Bytes::new(),
config,
random_cert_nonce,
None,
)
}
fn finalize_epoch<S: crate::runtime::internal::StateAccessor, I: crate::runtime::internal::StateAccessor>(
ctx: &mut crate::runtime::InstanceContextImpl<S, I>, topic: &str, target: OracleTarget,
) -> LyquidResult<Option<CallParams>> {
let Some(target_info) = lyquor_api::fetch_oracle_info(topic.to_string(), target, false)? else {
return Ok(None);
};
let Some((source_epoch, source_hash, source_config)) = crate::runtime::internal::builtin_network_state()
.oracle_src(topic)
.and_then(|oracle| oracle.source_state(target))
.and_then(|state| state.finalize_cert_context(target_info.change_count))
else {
return Ok(None);
};
let source_target = oracle_target_from_address(Address::from(ctx.lyquid_id), false)?;
let group = group_from_chunks(topic, &[Some(EPOCH_GROUP_SUFFIX)]);
let params = CallParams {
origin: Address::ZERO,
caller: Address::from(ctx.lyquid_id),
group: group.clone(),
method: FINALIZE_EPOCH_METHOD.into(),
input: encode_by_fields!(
target: OracleTarget = target,
target_info: OracleEpochInfo = target_info
)
.into(),
abi: InputABI::Lyquor,
};
validate_phase(
ctx.lyquid_id,
ctx.node_id,
&group,
source_target,
source_epoch,
source_hash.into(),
params,
Bytes::new(),
source_config,
random_cert_nonce,
None,
)
}
impl<'a> StateVar<'a> {
#[inline]
pub fn certify<S: crate::runtime::internal::StateAccessor, I: crate::runtime::internal::StateAccessor>(
&self, ctx: &mut crate::runtime::InstanceContextImpl<S, I>, params: CertifiedCallParams, extra: Bytes,
group_suffix: Option<&'static str>, timeout_ms: Option<u64>,
) -> LyquidResult<Option<CallParams>> {
certify(ctx, self.topic(), params, extra, group_suffix, timeout_ms)
}
#[inline]
pub fn propose_and_certify<
S: crate::runtime::internal::StateAccessor,
I: crate::runtime::internal::StateAccessor,
>(
&self, ctx: &mut crate::runtime::InstanceContextImpl<S, I>, target: OracleTarget, init: Bytes,
group_suffix: Option<&'static str>, timeout_ms: Option<u64>,
) -> LyquidResult<Option<CallParams>> {
propose_and_certify(ctx, self.topic(), target, init, group_suffix, timeout_ms)
}
#[inline]
pub fn advance_epoch<S: crate::runtime::internal::StateAccessor, I: crate::runtime::internal::StateAccessor>(
&self, ctx: &mut crate::runtime::InstanceContextImpl<S, I>, target: OracleTarget,
) -> LyquidResult<Option<CallParams>> {
advance_epoch(ctx, self.topic(), target)
}
#[inline]
pub fn finalize_epoch<S: crate::runtime::internal::StateAccessor, I: crate::runtime::internal::StateAccessor>(
&self, ctx: &mut crate::runtime::InstanceContextImpl<S, I>, target: OracleTarget,
) -> LyquidResult<Option<CallParams>> {
finalize_epoch(ctx, self.topic(), target)
}
pub fn __pre_validation(
&self, header: &OracleHeader, params: &CallParams, group: &str, from: NodeID, lyquid_id: LyquidID,
) -> Option<bool> {
let oracle = crate::runtime::internal::builtin_network_state().oracle_src(self.topic())?;
self.__pre_validation_with_oracle(oracle, header, params, group, from, lyquid_id)
}
fn __pre_validation_with_oracle(
&self, oracle: &OracleSrc, header: &OracleHeader, params: &CallParams, group: &str, from: NodeID,
lyquid_id: LyquidID,
) -> Option<bool> {
if from != header.proposer {
return None;
}
let is_epoch_vote = group == &group_from_chunks(self.topic(), &[Some(EPOCH_GROUP_SUFFIX)]);
let is_advance_epoch = is_epoch_vote && params.method == ADVANCE_EPOCH_METHOD;
let is_finalize_epoch = is_epoch_vote && params.method == FINALIZE_EPOCH_METHOD;
if is_advance_epoch {
if params.abi != lyquor_primitives::InputABI::Lyquor ||
params.group != "oracle::internal" ||
params.method != ADVANCE_EPOCH_METHOD ||
params.origin != Address::ZERO ||
params.caller != Address::from(lyquid_id)
{
return None;
}
let payload = match lyquor_primitives::decode_by_fields!(
¶ms.input,
topic: String,
config_delta: OracleConfigDeltaWire,
change_count: u32
) {
Some(payload) => payload,
None => return None,
};
if !oracle.validate_advance_epoch(
header.target,
payload.topic.as_str(),
header.epoch,
&header.config_hash,
&payload.config_delta,
payload.change_count,
) {
return None;
}
} else if is_finalize_epoch {
let payload = match lyquor_primitives::decode_by_fields!(
¶ms.input,
target: OracleTarget,
target_info: OracleEpochInfo
) {
Some(payload) => payload,
None => return None,
};
if params.abi != lyquor_primitives::InputABI::Lyquor ||
params.group != group ||
params.method != FINALIZE_EPOCH_METHOD ||
params.origin != Address::ZERO ||
params.caller != Address::from(lyquid_id)
{
return None;
}
if header.target !=
(OracleTarget {
seq_id: lyquor_api::sequence_backend_id().ok()?,
target: OracleServiceTarget::LVM(lyquid_id),
})
{
return None;
}
if !oracle.validate_finalize_epoch(payload.target, &payload.target_info) {
return None;
}
} else {
let state = oracle.source_state(header.target)?;
let abi_ok = match header.target.target {
OracleServiceTarget::LVM(_) => params.abi == InputABI::Lyquor,
OracleServiceTarget::EVM { .. } => params.abi == InputABI::Eth,
};
if !abi_ok ||
params.group != group ||
params.caller != Address::from(lyquid_id) ||
header.epoch != state.current_config().epoch ||
state.current_config_hash() != *header.config_hash
{
return None;
}
}
match header.target.target {
OracleServiceTarget::LVM(_) => true,
OracleServiceTarget::EVM { eth_contract, .. } => lyquor_api::eth_contract()
.ok()
.flatten()
.is_some_and(|contract| eth_contract == contract),
}
.then_some(is_epoch_vote)
}
pub fn __pre_validation_two_phase(
&self, header: &OracleHeader, extra: &Bytes, lyquid_id: LyquidID, group: &str, proposer: NodeID,
) -> LyquidResult<Option<(Bytes, HashBytes, Vec<ProposalInput>)>> {
let Some(oracle) = crate::runtime::internal::builtin_network_state().oracle_src(self.topic()) else {
return Ok(None);
};
self.__pre_validation_two_phase_with_oracle(oracle, header, extra, lyquid_id, group, proposer)
}
fn __pre_validation_two_phase_with_oracle(
&self, oracle: &OracleSrc, header: &OracleHeader, extra: &Bytes, lyquid_id: LyquidID, group: &str,
proposer: NodeID,
) -> LyquidResult<Option<(Bytes, HashBytes, Vec<ProposalInput>)>> {
let payload = match lyquor_primitives::decode_by_fields!(
extra,
init: Bytes,
nonce: HashBytes,
inputs: Vec<ProposalInput>
) {
Some(v) => v,
None => return Ok(None),
};
if two_phase_cert_nonce(payload.nonce) != header.nonce {
return Ok(None);
}
let Some(config) = oracle.source_state(header.target).map(|state| state.current_config()) else {
return Ok(None);
};
let mut seen = new_hashset();
for input in &payload.inputs {
if !input.verify(
lyquid_id,
self.topic(),
group,
proposer,
payload.init.clone(),
payload.nonce,
config,
&mut seen,
)? {
return Ok(None);
}
}
Ok(Some((payload.init, payload.nonce, payload.inputs)))
}
pub fn __post_validation(
&self, header: OracleHeader, params: CallParams, approval: bool,
) -> LyquidResult<ValidateResponse> {
let preimage = ValidatePreimage {
header,
params,
approval,
};
let (cipher, m) = match header.target.target {
OracleServiceTarget::EVM { .. } => (
Cipher::Secp256k1,
eth::ValidatePreimage::try_from(preimage).unwrap().to_preimage(),
),
OracleServiceTarget::LVM(_) => (Cipher::Ed25519, preimage.to_preimage()),
};
let sig = lyquor_api::sign(m.into(), cipher)?;
Ok(ValidateResponse { approval, sig })
}
pub fn __post_propose(
&self, lyquid_id: LyquidID, group: &str, proposer: NodeID, init: Bytes, nonce: HashBytes, input: Bytes,
) -> LyquidResult<ProposeResponse> {
let preimage = ProposePreimage {
lyquid_id,
topic: self.topic().to_string(),
group: group.into(),
proposer,
init,
nonce,
input: input.clone(),
};
let sig = lyquor_api::sign(preimage.to_preimage().into(), Cipher::Ed25519)?;
Ok(ProposeResponse { input, sig })
}
}
pub struct ValidateAggregation {
header: OracleHeader,
yea_msg: Bytes,
nay_msg: Bytes,
committee: HashMap<NodeID, Signer>,
threshold: u16,
collected: HashSet<NodeID>,
approved_sigs: Vec<(SignerID, Bytes)>,
approved: u16,
result: Option<Option<OracleCert>>,
}
pub struct ProposalAggregation {
committee: HashMap<NodeID, Signer>,
threshold: u16,
init: Bytes,
nonce: HashBytes,
collected: HashSet<NodeID>,
inputs: Vec<ProposalInput>,
output: Option<Option<Proposal>>,
}
impl ProposalAggregation {
pub fn new(init: Bytes, nonce: HashBytes, config: OracleConfig) -> Self {
let committee = config.committee;
let threshold = config.threshold;
Self {
committee,
threshold,
init,
nonce,
collected: new_hashset(),
inputs: Vec::new(),
output: None,
}
}
pub fn add_response(
&mut self, node: NodeID, resp: ProposeResponse,
agg: fn(ProposalAggregationContext) -> LyquidResult<Option<CertifiedCallParams>>, lyquid_id: LyquidID,
topic: &str, group: &str, proposer: NodeID,
) -> Option<Option<Proposal>> {
if self.output.is_some() {
return self.output.clone();
}
let signer = match self.committee.get(&node) {
Some(signer) => signer,
None => return self.output.clone(),
};
if !self.collected.insert(node) {
return self.output.clone();
}
if self.threshold == 0 || self.committee.len() < self.threshold as usize {
self.output = Some(None);
return self.output.clone();
}
let key = signer.get_verifying_key(Cipher::Ed25519);
let preimage = ProposePreimage {
lyquid_id,
topic: topic.to_string(),
group: group.into(),
proposer,
init: self.init.clone(),
nonce: self.nonce,
input: resp.input.clone(),
};
let ok = preimage.verify(resp.sig.clone(), key).ok().unwrap_or(false);
if ok {
self.inputs.push(ProposalInput {
from: node,
input: resp.input,
sig: resp.sig,
});
}
if let Ok(Some(output)) = agg(ProposalAggregationContext {
init: &self.init,
inputs: &self.inputs,
lyquid_id,
}) {
self.output = Some(Some(Proposal {
nonce: self.nonce,
inputs: self.inputs.clone(),
output,
}));
} else if self.committee.len() + self.inputs.len() < self.threshold as usize + self.collected.len() {
self.output = Some(None);
}
self.output.clone()
}
}
impl ValidateAggregation {
pub fn new(header: OracleHeader, yea_msg: Bytes, nay_msg: Bytes, vote_config: OracleConfig) -> Self {
let committee = vote_config.committee;
let threshold = vote_config.threshold;
Self {
header,
yea_msg,
nay_msg,
committee,
threshold,
collected: new_hashset(),
approved_sigs: Vec::new(),
approved: 0,
result: None,
}
}
pub fn add_response(&mut self, node: NodeID, resp: ValidateResponse) -> Option<Option<OracleCert>> {
if self.result.is_some() {
return self.result.clone();
}
let signer = match self.committee.get(&node).copied() {
Some(signer) => signer,
None => return self.result.clone(),
};
if !self.collected.insert(node) {
return self.result.clone();
}
if self.threshold == 0 || self.committee.len() < self.threshold as usize {
self.result = Some(None);
return self.result.clone();
}
let cipher = self.header.target.cipher();
let key = signer.get_verifying_key(cipher);
let ok = super::lyquor_api::verify(
if resp.approval { &self.yea_msg } else { &self.nay_msg }.clone(),
cipher,
resp.sig.clone(),
key,
)
.ok()
.unwrap_or(false);
if ok && resp.approval {
self.approved_sigs.push((signer.id, resp.sig.clone()));
self.approved += 1
}
if self.approved >= self.threshold {
let mut approved_sigs = self.approved_sigs.clone();
approved_sigs.sort_by_key(|(id, _)| *id);
let (signers, signatures): (Vec<_>, Vec<_>) = approved_sigs.into_iter().unzip();
self.result = Some(Some(OracleCert {
header: self.header,
signers,
signatures,
}))
} else if self.committee.len() + (self.approved as usize) < (self.threshold as usize) + self.collected.len() {
self.result = Some(None)
}
self.result.clone()
}
}
pub struct ProposalAggregationContext<'a> {
pub init: &'a [u8],
pub inputs: &'a [ProposalInput],
pub lyquid_id: LyquidID,
}