Skip to content
Merged
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
9 changes: 9 additions & 0 deletions audit-trail-move/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ cd audit-trail-move

The publish script prints `IOTA_AUDIT_TRAIL_PKG_ID` and, on `localnet`, also exports `IOTA_TF_COMPONENTS_PKG_ID`.

Upgrade the package for the active IOTA environment:

```bash
cd audit-trail-move
eval $(./scripts/upgrade_package.sh)
```

The upgrade script reads the active chain from the IOTA CLI, resolves the current Audit Trails package ID from `Move.lock`, finds the matching `UpgradeCap` in the active wallet, and prints the upgraded `IOTA_AUDIT_TRAIL_PKG_ID`.

The package history files [`Move.lock`](./Move.lock) and [`Move.history.json`](./Move.history.json) are used by the Rust crate to resolve and track deployed package versions.

## Related Libraries
Expand Down
13 changes: 13 additions & 0 deletions audit-trail-move/api_mapping.toml
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,19 @@ wasm = [
"WasmTrailRecords::add",
]

[audit_trails.main.correct_record]
rust = [
"CorrectRecord",
"CorrectRecord::new",
"TrailRecords::correct",
]
wasm = [
"WasmCorrectRecord",
"WasmCorrectRecord::build_programmable_transaction",
"WasmCorrectRecord::apply_with_events",
"WasmTrailRecords::correct",
]

