Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
8 changes: 6 additions & 2 deletions crates/contract-history/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
pub const fn current_mainnet() -> &'static [u8] {
version_3_13_0()
version_3_14_0()
}

pub const fn current_testnet() -> &'static [u8] {
version_3_13_0()
version_3_14_0()
}

pub const fn version_2_2_0() -> &'static [u8; 566653] {
Expand Down Expand Up @@ -70,6 +70,10 @@ pub const fn version_3_13_0() -> &'static [u8; 1509901] {
include_bytes!("../archive/signer-3_13_0.wasm")
}

pub const fn version_3_14_0() -> &'static [u8; 1187525] {
include_bytes!("../archive/signer-3_14_0.wasm")
}

#[cfg(test)]
#[cfg(feature = "external-services-tests")]
mod tests {
Expand Down
2 changes: 1 addition & 1 deletion crates/contract/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const DEFAULT_VERIFIER_TERA_GAS: u64 = 200;
/// post-DCAP work (allowlist match, RTMR3 replay, app-compose validation, store).
const DEFAULT_RESOLVE_VERIFICATION_TERA_GAS: u64 = 60;
/// Default TTL after which a launcher image hash unused by any participant is evicted.
pub(crate) const DEFAULT_LAUNCHER_HASH_UNUSED_TTL_SECONDS: u64 = 14 * 24 * 60 * 60; // 14 days
pub(crate) const DEFAULT_LAUNCHER_HASH_UNUSED_TTL_SECONDS: u64 = 21 * 24 * 60 * 60; // 21 days

/// Config for V2 of the contract.
#[near(serializers=[borsh, json])]
Expand Down
79 changes: 76 additions & 3 deletions crates/contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub mod update;
#[cfg(feature = "dev-utils")]
pub mod utils;

pub mod v3_13_0_state;
pub mod v3_14_0_state;

#[cfg(feature = "bench-contract-methods")]
mod bench;
Expand Down Expand Up @@ -2125,11 +2125,11 @@ impl MpcContract {
pub fn migrate() -> Result<Self, Error> {
log!("migrating contract");

match try_state_read::<v3_13_0_state::MpcContract>() {
match try_state_read::<v3_14_0_state::MpcContract>() {
Ok(Some(state)) => return Ok(state.into()),
Ok(None) => return Err(InvalidState::ContractStateIsMissing.into()),
Err(err) => {
log!("failed to deserialize state into 3.13.0 state: {:?}", err);
log!("failed to deserialize state into 3.14.0 state: {:?}", err);
}
};

Expand Down Expand Up @@ -8513,4 +8513,77 @@ mod tests {
{WORST_CASE_ENTRY_COST_CEILING} at today's storage price"
);
}
#[test]
Comment on lines 8515 to +8516

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
#[test]
}
#[test]

fn migrate__should_extend_dstack_expiries_and_raise_the_launcher_ttl() {
// Given persisted 3.14.0 state holding a dstack and a mock attestation.
const EXPIRY_SECONDS: u64 = 1_000_000;
let mock = MpcMockAttestation::WithConstraints {
mpc_docker_image_hash: None,
launcher_docker_compose_hash: None,
expiry_timestamp_seconds: Some(EXPIRY_SECONDS),
expected_measurements: None,
};
let (mut contract, _, _, _) = setup_running_contract_with_domain(3, 2, 2);
contract.config.launcher_hash_unused_ttl_seconds = 1_209_600; // deployed 3.14.0 value
let dstack_key = insert_attestation(
&mut contract,
"dstack.near",
VerifiedAttestation::Dstack(ValidatedDstackAttestation {
mpc_image_hash: MAX_HASH.into(),
launcher_compose_hash: MAX_HASH.into(),
expiry_timestamp_seconds: EXPIRY_SECONDS,
measurements: default_measurements()[0],
}),
);
let mock_key = insert_attestation(
&mut contract,
"mock.near",
VerifiedAttestation::Mock(mock.clone()),
);
env::state_write(&contract);
drop(contract);

// When migrating, then writing state back as the `#[init]` wrapper does on chain.
let migrated = MpcContract::migrate().expect("3.14.0 state must migrate");
env::state_write(&migrated);
drop(migrated);

// Then the dstack expiry moved by one attestation window and the mock is untouched.
let reloaded = try_state_read::<MpcContract>().unwrap().unwrap();
let stored = &reloaded.tee_state.stored_attestations;
assert_matches!(
&stored.get(&dstack_key).unwrap().verified_attestation,
VerifiedAttestation::Dstack(dstack)
if dstack.expiry_timestamp_seconds
== EXPIRY_SECONDS
+ mpc_attestation::attestation::DEFAULT_EXPIRATION_DURATION_SECONDS
);
assert_matches!(
&stored.get(&mock_key).unwrap().verified_attestation,
VerifiedAttestation::Mock(stored) if *stored == mock
);

// And the launcher TTL was raised, keeping `Config::validate`'s invariant true.
assert_eq!(
reloaded.config.launcher_hash_unused_ttl_seconds,
config::DEFAULT_LAUNCHER_HASH_UNUSED_TTL_SECONDS
);
}

fn insert_attestation(
contract: &mut MpcContract,
account_id: &str,
verified_attestation: VerifiedAttestation,
) -> Ed25519PublicKey {
let node_id = create_node_id(&account_id.parse().unwrap(), &bogus_ed25519_public_key());
let tls_public_key = node_id.tls_public_key.clone();
contract.tee_state.stored_attestations.insert(
tls_public_key.clone(),
NodeAttestation {
node_id,
verified_attestation,
},
);
tls_public_key
}
}
5 changes: 0 additions & 5 deletions crates/contract/src/tee/proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -434,11 +434,6 @@ impl AllowedLauncherImages {
}
}

/// Migration constructor.
pub(crate) fn from_entries(entries: Vec<AllowedLauncherImage>) -> Self {
Self { entries }
}

/// Removes a launcher image hash and all its associated compose hashes.
/// Returns `false` if the launcher hash was not found or if removal would leave the list empty.
pub fn remove(&mut self, launcher_hash: &LauncherImageHash) -> bool {
Expand Down
28 changes: 28 additions & 0 deletions crates/contract/src/tee/tee_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,34 @@ impl TeeState {
removed
}

pub(crate) fn extend_dstack_attestation_expiries(&mut self, extension: Duration) {

@pbeza pbeza Aug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a TODO to remove this temporary method once the nodes’ firmware is upgraded and the issue is resolved? Though it’ll probably be hard to miss anyway, since Clippy should complain at that point.

let extension_seconds = extension.as_secs();

let dstack_tls_keys: Vec<Ed25519PublicKey> = self
.stored_attestations
.iter()
.filter(|(_, node_attestation)| {
matches!(
node_attestation.verified_attestation,
VerifiedAttestation::Dstack(_)
)
})
.map(|(tls_pk, _)| tls_pk.clone())
.collect();

for tls_pk in dstack_tls_keys {
let Some(node_attestation) = self.stored_attestations.get_mut(&tls_pk) else {
continue;
};
if let VerifiedAttestation::Dstack(dstack) = &mut node_attestation.verified_attestation
{
dstack.expiry_timestamp_seconds = dstack
.expiry_timestamp_seconds
.saturating_add(extension_seconds);
}
}
}

/// Returns the list of accounts that currently have TEE attestations stored.
/// Note: This may include accounts that are no longer active protocol participants.
pub fn get_tee_accounts(&self) -> Vec<NodeId> {
Expand Down
Loading
Loading