## ----setup, include = FALSE--------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE ) ## ----connection_setup--------------------------------------------------------- # library(multichainr) # # # 1.1.1 Path Configuration # # Specify the directory containing MultiChain executables (multichaind, multichain-util). # # This ensures the R session can interact with the underlying blockchain engine. # mc_set_path(Sys.getenv("MULTICHAIN_PATH")) # # # 1.1.2 Unique Chain Initialization # # We generate a unique name for our pharmaceutical ledger to avoid collision # # with existing validated environments. # chain_name <- paste0("pharma_chain_", round(as.numeric(Sys.time()))) # # # Create the blockchain configuration and start the node as a background daemon. # mc_node_init(chain_name) # mc_node_start(chain_name) # # # 1.1.3 Establishing the communication link # # mc_get_config reads the automatically generated 'multichain.conf' to retrieve # # the RPC username, password, and port required for authentication. # conf <- mc_get_config(chain_name) # conn <- mc_connect(conf) # # # 1.1.4 Verification of Initialization Status # # We must confirm the node is fully initialized before proceeding with # # regulatory data entries. # status <- mc_get_init_status(conn) # if (status$initialized) { # message("Infrastructure Status: Blockchain network is fully initialized and ready for GxP operations.") # } ## ----departmental_identities-------------------------------------------------- # # 1.2.1 Quality Assurance (QA) Address # # The QA department acts as the 'Document Owner' and 'System Administrator'. # addr_qa <- mc_get_new_address(conn) # # # 1.2.2 Production Department Address # # The Production unit represents the 'Data Consumers' who execute the SOPs. # addr_prod <- mc_get_new_address(conn) # # # 1.2.3 Regulatory Auditor Address # # Represents an external inspector (e.g., FDA or EMA) who requires read-only access. # addr_auditor <- mc_get_new_address(conn) # # cat("QA Department Digital Signature Address: ", addr_qa, "\n") # cat("Production Department Digital Identity: ", addr_prod, "\n") # cat("External Regulatory Auditor Access Point: ", addr_auditor, "\n") ## ----permission_governance---------------------------------------------------- # # 1.3.1 Identify the Master Administrator # # At genesis, the address that initialized the chain holds 'admin' rights. # admin_perms <- mc_list_permissions(conn, "admin") # master_admin <- admin_perms$address[1] # # # 1.3.2 QA Privileges: FULL MANAGEMENT # # QA is granted 'send' (publish data), 'receive' (interact), and 'create' (spawn new registries). # mc_grant(conn, addr_qa, "send,receive,create") # # # 1.3.3 Production Privileges: RESTRICTED ACCESS # # Production is restricted to 'receive' only, preventing them from # # tampering with the Master Document Registry. # mc_grant(conn, addr_prod, "receive") # # # 1.3.4 Auditor Privileges: READ-ONLY OBSERVATION # # Auditors are granted 'receive' to monitor transactions without # # having authority to alter the ledger. # mc_grant(conn, addr_auditor, "receive") ## ----verification------------------------------------------------------------- # # 1.4.1 Logical Verification # # Ensure that the Change Control process can be initiated by QA but not by unauthorized parties. # can_qa_publish <- mc_verify_permission(conn, addr_qa, "send") # can_prod_read <- mc_verify_permission(conn, addr_prod, "receive") # # if (can_qa_publish && can_prod_read) { # message("Quality Control: RBAC model successfully deployed.") # } else { # # If the permissions are not correctly set, we stop the workflow to prevent compliance breaches. # stop("Regulatory Alert: Permission mismatch detected. The validated session cannot continue.") # } # # # 1.4.2 Documenting the Initial State # # We pull a full list of active permissions to be included in the validation report. # audit_trail_permissions <- mc_list_permissions(conn) # print(audit_trail_permissions) ## ----registry_initialization-------------------------------------------------- # # 2.1.1 Define Registry Metadata # # These fields provide context for the entire SOP ledger, such as the facility # # location and the governing quality standards. # registry_metadata <- list( # site_id = "PLANT-01", # quality_standard = "ISO-9001:2015 / GMP", # department_owner = "Quality Assurance" # ) # # # 2.1.2 Create a Restricted Stream # # 'open = FALSE' is critical: it prevents the 'Production' department or # # any guest from writing to this stream. Only QA will be granted 'write' access. # tx_stream_id <- mc_create_stream_from( # conn, # from_address = addr_qa, # name = "SOP_Registry", # open = FALSE, # Enforce strict write-access control # custom_fields = registry_metadata # ) # # message("Registry Status: Master SOP Stream created. Transaction ID: ", tx_stream_id) # # # 2.1.3 Ensure Data Persistence # # We block execution until the registry creation is confirmed in a block. # # This ensures that the registry exists before we attempt to grant write permissions. # mc_wait_for_confirmation(conn, tx_stream_id) ## ----inspect_registry--------------------------------------------------------- # # Retrieve detailed technical metadata about the "SOP_Registry" # sop_registry_info <- mc_get_stream_info(conn, "SOP_Registry", verbose = TRUE) # # # The 'createtxid' acts as the permanent digital fingerprint of this registry. # print(sop_registry_info) # # # The 'restrict' field in the output confirms that 'write' access is NOT public. ## ----list_registries---------------------------------------------------------- # all_registries <- mc_list_streams(conn) # # # Displaying all active document registries currently hosted on this node. # print(all_registries[, c("name", "createtxid", "subscribed")]) ## ----synchronize_registry----------------------------------------------------- # # 2.4.1 Activate Indexing # # Subscribing tells the node to start building a local database of the stream's contents. # # 'rescan = TRUE' ensures that even if we joined the network late, we download all # # historical SOP records from the genesis block. # mc_subscribe(conn, "SOP_Registry", rescan = TRUE) # # # 2.4.2 Verify Synchronization Status # updated_list <- mc_list_streams(conn, "SOP_Registry") # if (updated_list$subscribed[1]) { # message("Compliance Sync: Node is now actively indexing the SOP_Registry.") # } ## ----authorize_qa------------------------------------------------------------- # # 2.5.1 Grant Write Access # # We use the master_admin (from Section 1) to authorize the QA address. # mc_grant(conn, addr_qa, "SOP_Registry.write") # # # 2.5.2 Operation Verification Check # # Perform a final check before opening the registry for data entry. # if (mc_verify_permission(conn, addr_qa, "SOP_Registry.write")) { # message("GxP Readiness: QA Department successfully authorized to manage SOP records.") # } ## ----publishing_metadata------------------------------------------------------ # # 3.1.1 Define SOP Attributes # # We define the document key (SOP-QC-001) and its regulatory attributes. # sop_key <- "SOP-QC-001" # sop_metadata <- list( # json = list( # title = "Standard Procedure for Raw Material Sampling", # version = "1.0", # file_hash = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", # format = "PDF", # classification = "Restricted" # ) # ) # # # 3.1.2 Attributable Publication # # By using mc_publish_from with addr_qa, we satisfy the 'Attributable' requirement. # # The blockchain will forever link this record to the QA Department's digital identity. # tx_pub_1 <- mc_publish_from(conn, addr_qa, "SOP_Registry", sop_key, sop_metadata) # # message("Audit Trail: SOP-QC-001 version 1.0 registered. Transaction ID: ", tx_pub_1) ## ----atomic_publishing-------------------------------------------------------- # # 3.2.1 Define Linked Records # # We bundle the SOP registration and the Personnel Assignment into a single list. # multi_items <- list( # # Item 1: The document metadata # list( # key = "SOP-PROD-002", # data = list(json = list(title = "Cleaning Validation", version = "2.0")) # ), # # Item 2: The assignment of responsibility # list( # key = "ASSIGNMENT-PROD-002", # data = list(json = list(assignee = "Production_Manager_01", role = "Custodian")) # ) # ) # # # 3.2.2 Atomic Execution # # mc_publish_multi_from ensures that both items share the exact same Block Time and TXID. # tx_pub_multi <- mc_publish_multi_from(conn, addr_qa, "SOP_Registry", multi_items) # # message("Compliance: Atomic SOP registration and Custodian assignment complete. TXID: ", tx_pub_multi) ## ----binary_cache_workflow---------------------------------------------------- # # 3.3.1 Initialize Cache Item # # Create a temporary, unique staging identifier on the MultiChain node. # cache_id <- mc_create_binary_cache(conn) # # # 3.3.2 Data Upload (Staging) # # Simulate a large manufacturing specification (e.g., detailed equipment settings). # large_spec_content <- paste0("MBR_START_", paste(rep("DATA_SEGMENT_", 1000), collapse=""), "_MBR_END") # # # Upload the data to the node's local cache. # # The function returns the total size in bytes once the upload is confirmed. # actual_size <- mc_append_binary_cache(conn, cache_id, list(text = large_spec_content)) # message("System: Large document staged in binary cache. Size: ", actual_size, " bytes.") # # # 3.3.3 Commit Staged Data to Blockchain # # We publish a reference to the cache item. The node will automatically # # retrieve the binary data and wrap it into a permanent transaction. # sop_large_key <- "SOP-TECH-003" # tx_pub_cache <- mc_publish_from(conn, addr_qa, "SOP_Registry", # sop_large_key, # list(cacheitem = cache_id)) # # # 3.3.4 Buffer Cleanup # # Once the data is recorded in the blockchain, the temporary cache item is deleted # # to maintain node hygiene and storage efficiency. # mc_delete_binary_cache(conn, cache_id) # # message("Data Integrity: Technical Dossier SOP-TECH-003 committed to ledger. TXID: ", tx_pub_cache) ## ----verify_publishing-------------------------------------------------------- # # 3.4.1 Wait for Confirmation # # Ensure the last transaction is included in a block and confirmed by the network validators. # mc_wait_for_confirmation(conn, tx_pub_cache) # # # 3.4.2 Retrieve and Verify Metadata # # We use mc_get_stream_item to pull the original JSON record by its TXID. # item_info <- mc_get_stream_item(conn, "SOP_Registry", tx_pub_1) # # # Output the verified data to show that the 'Source' remains unchanged. # cat("--- VERIFIED SOP RECORD ---\n") # cat("SOP Identifier: ", item_info$key[[1]], "\n") # cat("Approved Title: ", item_info$data$json$title, "\n") # cat("Document Hash: ", item_info$data$json$file_hash, "\n") # cat("Time of Entry: ", as.character(as.POSIXct(item_info$blocktime, origin="1970-01-01")), "\n") ## ----digital_signing---------------------------------------------------------- # # 4.1.1 Define the Approval Target # # We use the SHA-256 hash of the document (from Step 3) as the "message". # # This ensures that even a single-character change in the PDF would invalidate the signature. # doc_hash <- "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" # # # 4.1.2 Apply Cryptographic Signature # # mc_sign_message uses the private key associated with addr_qa to sign the hash. # # This results in a Base64-encoded signature string. # qa_signature <- mc_sign_message(conn, addr_qa, doc_hash) # # cat("Regulatory Status: SOP-QC-001 has been digitally signed by QA.\n") # cat("Approval Signature: ", qa_signature, "\n") ## ----signature_verification--------------------------------------------------- # # 4.2.1 Perform Independent Audit # # The auditor runs mc_verify_message. The blockchain logic returns TRUE # # only if the signature was indeed generated by the QA address for this specific hash. # is_legit <- mc_verify_message(conn, addr_qa, qa_signature, doc_hash) # # if (is_legit) { # message("Audit Success: Signature is authentic. Document integrity and authorship verified.") # } else { # # In a real environment, this would trigger a major compliance investigation. # stop("Compliance Alert: Digital signature verification failed! Data integrity breach.") # } ## ----training_tokens---------------------------------------------------------- # # 4.3.1 Define the Competency Asset # training_asset_name <- "TRAIN-QC-001" # # # 4.3.2 Metadata-Enriched Permit # # We attach training-specific context to the asset itself, fulfilling ALCOA+ # # requirements for comprehensive documentation. # cert_metadata <- list( # sop_reference = "SOP-QC-001", # trainer = "Lead_Pharmacist_01", # training_method = "Read and Understand", # expiry_date = "2027-04-03" # ) # # # 4.3.3 Distribute the Permit # # We issue 1 unit of the permit directly to the Production Department. # tx_issue_token <- mc_issue( # conn, # address = addr_prod, # name = training_asset_name, # quantity = 1, # One permit per training session # units = 1, # Asset is non-divisible # custom_fields = cert_metadata # ) # # message("Learning Management: Training permit issued to Production. TXID: ", tx_issue_token) # mc_wait_for_confirmation(conn, tx_issue_token) ## ----access_control----------------------------------------------------------- # # 4.4.1 Query Active Qualifications # # We check the balances for the Production Department's wallet. # prod_balances <- mc_get_address_balances(conn, addr_prod) # # # 4.4.2 Decision Logic # if (any(prod_balances$name == training_asset_name)) { # current_token <- prod_balances[prod_balances$name == training_asset_name, ] # message("Access Control: Production Department is QUALIFIED for this operation.") # # # Display the token details for the operator # print(current_token[, c("name", "qty")]) # } else { # stop("Access Denied: Required training permit not found in the department wallet.") # } ## ----validation_library------------------------------------------------------- # # 5.1.1 Define Reusable JavaScript Logic # # This function uses a Regular Expression to ensure the ID starts with 'SOP-' # # followed by numeric digits. # lib_js <- " # function isValidSopFormat(key) { # var regex = /^SOP-\\d+$/; # return regex.test(key); # } # " # # # 5.1.2 Deploy the Library # # We use 'instant' update mode so the logic is available immediately for our demo. # tx_lib <- mc_create_library(conn, "PharmaUtils", updatemode = "instant", js_code = lib_js) # message("System: Validation library 'PharmaUtils' deployed. TXID: ", tx_lib) ## ----filter_simulation-------------------------------------------------------- # # 5.2.1 Define Test Logic # # In this simulation, we check for a specific prefix. # test_filter_js <- " # function filterstreamitem() { # var item = getfilterstreamitem(); # var primaryKey = item.keys[0]; # # if (primaryKey.indexOf('SOP-') !== 0) { # return 'Invalid Prefix: ' + primaryKey; // Returning a string REJECTS the TX # } # return null; // Returning null ACCEPTS the TX # } # " # # # 5.2.2 Run Simulation # # We test the logic against tx_pub_1 (our valid record from Step 3). # sim_res <- mc_test_stream_filter(conn, list(libraries = list()), # test_filter_js, tx = tx_pub_1, vout = 0) # # # 5.2.3 Interpret Simulation Results # if (sim_res$compiled) { # message("CSV Verification: JavaScript logic is syntactically valid.") # } # # if (sim_res$passed) { # message("Simulation Result: The transaction was ACCEPTED (Compliant data).") # } else { # cat("Simulation Result: REJECTED. Reason given by system:", sim_res$reason, "\n") # } ## ----production_filter-------------------------------------------------------- # # 5.3.1 Define Complex Multi-Step Validation # filter_js <- " # function filterstreamitem() { # var item = getfilterstreamitem(); # var primaryKey = item.keys[0]; # # // Rule 1: Check ID format using the PharmaUtils library # if (!isValidSopFormat(primaryKey)) { # return 'Rejected: Key ' + primaryKey + ' does not match SOP-XXX format.'; # } # # // Rule 2: Enforce Version Traceability (Change Control) # var data = item.data.json; # if (data && data.is_update === true && !data.prev_version_txid) { # return 'Rejected: SOP update must contain a reference to the previous version TXID.'; # } # # return null; // The record is GxP compliant # } # " # # # 5.3.2 Global Filter Creation # # We create the filter and declare its dependency on the 'PharmaUtils' library. # tx_filter <- mc_create_stream_filter(conn, "SopChainValidator", # options = list(libraries = list("PharmaUtils")), # js_code = filter_js) # # message("Compliance Engineering: Stream filter created. TXID: ", tx_filter) # mc_wait_for_confirmation(conn, tx_filter) ## ----filter_activation-------------------------------------------------------- # # 5.4.1 Grant Stream-Level Admin Rights # # The master administrator must be authorized to manage this specific registry. # grant_admin_tx <- mc_grant(conn, master_admin, "SOP_Registry.admin") # mc_wait_for_confirmation(conn, grant_admin_tx) # # # 5.4.2 Official Attachment # # Attach the 'SopChainValidator' logic specifically to the 'SOP_Registry' stream. # attachment_logic <- list("for" = "SOP_Registry", approve = TRUE) # # tx_appr <- mc_approve_from(conn, master_admin, "SopChainValidator", # approve = attachment_logic) # # message("Regulatory Governance: Filter officially attached to SOP_Registry by Admin.") # mc_wait_for_confirmation(conn, tx_appr) ## ----negative_test------------------------------------------------------------ # bad_sop_data <- list(json = list(title = "Non-compliant entry", version = "2.0")) # # tryCatch({ # # Attempting to publish with a bad key 'sop-123' # mc_publish_from(conn, addr_qa, "SOP_Registry", "sop-123", bad_sop_data) # }, error = function(e) { # # The blockchain rejects the transaction and R captures the error message # message("Success: The Smart Filter blocked the non-compliant entry!") # cat("Blockchain Error Message: ", e$message, "\n") # }) ## ----positive_test------------------------------------------------------------ # good_sop_data <- list( # json = list( # title = "Validated Sampling Plan", # version = "1.0", # is_update = FALSE # Satisfies the 'no previous version required' logic # ) # ) # # # This transaction passes the format check and the change control check. # tx_good <- mc_publish_from(conn, addr_qa, "SOP_Registry", "SOP-999", good_sop_data) # message("Compliance: Valid record accepted by the ledger. TXID: ", tx_good) ## ----create_vmp_variable------------------------------------------------------ # # 6.1.1 Define the Variable Name # # The VMP ID is a cornerstone of pharmaceutical facility management. # vmp_name <- "Current_VMP_ID" # # # 6.1.2 Create the Variable # # Protocol Note: We set open = TRUE as required by the MultiChain 2.x protocol. # # Security Note: Access is still controlled; only nodes with 'create' # # permissions can initialize these global objects. # tx_var_create <- mc_create_variable( # conn, # name = vmp_name, # open = TRUE, # value = "VMP-2026-SITE01" # ) # # message("QMS Initialization: Global VMP Variable created. TXID: ", tx_var_create) # # # Standard GxP practice: wait for confirmation before attempting to use the new object. # mc_wait_for_confirmation(conn, tx_var_create) ## ----system_status_tracking--------------------------------------------------- # # 6.2.1 Initialize Operational Status # status_var <- "QMS_Operational_Status" # tx_status_init <- mc_create_variable(conn, status_var, open = TRUE, value = "Normal Operations") # mc_wait_for_confirmation(conn, tx_status_init) # Brief pause for ledger indexing # # # 6.2.2 Update State (Contemporaneous Documentation) # # Scenario: An FDA inspector arrives at the site. We update the global state. # # This creates a permanent record of the exact moment the audit mode began. # tx_var_update <- mc_set_variable_value(conn, status_var, value = "FDA Audit in Progress") # # message("Compliance Event: System status updated to 'FDA Audit in Progress'. TXID: ", tx_var_update) ## ----variable_history--------------------------------------------------------- # # 6.3.1 Retrieve Latest State # current_status <- mc_get_variable_value(conn, status_var) # cat("Current Validated Status: ", current_status, "\n") # # # 6.3.2 Extract Historical Audit Trail # # 'verbose = TRUE' is used to see the 'writers' (addresses) and timestamps. # status_history <- mc_get_variable_history(conn, status_var, verbose = TRUE) # # # We display the 'writers' column to prove who authorized each state change. # print(status_history[, c("blocktime", "writers", "value")]) ## ----node_configuration------------------------------------------------------- # # 6.4.1 Set Technical Limits # # We adjust 'maxshowndata' to 5001 bytes. This ensures that any technical # # metadata displayed in our QMS dashboard is truncated to a manageable size, # # preventing log-flooding while maintaining the integrity of the underlying data. # mc_set_runtime_param(conn, "maxshowndata", 5001) # # message("IT Governance: Node runtime parameter 'maxshowndata' enforced.") # # # 6.4.2 Verify Active Parameters # # It is vital to document that the technical controls are active. # runtime_params <- mc_get_runtime_params(conn) # cat("Active System Limit (maxshowndata): ", runtime_params$maxshowndata, " bytes.\n") ## ----system_snapshot---------------------------------------------------------- # # 6.5.1 Capture Environment Metadata # node_info <- mc_get_info(conn) # # cat("--- VALIDATED INFRASTRUCTURE SNAPSHOT ---\n") # cat("Software Version: ", node_info$version, "\n") # cat("Blockchain Height: ", node_info$blocks, "\n") # cat("Network Node ID: ", node_info$nodeaddress, "\n") ## ----exchange_setup----------------------------------------------------------- # # 7.1.1 Define Permit Version Names # # In a GxP system, these represent specific training qualifications. # token_v1 <- "TRAIN-QC-003-V1" # token_v2 <- "TRAIN-QC-003-V2" # # # 7.1.2 Initial Distribution (Baseline) # # Production holds the old version that needs to be surrendered. # tx_v1 <- mc_issue(conn, addr_prod, token_v1, 1) # # QA holds the new approved version ready for distribution. # tx_v2 <- mc_issue(conn, addr_qa, token_v2, 1) # # # Ensure the ledger reflects these issuances before attempting the swap. # mc_wait_for_confirmation(conn, tx_v1) # mc_wait_for_confirmation(conn, tx_v2) # # message("Pre-requisite: Superseded and Active permits confirmed on-chain.") ## ----qa_locking--------------------------------------------------------------- # # 7.2.1 Define the isolated quantity # # We must use dynamic list assignment in R to ensure the variable evaluates # # to the asset name string "TRAIN-QC-003-V2". # amounts_v2 <- list() # amounts_v2[[token_v2]] <- 1 # # # 7.2.2 Isolate the asset (Locking) # # mc_prepare_lock_unspent_from moves the asset into a 'reserved' state. # locked_v2 <- mc_prepare_lock_unspent_from( # conn, # from_address = addr_qa, # amounts = amounts_v2 # ) # # message("Quality Assurance: New permit (V2) isolated and locked for issuance.") ## ----exchange_offer----------------------------------------------------------- # # 7.3.1 Define the 'Return' requirement # # QA requests exactly 1 unit of the superseded version (V1) in return. # amounts_v1_req <- list() # amounts_v1_req[[token_v1]] <- 1 # # # 7.3.2 Construct the Offer # # This returns a raw hexadecimal string representing a 'Proposal'. # offer_hex <- mc_create_raw_exchange( # conn, # txid = locked_v2$txid, # vout = locked_v2$vout, # amounts = amounts_v1_req # ) # # cat("System: Atomic Exchange Offer generated (Partial Hex created).\n") ## ----production_surrender----------------------------------------------------- # # 7.4.1 Grant Surrender Authorization # # To return a permit, the address must have 'send' and 'receive' permissions. # grant_tx <- mc_grant(conn, addr_prod, "send,receive") # mc_wait_for_confirmation(conn, grant_tx) # # # 7.4.2 Isolate the Superseded Asset # amounts_v1_own <- list() # amounts_v1_own[[token_v1]] <- 1 # # # Production locks their V1 permit to signify readiness for the swap. # locked_v1 <- mc_prepare_lock_unspent_from( # conn, # from_address = addr_prod, # amounts = amounts_v1_own # ) # # message("Production: Superseded permit (V1) locked and ready for decommissioning.") ## ----finalize_exchange-------------------------------------------------------- # # 7.5.1 Define the Receipt # # Production confirms they are receiving 1 unit of V2. # amounts_v2_receive <- list() # amounts_v2_receive[[token_v2]] <- 1 # # # 7.5.2 Complete the Multi-Party Transaction # # mc_complete_raw_exchange merges QA's offer with Production's acceptance. # # We include a JSON metadata object for the Audit Trail. # final_tx_hex <- mc_complete_raw_exchange( # conn, # tx_hex = offer_hex, # txid = locked_v1$txid, # vout = locked_v1$vout, # amounts = amounts_v2_receive, # data = list( # change_control_id = "CC-2026-0045", # rationale = "Replacing V1 with V2 due to annual document review." # ) # ) # # message("Compliance: Atomic exchange finalized and ready for network commitment.") ## ----broadcast_exchange------------------------------------------------------- # # 7.6.1 Broadcast the Final Transaction # swap_txid <- mc_send_raw_transaction(conn, final_tx_hex) # # # 7.6.2 Confirm Perpetual Record # mc_wait_for_confirmation(conn, swap_txid) # # message("Data Integrity: SOP Version Exchange complete! Transaction ID: ", swap_txid) # # # 7.6.3 Audit Verification # # Verification of final balances to prove compliance # prod_bal <- mc_get_address_balances(conn, addr_prod) # if (token_v2 %in% prod_bal$name && !(token_v1 %in% prod_bal$name)) { # message("Regulatory Verification: Change Control successfully enforced. V1 removed, V2 active.") # } ## ----blockchain_integrity----------------------------------------------------- # # 8.1.1 Retrieve Global Ledger Status # # Get the current technical state of the blockchain. # chain_info <- mc_get_blockchain_info(conn) # cat("Current Facility Ledger Height (Total Blocks): ", chain_info$blocks, "\n") # # # 8.1.2 Locate a Specific Record in the "Vault" # # We pick our initial SOP registration (tx_pub_1 from Step 3). # tx_audit_info <- mc_get_wallet_transaction(conn, tx_pub_1) # block_height <- tx_audit_info$blockheight # # # 8.1.3 Retrieve the Raw Block Data # # We extract the specific block that "sealed" our SOP record. # sop_block <- mc_get_block(conn, block_height) # # # This hash is the unique, immutable fingerprint of the entire block. # cat("Regulatory Proof: SOP-QC-001 is sealed in Block Hash: ", sop_block$hash, "\n") # # Documenting the exact time the record became official. # cat("Block Timestamp (Contemporaneous Entry): ", # as.character(as.POSIXct(sop_block$time, origin="1970-01-01")), "\n") ## ----master_list-------------------------------------------------------------- # # 8.2.1 Generate the Document Inventory # # This acts as the "Table of Contents" for the SOP Registry. # sop_registry_list <- mc_list_stream_keys(conn, "SOP_Registry") # # # We display the unique document IDs and the number of versions (items) for each. # # 'confirmed' shows how many have been finalized in the blockchain. # print(sop_registry_list[, c("key", "items", "confirmed")]) ## ----document_traceability---------------------------------------------------- # # 8.3.1 Extract the Full Lifecycle of a specific SOP # # mc_list_stream_key_items retrieves every transaction associated with this key. # sop_history <- mc_list_stream_key_items(conn, "SOP_Registry", "SOP-QC-001") # # # 8.3.2 Display the Audit Trail # # We format the blocktime to a human-readable date for the audit report. # sop_history$timestamp <- as.POSIXct(sop_history$blocktime, origin="1970-01-01") # # # The 'publishers' column proves 'Who', 'timestamp' proves 'When', and 'txid' is the 'Proof'. # print(sop_history[, c("timestamp", "txid", "publishers")]) ## ----single_source_truth------------------------------------------------------ # # 8.4.1 Aggregate the Latest Metadata # # This function collapses all versions into one "Current Valid State" object. # current_sop_state <- mc_get_stream_key_summary(conn, "SOP_Registry", "SOP-QC-001") # # cat("--- CURRENT EFFECTIVE SOP DASHBOARD ---\n") # cat("SOP Title: ", current_sop_state$title, "\n") # cat("Effective Version: ", current_sop_state$version, "\n") # cat("Validated File Hash: ", current_sop_state$file_hash, "\n") # # This data can be used to verify the physical PDF file before a production run. ## ----publisher_audit---------------------------------------------------------- # # 8.5.1 Generate Action Report for QA Department # # We list every record ever published by the QA address (addr_qa). # qa_audit_trail <- mc_list_stream_publisher_items(conn, "SOP_Registry", addr_qa) # # message("Compliance Report: Actions performed by QA Department (", addr_qa, "):") # cat("Total regulatory records initiated: ", nrow(qa_audit_trail), "\n") # # # Provide the most recent 5 actions for quick review # qa_audit_trail$date <- as.POSIXct(qa_audit_trail$blocktime, origin="1970-01-01") # print(head(qa_audit_trail[, c("date", "keys", "txid")], 5)) ## ----system_oversight--------------------------------------------------------- # # 8.6.1 Retrieve Global Chain Totals # # This provides a snapshot of the volume of data in the QMS. # totals <- mc_get_chain_totals(conn) # # cat("--- FACILITY QMS OVERVIEW ---\n") # cat("Total SOP Registry Entries: ", totals$streams, "\n") # cat("Total Digital Permits Issued: ", totals$assets, "\n") # cat("Total Authorized Addresses: ", length(mc_get_addresses(conn)), "\n") ## ----discovery---------------------------------------------------------------- # # 9.1.1 Scan Departmental Inventory # # We retrieve all unspent transaction outputs (UTXOs) for the Production address. # unspent_prod <- mc_list_unspent(conn, addresses = addr_prod) # # # 9.1.2 Automated Asset Detection # # We search the list-columns for any asset starting with "TRAIN" (our permit prefix). # # This logic ensures the workflow is robust against naming variations in versioning. # discovery <- sapply(unspent_prod$assets, function(row_assets) { # if (length(row_assets) == 0) return(NA) # names_in_row <- sapply(row_assets, function(a) a$name) # match <- names_in_row[grepl("^TRAIN", names_in_row)][1] # return(match) # }) # # # Identify the exact record for the manual archive pipeline # target_row_idx <- which(!is.na(discovery))[1] # target_asset <- discovery[target_row_idx] # target_utxo <- unspent_prod[target_row_idx, ] # # # 9.1.3 Extract Precision Data # # We extract the exact quantity using unlist() to ensure we have a plain numeric value. # # This prevents type-mismatch errors during the raw transaction construction. # row_assets_list <- target_utxo$assets[[1]][[1]] # target_qty <- as.numeric(unlist(row_assets_list$qty[row_assets_list$name == target_asset])) # # message("Inventory Management: Discovered active permit '", target_asset, "' (Qty: ", target_qty, ")") # # # Prepare the blockchain input (UTXO pointer) # inputs <- list(list( # txid = target_utxo$txid, # vout = as.integer(target_utxo$vout) # )) ## ----manual_construction------------------------------------------------------ # # 9.2.1 Initialize Secure Archive Address # archive_addr <- mc_get_new_address(conn) # # # 9.2.2 Regulatory Authorization (The Receive Grant) # # No address can hold assets without an explicit grant, satisfying CFR 21 Part 11 # # requirements for authorized system access. # grant_tx <- mc_grant(conn, archive_addr, "receive") # mc_wait_for_confirmation(conn, grant_tx) # # # 9.2.3 Construct the Balanced Output # # We map the archive address to the exact asset quantity found in Step 9.1. # outputs <- list() # asset_movement <- list() # asset_movement[[target_asset]] <- target_qty # outputs[[archive_addr]] <- asset_movement # # # 9.2.4 Generate the Raw Hex # # This creates a "Draft" transaction that exists only in our R session memory. # raw_tx_hex <- mc_create_raw_transaction(conn, inputs, outputs) # # message("GxP Engineering: Draft transaction hex constructed for ", target_asset) ## ----data_enrichment---------------------------------------------------------- # # 9.3.1 Define Compliance Metadata # validation_metadata <- list( # validation_run_id = "VAL-RUN-2026-099", # inspector_comment = "Archiving permit due to site-wide software update." # ) # # # 9.3.2 Enforce the Audit Trail # # We append this data to the existing raw hex. The transaction now carries # # both the asset transfer logic and the regulatory justification. # raw_tx_hex <- mc_append_raw_data(conn, raw_tx_hex, validation_metadata) # # message("Compliance: Validation metadata successfully embedded into the draft.") ## ----technical_audit---------------------------------------------------------- # # 9.4.1 Transparent Inspection # # We convert the cryptic hex back into a human-readable R list. # decoded_tx <- mc_decode_raw_transaction(conn, raw_tx_hex) # # # 9.4.2 Verification of Intent # cat("--- PRE-SIGNING REGULATORY INSPECTION ---\n") # cat("Target Archive: ", decoded_tx$vout[[1]]$scriptPubKey$addresses[[1]], "\n") # cat("Asset Identified: ", decoded_tx$vout[[1]]$assets[[1]]$name, "\n") # # We use hex_to_char to read the embedded JSON comment # cat("Embedded Comment: ", multichainr::hex_to_char(decoded_tx$vout[[2]]$data[[1]]), "\n") ## ----signing------------------------------------------------------------------ # # 9.5.1 Provide Input Context # # We re-package the original UTXO metadata to assist the signing engine. # parents_info <- list(list( # txid = target_utxo$txid, # vout = as.integer(target_utxo$vout), # scriptPubKey = target_utxo$scriptPubKey[[1]], # amount = as.numeric(target_utxo$amount), # assets = target_utxo$assets[[1]] # )) # # # 9.5.2 Apply Departmental Signature # # This finalizes the transaction, linking it to the department's private key. # signed_obj <- mc_sign_raw_transaction(conn, raw_tx_hex, parents = parents_info) # # if (signed_obj$complete) { # message("Digital Signature: Transaction successfully signed and authorized.") # } ## ----final_commitment--------------------------------------------------------- # # 9.6.1 Broadcast to the Permanent Ledger # final_txid <- mc_send_raw_transaction(conn, signed_obj$hex) # mc_wait_for_confirmation(conn, final_txid) # # # 9.6.2 Final Reconciliation Report # # Verify that Production no longer holds the archived permit. # archive_bal <- mc_get_address_balances(conn, archive_addr) # # message("Audit Finalization: Permit archiving verified.") # print(archive_bal[archive_bal$name == target_asset, c("name", "qty")]) ## ----backup_prep-------------------------------------------------------------- # # 10.1.1 Define Archive Paths # # Note: These paths are relative to the machine where the MultiChain daemon is running. # backup_folder <- tempdir() # binary_backup_path <- file.path(backup_folder, "Pharma_Wallet_Backup.dat") # text_dump_path <- file.path(backup_folder, "Pharma_Keys_Export.txt") # # message("System Administration: Backup paths initialized in validated storage zone.") ## ----binary_backup------------------------------------------------------------ # # 10.2.1 Execute Binary Backup # # mc_backup_wallet performs a safe, hot-backup of the wallet database. # mc_backup_wallet(conn, binary_backup_path) # # if (file.exists(binary_backup_path)) { # message("Disaster Recovery: Binary wallet backup successfully generated.") # cat("Binary Archive Location: ", binary_backup_path, "\n") # } ## ----key_dump----------------------------------------------------------------- # # 10.3.1 Export Private Keys # # mc_dump_wallet creates a text file listing every address and its private key. # mc_dump_wallet(conn, text_dump_path) # # if (file.exists(text_dump_path)) { # message("Data Retention: Human-readable key export completed for safe-deposit archiving.") # # SAFETY WARNING: In a GxP environment, this file must be handled with # # extreme security as it contains the "Master Keys" to the departmental identities. # } ## ----final_snapshot----------------------------------------------------------- # # 10.4.1 Capture Final Ledger Metadata # final_stats <- mc_get_blockchain_info(conn) # # cat("--- FINAL REGULATORY LEDGER SUMMARY ---\n") # cat("Chain Name: ", chain_name, "\n") # cat("Final Block Height: ", final_stats$blocks, "\n") # cat("Protocol Version: ", final_stats$protocol, "\n") ## ----system_shutdown---------------------------------------------------------- # # 10.5.1 Terminate Blockchain Daemon # # mc_node_stop sends the SIGTERM signal to the multichaind process. # mc_node_stop(chain_name) # # # Allow the operating system to release file locks before directory removal. # Sys.sleep(2) # # # 10.5.2 Post-Audit Cleanup # # In a real validated environment, the chain_dir would be preserved. # # Here we remove it to reset the test environment. # # if (.Platform$OS.type == "windows") { # base_dir <- file.path(Sys.getenv("APPDATA"), "MultiChain") # } else if (Sys.info()["sysname"] == "Darwin") { # base_dir <- file.path(Sys.getenv("HOME"), "Library/Application Support/MultiChain") # } else { # base_dir <- file.path(Sys.getenv("HOME"), ".multichain") # } # # chain_dir <- file.path(base_dir, chain_name) # # if (dir.exists(chain_dir)) { # unlink(chain_dir, recursive = TRUE) # message("IT Compliance: Local session data removed after successful archiving.") # } # # message("Workflow Status: Pharma SOP Registry session successfully closed.")