[audit_trails.main.delete_record]
rust = [
"DeleteRecord",
Expand Down
78 changes: 78 additions & 0 deletions audit-trail-move/scripts/upgrade_package.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/bin/bash

# Copyright 2020-2026 IOTA Stiftung
# SPDX-License-Identifier: Apache-2.0

set -euo pipefail

script_dir=$(cd "$(dirname "$0")" && pwd)
package_dir="$script_dir/.."

active_env=$(iota client active-env --json | jq -r '.')
chain_id=$(iota client chain-identifier)

current_pkg_id=$(
toml get "$package_dir/Move.lock" env \
| jq -r --arg chain_id "$chain_id" '
map(values | select(."chain-id" == $chain_id) ."latest-published-id")
| first
'
)

if [[ -z "$current_pkg_id" || "$current_pkg_id" == "null" ]]; then
echo "failed to find current Audit Trails package ID for chain $chain_id in Move.lock" >&2
exit 1
fi

upgrade_cap_id=$(
iota client objects --json \
| jq -r --arg current_pkg_id "$current_pkg_id" '
map(
select(
.data.type == "0x2::package::UpgradeCap"
and .data.content.fields.package == $current_pkg_id
)
)
| first
| .data.objectId
'
)

if [[ -z "$upgrade_cap_id" || "$upgrade_cap_id" == "null" ]]; then
echo "failed to find UpgradeCap for Audit Trails package $current_pkg_id" >&2
exit 1
fi

upgrade_args=(
iota client upgrade
--upgrade-capability "$upgrade_cap_id"
--verify-compatibility
--silence-warnings
--json
--gas-budget 500000000
)

if [[ "$active_env" == "localnet" ]]; then
upgrade_args+=(--with-unpublished-dependencies)
fi

echo "upgrading Audit Trails package $current_pkg_id using UpgradeCap $upgrade_cap_id" >&2

response=$("${upgrade_args[@]}" "$package_dir")

audit_trail_package_id=$(
echo "$response" | jq -r '
.objectChanges[]
| select(.type == "published")
| .packageId
'
)

if [[ -z "$audit_trail_package_id" || "$audit_trail_package_id" == "null" ]]; then
echo "$response" >&2
echo "failed to extract upgraded Audit Trails package ID from upgrade response" >&2
exit 1
fi

export IOTA_AUDIT_TRAIL_PKG_ID="$audit_trail_package_id"
printf 'export IOTA_AUDIT_TRAIL_PKG_ID=%s\n' "$IOTA_AUDIT_TRAIL_PKG_ID"
104 changes: 103 additions & 1 deletion audit-trail-move/sources/audit_trail.move
Comment thread
itsyaasir marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use audit_trails::{
record::{Self, Record, InitialRecord},
record_tags::{Self, RoleTags, TagRegistry}
};
use iota::{clock::{Self, Clock}, event, linked_table::{Self, LinkedTable}, vec_set::VecSet};
use iota::{clock::{Self, Clock}, event, linked_table::{Self, LinkedTable}, vec_set::{Self, VecSet}};
use std::string::String;
use tf_components::{capability::Capability, role_map::{Self, RoleMap}, timelock::TimeLock};

Expand Down Expand Up @@ -51,6 +51,8 @@ const ERecordTagAlreadyDefined: vector<u8> =
#[error]
const ERecordTagInUse: vector<u8> =
b"The requested tag cannot be removed because it is already used by an existing record or role";
#[error]
const ERecordAlreadyReplaced: vector<u8> = b"The record has already been replaced";

// ===== Constants =====

Expand Down Expand Up @@ -533,6 +535,106 @@ public fun add_record<D: store + copy>(
output
}

/// Adds a correction record that supersedes an existing record.
///
/// The original record remains immutable. The new correction record is appended
/// at the next sequence number with a correction tracker whose `replaces` set
/// contains `sequence_number`. The replaced record receives an `is_replaced_by`
/// back-pointer to the new correction so clients can resolve the current
/// canonical record by following the replacement chain.
///
/// Requires a capability granting the `CorrectRecord` permission. When either
/// the replaced record or the new correction record carries a tag, that same
/// capability must also allow the corresponding tag.
///
/// Aborts with:
/// * `EPackageVersionMismatch` when the trail is at a different package version.
/// * any error documented by `RoleMap::assert_capability_valid` when `cap` fails
/// authorization checks.
/// * `ETrailWriteLocked` while `write_lock` is active.
/// * `ERecordNotFound` when no record exists at `sequence_number`.
/// * `ERecordAlreadyReplaced` when `sequence_number` already points to a newer
/// correction.
/// * `ERecordTagNotDefined` when `record_tag` is not in the trail's tag registry.
/// * `ERecordTagNotAllowed` when `cap`'s role does not allow the replaced
/// record tag or the new correction tag.
///
/// Emits a `RecordAdded` event for the correction record on success.
///
/// Returns the same receipt that is emitted as the `RecordAdded` event.
public fun correct_record<D: store + copy>(
Comment thread
itsyaasir marked this conversation as resolved.
self: &mut AuditTrail<D>,
cap: &Capability,
sequence_number: u64,
stored_data: D,
record_metadata: Option<String>,
record_tag: Option<String>,
clock: &Clock,
ctx: &mut TxContext,
): RecordAdded {
assert!(self.version == PACKAGE_VERSION, EPackageVersionMismatch);
self
.roles
.assert_capability_valid(
cap,
&permission::correct_record(),
clock,
ctx,
);
assert!(!self.locking_config.is_write_locked(clock), ETrailWriteLocked);
assert!(self.records.contains(sequence_number), ERecordNotFound);
assert!(
!self.records.borrow(sequence_number).correction().is_replaced(),
ERecordAlreadyReplaced,
);
assert!(
is_record_tag_allowed(
self,
cap,
self.records.borrow(sequence_number).tag(),
),
ERecordTagNotAllowed,
);
assert!(is_record_tag_allowed(self, cap, &record_tag), ERecordTagNotAllowed);

let caller = ctx.sender();
let timestamp = clock.timestamp_ms();
let trail_id = self.id();
let seq = self.sequence_number;

if (record_tag.is_some()) {
self.tags.increment_usage_count(record_tag.borrow());
};

let mut replaces = vec_set::empty();
replaces.insert(sequence_number);

let correction = record::new(
stored_data,
record_metadata,
record_tag,
seq,
caller,
timestamp,
record::with_replaces(replaces),
);

self.records.borrow_mut(sequence_number).correction_mut().set_replaced_by(seq);

linked_table::push_back(&mut self.records, seq, correction);
self.sequence_number = self.sequence_number + 1;

let output = RecordAdded {
trail_id,
sequence_number: seq,
added_by: caller,
timestamp,
};

event::emit(copy output);
output
}

/// Deletes the record at `sequence_number` from the trail.
///
/// When the deleted record carries a tag, the trail's tag-registry usage count for
Expand Down
5 changes: 5 additions & 0 deletions audit-trail-move/sources/record.move
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ public fun correction<D: store + copy>(self: &Record<D>): &RecordCorrection {
&self.correction
}

/// Returns a mutable reference to the record's bidirectional correction tracker.
public(package) fun correction_mut<D: store + copy>(self: &mut Record<D>): &mut RecordCorrection {
&mut self.correction
}

/// Destroys a `Record` by destructuring it.
public(package) fun destroy<D: store + copy + drop>(self: Record<D>) {
let Record {
Expand Down
Loading
Loading