use std::borrow::Borrow;
use std::fmt::Display;
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use std::str::FromStr;
use blake3::{traits::digest::Digest, Hasher as Blake3};
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use crate::client_api::{TryFromFbs, WsApiError};
use crate::code_hash::CodeHash;
use crate::common_generated::common::ContractKey as FbsContractKey;
use crate::parameters::Parameters;
use super::code::ContractCode;
use super::CONTRACT_KEY_SIZE;
#[serde_as]
#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Hash)]
#[cfg_attr(
any(feature = "testing", all(test, any(unix, windows))),
derive(arbitrary::Arbitrary)
)]
#[repr(transparent)]
pub struct ContractInstanceId(#[serde_as(as = "[_; CONTRACT_KEY_SIZE]")] [u8; CONTRACT_KEY_SIZE]);
impl ContractInstanceId {
pub fn from_params_and_code<'a>(
params: impl Borrow<Parameters<'a>>,
code: impl Borrow<ContractCode<'a>>,
) -> Self {
generate_id(params.borrow(), code.borrow())
}
pub const fn new(key: [u8; CONTRACT_KEY_SIZE]) -> Self {
Self(key)
}
pub fn encode(&self) -> String {
bs58::encode(self.0)
.with_alphabet(bs58::Alphabet::BITCOIN)
.into_string()
}
pub fn as_bytes(&self) -> &[u8] {
self.0.as_slice()
}
pub fn from_base58(bytes: impl AsRef<[u8]>) -> Result<Self, bs58::decode::Error> {
let mut spec = [0; CONTRACT_KEY_SIZE];
bs58::decode(bytes)
.with_alphabet(bs58::Alphabet::BITCOIN)
.onto(&mut spec)?;
Ok(Self(spec))
}
#[deprecated(
note = "renamed to `from_base58`: this parses base58 TEXT, not raw bytes. \
For a raw 32-byte id use `ContractInstanceId::new`."
)]
pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self, bs58::decode::Error> {
Self::from_base58(bytes)
}
}
impl Deref for ContractInstanceId {
type Target = [u8; CONTRACT_KEY_SIZE];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl FromStr for ContractInstanceId {
type Err = bs58::decode::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
ContractInstanceId::from_base58(s)
}
}
impl TryFrom<String> for ContractInstanceId {
type Error = bs58::decode::Error;
fn try_from(s: String) -> Result<Self, Self::Error> {
ContractInstanceId::from_base58(s)
}
}
impl Display for ContractInstanceId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.encode())
}
}
impl std::fmt::Debug for ContractInstanceId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("ContractInstanceId")
.field(&self.encode())
.finish()
}
}
#[serde_as]
#[derive(Debug, Eq, Copy, Clone, Serialize, Deserialize)]
#[cfg_attr(
any(feature = "testing", all(test, any(unix, windows))),
derive(arbitrary::Arbitrary)
)]
pub struct ContractKey {
instance: ContractInstanceId,
code: CodeHash,
}
impl ContractKey {
pub fn from_params_and_code<'a>(
params: impl Borrow<Parameters<'a>>,
wasm_code: impl Borrow<ContractCode<'a>>,
) -> Self {
let code = wasm_code.borrow();
let id = generate_id(params.borrow(), code);
let code_hash = *code.hash();
Self {
instance: id,
code: code_hash,
}
}
pub fn as_bytes(&self) -> &[u8] {
self.instance.0.as_ref()
}
pub fn code_hash(&self) -> &CodeHash {
&self.code
}
pub fn encoded_code_hash(&self) -> String {
bs58::encode(self.code.0)
.with_alphabet(bs58::Alphabet::BITCOIN)
.into_string()
}
pub fn from_params(
code_hash: impl Into<String>,
parameters: Parameters,
) -> Result<Self, bs58::decode::Error> {
let mut code_key = [0; CONTRACT_KEY_SIZE];
bs58::decode(code_hash.into())
.with_alphabet(bs58::Alphabet::BITCOIN)
.onto(&mut code_key)?;
let mut hasher = Blake3::new();
hasher.update(code_key.as_slice());
hasher.update(parameters.as_ref());
let full_key_arr = hasher.finalize();
let mut spec = [0; CONTRACT_KEY_SIZE];
spec.copy_from_slice(&full_key_arr);
Ok(Self {
instance: ContractInstanceId(spec),
code: CodeHash(code_key),
})
}
pub fn encoded_contract_id(&self) -> String {
self.instance.encode()
}
pub fn id(&self) -> &ContractInstanceId {
&self.instance
}
pub fn from_id_and_code(instance_id: ContractInstanceId, code_hash: CodeHash) -> Self {
Self {
instance: instance_id,
code: code_hash,
}
}
}
impl PartialEq for ContractKey {
fn eq(&self, other: &Self) -> bool {
self.instance == other.instance
}
}
impl std::hash::Hash for ContractKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.instance.0.hash(state);
}
}
impl From<ContractKey> for ContractInstanceId {
fn from(key: ContractKey) -> Self {
key.instance
}
}
impl Deref for ContractKey {
type Target = [u8; CONTRACT_KEY_SIZE];
fn deref(&self) -> &Self::Target {
&self.instance.0
}
}
impl std::fmt::Display for ContractKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.instance.fmt(f)
}
}
fn code_hash_error(observed: Option<usize>) -> String {
let seen = match observed {
Some(len) => format!("got {len} bytes"),
None => "the field was absent".to_string(),
};
format!(
"ContractKey.code must be the {CONTRACT_KEY_SIZE}-byte contract code hash; {seen}. \
An UPDATE cannot be addressed by instance id alone: the node gates an UPDATE on \
already holding the contract's code blob and probes for it by code hash, so a \
missing or wrong-length hash is rejected here instead of failing later as \
\"missing contract: <key>\". Build the key with both parts: in the TypeScript SDK \
use `new ContractKey(instance, code)` rather than \
`ContractKey.fromInstanceId(...)`. See freenet/freenet-core#4978."
)
}
pub(crate) fn instance_id_from_fbs(
field: &str,
data: &[u8],
) -> Result<ContractInstanceId, WsApiError> {
crate::client_api::fixed_size_field::<CONTRACT_KEY_SIZE>(field, data)
.map(ContractInstanceId::new)
}
impl<'a> TryFromFbs<&FbsContractKey<'a>> for ContractKey {
fn try_decode_fbs(key: &FbsContractKey<'a>) -> Result<Self, WsApiError> {
let instance =
instance_id_from_fbs("ContractKey.instance.data", key.instance().data().bytes())?;
let code_bytes = key
.code()
.map(|code_hash| code_hash.bytes())
.ok_or_else(|| WsApiError::deserialization(code_hash_error(None)))?;
let code = CodeHash::try_from(code_bytes)
.map_err(|_| WsApiError::deserialization(code_hash_error(Some(code_bytes.len()))))?;
Ok(ContractKey { instance, code })
}
}
fn generate_id<'a>(
parameters: &Parameters<'a>,
code_data: &ContractCode<'a>,
) -> ContractInstanceId {
let contract_hash = code_data.hash();
let mut hasher = Blake3::new();
hasher.update(contract_hash.0.as_slice());
hasher.update(parameters.as_ref());
let full_key_arr = hasher.finalize();
debug_assert_eq!(full_key_arr[..].len(), CONTRACT_KEY_SIZE);
let mut spec = [0; CONTRACT_KEY_SIZE];
spec.copy_from_slice(&full_key_arr);
ContractInstanceId(spec)
}
#[inline]
pub(super) fn internal_fmt_key(
key: &[u8; CONTRACT_KEY_SIZE],
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
let r = bs58::encode(key)
.with_alphabet(bs58::Alphabet::BITCOIN)
.into_string();
write!(f, "{}", &r[..8])
}
#[cfg(test)]
mod fbs_tests {
use super::*;
use crate::common_generated::common::{
ContractInstanceId as FbsContractInstanceId, ContractInstanceIdArgs, ContractKeyArgs,
};
#[test]
fn contract_key_code_hash_passes_through_fbs_decode() {
let code_bytes = [42u8; CONTRACT_KEY_SIZE];
let decoded = decode_with_code(Some(&code_bytes)).expect("decode ContractKey");
assert_eq!(
decoded.code_hash().as_ref(),
&code_bytes,
"decoder must pass the code hash through unchanged, not re-hash it"
);
assert_eq!(decoded.id().as_bytes(), &[7u8; CONTRACT_KEY_SIZE]);
}
fn decode_with_code(code_bytes: Option<&[u8]>) -> Result<ContractKey, WsApiError> {
decode_key(&[7u8; CONTRACT_KEY_SIZE], code_bytes)
}
fn encode_key(instance_bytes: &[u8], code_bytes: Option<&[u8]>) -> Vec<u8> {
let mut builder = flatbuffers::FlatBufferBuilder::new();
let instance_data = builder.create_vector(instance_bytes);
let instance_offset = FbsContractInstanceId::create(
&mut builder,
&ContractInstanceIdArgs {
data: Some(instance_data),
},
);
let code = code_bytes.map(|bytes| builder.create_vector(bytes));
let key_offset = FbsContractKey::create(
&mut builder,
&ContractKeyArgs {
instance: Some(instance_offset),
code,
},
);
builder.finish_minimal(key_offset);
builder.finished_data().to_vec()
}
fn decode_key(
instance_bytes: &[u8],
code_bytes: Option<&[u8]>,
) -> Result<ContractKey, WsApiError> {
let bytes = encode_key(instance_bytes, code_bytes);
let fbs_key =
flatbuffers::root::<FbsContractKey>(&bytes).expect("valid ContractKey flatbuffer");
ContractKey::try_decode_fbs(&fbs_key)
}
const CONTRACT_KEY_WIRE_FORMAT: &[u8] = &[
12, 0, 0, 0, 8, 0, 12, 0, 4, 0, 8, 0, 8, 0, 0, 0, 52, 0, 0, 0, 4, 0, 0, 0, 32, 0, 0, 0, 42,
42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
42, 42, 42, 42, 42, 42, 42, 42, 0, 0, 6, 0, 8, 0, 4, 0, 6, 0, 0, 0, 4, 0, 0, 0, 32, 0, 0,
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7,
];
#[test]
fn contract_key_wire_format_is_stable() {
let fbs_key = flatbuffers::root::<FbsContractKey>(CONTRACT_KEY_WIRE_FORMAT)
.expect("the pinned ContractKey bytes must still parse");
let decoded = ContractKey::try_decode_fbs(&fbs_key)
.expect("the pinned ContractKey bytes must still decode");
assert_eq!(
decoded.id().as_bytes(),
&[7u8; CONTRACT_KEY_SIZE],
"instance id moved: `common.ContractKey`'s vtable layout changed, \
which breaks every already-deployed client"
);
assert_eq!(
decoded.code_hash().as_ref(),
&[42u8; CONTRACT_KEY_SIZE],
"code hash moved: `common.ContractKey`'s vtable layout changed, \
which breaks every already-deployed client"
);
}
#[test]
fn contract_key_decode_rejects_short_instance_without_panicking() {
let err = decode_key(&[1u8; 8], Some(&[42u8; CONTRACT_KEY_SIZE]))
.expect_err("a short instance vector must be rejected");
let msg = err.to_string();
assert!(
msg.contains("ContractKey.instance") && msg.contains("got 8 bytes"),
"the error must name the field and the observed length, got: {msg}"
);
}
#[test]
fn contract_key_decode_rejects_long_instance() {
let err = decode_key(&[1u8; 64], Some(&[42u8; CONTRACT_KEY_SIZE]))
.expect_err("an over-long instance vector must be rejected");
assert!(err.to_string().contains("got 64 bytes"), "got: {err}");
}
#[test]
fn contract_key_decode_rejects_empty_code_with_actionable_error() {
let err = decode_with_code(Some(&[])).expect_err("empty code must be rejected");
let msg = err.to_string();
assert!(
msg.contains("ContractKey.code"),
"error must name the offending field, got: {msg}"
);
assert!(
msg.contains("32-byte"),
"error must state the expected length, got: {msg}"
);
assert!(
msg.contains("got 0 bytes"),
"error must state the actual length, got: {msg}"
);
assert!(
msg.contains("instance id alone"),
"error must explain why the hash is required, got: {msg}"
);
assert!(
msg.contains("4978"),
"error must point at the tracking issue for the real fix, got: {msg}"
);
}
#[test]
fn contract_key_decode_rejects_absent_code_with_actionable_error() {
let err = decode_with_code(None).expect_err("absent code must be rejected");
let msg = err.to_string();
assert!(
msg.contains("ContractKey.code") && msg.contains("4978"),
"absent-code error must carry the same guidance as the empty case, got: {msg}"
);
assert!(
msg.contains("absent"),
"absent-code error must distinguish itself from a wrong-length one, got: {msg}"
);
}
#[test]
fn contract_key_decode_rejects_wrong_length_code() {
let err = decode_with_code(Some(&[1u8; 16])).expect_err("16-byte code must be rejected");
assert!(
err.to_string().contains("got 16 bytes"),
"error must report the observed length, got: {err}"
);
}
}