diff --git a/.cap/logs/1780406203-54124.log b/.cap/logs/1780406203-54124.log deleted file mode 100644 index 2c49e557..00000000 --- a/.cap/logs/1780406203-54124.log +++ /dev/null @@ -1,1125 +0,0 @@ -== stdout == - -running 776 tests -test cli::commands::tests::expected_metadata_hash_comparison_is_case_sensitive ... ok -test cli::commands::tests::invalid_parser_mapping_returns_error_instead_of_panicking ... ok -test cli::commands::tests::test_command_execution ... ok -test ckb_hash_tests::ckb_blake2b256_matches_blank_hash_vector ... ok -test codegen::assembler::tests::strict_audit_internal_assembler_oracle_for_core_instruction_bytes ... ok -test codegen::assembler::tests::strict_audit_riscv_immediate_boundaries_are_enforced ... ok -test cli::commands::tests::production_policy_finds_evidence_less_checked_runtime_proof_plan_gap ... ok -test codegen::assembler::tests::strict_audit_li_split_handles_negative_32_bit_boundaries ... ok -test cli::commands::tests::production_policy_finds_evidence_less_on_chain_checked_proof_plan_gap ... ok -test codegen::calls::tests::fixed_u64_le_width_accepts_hashes_and_byte_arrays ... ok -test codegen::cell_ops::tests::consumed_operand_var_accepts_named_cell_operands_only ... ok -test codegen::cell_ops::tests::destroy_absence_scan_is_limited_to_singleton_and_type_id_unique_policies ... ok -test codegen::cell_ops::tests::identity_and_destruction_policy_labels_are_stable ... ok -test codegen::calls::tests::canonical_type_names_strip_reference_wrappers ... ok -test codegen::calls::tests::packed_hash_width_uses_codegen_fixed_byte_type_rules ... ok -test codegen::frame::tests::large_addi_materializes_out_of_range_immediates ... ok -test codegen::frame::tests::stack_access_helpers_emit_sp_relative_instructions ... ok -test codegen::frame::tests::large_addi_uses_single_addi_for_small_immediates ... ok -test codegen::expr::tests::bool_canonical_check_emits_zero_one_guard ... ok -test codegen::expr::tests::divisor_nonzero_guard_fails_closed_on_zero ... ok -test cli::commands::tests::ckb_hash_file_rejects_inputs_above_limit ... ok -test codegen::runtime::tests::ckb_runtime_syscall_abi_matches_declared_constants ... ok -test codegen::runtime::tests::checked_runtime_status_register_defaults_to_a1_for_unknown_helpers ... ok -test codegen::runtime::tests::runtime_helper_classification_tracks_checked_and_hash_helpers ... ok -test codegen::schema::tests::aggregate_field_layouts_track_tuple_offsets ... ok -test codegen::schema::tests::fixed_byte_constants_materialize_little_endian_bytes ... ok -test codegen::schema::tests::fixed_width_helpers_classify_scalar_and_byte_storage ... ok -test codegen::tests::cell_operation_identity_helpers_stay_in_cell_ops ... ok -test codegen::tests::dynamic_syscall_index_is_copied_before_large_stack_staging ... ok -test codegen::assembler::tests::strict_audit_elf_header_and_segments_are_internally_consistent ... ok -test codegen::tests::consumed_schema_params_use_loaded_cell_size_for_field_checks ... ok -test codegen::tests::explicit_external_toolchain_paths_are_strict ... ok -test codegen::tests::generated_collection_assembly_is_internal_assembler_clean ... ok -test codegen::tests::generated_large_offsets_are_normalized_before_assembly ... ok -test codegen::tests::internal_assembler_encodes_emitted_instruction_surface ... ok -test codegen::tests::generated_public_assembly_mnemonics_are_declared ... ok -test codegen::tests::generated_stdlib_assembly_is_internal_assembler_clean ... ok -test codegen::tests::generated_functions_use_shared_epilogue_tail ... ok -test codegen::tests::internal_assembler_encodes_full_width_li_literals ... ok -test codegen::tests::internal_assembler_keeps_near_unconditional_jump_compact ... ok -test codegen::tests::internal_assembler_rejects_intentionally_unsupported_mnemonics ... ok -test codegen::tests::internal_assembler_rejects_unresolved_call_targets ... ok -test codegen::tests::internal_assembler_encodes_register_conditional_branches ... ok -test codegen::tests::large_addi_avoids_clobbering_source_register ... ok -test codegen::tests::machine_cfg_tracks_call_edges_to_local_helpers ... ok -test codegen::tests::machine_layout_order_rejects_missing_duplicate_or_unknown_blocks ... ok -test codegen::assembler::tests::strict_audit_relaxed_conditional_branch_within_jal_range_preserves_registers ... ok -test codegen::tests::machine_layout_plan_builds_explicit_machine_blocks ... ok -test codegen::tests::machine_layout_plan_builds_register_conditional_branch_blocks ... ok -test codegen::tests::machine_layout_plan_rejects_branch_target_outside_text ... ok -test codegen::tests::binary_codegen_materializes_narrow_integer_constants ... ok -test codegen::tests::machine_reachability_uses_entry_label_not_every_global ... ok -test codegen::tests::division_codegen_guards_zero_divisors ... ok -test codegen::tests::outgoing_stack_arg_area_is_16_byte_aligned_at_call_boundaries ... ok -test codegen::tests::read_ref_runtime_fallback_records_cell_buffer_state ... ok -test codegen::tests::register_contract_allows_only_entry_wrapper_writes_to_direct_registers ... ok -test codegen::tests::dynamic_molecule_vector_field_access_validates_full_table_offsets ... ok -test codegen::tests::rv64_li_boundary_values_materialize_correct_bits ... ok -test codegen::tests::semantic_molecule_field_access_uses_validated_api_gate ... ok -test codegen::tests::sp_addi_large_offsets_clobber_only_destination_register ... ok -test codegen::tests::dynamic_molecule_fixed_field_codegen_checks_full_header_and_exact_span ... ok -test codegen::tests::state_transition_edges_use_explicit_consumed_binding ... ok -test codegen::tests::strict_audit_outgoing_stack_args_are_staged_inside_current_frame ... ok -test codegen::tests::type_hash_missing_output_buffer_slots_report_compile_error ... ok -test codegen::tests::type_hash_missing_param_slots_report_compile_error ... ok -test codegen::tests::u128_const_without_fixed_storage_reports_compile_error ... ok -test codegen::tests::machine_layout_plan_reports_branch_relaxation_metrics ... ok -test codegen::tests::unaligned_scalar_load_large_offsets_preserve_live_accumulator ... ok -test codegen::tests::unrepresentable_memory_load_offsets_report_compile_error ... ok -test codegen::tests::runtime_cast_codegen_checks_narrowing_and_bool_canonicality ... ok -test codegen::tests::unrepresentable_stack_offsets_report_compile_error ... ok -test codegen::tests::narrow_arithmetic_codegen_truncates_to_declared_width ... ok -test debug::tests::test_debug_info_generator ... ok -test debug::tests::test_dwarf_generation ... ok -test debug::tests::test_type_registration ... ok -test debug::tests::test_line_table ... ok -test docgen::tests::docgen_emits_flat_pool_runtime_input_requirements ... ok -test codegen::tests::schema_ref_call_preserves_schema_abi_length ... ok -test docgen::tests::docgen_emits_markdown_for_action ... ok -test docgen::tests::docgen_emits_transaction_invariant_checked_subconditions ... ok -test docgen::tests::docgen_html_escapes_module_and_item_text ... ok -test error::tests::caret_padding_starts_at_span_column ... ok -test error::tests::caret_width_counts_characters_not_bytes ... ok -test flow::tests::consumed_flow_tracking_follows_expression_aliases ... ok -test fmt::tests::format_action_transition_block_for_multiple_edges ... ok -test fmt::tests::format_indents_preserve_fields_inside_expression_block ... ok -test fmt::tests::format_preserves_type_policy_metadata ... ok -test fmt::tests::format_preserves_single_element_tuple_expression ... ok -test fmt::tests::format_round_trips_inline_if_tuple_expression ... ok -test fmt::tests::format_round_trips_multiline_expression_block ... ok -test codegen::tests::internal_assembler_relaxes_out_of_range_conditional_branch ... ok -test fmt::tests::format_round_trips_preserve_block ... ok -test fmt::tests::format_round_trips_require_block ... ok -test fmt::tests::format_round_trips_simple_module ... ok -test fmt::tests::format_round_trips_stdlib_lifecycle_field_block ... ok -test fmt::tests::format_single_expr_require_block_uses_compact_form ... ok -test fmt::tests::format_uses_canonical_assert_and_no_const_semicolon ... ok -test fmt::tests::format_uses_field_shorthand_when_value_matches_name ... ok -test incremental::tests::clean_cache_rejects_overflowing_max_age ... ok -test codegen::tests::stack_pointer_offsets_are_emitted_through_helpers ... ok -test incremental::tests::clean_cache_skips_output_paths_outside_trusted_root ... ok -test codegen::tests::vm2_syscall_helpers_emit_executable_status_checked_wrappers ... ok -test incremental::tests::test_change_detector ... ok -test ir::tests::all_diverging_match_expression_does_not_leave_unreachable_join ... ok -test incremental::tests::test_dependency_graph ... ok -test ir::tests::assert_in_pure_function_lowers_failure_to_abort_terminator ... ok -test incremental::tests::load_cache_drops_units_with_paths_outside_trusted_root ... ok -test ir::tests::binary_arithmetic_result_type_preserves_left_operand_width ... ok -test ir::tests::constant_cast_rejects_out_of_range_u128_narrowing ... ok -test ir::tests::contextual_integer_binary_operands_lower_to_peer_width ... ok -test ir::tests::ir_generation_aggregates_lowering_errors_with_source_spans ... ok -test ir::tests::exhaustive_enum_match_unmatched_path_lowers_to_abort_terminator ... ok -test ir::tests::ir_straight_line_lifecycle_certificate_rejects_duplicate_consume_without_typecheck ... ok -test ir::tests::ir_straight_line_lifecycle_certificate_rejects_branch_local_create_without_typecheck ... ok -test ir::tests::ir_type_value_kind_never_derives_status_kinds ... ok -test ir::tests::poison_lowering_keeps_value_invalid_while_block_stays_live ... ok -test incremental::tests::test_incremental_compiler ... ok -test ir::tests::mixed_width_expression_local_widening_lowers_as_explicit_casts ... ok -test ir::tests::reference_and_deref_unary_result_types_match_ast_types ... ok -test ir::tests::require_block_lowers_to_atomic_requires ... ok -test ir::tests::runtime_narrowing_cast_lowers_as_cast_instruction ... ok -test ir::tests::status_boundary_ir_verifier_allows_domain_u64_return_tuple_and_call_argument ... ok -test ir::tests::logical_operators_lower_as_short_circuit_control_flow ... ok -test ir::tests::status_boundary_ir_verifier_rejects_dropped_raw_syscall_status ... ok -test ir::tests::preserve_sugar_populates_preserved_fields ... ok -test ir::tests::status_boundary_ir_verifier_allows_unit_runtime_helper_when_status_is_checked_by_codegen_boundary ... ok -test ir::tests::status_boundary_ir_verifier_rejects_raw_syscall_status_as_domain_call_argument ... ok -test ir::tests::status_boundary_ir_verifier_rejects_raw_syscall_status_produced_without_checked_consumer ... ok -test ir::tests::status_boundary_ir_verifier_rejects_raw_syscall_status_in_tuple_field ... ok -test ir::tests::status_boundary_ir_verifier_rejects_raw_syscall_status_stored_as_dsl_local ... ok -test ir::tests::status_boundary_ir_verifier_rejects_raw_syscall_status_returned_as_domain_u64 ... ok -test ir::tests::status_boundary_ir_verifier_rejects_unit_runtime_helper_status_stored_as_domain_u64 ... ok -test ir::tests::stdlib_claim_lowers_to_consumed_receipt_and_locked_declared_output ... ok -test ir::tests::strict_audit_ir_verifier_rejects_constant_destination_width_mismatch ... ok -test ir::tests::strict_audit_ir_lowering_records_instruction_level_provenance ... ok -test ir::tests::strict_audit_ir_verifier_rejects_empty_body_blocks ... ok -test ir::tests::stdlib_transfer_lowers_to_single_consumed_input_and_locked_output ... ok -test ir::tests::stdlib_settle_lowers_to_consumed_input_and_locked_output ... ok -test ir::tests::strict_audit_ir_verifier_rejects_extra_consume_set_metadata ... ok -test ir::tests::strict_audit_ir_verifier_rejects_missing_create_set_metadata ... ok -test ir::tests::strict_audit_ir_verifier_rejects_missing_terminator_target ... ok -test ir::tests::strict_audit_ir_verifier_rejects_poisoned_lowering_module ... ok -test ir::tests::strict_audit_ir_verifier_rejects_poisoned_load_const ... ok -test ir::tests::strict_audit_ir_verifier_rejects_poisoned_lowering_operand ... ok -test ir::tests::strict_audit_ir_verifier_rejects_use_not_defined_on_all_paths ... ok -test ir::tests::strict_audit_ir_verifier_rejects_stale_write_intents_metadata ... ok -test ir::tests::strict_audit_ir_verifier_reports_instruction_provenance ... ok -test ir::tests::strict_audit_schema_field_accesses_are_rematerialized_per_cfg_path ... ok -test codegen::tests::u128_delta_arithmetic_codegen_uses_fixed_byte_storage ... ok -test lexer::tests::test_byte_string ... ok -test lexer::tests::test_comment ... ok -test lexer::tests::test_identifiers ... ok -test lexer::tests::test_keywords ... ok -test lexer::tests::test_numbers ... ok -test lexer::tests::rejects_oversized_identifier ... ok -test lexer::tests::test_operators ... ok -test lexer::tests::test_punctuation ... ok -test lexer::tests::test_string ... ok -test lexer::tests::test_unterminated_byte_string_errors ... ok -test lexer::tests::test_unterminated_string_errors ... ok -test lsp::tests::lsp_position_conversion_treats_crlf_as_single_line_ending ... ok -test docgen::tests::docgen_emits_invariant_coverage_summary ... ok -test lsp::tests::lsp_position_incremental_change_applies_crlf_ranges ... ok -test lsp::tests::lsp_primitive_strict_rejects_legacy_capabilities ... ok -test lsp::tests::goto_definition_prefers_local_scope_over_top_level_symbol ... ok -test lsp::tests::lsp_rejects_document_count_over_limit ... ok -test lsp::tests::find_references_for_locals_stays_in_enclosing_callable_scope ... ok -test lsp::tests::lsp_reads_primitive_strict_from_manifest ... ok -test lsp::tests::lsp_rejects_oversized_documents ... ok -test lsp::tests::test_ckb_namespace_completions ... ok -test lsp::tests::test_flow_namespace_completions ... ok -test lsp::tests::test_code_actions_for_lowering_diagnostics ... ok -test lsp::tests::test_goto_definition_and_references ... ok -test lsp::tests::test_format_document ... ok -test lsp::tests::test_incremental_change_applies_utf16_ranges_after_non_bmp_text ... ok -test lsp::tests::test_incremental_change_ignores_invalid_utf16_ranges ... ok -test lsp::tests::test_keyword_completions ... ok -test codegen::tests::entry_dynamic_witness_stack_arg_staging_preserves_cursor_register ... ok -test lsp::tests::test_lsp_position_conversion_uses_utf16_columns ... ok -test lsp::tests::test_flow_u8_namespace_completions ... ok -test lsp::tests::test_parse_errors_become_diagnostics ... ok -test lsp::tests::test_lsp_server ... ok -test lsp::tests::test_hover ... ok -test lsp::tests::test_vec_member_completions_match_supported_helpers ... ok -test lsp::tests::test_action_hover_includes_lowering_metadata ... ok -test lsp::tests::test_selection_range_orders_child_before_parent ... ok -test lsp::tests::test_workspace_diagnostics_check_imported_type_id_collisions ... ok -test lsp::tests::test_workspace_goto_definition_across_modules ... ok -test lsp::tests::test_workspace_rename_is_disabled_until_symbol_scoped ... ok -test optimize::tests::does_not_inline_block_bodies_that_can_capture_call_site_names ... ok -test optimize::tests::folds_boolean_expressions ... ok -test optimize::tests::folds_integer_arithmetic ... ok -test optimize::tests::folds_unsigned_high_bit_integer_operations ... ok -test optimize::tests::folds_literal_if_statements_without_touching_cell_ops ... ok -test optimize::tests::propagates_constants_inlines_small_functions_and_removes_dead_code ... ok -test optimize::tests::unused_let_elimination_preserves_calls_and_stdlib_constraints ... ok -test package::tests::git_cache_child_check_rejects_path_escape ... ok -test lsp::tests::test_workspace_references_across_modules ... ok -test package::tests::git_cache_entry_name_is_hash_only ... ok -test package::tests::lockfile_consistency_allows_resolved_transitive_path_dependencies ... ok -test package::tests::lockfile_consistency_reports_stale_and_mismatched_path_sources ... ok -test lsp::tests::test_lowering_diagnostics_warn_for_fail_closed_runtime_actions ... ok -test package::tests::lockfile_replace_with_resolved_prunes_removed_dependencies ... ok -test package::tests::lockfile_consistency_requires_exact_git_revision_match ... ok -test package::tests::package_manager_accepts_allowed_git_url_transports ... ok -test package::tests::package_manager_git_checkout_revalidates_full_commit_refs ... ok -test package::tests::package_manager_git_commands_separate_user_controlled_ref_arguments ... ok -test package::tests::lockfile_read_from_root_rejects_malformed_lockfiles ... ok -test package::tests::package_manager_allows_path_dependency_without_version ... ok -test package::tests::package_manager_rejects_branch_or_tag_git_dependency_before_fetch ... ok -test package::tests::package_manager_rejects_registry_dependencies_fail_closed ... ok -test package::tests::package_manager_rejects_local_path_dependency_traversal ... ok -test package::tests::package_manager_rejects_unpinned_git_dependency_before_fetch ... ok -test package::tests::package_manager_rejects_unsafe_git_url_transports ... ok -test package::tests::package_manager_rejects_transitive_path_dependency_cycles ... ok -test package::tests::package_manager_resolves_local_path_dependencies ... ok -test package::tests::test_dependency_graph ... ok -test package::tests::package_manager_resolves_transitive_local_path_dependencies ... ok -test package::tests::test_version_compatibility ... ok -test package::tests::test_manifest_serialization ... ok -test parser::tests::action_where_block_keeps_indented_keyword_like_binding_in_body ... ok -test parser::tests::action_where_block_allows_indented_following_top_level_item ... ok -test parser::tests::array_size_uses_checked_target_width_conversion ... ok -test parser::tests::assignment_range_and_cast_spans_cover_full_expression ... ok -test parser::tests::binary_expr_spans_cover_full_expression ... ok -test parser::tests::generic_type_arguments_allow_newlines ... ok -test parser::tests::hex_literal_exprs_parse_as_integers ... ok -test parser::tests::identity_policy_diagnostic_uses_bad_policy_span ... ok -test parser::tests::named_arg_diagnostic_uses_bad_name_span ... ok -test parser::tests::parser_empty_token_slice_returns_controlled_error ... ok -test parser::tests::parser_rejects_bang_assert_syntax ... ok -test parser::tests::parser_rejects_deep_if_expression_before_stack_overflow ... ok -test parser::tests::parser_rejects_deep_unary_expression_before_stack_overflow ... ok -test parser::tests::postfix_exprs_cover_the_consumed_source_range ... ok -test parser::tests::postfix_expr_spans_cover_the_full_postfix_chain ... ok -test parser::tests::primitive_and_container_exprs_keep_source_spans ... ok -test parser::tests::struct_init_span_covers_type_name_and_body ... ok -test parser::tests::test_action_where_column_one_flow_identifier_stays_in_body ... ok -test parser::tests::test_launch_expression_is_reserved_until_lowering_exists ... ok -test parser::tests::test_parse_action ... ok -test lexer::tests::rejects_oversized_string_literal ... ok -test parser::tests::test_parse_aggregate_invariant_primitives ... ok -test parser::tests::test_parse_action_transition_block ... ok -test parser::tests::test_parse_create_field_shorthand ... ok -test parser::tests::test_parse_expression ... ok -test parser::tests::test_parse_grouped_use_imports ... ok -test parser::tests::test_parse_flow_and_action_transition_clause ... ok -test parser::tests::test_parse_invariant ... ok -test parser::tests::test_parse_invariant_assert_statement ... ok -test parser::tests::test_parse_merges_attribute_and_inline_capabilities ... ok -test parser::tests::test_parse_prefix_source_before_keyword_like_name ... ok -test parser::tests::test_parse_prefix_source_and_create_target ... ok -test parser::tests::test_parse_preserve_block ... ok -test parser::tests::test_parse_preserve_single_field ... ok -test parser::tests::test_parse_require_block ... ok -test parser::tests::test_parse_resource ... ok -test parser::tests::test_parse_type_id_attribute ... ok -test parser::tests::test_postfix_does_not_cross_statement_newline ... ok -test parser::tests::test_reject_bare_preserve ... ok -test parser::tests::test_reject_empty_require_block ... ok -test parser::tests::test_reject_empty_preserve_block ... ok -test parser::tests::test_reject_preserve_except ... ok -test parser::tests::test_reject_preserve_wildcard ... ok -test parser::tests::test_reject_require_block_with_consume ... ok -test parser::tests::test_reject_require_block_with_control_flow ... ok -test parser::tests::test_rejects_action_brace_body ... ok -test parser::tests::test_rejects_generic_resource_definition ... ok -test parser::tests::test_rejects_empty_transition_block ... ok -test parser::tests::test_rejects_legacy_move_clause ... ok -test parser::tests::test_rejects_read_ref_as_type_qualifier ... ok -test parser::tests::test_rejects_transition_clause_without_state_colons ... ok -test parser::tests::test_rejects_type_id_on_action ... ok -test parser::tests::test_rejects_typed_let_without_initializer ... ok -test parser::tests::test_rejects_use_as_without_alias ... ok -test parser::tests::test_rejects_unbraced_match_arms ... ok -test proof_plan::soundness::tests::strict_pp0103_only_applies_to_checked_runtime_records ... ok -test proof_plan::soundness::tests::strict_pp0201_only_applies_to_executing_script_args ... ok -test proof_plan::tests::checked_runtime_without_concrete_evidence_is_not_marked_covered ... ok -test proof_plan::tests::checked_runtime_proof_plan_claims_include_executable_evidence ... ok -test proof_plan::tests::metadata_only_invariant_proof_plan_has_no_executable_evidence ... ok -test proof_plan::tests::checked_static_detail_does_not_create_executable_runtime_evidence ... ok -test parser::tests::test_rejects_output_parameter_source_prefix ... ok -test proof_plan::tests::replace_unique_features_are_transaction_scoped ... ok -test proof_plan::tests::unique_lifecycle_features_have_specific_codegen_evidence_ids ... ok -test repl::tests::repl_read_limited_line_accepts_bounded_input ... ok -test resolve::tests::rejects_cross_module_type_dependency_cycles ... ok -test resolve::tests::test_global_type_resolution_rejects_ambiguous_symbol ... ok -test resolve::tests::test_grouped_use_resolves_multiple_symbols ... ok -test resolve::tests::test_imported_type_resolution_uses_exact_module_path ... ok -test resolve::tests::test_module_resolver ... ok -test resolve::tests::test_path_resolver ... ok -test resolve::tests::test_register_module_rejects_deferred_missing_import_when_target_arrives ... ok -test resolve::tests::test_register_module_rejects_missing_imported_symbol_when_target_is_loaded ... ok -test resolve::tests::test_rejects_duplicate_local_symbols ... ok -test resolve::tests::test_rejects_import_alias_collisions ... ok -test runtime_errors::tests::diagnostic_messages_map_to_runtime_error_codes_where_possible ... ok -test lsp::tests::test_receipt_hover_includes_flow_metadata ... ok -test runtime_errors::tests::runtime_error_docs_explain_ckb_code_overlap_channels ... ok -test runtime_errors::tests::runtime_error_registry_roundtrips_and_has_unique_codes ... ok -test simulate::tests::array_size_simulator_uses_checked_target_width_for_indices ... ok -test simulate::tests::simulate_cell_operation_traces ... ok -test runtime_errors::tests::runtime_error_docs_cover_every_registered_code ... ok -test simulate::tests::simulate_if_branch ... ok -test simulate::tests::simulate_pure_arithmetic_action ... ok -test simulate::tests::simulate_read_ref_traces ... ok -test simulate::tests::simulate_rejects_wrong_action_arity ... ok -test simulate::tests::simulate_step_limit ... ok -test simulate::tests::simulate_unsigned_high_bit_integer_operations ... ok -test stdlib::collections::tests::collection_public_helpers_do_not_dereference_raw_a0_handles ... ok -test stdlib::collections::tests::collection_assembly_has_no_raw_syscalls_or_unclassified_helpers ... ok -test stdlib::collections::tests::test_collection_functions ... ok -test repl::tests::repl_read_limited_line_rejects_oversized_input ... ok -test stdlib::collections::tests::test_generate_assembly ... ok -test stdlib::tests::generated_stdlib_has_no_raw_syscall_wrapper_symbols ... ok -test stdlib::tests::test_generate_assembly ... ok -test stdlib::tests::generated_stdlib_omits_raw_syscall_wrappers ... ok -test stdlib::tests::test_get_function ... ok -test stdlib::tests::test_scheduler_metadata_generate_molecule_uses_table_layout ... ok -test stdlib::tests::test_std_functions ... ok -test syscalls::tests::ckb_debug_syscall_is_not_a_production_inventory_surface ... ok -test stdlib::tests::test_generate_ckb_assembly_uses_checked_env_helpers ... ok -test syscalls::tests::emitted_manual_runtime_and_stdlib_helpers_are_classified ... ok -test syscalls::tests::every_low_level_syscall_spec_is_inventoried ... ok -test syscalls::tests::helper_inventory_has_no_duplicate_symbols ... ok -test tests::action_scheduler_witness_bytes_rejects_conflicting_molecule_alias ... ok -test syscalls::tests::ckb_syscall_abi_matches_checked_baseline ... ok -test tests::ckb_capacity_calculation_saturates_on_extreme_sizes ... ok -test tests::branch_local_anonymous_creates_are_rejected_until_effects_are_cfg_aware ... ok -test tests::ckb_deploy_manifest_rejects_conflicting_cell_dep_locations ... ok -test tests::ckb_constraints_surface_capacity_planning_for_created_outputs ... ok -test tests::ckb_deploy_manifest_rejects_incomplete_split_cell_dep_location ... ok -test runtime_errors::tests::codegen_does_not_emit_unregistered_numeric_fail_literals ... ok -test tests::ckb_deploy_manifest_rejects_invalid_dep_type ... ok -test package::tests::package_manager_git_dependency_fails_for_invalid_url ... ok -test tests::ckb_deploy_manifest_rejects_invalid_hash_type ... ok -test tests::ckb_deploy_manifest_surfaces_hash_type_and_dep_group_policy ... ok -test lexer::tests::rejects_oversized_block_comment ... ok -test codegen::tests::emitted_runtime_helper_symbols_are_classified_in_syscall_inventory ... ok -test package::tests::package_manager_git_update_fails_closed_on_fetch_error ... ok -test tests::ckb_target_profile_has_no_policy_exception ... ok -test tests::collection_fail_closed_feature_names_are_stable ... ok -test lsp::tests::lsp_loads_sibling_modules_for_standalone_example_imports ... ok -test tests::ckb_lock_false_return_lowers_to_script_failure ... ok -test tests::compile_accepts_chain_neutral_timepoint_under_ckb_profile ... ok -test tests::ckb_u64_syscall_helpers_check_return_code_and_size ... ok -test tests::compile_accepts_action_witness_source_qualifier ... ok -test tests::compile_accepts_ckb_header_epoch_api_only_for_ckb_profile ... ok -test tests::ckb_entry_lock_scope_selects_lock_entrypoint ... ok -test tests::compile_accepts_ckb_target_profile_timepoint ... ok -test tests::ckb_dynamic_vector_len_can_drive_mutate_transition ... ok -test tests::compile_accepts_complete_branch_return_paths ... ok -test tests::compile_accepts_ckb_shared_create_when_verifier_covered ... ok -test codegen::tests::internal_assembler_relaxes_out_of_range_register_conditional_branch ... ok -test tests::ckb_entry_scope_keeps_vec_element_schema_dependencies ... ok -test tests::compile_accepts_empty_vec_literal_with_declared_type ... ok -test tests::compile_accepts_flow_initial_create_at_any_declared_state ... ok -test tests::compile_accepts_explicit_flow_action_edges ... ok -test tests::compile_accepts_core_input_output_state_transition_edges ... ok -test tests::compile_accepts_flow_state_name_initializers ... ok -test tests::compile_accepts_create_field_shorthand ... ok -test tests::compile_accepts_kernel_effect_capabilities_for_destroy ... ok -test tests::compile_accepts_flow_edge_returning_to_first_state ... ok -test tests::compile_accepts_flow_on_custom_state_field ... ok -test tests::compile_accepts_kernel_effect_capabilities_for_transfer ... ok -test tests::compile_accepts_lock_args_script_args_binding ... ok -test tests::compile_accepts_non_initial_flow_create_without_consumed_prior_state ... ok -test tests::compile_accepts_pure_ckb_target_profile ... ok -test tests::compile_accepts_named_action_output_and_create_binding ... ok -test tests::ckb_entry_action_scope_excludes_unselected_unsupported_code ... ok -test tests::compile_accepts_lock_boundary_param_sources_and_require ... ok -test tests::compile_accepts_prefix_read_params_as_cell_dep_bindings ... ok -test tests::compile_accepts_qualified_flow_state_names ... ok -test tests::compile_allows_struct_type_id_under_ckb_profile ... ok -test tests::compile_accepts_static_flow_update_to_non_initial_state ... ok -test tests::compile_allows_actions_and_locks_to_call_pure_functions ... ok -test tests::compile_allows_unit_function_calls_as_statements ... ok -test tests::compile_allows_flow_update_to_declared_initial_state_at_type_check ... ok -test tests::compile_accepts_vec_literals_in_create_fields ... ok -test tests::compile_accepts_symmetric_where_branch_output_constraints ... ok -test tests::compile_binds_duplicate_read_refs_by_order_not_name ... ok -test tests::compile_classifies_resource_merge_amount_sum_as_checked_runtime ... ok -test tests::compile_binds_read_ref_entry_params_to_cell_deps ... ok -test tests::compile_binds_read_action_schema_params_to_cell_deps ... ok -test tests::compile_create_unique_field_identity_emits_runtime_anchor ... ok -test tests::compile_emits_create_output_field_verification_for_fixed_u64_fields ... ok -test tests::compile_emits_direct_user_function_calls ... ok -test tests::compile_classifies_resource_split_amount_subtraction_as_checked_runtime ... ok -test tests::compile_exposes_ckb_type_id_contract_under_ckb_profile ... ok -test tests::compile_entry_witness_rejects_payloads_larger_than_buffer ... ok -test tests::compile_emits_ckb_style_load_cell_abi_for_cell_runtime_summary ... ok -test tests::compile_classifies_protocol_agnostic_guarded_transition_as_checked_runtime ... ok -test tests::compile_destroy_policies_are_policy_aware ... ok -test tests::compile_file_explicit_target_overrides_manifest_build_target ... ok -test tests::compile_emits_protocol_agnostic_guard_equality_proofplan_records ... ok -test tests::compile_file_uses_manifest_ckb_target_profile ... ok -test tests::compile_classifies_guarded_identity_field_merge_as_checked_runtime ... ok -test tests::compile_folds_local_fixed_array_len_to_constant ... ok -test tests::compile_file_uses_manifest_build_target_by_default ... ok -test tests::compile_file_loads_local_path_dependencies_from_cell_manifest ... ok -test tests::compile_identity_none_is_default_and_hidden ... ok -test tests::compile_identity_ckb_type_id_emits_metadata ... ok -test tests::compile_infers_and_validates_read_only_effects ... ok -test tests::compile_lowers_array_of_tuples_static_index_projection ... ok -test tests::compile_identity_singleton_type_emits_metadata ... ok -test tests::compile_ignores_trivial_self_equality_guard_records ... ok -test tests::compile_lowers_assert_invariant_into_fail_closed_cfg ... ok -test tests::compile_identity_field_emits_path ... ok -test tests::compile_lowers_block_tail_if_expressions ... ok -test tests::compile_identity_script_args_emits_metadata ... ok -test tests::compile_lowers_byte_string_literals_with_expected_array_type ... ok -test tests::compile_file_source_content_hash_is_path_independent ... ok -test tests::compile_lowers_bounded_vec_literal_to_stack_collection ... ok -test tests::bundled_token_example_strict_ckb_compile_is_admitted ... ok -test tests::compile_lowers_consumed_input_field_access_through_loaded_cell_bytes ... ok -test tests::compile_lowers_exhaustive_enum_match_without_wildcard ... ok -test tests::compile_lowers_if_expression_fixed_byte_const_join_move ... ok -test tests::compile_lowers_for_range_into_counted_loop_cfg ... ok -test tests::compile_lowers_fixed_byte_schema_field_comparison ... ok -test tests::compile_lowers_ckb_group_source_large_immediate_to_riscv_elf ... ok -test tests::compile_lowers_if_statement_into_basic_blocks ... ok -test tests::compile_lowers_local_fixed_array_static_index_reads_and_writes ... ok -test tests::compile_keeps_unchecked_transition_field_runtime_required ... ok -test tests::compile_lowers_local_constants_into_real_operands ... ok -test tests::compile_lowers_if_expression_with_join_move ... ok -test tests::compile_lowers_len_method_to_length_instruction ... ok -test tests::compile_lowers_local_struct_field_reads_and_writes ... ok -test tests::compile_lowers_local_tuple_destructuring_to_field_slots ... ok -test tests::compile_lowers_match_expression_into_branch_cfg ... ok -test tests::compile_lowers_local_tuple_static_field_reads_and_writes ... ok -test tests::compile_lowers_numeric_cast_without_zero_fallback ... ok -test tests::compile_lowers_mutable_assignments_in_loop_bodies ... ok -test tests::compile_lowers_packed_bool_and_u32_schema_fields_without_aligned_loads ... ok -test tests::compile_lowers_pure_function_assert_failure_to_abort ... ok -test tests::compile_lowers_read_ref_schema_field_to_ckb_runtime_assembly ... ok -test tests::compile_lowers_stack_vec_extend_from_fixed_bytes ... ok -test tests::compile_lowers_stack_vec_clear_and_is_empty ... ok -test tests::compile_lowers_read_ref_schema_field_to_ckb_runtime_elf ... ok -test tests::compile_lowers_stack_vec_fixed_byte_capacity ... ok -test tests::compile_lowers_stack_vec_fixed_byte_pop ... ok -test tests::compile_lowers_schema_backed_parameter_field_access_to_elf ... ok -test tests::compile_lowers_stack_vec_fixed_byte_first_last ... ok -test tests::compile_lowers_stack_vec_fixed_byte_contains ... ok -test tests::compile_lowers_stack_vec_fixed_byte_set ... ok -test tests::compile_lowers_stack_vec_fixed_byte_runtime_push_index ... ok -test tests::compile_lowers_stack_vec_fixed_byte_insert ... ok -test tests::compile_lowers_stack_vec_fixed_byte_reverse ... ok -test tests::compile_lowers_stack_vec_fixed_byte_remove ... ok -test tests::compile_lowers_stack_vec_fixed_byte_truncate ... ok -test tests::compile_lowers_stack_vec_fixed_byte_swap ... ok -test tests::compile_lowers_stack_vec_scalar_capacity ... ok -test tests::compile_lowers_stack_vec_scalar_contains ... ok -test tests::compile_lowers_stack_vec_scalar_pop ... ok -test tests::compile_lowers_stack_vec_scalar_first_last ... ok -test tests::compile_lowers_stack_vec_scalar_insert ... ok -test tests::compile_lowers_stack_vec_scalar_remove ... ok -test tests::compile_lowers_stack_vec_scalar_runtime_push_len_index ... ok -test tests::compile_lowers_stack_vec_scalar_set ... ok -test tests::compile_lowers_stack_vec_scalar_reverse ... ok -test tests::compile_lowers_stack_vec_scalar_truncate ... ok -test tests::compile_lowers_tail_expr_as_action_return ... ok -test tests::compile_lowers_stack_vec_scalar_swap ... ok -test tests::compile_lowers_tail_if_as_action_return ... ok -test tests::compile_lowers_type_hash_without_generic_call ... ok -test tests::compile_lowers_vec_with_capacity_to_stack_collection_new ... ok -test tests::compile_lowers_u128_equality_as_fixed_byte_comparison ... ok -test tests::compile_merges_if_branch_linear_states_conservatively ... ok -test codegen::tests::codegen_rejects_generated_far_jump_scratch_relaxation ... ok -test tests::compile_classifies_hash_committed_output_field_as_guarded ... ok -test tests::compile_lowers_vec_builtins_without_generic_calls ... ok -test tests::compile_lowers_while_statement_into_loop_cfg ... ok -test tests::compile_lowers_zero_builtin_without_generic_call ... ok -test tests::compile_merges_linear_transfers_inside_if_expressions ... ok -test tests::compile_lowers_u128_mutate_delta_with_carry_arithmetic ... ok -test tests::compile_marks_cell_backed_vec_runtime_features ... ok -test tests::compile_metadata_exposes_ckb_type_id_create_output_plan_under_ckb_profile ... ok -test tests::compile_metadata_exposes_declared_invariant_proof_plan ... ok -test tests::compile_merges_linear_transfers_inside_block_tail_if_expressions ... ok -test tests::compile_metadata_exposes_transaction_and_selected_cell_aggregate_invariants ... ok -test tests::compile_metadata_exposes_lock_group_proof_plan_for_lock_entry ... ok -test tests::compile_merges_linear_transfers_inside_match_expressions ... ok -test tests::compile_metadata_with_options_rejects_strict_legacy_capabilities ... ok -test tests::compile_metadata_exposes_aggregate_invariant_primitives_in_proof_plan ... ok -test tests::compile_metadata_proof_plan_preserves_lock_args_source ... ok -test tests::compile_metadata_warns_for_lock_group_transaction_invariant_scope ... ok -test tests::compile_metadata_reports_parameterless_action_entrypoint_selection ... ok -test tests::compile_metadata_declares_molecule_vm_abi ... ok -test tests::compile_normalizes_same_module_qualified_helper_calls ... ok -test tests::compile_path_rejects_duplicate_modules_across_source_roots ... ok -test tests::compile_path_rejects_missing_configured_source_root ... ok -test tests::compile_path_rejects_missing_path_dependency_manifest ... ok -test tests::compile_path_accepts_package_root ... ok -test tests::compile_path_ignores_examples_outside_package_source_roots ... ok -test tests::compile_path_rejects_non_path_dependencies ... ok -test tests::compile_package_import_alias_emits_matching_external_callable ... ok -test tests::compile_metadata_exposes_covenant_proof_plan_for_transfer ... ok -test tests::compile_path_rejects_path_dependency_traversal ... ok -test tests::compile_path_rejects_path_dependency_cycles ... ok -test tests::compile_metadata_with_options_uses_ast_optimizer_for_nonzero_levels ... ok -test tests::compile_path_supports_custom_entry_directory_modules ... ok -test tests::compile_materializes_local_fixed_byte_constants_into_rodata ... ok -test tests::compile_prefers_no_arg_main_for_entry_wrapper ... ok -test tests::compile_path_supports_configured_source_roots_without_src ... ok -test tests::compile_merges_linear_transfers_inside_block_expressions ... ok -test tests::compile_preserves_if_tuple_aggregate_slots ... ok -test tests::compile_preserves_if_array_aggregate_slots ... ok -test tests::compile_preserves_create_instructions_in_assembly ... ok -test tests::compile_preserves_index_and_tuple_projection_in_assembly ... ok -test tests::compile_preserves_match_tuple_aggregate_slots ... ok -test tests::compile_rejects_aggregate_invariant_non_fixed_field ... ok -test tests::compile_rejects_assert_delta_argument_from_cell_read ... ok -test tests::compile_rejects_assert_invariant_as_tail_return_value ... ok -test tests::compile_preserves_consume_and_destroy_instructions_in_assembly ... ok -test tests::compile_rejects_assignment_through_read_only_references ... ok -test tests::compile_rejects_assignment_to_immutable_array_element ... ok -test tests::compile_rejects_assignment_to_immutable_tuple_field ... ok -test tests::compile_preserves_dynamic_witness_cursor_after_lock_args ... ok -test tests::compile_rejects_assignment_to_temporary_field_targets ... ok -test tests::compile_rejects_bad_flow_state_field_type_on_main_path ... ok -test tests::compile_rejects_bare_return_from_value_actions ... ok -test tests::compile_rejects_asymmetric_where_branch_output_constraints ... ok -test tests::compile_rejects_binding_assert_invariant_results ... ok -test tests::compile_rejects_builtin_call_argument_mismatches ... ok -test tests::compile_rejects_bounded_vec_literal_type_mismatch ... ok -test tests::compile_lowers_ckb_hash_commitment_comparison_without_fixed_byte_fail_closed ... ok -test tests::compile_rejects_binding_unit_function_results ... ok -test tests::compile_rejects_cell_metadata_stdlib_on_non_cell_args ... ok -test tests::compile_rejects_duplicate_flow_for_same_state_field ... ok -test tests::compile_rejects_destroy_without_destroy_capability ... ok -test tests::compile_rejects_core_state_transition_edge_not_in_graph ... ok -test tests::compile_rejects_duplicate_stable_type_ids ... ok -test tests::compile_rejects_duplicate_top_level_symbols ... ok -test tests::compile_rejects_dynamic_require_messages ... ok -test tests::compile_rejects_empty_array_length_mismatch ... ok -test tests::compile_rejects_dynamic_assert_invariant_messages ... ok -test tests::compile_rejects_dynamic_initial_flow_create_state ... ok -test tests::compile_rejects_dynamic_unique_identity_field ... ok -test tests::compile_rejects_empty_literal_in_non_vec_context ... ok -test tests::compile_rejects_enum_payload_variants_until_lowering_exists ... ok -test tests::compile_rejects_flow_payload_enum_state_field ... ok -test tests::compile_rejects_flow_by_action_when_explicit_move_uses_different_edge ... ok -test tests::compile_rejects_flow_by_action_without_exact_move_clause ... ok -test tests::compile_rejects_forbidden_unwrap_helpers ... ok -test tests::compile_rejects_flow_receipt_without_state_field ... ok -test tests::compile_rejects_flow_on_plain_struct ... ok -test tests::compile_rejects_helper_functions_that_indirectly_call_impure_actions ... ok -test tests::compile_rejects_heterogeneous_array_literals ... ok -test tests::compile_rejects_if_expression_branch_type_mismatch ... ok -test tests::compile_rejects_input_source_outside_action_cell_params ... ok -test tests::compile_rejects_function_call_argument_mismatches ... ok -test tests::compile_rejects_incomplete_branch_return_paths ... ok -test tests::compile_rejects_impure_helper_functions ... ok -test tests::compile_rejects_invalid_create_field_initializers ... ok -test tests::compile_rejects_invalid_destroy_policy_shapes ... ok -test tests::compile_rejects_invalid_enum_match_patterns ... ok -test tests::compile_rejects_invalid_invariant_assert_expression ... ok -test tests::compile_rejects_invariant_assert_runtime_operation ... ok -test tests::compile_rejects_invariant_without_explicit_trigger_and_scope ... ok -test tests::compile_rejects_local_binding_name_reuse ... ok -test tests::compile_rejects_local_fixed_array_static_oob_read ... ok -test tests::compile_rejects_linear_state_changes_hidden_inside_loops ... ok -test tests::compile_rejects_local_fixed_array_static_oob_write ... ok -test tests::compile_rejects_local_mutable_reference_aliases ... ok -test tests::compile_rejects_missing_action_return_paths ... ok -test tests::compile_rejects_missing_flow_state_create_on_main_path ... ok -test tests::compile_rejects_missing_function_return_paths ... ok -test tests::compile_produces_non_empty_riscv_assembly ... ok -test tests::compile_rejects_non_bool_lock_definitions ... ok -test tests::compile_rejects_non_bool_assert_condition ... ok -test tests::compile_rejects_noop_flow_transition_on_main_path ... ok -test tests::compile_rejects_out_of_range_flow_state_create_on_main_path ... ok -test tests::compile_preserves_schema_backed_parameter_field_access_in_assembly ... ok -test tests::compile_rejects_owned_linear_field_assignment ... ok -test tests::compile_rejects_pure_functions_that_call_locks ... ok -test tests::compile_rejects_local_references_to_linear_roots ... ok -test tests::compile_rejects_read_ref_for_non_cell_backed_types ... ok -test tests::compile_rejects_return_values_from_unit_actions ... ok -test tests::compile_rejects_payload_or_unknown_enum_variant_values ... ok -test tests::compile_rejects_pure_functions_that_call_env_runtime_builtins ... ok -test tests::compile_rejects_pure_functions_that_call_ckb_header_runtime_builtins ... ok -test tests::compile_rejects_pure_functions_that_call_type_hash_runtime_builtin ... ok -test tests::compile_rejects_returning_unit_function_results ... ok -test tests::compile_rejects_state_edge_that_does_not_consume_binding ... ok -test tests::compile_rejects_string_literals_as_runtime_values ... ok -test tests::compile_rejects_unbound_assert_delta_argument ... ok -test tests::compile_rejects_stateful_operations_without_named_linear_cell_operands ... ok -test tests::compile_rejects_reference_escape_boundaries ... ok -test tests::compile_rejects_undeclared_action_state_edge ... ok -test tests::compile_rejects_underdeclared_effect_annotations ... ok -test tests::compile_rejects_underdeclared_effects_through_calls ... ok -test tests::compile_rejects_unknown_functions ... ok -test tests::compile_rejects_unknown_struct_fields ... ok -test tests::compile_rejects_unknown_or_reserved_named_types ... ok -test tests::compile_rejects_underdeclared_effects_through_qualified_calls ... ok -test tests::compile_rejects_unknown_target_profile ... ok -test tests::compile_rejects_unknown_target_during_option_validation ... ok -test tests::compile_rejects_unreachable_statements_after_complete_branch_return ... ok -test tests::compile_rejects_unreachable_statements_after_return ... ok -test tests::compile_rejects_unsupported_optimization_level ... ok -test tests::compile_rejects_unstable_schema_field_names ... ok -test tests::compile_rejects_unstable_callable_parameter_names ... ok -test tests::compile_rejects_unsound_mutable_parameter_forms ... ok -test tests::compile_rejects_untyped_empty_array_literals ... ok -test tests::compile_rejects_unsupported_vec_helper_type_combinations ... ok -test tests::compile_rejects_wrong_qualified_flow_state_field_initializer ... ok -test tests::compile_preserves_read_ref_instructions_in_assembly ... ok -test tests::compile_produces_ckb_elf_without_vm_abi_trailer ... ok -test tests::compile_result_exposes_nested_fixed_molecule_schema_metadata ... ok -test tests::compile_produces_non_empty_riscv_elf ... ok -test tests::compile_rejects_state_transitions_inside_locks ... ok -test tests::compile_result_exposes_schema_layout_metadata ... ok -test tests::compile_rejects_lock_boundary_sources_outside_supported_scope ... ok -test tests::compile_result_validation_rejects_assembly_with_vm_abi_trailer ... ok -test tests::compile_result_validation_rejects_compiler_version_mismatch ... ok -test tests::compile_replace_unique_field_identity_compares_input_and_output ... ok -test tests::compile_result_validation_rejects_constraints_artifact_format_mismatch ... ok -test tests::compile_result_validation_rejects_constraints_artifact_size_mismatch ... ok -test tests::compile_result_validation_rejects_metadata_artifact_hash_mismatch ... ok -test tests::compile_result_validation_rejects_metadata_artifact_format_mismatch ... ok -test tests::compile_result_exposes_scheduler_metadata_sidecar ... ok -test tests::compile_result_validation_accepts_current_outputs ... ok -test tests::compile_result_validation_rejects_mismatched_ckb_output_data_binding ... ok -test tests::compile_reports_equivalent_state_transition_obligation_for_sugar_and_core_forms ... ok -test tests::compile_result_validation_rejects_mismatched_ckb_type_id_create_output_plan ... ok -test tests::compile_result_validation_rejects_metadata_schema_downgrade ... ok -test tests::compile_result_validation_rejects_metadata_artifact_size_mismatch ... ok -test tests::compile_result_validation_rejects_metadata_source_content_hash_mismatch ... ok -test tests::compile_result_validation_rejects_metadata_schema_version_mismatch ... ok -test tests::compile_result_validation_rejects_metadata_source_hash_mismatch ... ok -test tests::compile_result_validation_rejects_metadata_target_profile_mismatch ... ok -test tests::compile_result_validation_rejects_metadata_target_profile_v0_14_abi_mismatch ... ok -test tests::compile_result_validation_rejects_type_id_hash_mismatch ... ok -test tests::compile_result_validation_rejects_molecule_schema_hash_mismatch ... ok -test tests::compile_result_validation_rejects_noncanonical_source_unit_hash ... ok -test tests::compile_result_validation_rejects_missing_metadata_artifact_size ... ok -test tests::compile_result_writes_artifact_to_disk ... ok -test tests::compile_result_validation_rejects_tampered_artifact_hash ... ok -test tests::compile_riscv_elf_accepts_full_width_u64_literals ... ok -test tests::compile_supports_typed_empty_array_literals ... ok -test tests::compile_spills_parameters_and_returns_computed_value ... ok -test tests::compile_tracks_linear_values_returned_from_complete_branches ... ok -test tests::compile_tracks_linear_values_returned_from_tail_if_branches ... ok -test tests::compile_unrolls_local_fixed_array_foreach_without_runtime_indexing ... ok -test tests::compile_unrolls_fixed_param_array_foreach_with_pointer_abi ... ok -test tests::compile_unrolls_local_array_of_tuples_foreach_destructuring ... ok -test tests::compile_lowers_stack_vec_fixed_schema_values ... ok -test tests::compile_surfaces_type_level_hash_type_dsl_metadata ... ok -test tests::compile_tracks_linear_values_inside_aggregate_bindings ... ok -test tests::compile_verifies_create_output_against_consumed_input_field_alias ... ok -test tests::compile_uses_ast_optimizer_for_nonzero_optimization_levels ... ok -test tests::compiled_riscv_elf_contains_exit_trampoline ... ok -test tests::default_output_path_for_package_input_uses_build_dir ... ok -test tests::default_output_path_for_package_input_uses_manifest_out_dir ... ok -test tests::compile_verifies_create_output_against_computed_scalar_stack_value ... ok -test tests::compile_verifies_created_scalar_fields_against_consumed_input_aliases ... ok -test tests::create_output_verifier_accepts_const_lock_hash ... ok -test tests::compile_verifies_created_output_bool_and_u32_fields ... ok -test tests::create_output_verifier_accepts_fixed_byte_params_and_consts ... ok -test tests::compile_verifies_constructed_fixed_width_vec_output ... ok -test tests::entry_abi_constraints_mark_extreme_slot_counts_unsupported ... ok -test tests::compile_verifies_large_output_field_requirements_without_partial_fallback ... ok -test tests::entry_witness_bool_params_are_canonicalized ... ok -test tests::entry_witness_encoder_matches_u64_wrapper_abi ... ok -test tests::entry_witness_encoder_includes_schema_backed_params_as_length_prefixed_bytes ... ok -test tests::entry_witness_encoder_supports_fixed_byte_params ... ok -test tests::dynamic_mutable_schema_transitions_are_checked_after_table_decoding ... ok -test tests::dynamic_schema_fixed_field_access_is_table_decoded ... ok -test tests::compile_unique_script_args_and_singleton_identity_emit_hash_checks ... ok -test tests::internal_calls_keep_outgoing_stack_area_abi_aligned ... ok -test tests::fixed_enum_fields_have_molecule_schema_metadata ... ok -test tests::dynamic_schema_fixed_vec_length_is_table_decoded ... ok -test tests::ir_carries_flow_rules ... ok -test tests::ir_lowers_unit_function_calls_without_result_destinations ... ok -test tests::ir_preserves_function_call_return_types ... ok -test tests::ir_rejects_unknown_call_return_types_without_u64_fallback ... ok -test tests::dynamic_schema_fixed_vec_iteration_is_table_decoded ... ok -test tests::generated_outgoing_stack_reservations_are_psabi_aligned ... ok -test tests::ir_summary_captures_cell_runtime_accesses ... ok -test tests::load_modules_for_input_collects_package_source_roots ... ok -test tests::dynamic_named_output_constraints_are_proven_in_where_block ... ok -test tests::package_entry_must_stay_inside_package_root ... ok -test tests::package_out_dir_must_stay_inside_package_root ... ok -test tests::compile_riscv_elf_accepts_large_schema_field_offsets ... ok -test tests::package_source_roots_must_stay_inside_package_root ... ok -test tests::primitive_compat_predicates_match_validator_modes ... ok -test tests::generic_shared_mutation_does_not_emit_pool_pattern_metadata ... ok -test tests::loaded_artifact_validation_rejects_metadata_artifact_size_mismatch ... ok -test tests::fixed_byte_mutable_state_set_transition_is_checked_under_ckb_profile ... ok -test tests::compile_riscv_elf_accepts_large_stack_offsets ... ok -test tests::resolve_input_path_accepts_package_root_and_manifest ... ok -test tests::scheduler_witness_hex_decode_rejects_invalid_metadata_hex ... ok -test tests::proof_plan_cross_references_matching_action_obligation_for_invariant ... ok -test tests::entry_witness_wrapper_supports_scalar_stack_args ... ok -test tests::tuple_return_abi_rejects_more_than_eight_fields ... ok -test tests::source_unit_disk_verification_accepts_paths_inside_trusted_root ... ok -test tests::named_action_output_create_binding_reuses_declared_output_index ... ok -test tests::source_unit_disk_verification_rejects_paths_outside_trusted_root ... ok -test tests::vm_abi_trailer_detection_requires_complete_zero_reserved_trailer ... ok -test types::tests::block_expression_merges_existing_vec_refinements ... ok -test types::tests::branch_local_consume_is_rejected_until_lifecycle_effects_are_cfg_aware ... ok -test tests::proof_plan_checked_static_excluded_from_on_chain_checked_obligations ... ok -test types::tests::branch_local_create_is_rejected_until_lifecycle_effects_are_cfg_aware ... ok -test types::tests::byte_string_literal_type_uses_actual_length ... ok -test types::tests::call_arguments_do_not_coerce_mut_ref_to_ref ... ok -test types::tests::check_without_resolver_rejects_imports ... ok -test types::tests::compound_assign_rejects_implicit_narrowing ... ok -test types::tests::compound_assign_uses_numeric_binary_rules ... ok -test types::tests::const_initializers_allow_supported_literals ... ok -test types::tests::const_initializers_reject_cell_backed_types ... ok -test types::tests::const_initializers_reject_cell_lifecycle_expressions ... ok -test types::tests::const_initializers_reject_computed_expressions ... ok -test types::tests::constant_narrowing_casts_must_fit ... ok -test types::tests::contextual_integer_literals_fit_declared_widths ... ok -test types::tests::cyclic_schema_type_dependencies_are_rejected ... ok -test types::tests::duplicate_lifecycle_binding_is_rejected_until_effects_are_cfg_aware ... ok -test types::tests::expected_type_does_not_widen_non_literal_abi_arg_boundary ... ok -test types::tests::expected_type_does_not_widen_non_literal_let_boundary ... ok -test types::tests::expected_type_does_not_widen_non_literal_field_boundary ... ok -test tests::v014_runtime_helpers_fail_closed_when_not_executable ... ok -test tests::parameterized_entrypoint_emits_witness_entry_wrapper ... ok -test types::tests::expected_type_does_not_widen_non_literal_return_boundary ... ok -test types::tests::explicit_cast_can_cross_integer_width_boundary ... ok -test types::tests::expression_branch_unreachable_code_is_rejected_by_typechecker ... ok -test types::tests::generic_reference_detection_uses_type_structure ... ok -test tests::strict_audit_codegen_emits_only_aligned_stack_pointer_deltas ... ok -test tests::proof_plan_marks_invariant_action_evidence_as_non_exhaustive ... ok -test types::tests::if_statement_merges_matching_vec_refinements ... ok -test types::tests::if_statement_rejects_divergent_vec_refinements ... ok -test types::tests::if_statement_rejects_one_sided_vec_refinement ... ok -test types::tests::imported_and_qualified_names_compare_as_same_type ... ok -test types::tests::if_expression_preserves_typed_vec_result_with_empty_constructor_branch ... ok -test types::tests::imported_type_ids_must_not_collide_in_visible_module_scope ... ok -test types::tests::invalid_schema_field_types_are_not_registered_as_valid_fields ... ok -test types::tests::imported_token_type_is_treated_as_linear ... ok -test types::tests::numeric_named_type_equality_is_commutative ... ok -test types::tests::numeric_type_equality_respects_width ... ok -test types::tests::lifecycle_capability_gates_reject_undeclared_kernel_effects ... ok -test types::tests::qualified_identifier_must_resolve_to_value ... ok -test types::tests::mixed_width_arithmetic_and_ordering_are_rejected ... ok -test types::tests::preserve_rejects_mismatched_field_types ... ok -test tests::ordered_named_output_create_constraints_are_checked_in_body_order ... ok -test types::tests::match_requires_enum_scrutinee ... ok -test types::tests::non_tail_linear_expression_statements_are_rejected ... ok -test types::tests::recursive_enum_payloads_are_rejected ... ok -test types::tests::require_block_rejects_lifecycle_stdlib_call ... ok -test types::tests::require_rejects_nested_cell_operation ... ok -test types::tests::imported_linear_argument_is_marked_consumed_after_call ... ok -test types::tests::require_block_rejects_assignment_expression ... ok -test types::tests::statically_visible_division_by_zero_is_rejected ... ok -test types::tests::stdlib_claim_output_requires_complete_field_coverage ... ok -test types::tests::stdlib_claim_output_requires_declared_claim_output_type ... ok -test types::tests::stdlib_claim_rejects_declared_output_type_mismatch ... ok -test types::tests::stdlib_claim_rejects_extra_arguments ... ok -test types::tests::stdlib_claim_requires_explicit_output_and_lock_arguments ... ok -test types::tests::stdlib_claim_rejects_non_receipt_input ... ok -test types::tests::stdlib_settle_requires_explicit_output_and_lock_arguments ... ok -test types::tests::stdlib_transfer_output_requires_complete_field_coverage ... ok -test types::tests::stdlib_transfer_rejects_extra_arguments ... ok -test types::tests::strict_mode_rejects_imported_legacy_capabilities ... ok -test types::tests::typed_vec_with_capacity_uses_declared_element_type ... ok -test types::tests::launch_module_type_checks_with_registered_imports ... ok -test types::tests::u128_ordering_and_arithmetic_still_rejected_on_widening ... ok -test types::tests::unsigned_integer_negation_is_rejected ... ok -test wasm::tests::wasm_audit_reports_audit_only_for_type_only_module ... ok -test wasm::tests::wasm_compiler_rejects_pure_action_modules ... ok -test wasm::tests::wasm_encoder_emits_magic_version_and_status_custom_section ... ok -test wasm::tests::wasm_runtime_instantiates_metadata_module_but_refuses_calls ... ok -test types::tests::vec_type_arguments_are_validated ... ok -test types::tests::tail_match_expressions_are_valid_return_values ... ok -test types::tests::unsupported_u128_arithmetic_is_rejected ... ok -test types::tests::widening_boundary_matrix ... ok -test tests::u128_mutable_state_transition_with_u64_delta_is_checked ... ok -test tests::payload_enum_fields_use_dynamic_molecule_schema_metadata ... ok -test tests::optimized_entry_lock_keeps_inlined_schema_pointer_field_access_checked ... ok -test codegen::tests::internal_assembler_relaxes_far_conditional_branch_with_long_jump ... ok -test codegen::tests::internal_assembler_encodes_far_unconditional_jump ... ok -test codegen::tests::bundled_example_codegen_mnemonics_are_declared ... ok - -test result: ok. 776 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.90s - - -running 7 tests -test adversarial_parser_preserves_operator_precedence_in_ambiguous_sequences ... ok -test adversarial_parser_rejects_deep_unary_expression_without_panicking ... ok -test adversarial_parser_binds_else_to_nearest_if ... ok -test adversarial_0_13_rejects_invalid_hash_type_dsl ... ok -test adversarial_parser_rejects_deep_nested_control_flow_without_panicking ... ok -test adversarial_integer_literals_fail_closed_on_lexical_and_contextual_overflow ... ok -test adversarial_0_13_rejects_unsupported_generic_collection_surfaces ... ok - -test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s - - -running 11 tests -test runtime_u64_helpers_fail_closed_before_value_use ... ok -test snapshot_simple_action_assembly ... ok -test runtime_void_helpers_fail_closed_before_continuing ... ok -test snapshot_lock_args_assembly ... ok -test snapshot_type_id_create_output_assembly ... ok -test snapshot_spawn_ipc_executable_status_checked_assembly ... ok -test runtime_witness_helpers_fail_closed_before_pointer_use ... ok -test snapshot_witness_schema_syscall_assembly ... ok -test snapshot_collection_lowering_assembly ... ok -test snapshot_blake2b_helper_assembly ... ok -test snapshot_assemblies_contain_no_leaked_overflow_diagnostics ... ok - -test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.26s - - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s - - -running 86 tests -test cellc_add_and_remove_subcommands_honor_dev_path_and_json ... ok -test cellc_add_git_requires_full_rev_and_records_pin ... ok -Check succeeded - Target profile: ckb - Checked: package default (RISC-V assembly) -test cellc_check_denies_metadata_only_declared_invariant ... ok -test cellc_check_accepts_ckb_profile_timepoint ... ok -test cellc_check_accepts_pure_ckb_target_profile ... ok -test cellc_abi_subcommand_explains_entry_witness_layout ... ok -test cellc_build_uses_manifest_policy_before_writing_artifacts ... ok -test cellc_action_build_emits_builder_plan_json ... ok -Build complete - Artifact format: RISC-V assembly - Target profile: ckb - Output: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp1eORVW/build/main.s - Metadata: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp1eORVW/build/main.s.meta.json -test cellc_check_production_rejects_incomplete_output_verification ... ok -test cellc_check_production_rejects_fail_closed_runtime_paths ... ok -test cellc_build_accepts_pure_ckb_target_profile_without_vm_abi_trailer ... ok -test cellc_check_reports_claim_source_predicate_blocker_class ... ok -test cellc_check_can_reject_runtime_required_obligations ... ok -test cellc_check_denies_checked_partial_proof_plan_gap ... ok -test cellc_build_and_check_subcommands_use_package_flow ... ok -test cellc_clean_subcommand_supports_json_summary ... ok -test cellc_check_accepts_u128_mutable_state_transition_with_u64_delta ... ok -test cellc_ckb_hash_emits_default_blake2b_vector ... ok -test cellc_check_all_targets_checks_asm_and_elf_without_writing_artifacts ... ok -test cellc_check_reports_linear_collection_ownership_blocker_class ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [5d, 8b, d, ed, a5, 24, 99, 9f, c3, fe, 29, 67, 78, 19, 2a, 46, 8f, a3, b5, 44, cb, 36, cf, cc, e2, 10, 50, 24, 59, 4b, 5b, 37] - Output: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp3DGMcN/artifacts/main.s - Metadata: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp3DGMcN/artifacts/main.s.meta.json -test cellc_cli_target_overrides_manifest_build_target ... ok -test cellc_check_reports_settle_finalization_blocker_class ... ok -test cellc_check_uses_manifest_policy_defaults ... ok -test cellc_check_reports_resource_conservation_blocker_class ... ok -test cellc_doc_subcommand_generates_markdown_docs ... ok -test cellc_constraints_subcommand_surfaces_ckb_deployment_manifest ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [9f, aa, 3c, b9, 5, 1b, a7, 19, e4, ea, e4, 1, 79, 7, 11, 89, 7f, 40, ba, 26, 7e, 86, ba, 8c, d5, a3, a, 4d, 3, 45, eb, 54] - Output: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpG6FydO/app_pkg/build/main.s - Metadata: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpG6FydO/app_pkg/build/main.s.meta.json -test cellc_check_reports_explicit_output_binding_without_mutable_state_blockers ... ok -test cellc_compiles_package_with_local_path_dependency ... ok -test cellc_entry_witness_subcommand_emits_parameterized_witness_json ... ok -test cellc_explain_profile_reports_ckb_v0_14_contract ... ok -test cellc_entry_witness_subcommand_encodes_schema_backed_params ... ok -test cellc_entry_witness_subcommand_rejects_wrong_width_fixed_bytes ... ok -test cellc_explain_proof_reports_covenant_proof_plan ... ok -test cellc_explain_proof_reports_declared_invariant ... ok -test cellc_explain_subcommand_reports_runtime_error ... ok -test cellc_explain_proof_warns_for_lock_group_transaction_scope ... ok -Formatting complete - Updated 1 file(s) -test cellc_errors_include_runtime_ecode_when_policy_failure_maps_to_runtime_registry ... ok -test cellc_info_subcommand_supports_json_summary ... ok -test cellc_init_subcommand_supports_json_summary ... ok -test cellc_lsp_flag_rejects_trailing_arguments ... ok -test cellc_fmt_subcommand_formats_sources ... ok -test cellc_new_subcommand_supports_json_summary_and_vcs_none ... ok -test cellc_explain_proof_human_reports_macro_provenance ... ok -test cellc_rejects_registry_package_dependencies_fail_closed ... ok -test cellc_explain_proof_reports_invariant_action_coverage_match ... ok -test cellc_install_path_updates_lockfile_and_remove_prunes_it ... ok -test cellc_rejects_external_dependency_function_calls_until_linking_exists ... ok -test cellc_run_subcommand_without_vm_runner_degrades_gracefully ... ok -test cellc_test_subcommand_rejects_conflicting_expectations ... ok -test cellc_test_subcommand_rejects_empty_expected_error_line_text ... ok -test cellc_metadata_subcommand_emits_lowering_runtime_json ... ok -test cellc_rejects_underdeclared_effects_from_path_dependency_calls ... ok -test cellc_explain_proof_summary_reports_fail_closed_diagnostics ... ok -test cellc_test_subcommand_rejects_missing_expected_error_text ... ok -test cellc_test_subcommand_rejects_unknown_directives ... ok -test cellc_test_subcommand_rejects_wrong_expected_error_line ... ok -test cellc_test_subcommand_rejects_missing_entrypoint_metadata ... ok -test cellc_check_reports_pool_invariant_policy_families ... ok -test cellc_opt_report_compares_all_optimization_levels ... ok -test cellc_test_subcommand_supports_expected_compile_failures ... ok -test cellc_test_subcommand_compiles_test_sources ... ok -test cellc_test_subcommand_supports_expected_error_line_directive ... ok -test cellc_test_subcommand_rejects_missing_runtime_metadata ... ok -test cellc_new_subcommand_initializes_git_by_default ... ok -test cellc_check_reports_transaction_invariant_checked_subconditions ... ok -test cellc_scheduler_plan_consumes_shared_touch_hints ... ok -test cellc_test_subcommand_supports_entrypoint_metadata_directives ... ok -test cellc_top_level_primitive_strict_rejects_legacy_capabilities ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [5d, 8b, d, ed, a5, 24, 99, 9f, c3, fe, 29, 67, 78, 19, 2a, 46, 8f, a3, b5, 44, cb, 36, cf, cc, e2, 10, 50, 24, 59, 4b, 5b, 37] - Output: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpZo3tql/artifacts/main.s - Metadata: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpZo3tql/artifacts/main.s.meta.json -test cellc_uses_manifest_build_out_dir_for_package_input ... ok -test cellc_test_subcommand_supports_runtime_metadata_directives ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpC02XyB/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpC02XyB/sample.s.meta.json -test cellc_test_subcommand_supports_target_directive ... ok -test cellc_test_subcommand_supports_policy_directives ... ok -test cellc_top_level_accepts_primitive_strict_for_kernel_effect_capabilities ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpYNDiwB/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpYNDiwB/sample.s.meta.json -success: compiled successfully - Artifact format: RISC-V ELF - Target profile: ckb - Artifact hash: [cf, 7, cf, ac, d0, a, 43, a3, a8, cc, 8b, 6e, 66, e1, 29, b2, 32, 60, 2f, 76, a3, 55, 4d, 52, d5, 38, 51, 1f, c8, b, 49, 2] - Output: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpSgHO62/artifacts/main.elf - Metadata: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpSgHO62/artifacts/main.elf.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp8cifZl/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp8cifZl/sample.s.meta.json -test cellc_uses_manifest_build_target_by_default ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpvCyiJF/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpvCyiJF/sample.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [a7, d4, 78, cc, c3, f5, cd, 81, cd, de, 51, 44, ee, 83, 4d, 64, 46, df, bd, 40, 58, 5f, 51, 6c, d1, 56, 6b, b7, 44, 9d, 96, 8d] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpjONY9q/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpjONY9q/sample.s.meta.json -test cellc_verify_artifact_accepts_matching_sidecar ... ok -test cellc_verify_artifact_rejects_metadata_schema_downgrade ... ok -test cellc_verify_artifact_rejects_noncanonical_source_unit_hash ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpmBH3ky/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpmBH3ky/sample.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpHMX3Om/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpHMX3Om/sample.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp6TxYwf/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp6TxYwf/sample.s.meta.json -test cellc_writes_requested_output_file ... ok -test cellc_verify_artifact_enforces_policy_flags ... ok -test cellc_verify_artifact_rejects_tampered_source_when_requested ... ok -test cellc_verify_artifact_rejects_tampered_artifact ... ok -test cellc_verify_artifact_primitive_strict_rechecks_disk_sources ... ok -test cellc_verify_artifact_enforces_expected_hashes ... ok -test cellc_explain_generics_reports_checked_vec_instantiations ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [79, a1, 5d, 7e, f7, 1f, 9a, 64, 89, da, 9e, 8b, a8, 90, b6, 15, f0, b5, 61, d1, 80, 6b, 39, 9f, f0, 5a, a4, 4d, 0, 5, 41, 3c] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpLsLJMI/amm_pool.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpLsLJMI/amm_pool.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [2a, 2, ca, c2, fd, b6, 4a, 53, 9e, 26, cb, a1, 31, 69, ab, f3, 1d, c1, 42, d, 18, d3, fd, 1d, 92, b7, a, 55, c6, d, 87, df] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpLsLJMI/launch.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpLsLJMI/launch.s.meta.json -test cellc_check_reports_checked_pool_invariant_families_without_runtime_blockers ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [8b, d7, 59, d4, b6, d1, 6, 8b, 97, 0, fd, e5, df, 72, ec, a6, 99, bf, 20, 34, 90, 55, b2, 17, 6d, 48, 55, 9e, ec, ed, 11, 2b] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpLsLJMI/multisig.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpLsLJMI/multisig.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [cd, d0, f5, 74, b7, 9d, 8e, 7d, 79, 50, 6a, cf, 3e, 13, b, 53, 5c, b9, 7f, 8c, d1, 1f, 88, bf, 1b, 8a, 3c, 3b, 34, 45, c6, 4e] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpLsLJMI/nft.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpLsLJMI/nft.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [fd, 14, d8, d9, e, 2c, e8, 71, 98, aa, e2, b6, b4, fc, b8, 93, aa, 84, 66, 2f, 21, 2e, 9d, 26, 5, 63, 5d, 74, 55, fd, 56, 87] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpLsLJMI/timelock.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpLsLJMI/timelock.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [3c, bd, 90, ed, de, 2c, 8d, 8c, 97, 1, a4, d9, 9, dc, 3d, bd, 22, 6b, 5b, 39, e7, 3e, 59, 9a, 5d, e1, 2c, 13, 61, 4d, 32, 46] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpLsLJMI/token.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpLsLJMI/token.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [2a, 7, 99, 55, e2, cb, e6, 1b, 39, 63, db, fc, 1, 63, fd, 57, 38, 6, a4, 7, ad, b2, 5d, 5c, f1, de, 41, e5, 2a, 29, 1, e5] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpLsLJMI/vesting.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpLsLJMI/vesting.s.meta.json -test cellc_compiles_bundled_examples_to_requested_outputs ... ok - -test result: ok. 86 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.92s - - -running 26 tests -test ckb_scoped_entry_keeps_called_action_helpers ... ok -test launch_seed_pool_composition_is_scheduler_visible ... ok -test registry_example_uses_bounded_local_vec_helpers_without_collection_debt ... ok -test amm_pool_input_output_params_are_scheduler_visible ... ok -test release_examples_are_free_of_placeholder_hashes_and_formatter_artifacts ... ok -test registry_example_with_insert_contains_compiles_to_elf ... ok -test nft_core_actions_expose_action_specific_builder_metadata ... ok -test token_cell_invariant_appears_in_proof_plan ... ok -test order_book_language_example_uses_local_vec_helpers_without_collection_debt ... ok -test stdlib_language_example_compiles_with_all_patterns ... ok -test token_mint_authority_input_output_binding_is_explicit ... ok -test v0_15_scoped_invariant_example_compiles_and_produces_proof_plan ... ok -test v0_15_identity_lifecycle_example_compiles_and_produces_proof_plan ... ok -test vesting_phase2_remaining_obligations_are_explicit ... ok -test vesting_read_ref_params_are_scheduler_visible ... ok -test multisig_core_actions_expose_threshold_flow_metadata ... ok -test timelock_core_actions_expose_time_and_release_metadata ... ok -test canonical_examples_compile_under_primitive_strict_015 ... ok -test canonical_examples_are_the_single_checked_in_business_source ... ok -test bundled_examples_emit_molecule_schema_manifest_report ... ok -test bundled_examples_compile_to_non_empty_assembly ... ok -test bundled_examples_backend_shape_report_serializes ... ok -test bundled_examples_stay_within_backend_shape_budgets ... ok -test bundled_examples_stay_near_backend_shape_release_baseline ... ok -test all_checked_in_cell_examples_compile ... ok -test bundled_examples_compile_to_elf ... ok - -test result: ok. 26 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 8.07s - - -running 7 tests -test fuzzy_oversized_static_widths_are_controlled_errors ... ok -test fuzzy_unicode_hex_inputs_are_controlled_errors ... ok -test fuzzy_entry_witness_encoding_never_panics ... ok -test fuzzy_metadata_tampering_never_panics ... ok -test fuzzy_mutated_sources_never_panic ... ok -test fuzzy_lsp_incremental_edits_never_panic ... ok -test fuzzy_semantic_codegen_mutations_reach_assembly ... ok - -test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.51s - - -running 4 tests -test ickb_diff_matrix_is_partial_and_consistent_with_model_fixtures ... ok -test ickb_positive_fixtures_pass_model_verifier ... ok -test ickb_negative_fixtures_fail_for_expected_invariant ... ok -test ickb_benchmark_specs_compile_and_expose_expected_entries ... ok - -test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.30s - - -running 1 test -test syntax_combo_quick_matrix_is_cargo_test_visible ... FAILED - -failures: - ----- syntax_combo_quick_matrix_is_cargo_test_visible stdout ---- - -thread 'syntax_combo_quick_matrix_is_cargo_test_visible' (8199859) panicked at tests/syntax_combo.rs:15:5: -syntax combo quick runner failed -status: exit status: 1 -stdout: - -stderr: -Traceback (most recent call last): - File "/Users/arthur/RustroverProjects/CellScript/scripts/cellscript_syntax_combo_audit.py", line 20, in - import tomllib -ModuleNotFoundError: No module named 'tomllib' - -note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace - - -failures: - syntax_combo_quick_matrix_is_cargo_test_visible - -test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.18s - - -== stderr == - Checking cellscript v0.16.0 (/Users/arthur/RustroverProjects/CellScript) - Finished `dev` profile [unoptimized + debuginfo] target(s) in 10.06s - Compiling cellscript v0.16.0 (/Users/arthur/RustroverProjects/CellScript) - Finished `test` profile [unoptimized + debuginfo] target(s) in 6.82s - Running unittests src/lib.rs (target/debug/deps/cellscript-198f0ba9a296fb91) - Running tests/adversarial_0_13.rs (target/debug/deps/adversarial_0_13-87ca0b7751a0cf60) - Running tests/assembly_snapshots.rs (target/debug/deps/assembly_snapshots-a1b0ee4291a50be7) - Running tests/ckb_acceptance.rs (target/debug/deps/ckb_acceptance-546e6523e51ab114) - Running tests/cli.rs (target/debug/deps/cli-cd9b4a3e7ed668b4) - Running tests/examples.rs (target/debug/deps/examples-885631b36bce043e) - Running tests/fuzzy_debug.rs (target/debug/deps/fuzzy_debug-a3c500396cf14cf4) - Running tests/ickb_benchmark.rs (target/debug/deps/ickb_benchmark-d0fd214d43bb34ab) - Running tests/syntax_combo.rs (target/debug/deps/syntax_combo-b9abeffd268b17da) -error: test failed, to rerun pass `-p cellscript --test syntax_combo` diff --git a/.cap/logs/1780406272-60047.log b/.cap/logs/1780406272-60047.log deleted file mode 100644 index e2365396..00000000 --- a/.cap/logs/1780406272-60047.log +++ /dev/null @@ -1,11 +0,0 @@ -== stdout == - -running 1 test -test syntax_combo_quick_matrix_is_cargo_test_visible ... ok - -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.47s - - -== stderr == - Finished `test` profile [unoptimized + debuginfo] target(s) in 0.05s - Running tests/syntax_combo.rs (target/debug/deps/syntax_combo-b9abeffd268b17da) diff --git a/.cap/logs/1780406289-61554.log b/.cap/logs/1780406289-61554.log deleted file mode 100644 index 2c352559..00000000 --- a/.cap/logs/1780406289-61554.log +++ /dev/null @@ -1,1123 +0,0 @@ -== stdout == - -running 776 tests -test cli::commands::tests::test_command_execution ... ok -test ckb_hash_tests::ckb_blake2b256_matches_blank_hash_vector ... ok -test cli::commands::tests::invalid_parser_mapping_returns_error_instead_of_panicking ... ok -test cli::commands::tests::expected_metadata_hash_comparison_is_case_sensitive ... ok -test codegen::assembler::tests::strict_audit_internal_assembler_oracle_for_core_instruction_bytes ... ok -test codegen::assembler::tests::strict_audit_li_split_handles_negative_32_bit_boundaries ... ok -test cli::commands::tests::production_policy_finds_evidence_less_on_chain_checked_proof_plan_gap ... ok -test cli::commands::tests::production_policy_finds_evidence_less_checked_runtime_proof_plan_gap ... ok -test codegen::assembler::tests::strict_audit_riscv_immediate_boundaries_are_enforced ... ok -test codegen::calls::tests::fixed_u64_le_width_accepts_hashes_and_byte_arrays ... ok -test codegen::calls::tests::canonical_type_names_strip_reference_wrappers ... ok -test codegen::cell_ops::tests::consumed_operand_var_accepts_named_cell_operands_only ... ok -test codegen::calls::tests::packed_hash_width_uses_codegen_fixed_byte_type_rules ... ok -test codegen::cell_ops::tests::destroy_absence_scan_is_limited_to_singleton_and_type_id_unique_policies ... ok -test codegen::cell_ops::tests::identity_and_destruction_policy_labels_are_stable ... ok -test codegen::expr::tests::divisor_nonzero_guard_fails_closed_on_zero ... ok -test codegen::frame::tests::large_addi_uses_single_addi_for_small_immediates ... ok -test codegen::frame::tests::large_addi_materializes_out_of_range_immediates ... ok -test codegen::expr::tests::bool_canonical_check_emits_zero_one_guard ... ok -test codegen::frame::tests::stack_access_helpers_emit_sp_relative_instructions ... ok -test codegen::runtime::tests::checked_runtime_status_register_defaults_to_a1_for_unknown_helpers ... ok -test codegen::runtime::tests::ckb_runtime_syscall_abi_matches_declared_constants ... ok -test codegen::runtime::tests::runtime_helper_classification_tracks_checked_and_hash_helpers ... ok -test codegen::schema::tests::aggregate_field_layouts_track_tuple_offsets ... ok -test codegen::schema::tests::fixed_byte_constants_materialize_little_endian_bytes ... ok -test codegen::schema::tests::fixed_width_helpers_classify_scalar_and_byte_storage ... ok -test codegen::assembler::tests::strict_audit_elf_header_and_segments_are_internally_consistent ... ok -test codegen::tests::consumed_schema_params_use_loaded_cell_size_for_field_checks ... ok -test codegen::tests::dynamic_syscall_index_is_copied_before_large_stack_staging ... ok -test codegen::tests::cell_operation_identity_helpers_stay_in_cell_ops ... ok -test codegen::tests::explicit_external_toolchain_paths_are_strict ... ok -test cli::commands::tests::ckb_hash_file_rejects_inputs_above_limit ... ok -test codegen::tests::generated_large_offsets_are_normalized_before_assembly ... ok -test codegen::tests::generated_collection_assembly_is_internal_assembler_clean ... ok -test codegen::tests::generated_public_assembly_mnemonics_are_declared ... ok -test codegen::tests::internal_assembler_encodes_emitted_instruction_surface ... ok -test codegen::tests::generated_stdlib_assembly_is_internal_assembler_clean ... ok -test codegen::tests::internal_assembler_encodes_full_width_li_literals ... ok -test codegen::tests::internal_assembler_keeps_near_unconditional_jump_compact ... ok -test codegen::tests::internal_assembler_encodes_register_conditional_branches ... ok -test codegen::tests::internal_assembler_rejects_intentionally_unsupported_mnemonics ... ok -test codegen::tests::internal_assembler_rejects_unresolved_call_targets ... ok -test codegen::tests::generated_functions_use_shared_epilogue_tail ... ok -test codegen::tests::large_addi_avoids_clobbering_source_register ... ok -test codegen::tests::binary_codegen_materializes_narrow_integer_constants ... ok -test codegen::tests::machine_cfg_tracks_call_edges_to_local_helpers ... ok -test codegen::tests::machine_layout_order_rejects_missing_duplicate_or_unknown_blocks ... ok -test codegen::assembler::tests::strict_audit_relaxed_conditional_branch_within_jal_range_preserves_registers ... ok -test codegen::tests::machine_layout_plan_builds_register_conditional_branch_blocks ... ok -test codegen::tests::machine_layout_plan_rejects_branch_target_outside_text ... ok -test codegen::tests::machine_layout_plan_builds_explicit_machine_blocks ... ok -test codegen::tests::machine_reachability_uses_entry_label_not_every_global ... ok -test codegen::tests::outgoing_stack_arg_area_is_16_byte_aligned_at_call_boundaries ... ok -test codegen::tests::division_codegen_guards_zero_divisors ... ok -test codegen::tests::read_ref_runtime_fallback_records_cell_buffer_state ... ok -test codegen::tests::dynamic_molecule_fixed_field_codegen_checks_full_header_and_exact_span ... ok -test codegen::tests::register_contract_allows_only_entry_wrapper_writes_to_direct_registers ... ok -test codegen::tests::rv64_li_boundary_values_materialize_correct_bits ... ok -test codegen::tests::dynamic_molecule_vector_field_access_validates_full_table_offsets ... ok -test codegen::tests::semantic_molecule_field_access_uses_validated_api_gate ... ok -test codegen::tests::sp_addi_large_offsets_clobber_only_destination_register ... ok -test codegen::tests::state_transition_edges_use_explicit_consumed_binding ... ok -test codegen::tests::strict_audit_outgoing_stack_args_are_staged_inside_current_frame ... ok -test codegen::tests::type_hash_missing_output_buffer_slots_report_compile_error ... ok -test codegen::tests::type_hash_missing_param_slots_report_compile_error ... ok -test codegen::tests::u128_const_without_fixed_storage_reports_compile_error ... ok -test codegen::tests::narrow_arithmetic_codegen_truncates_to_declared_width ... ok -test codegen::tests::runtime_cast_codegen_checks_narrowing_and_bool_canonicality ... ok -test codegen::tests::unaligned_scalar_load_large_offsets_preserve_live_accumulator ... ok -test codegen::tests::unrepresentable_memory_load_offsets_report_compile_error ... ok -test codegen::tests::machine_layout_plan_reports_branch_relaxation_metrics ... ok -test codegen::tests::unrepresentable_stack_offsets_report_compile_error ... ok -test debug::tests::test_debug_info_generator ... ok -test debug::tests::test_dwarf_generation ... ok -test debug::tests::test_line_table ... ok -test debug::tests::test_type_registration ... ok -test docgen::tests::docgen_emits_flat_pool_runtime_input_requirements ... ok -test docgen::tests::docgen_emits_markdown_for_action ... ok -test codegen::tests::schema_ref_call_preserves_schema_abi_length ... ok -test docgen::tests::docgen_emits_transaction_invariant_checked_subconditions ... ok -test docgen::tests::docgen_html_escapes_module_and_item_text ... ok -test error::tests::caret_padding_starts_at_span_column ... ok -test error::tests::caret_width_counts_characters_not_bytes ... ok -test flow::tests::consumed_flow_tracking_follows_expression_aliases ... ok -test fmt::tests::format_action_transition_block_for_multiple_edges ... ok -test codegen::tests::internal_assembler_relaxes_out_of_range_conditional_branch ... ok -test fmt::tests::format_preserves_single_element_tuple_expression ... ok -test fmt::tests::format_indents_preserve_fields_inside_expression_block ... ok -test fmt::tests::format_preserves_type_policy_metadata ... ok -test fmt::tests::format_round_trips_inline_if_tuple_expression ... ok -test fmt::tests::format_round_trips_multiline_expression_block ... ok -test fmt::tests::format_round_trips_preserve_block ... ok -test fmt::tests::format_round_trips_simple_module ... ok -test fmt::tests::format_round_trips_require_block ... ok -test fmt::tests::format_single_expr_require_block_uses_compact_form ... ok -test fmt::tests::format_uses_canonical_assert_and_no_const_semicolon ... ok -test fmt::tests::format_round_trips_stdlib_lifecycle_field_block ... ok -test fmt::tests::format_uses_field_shorthand_when_value_matches_name ... ok -test incremental::tests::clean_cache_rejects_overflowing_max_age ... ok -test incremental::tests::load_cache_drops_units_with_paths_outside_trusted_root ... ok -test incremental::tests::clean_cache_skips_output_paths_outside_trusted_root ... ok -test incremental::tests::test_dependency_graph ... ok -test incremental::tests::test_change_detector ... ok -test codegen::tests::stack_pointer_offsets_are_emitted_through_helpers ... ok -test ir::tests::all_diverging_match_expression_does_not_leave_unreachable_join ... ok -test incremental::tests::test_incremental_compiler ... ok -test ir::tests::assert_in_pure_function_lowers_failure_to_abort_terminator ... ok -test ir::tests::constant_cast_rejects_out_of_range_u128_narrowing ... ok -test ir::tests::binary_arithmetic_result_type_preserves_left_operand_width ... ok -test ir::tests::contextual_integer_binary_operands_lower_to_peer_width ... ok -test ir::tests::ir_generation_aggregates_lowering_errors_with_source_spans ... ok -test ir::tests::exhaustive_enum_match_unmatched_path_lowers_to_abort_terminator ... ok -test ir::tests::ir_type_value_kind_never_derives_status_kinds ... ok -test codegen::tests::vm2_syscall_helpers_emit_executable_status_checked_wrappers ... ok -test ir::tests::ir_straight_line_lifecycle_certificate_rejects_duplicate_consume_without_typecheck ... ok -test ir::tests::poison_lowering_keeps_value_invalid_while_block_stays_live ... ok -test ir::tests::ir_straight_line_lifecycle_certificate_rejects_branch_local_create_without_typecheck ... ok -test ir::tests::logical_operators_lower_as_short_circuit_control_flow ... ok -test ir::tests::reference_and_deref_unary_result_types_match_ast_types ... ok -test ir::tests::mixed_width_expression_local_widening_lowers_as_explicit_casts ... ok -test ir::tests::require_block_lowers_to_atomic_requires ... ok -test ir::tests::status_boundary_ir_verifier_allows_domain_u64_return_tuple_and_call_argument ... ok -test ir::tests::status_boundary_ir_verifier_allows_unit_runtime_helper_when_status_is_checked_by_codegen_boundary ... ok -test ir::tests::runtime_narrowing_cast_lowers_as_cast_instruction ... ok -test ir::tests::status_boundary_ir_verifier_rejects_dropped_raw_syscall_status ... ok -test ir::tests::status_boundary_ir_verifier_rejects_raw_syscall_status_as_domain_call_argument ... ok -test ir::tests::status_boundary_ir_verifier_rejects_raw_syscall_status_in_tuple_field ... ok -test ir::tests::preserve_sugar_populates_preserved_fields ... ok -test ir::tests::status_boundary_ir_verifier_rejects_unit_runtime_helper_status_stored_as_domain_u64 ... ok -test ir::tests::status_boundary_ir_verifier_rejects_raw_syscall_status_produced_without_checked_consumer ... ok -test ir::tests::status_boundary_ir_verifier_rejects_raw_syscall_status_returned_as_domain_u64 ... ok -test ir::tests::status_boundary_ir_verifier_rejects_raw_syscall_status_stored_as_dsl_local ... ok -test ir::tests::strict_audit_ir_lowering_records_instruction_level_provenance ... ok -test ir::tests::strict_audit_ir_verifier_rejects_constant_destination_width_mismatch ... ok -test ir::tests::strict_audit_ir_verifier_rejects_empty_body_blocks ... ok -test ir::tests::stdlib_claim_lowers_to_consumed_receipt_and_locked_declared_output ... ok -test ir::tests::stdlib_settle_lowers_to_consumed_input_and_locked_output ... ok -test ir::tests::strict_audit_ir_verifier_rejects_extra_consume_set_metadata ... ok -test ir::tests::stdlib_transfer_lowers_to_single_consumed_input_and_locked_output ... ok -test ir::tests::strict_audit_ir_verifier_rejects_poisoned_load_const ... ok -test ir::tests::strict_audit_ir_verifier_rejects_missing_terminator_target ... ok -test ir::tests::strict_audit_ir_verifier_rejects_missing_create_set_metadata ... ok -test ir::tests::strict_audit_ir_verifier_rejects_poisoned_lowering_module ... ok -test ir::tests::strict_audit_ir_verifier_rejects_poisoned_lowering_operand ... ok -test ir::tests::strict_audit_ir_verifier_rejects_use_not_defined_on_all_paths ... ok -test ir::tests::strict_audit_ir_verifier_rejects_stale_write_intents_metadata ... ok -test ir::tests::strict_audit_ir_verifier_reports_instruction_provenance ... ok -test ir::tests::strict_audit_schema_field_accesses_are_rematerialized_per_cfg_path ... ok -test lexer::tests::test_byte_string ... ok -test lexer::tests::test_comment ... ok -test lexer::tests::test_identifiers ... ok -test lexer::tests::test_keywords ... ok -test lexer::tests::test_numbers ... ok -test lexer::tests::test_operators ... ok -test lexer::tests::test_punctuation ... ok -test lexer::tests::test_string ... ok -test lexer::tests::test_unterminated_byte_string_errors ... ok -test lexer::tests::test_unterminated_string_errors ... ok -test lexer::tests::rejects_oversized_identifier ... ok -test codegen::tests::u128_delta_arithmetic_codegen_uses_fixed_byte_storage ... ok -test lsp::tests::lsp_position_conversion_treats_crlf_as_single_line_ending ... ok -test lsp::tests::lsp_position_incremental_change_applies_crlf_ranges ... ok -test lsp::tests::find_references_for_locals_stays_in_enclosing_callable_scope ... ok -test lsp::tests::goto_definition_prefers_local_scope_over_top_level_symbol ... ok -test lsp::tests::lsp_primitive_strict_rejects_legacy_capabilities ... ok -test docgen::tests::docgen_emits_invariant_coverage_summary ... ok -test lsp::tests::lsp_rejects_oversized_documents ... ok -test lsp::tests::lsp_reads_primitive_strict_from_manifest ... ok -test lsp::tests::test_ckb_namespace_completions ... ok -test lsp::tests::lsp_rejects_document_count_over_limit ... ok -test lsp::tests::test_flow_namespace_completions ... ok -test lsp::tests::test_format_document ... ok -test lsp::tests::test_flow_u8_namespace_completions ... ok -test lsp::tests::test_code_actions_for_lowering_diagnostics ... ok -test lsp::tests::test_goto_definition_and_references ... ok -test lsp::tests::test_incremental_change_applies_utf16_ranges_after_non_bmp_text ... ok -test lsp::tests::test_incremental_change_ignores_invalid_utf16_ranges ... ok -test lsp::tests::test_keyword_completions ... ok -test lsp::tests::test_lsp_position_conversion_uses_utf16_columns ... ok -test lsp::tests::test_action_hover_includes_lowering_metadata ... ok -test lsp::tests::test_parse_errors_become_diagnostics ... ok -test codegen::tests::entry_dynamic_witness_stack_arg_staging_preserves_cursor_register ... ok -test lsp::tests::test_hover ... ok -test lsp::tests::test_lsp_server ... ok -test lsp::tests::test_lowering_diagnostics_warn_for_fail_closed_runtime_actions ... ok -test lsp::tests::test_vec_member_completions_match_supported_helpers ... ok -test lsp::tests::test_selection_range_orders_child_before_parent ... ok -test lsp::tests::test_workspace_rename_is_disabled_until_symbol_scoped ... ok -test lsp::tests::test_workspace_goto_definition_across_modules ... ok -test optimize::tests::does_not_inline_block_bodies_that_can_capture_call_site_names ... ok -test optimize::tests::folds_boolean_expressions ... ok -test lsp::tests::test_workspace_references_across_modules ... ok -test lsp::tests::test_workspace_diagnostics_check_imported_type_id_collisions ... ok -test optimize::tests::folds_literal_if_statements_without_touching_cell_ops ... ok -test optimize::tests::folds_integer_arithmetic ... ok -test optimize::tests::folds_unsigned_high_bit_integer_operations ... ok -test optimize::tests::unused_let_elimination_preserves_calls_and_stdlib_constraints ... ok -test optimize::tests::propagates_constants_inlines_small_functions_and_removes_dead_code ... ok -test package::tests::git_cache_entry_name_is_hash_only ... ok -test package::tests::lockfile_consistency_allows_resolved_transitive_path_dependencies ... ok -test package::tests::lockfile_consistency_reports_stale_and_mismatched_path_sources ... ok -test package::tests::lockfile_consistency_requires_exact_git_revision_match ... ok -test package::tests::lockfile_replace_with_resolved_prunes_removed_dependencies ... ok -test package::tests::package_manager_accepts_allowed_git_url_transports ... ok -test package::tests::lockfile_read_from_root_rejects_malformed_lockfiles ... ok -test package::tests::package_manager_git_checkout_revalidates_full_commit_refs ... ok -test package::tests::package_manager_git_commands_separate_user_controlled_ref_arguments ... ok -test package::tests::package_manager_allows_path_dependency_without_version ... ok -test package::tests::git_cache_child_check_rejects_path_escape ... ok -test package::tests::package_manager_rejects_branch_or_tag_git_dependency_before_fetch ... ok -test package::tests::package_manager_rejects_local_path_dependency_traversal ... ok -test package::tests::package_manager_rejects_registry_dependencies_fail_closed ... ok -test package::tests::package_manager_rejects_unpinned_git_dependency_before_fetch ... ok -test package::tests::package_manager_rejects_unsafe_git_url_transports ... ok -test package::tests::package_manager_rejects_transitive_path_dependency_cycles ... ok -test package::tests::package_manager_resolves_local_path_dependencies ... ok -test package::tests::package_manager_resolves_transitive_local_path_dependencies ... ok -test package::tests::test_dependency_graph ... ok -test package::tests::test_version_compatibility ... ok -test package::tests::test_manifest_serialization ... ok -test parser::tests::action_where_block_allows_indented_following_top_level_item ... ok -test parser::tests::action_where_block_keeps_indented_keyword_like_binding_in_body ... ok -test parser::tests::assignment_range_and_cast_spans_cover_full_expression ... ok -test parser::tests::array_size_uses_checked_target_width_conversion ... ok -test lexer::tests::rejects_oversized_string_literal ... ok -test parser::tests::binary_expr_spans_cover_full_expression ... ok -test parser::tests::hex_literal_exprs_parse_as_integers ... ok -test parser::tests::generic_type_arguments_allow_newlines ... ok -test parser::tests::identity_policy_diagnostic_uses_bad_policy_span ... ok -test parser::tests::named_arg_diagnostic_uses_bad_name_span ... ok -test parser::tests::parser_empty_token_slice_returns_controlled_error ... ok -test parser::tests::parser_rejects_bang_assert_syntax ... ok -test parser::tests::parser_rejects_deep_unary_expression_before_stack_overflow ... ok -test lsp::tests::test_receipt_hover_includes_flow_metadata ... ok -test parser::tests::parser_rejects_deep_if_expression_before_stack_overflow ... ok -test parser::tests::postfix_exprs_cover_the_consumed_source_range ... ok -test parser::tests::postfix_expr_spans_cover_the_full_postfix_chain ... ok -test parser::tests::struct_init_span_covers_type_name_and_body ... ok -test parser::tests::primitive_and_container_exprs_keep_source_spans ... ok -test parser::tests::test_action_where_column_one_flow_identifier_stays_in_body ... ok -test parser::tests::test_launch_expression_is_reserved_until_lowering_exists ... ok -test parser::tests::test_parse_action ... ok -test parser::tests::test_parse_action_transition_block ... ok -test parser::tests::test_parse_aggregate_invariant_primitives ... ok -test parser::tests::test_parse_create_field_shorthand ... ok -test parser::tests::test_parse_expression ... ok -test parser::tests::test_parse_flow_and_action_transition_clause ... ok -test parser::tests::test_parse_grouped_use_imports ... ok -test parser::tests::test_parse_invariant_assert_statement ... ok -test parser::tests::test_parse_merges_attribute_and_inline_capabilities ... ok -test parser::tests::test_parse_invariant ... ok -test parser::tests::test_parse_prefix_source_and_create_target ... ok -test parser::tests::test_parse_prefix_source_before_keyword_like_name ... ok -test parser::tests::test_parse_preserve_single_field ... ok -test parser::tests::test_parse_preserve_block ... ok -test parser::tests::test_parse_require_block ... ok -test parser::tests::test_parse_resource ... ok -test parser::tests::test_postfix_does_not_cross_statement_newline ... ok -test parser::tests::test_parse_type_id_attribute ... ok -test parser::tests::test_reject_bare_preserve ... ok -test parser::tests::test_reject_empty_preserve_block ... ok -test parser::tests::test_reject_preserve_except ... ok -test parser::tests::test_reject_empty_require_block ... ok -test parser::tests::test_reject_preserve_wildcard ... ok -test parser::tests::test_reject_require_block_with_control_flow ... ok -test parser::tests::test_reject_require_block_with_consume ... ok -test parser::tests::test_rejects_action_brace_body ... ok -test parser::tests::test_rejects_empty_transition_block ... ok -test parser::tests::test_rejects_output_parameter_source_prefix ... ok -test parser::tests::test_rejects_generic_resource_definition ... ok -test parser::tests::test_rejects_legacy_move_clause ... ok -test parser::tests::test_rejects_read_ref_as_type_qualifier ... ok -test parser::tests::test_rejects_transition_clause_without_state_colons ... ok -test parser::tests::test_rejects_type_id_on_action ... ok -test parser::tests::test_rejects_typed_let_without_initializer ... ok -test parser::tests::test_rejects_unbraced_match_arms ... ok -test parser::tests::test_rejects_use_as_without_alias ... ok -test proof_plan::soundness::tests::strict_pp0103_only_applies_to_checked_runtime_records ... ok -test proof_plan::soundness::tests::strict_pp0201_only_applies_to_executing_script_args ... ok -test proof_plan::tests::checked_runtime_proof_plan_claims_include_executable_evidence ... ok -test proof_plan::tests::checked_runtime_without_concrete_evidence_is_not_marked_covered ... ok -test proof_plan::tests::checked_static_detail_does_not_create_executable_runtime_evidence ... ok -test proof_plan::tests::replace_unique_features_are_transaction_scoped ... ok -test proof_plan::tests::metadata_only_invariant_proof_plan_has_no_executable_evidence ... ok -test proof_plan::tests::unique_lifecycle_features_have_specific_codegen_evidence_ids ... ok -test repl::tests::repl_read_limited_line_accepts_bounded_input ... ok -test resolve::tests::rejects_cross_module_type_dependency_cycles ... ok -test resolve::tests::test_global_type_resolution_rejects_ambiguous_symbol ... ok -test resolve::tests::test_grouped_use_resolves_multiple_symbols ... ok -test resolve::tests::test_imported_type_resolution_uses_exact_module_path ... ok -test resolve::tests::test_path_resolver ... ok -test resolve::tests::test_module_resolver ... ok -test resolve::tests::test_register_module_rejects_deferred_missing_import_when_target_arrives ... ok -test resolve::tests::test_register_module_rejects_missing_imported_symbol_when_target_is_loaded ... ok -test resolve::tests::test_rejects_duplicate_local_symbols ... ok -test resolve::tests::test_rejects_import_alias_collisions ... ok -test runtime_errors::tests::diagnostic_messages_map_to_runtime_error_codes_where_possible ... ok -test runtime_errors::tests::runtime_error_docs_cover_every_registered_code ... ok -test runtime_errors::tests::runtime_error_docs_explain_ckb_code_overlap_channels ... ok -test simulate::tests::array_size_simulator_uses_checked_target_width_for_indices ... ok -test runtime_errors::tests::runtime_error_registry_roundtrips_and_has_unique_codes ... ok -test simulate::tests::simulate_cell_operation_traces ... ok -test simulate::tests::simulate_if_branch ... ok -test simulate::tests::simulate_pure_arithmetic_action ... ok -test simulate::tests::simulate_read_ref_traces ... ok -test simulate::tests::simulate_rejects_wrong_action_arity ... ok -test simulate::tests::simulate_step_limit ... ok -test simulate::tests::simulate_unsigned_high_bit_integer_operations ... ok -test repl::tests::repl_read_limited_line_rejects_oversized_input ... ok -test stdlib::collections::tests::collection_assembly_has_no_raw_syscalls_or_unclassified_helpers ... ok -test stdlib::collections::tests::collection_public_helpers_do_not_dereference_raw_a0_handles ... ok -test stdlib::collections::tests::test_collection_functions ... ok -test stdlib::collections::tests::test_generate_assembly ... ok -test stdlib::tests::generated_stdlib_has_no_raw_syscall_wrapper_symbols ... ok -test stdlib::tests::generated_stdlib_omits_raw_syscall_wrappers ... ok -test stdlib::tests::test_generate_assembly ... ok -test stdlib::tests::test_get_function ... ok -test stdlib::tests::test_generate_ckb_assembly_uses_checked_env_helpers ... ok -test stdlib::tests::test_scheduler_metadata_generate_molecule_uses_table_layout ... ok -test stdlib::tests::test_std_functions ... ok -test syscalls::tests::ckb_debug_syscall_is_not_a_production_inventory_surface ... ok -test syscalls::tests::emitted_manual_runtime_and_stdlib_helpers_are_classified ... ok -test syscalls::tests::every_low_level_syscall_spec_is_inventoried ... ok -test syscalls::tests::helper_inventory_has_no_duplicate_symbols ... ok -test syscalls::tests::ckb_syscall_abi_matches_checked_baseline ... ok -test tests::action_scheduler_witness_bytes_rejects_conflicting_molecule_alias ... ok -test tests::branch_local_anonymous_creates_are_rejected_until_effects_are_cfg_aware ... ok -test tests::ckb_capacity_calculation_saturates_on_extreme_sizes ... ok -test tests::ckb_constraints_surface_capacity_planning_for_created_outputs ... ok -test tests::ckb_deploy_manifest_rejects_conflicting_cell_dep_locations ... ok -test codegen::tests::emitted_runtime_helper_symbols_are_classified_in_syscall_inventory ... ok -test runtime_errors::tests::codegen_does_not_emit_unregistered_numeric_fail_literals ... ok -test lsp::tests::lsp_loads_sibling_modules_for_standalone_example_imports ... ok -test codegen::tests::internal_assembler_relaxes_out_of_range_register_conditional_branch ... ok -test lexer::tests::rejects_oversized_block_comment ... ok -test tests::ckb_deploy_manifest_rejects_invalid_hash_type ... ok -test tests::ckb_deploy_manifest_rejects_invalid_dep_type ... ok -test tests::ckb_deploy_manifest_rejects_incomplete_split_cell_dep_location ... ok -test package::tests::package_manager_git_dependency_fails_for_invalid_url ... ok -test tests::ckb_deploy_manifest_surfaces_hash_type_and_dep_group_policy ... ok -test tests::collection_fail_closed_feature_names_are_stable ... ok -test tests::ckb_target_profile_has_no_policy_exception ... ok -test tests::ckb_u64_syscall_helpers_check_return_code_and_size ... ok -test tests::ckb_lock_false_return_lowers_to_script_failure ... ok -test tests::ckb_dynamic_vector_len_can_drive_mutate_transition ... ok -test tests::compile_accepts_chain_neutral_timepoint_under_ckb_profile ... ok -test tests::compile_accepts_action_witness_source_qualifier ... ok -test tests::compile_accepts_ckb_header_epoch_api_only_for_ckb_profile ... ok -test tests::compile_accepts_ckb_target_profile_timepoint ... ok -test tests::ckb_entry_lock_scope_selects_lock_entrypoint ... ok -test tests::compile_accepts_complete_branch_return_paths ... ok -test tests::compile_accepts_ckb_shared_create_when_verifier_covered ... ok -test tests::ckb_entry_scope_keeps_vec_element_schema_dependencies ... ok -test tests::compile_accepts_empty_vec_literal_with_declared_type ... ok -test tests::compile_accepts_create_field_shorthand ... ok -test tests::compile_accepts_explicit_flow_action_edges ... ok -test tests::compile_accepts_flow_state_name_initializers ... ok -test tests::compile_accepts_flow_initial_create_at_any_declared_state ... ok -test package::tests::package_manager_git_update_fails_closed_on_fetch_error ... ok -test tests::compile_accepts_flow_on_custom_state_field ... ok -test tests::compile_accepts_flow_edge_returning_to_first_state ... ok -test tests::compile_accepts_kernel_effect_capabilities_for_destroy ... ok -test tests::compile_accepts_non_initial_flow_create_without_consumed_prior_state ... ok -test tests::compile_accepts_lock_args_script_args_binding ... ok -test tests::compile_accepts_core_input_output_state_transition_edges ... ok -test tests::compile_accepts_pure_ckb_target_profile ... ok -test tests::compile_accepts_kernel_effect_capabilities_for_transfer ... ok -test tests::compile_accepts_prefix_read_params_as_cell_dep_bindings ... ok -test tests::compile_accepts_lock_boundary_param_sources_and_require ... ok -test tests::compile_accepts_static_flow_update_to_non_initial_state ... ok -test tests::compile_accepts_named_action_output_and_create_binding ... ok -test tests::ckb_entry_action_scope_excludes_unselected_unsupported_code ... ok -test tests::compile_allows_struct_type_id_under_ckb_profile ... ok -test tests::compile_accepts_qualified_flow_state_names ... ok -test tests::compile_accepts_vec_literals_in_create_fields ... ok -test tests::compile_allows_actions_and_locks_to_call_pure_functions ... ok -test tests::compile_accepts_symmetric_where_branch_output_constraints ... ok -test tests::compile_allows_unit_function_calls_as_statements ... ok -test tests::compile_allows_flow_update_to_declared_initial_state_at_type_check ... ok -test tests::compile_binds_duplicate_read_refs_by_order_not_name ... ok -test tests::compile_binds_read_ref_entry_params_to_cell_deps ... ok -test tests::compile_binds_read_action_schema_params_to_cell_deps ... ok -test tests::compile_classifies_resource_merge_amount_sum_as_checked_runtime ... ok -test tests::compile_create_unique_field_identity_emits_runtime_anchor ... ok -test tests::compile_classifies_resource_split_amount_subtraction_as_checked_runtime ... ok -test tests::compile_emits_create_output_field_verification_for_fixed_u64_fields ... ok -test tests::compile_destroy_policies_are_policy_aware ... ok -test tests::compile_emits_ckb_style_load_cell_abi_for_cell_runtime_summary ... ok -test tests::compile_entry_witness_rejects_payloads_larger_than_buffer ... ok -test tests::compile_emits_direct_user_function_calls ... ok -test tests::compile_exposes_ckb_type_id_contract_under_ckb_profile ... ok -test tests::compile_emits_protocol_agnostic_guard_equality_proofplan_records ... ok -test tests::compile_classifies_protocol_agnostic_guarded_transition_as_checked_runtime ... ok -test tests::compile_file_explicit_target_overrides_manifest_build_target ... ok -test tests::compile_file_uses_manifest_ckb_target_profile ... ok -test tests::compile_folds_local_fixed_array_len_to_constant ... ok -test tests::compile_file_loads_local_path_dependencies_from_cell_manifest ... ok -test tests::compile_classifies_guarded_identity_field_merge_as_checked_runtime ... ok -test tests::compile_identity_ckb_type_id_emits_metadata ... ok -test tests::compile_ignores_trivial_self_equality_guard_records ... ok -test tests::compile_identity_none_is_default_and_hidden ... ok -test tests::compile_identity_singleton_type_emits_metadata ... ok -test tests::compile_file_uses_manifest_build_target_by_default ... ok -test tests::compile_identity_field_emits_path ... ok -test tests::compile_file_source_content_hash_is_path_independent ... ok -test tests::compile_identity_script_args_emits_metadata ... ok -test tests::compile_infers_and_validates_read_only_effects ... ok -test tests::compile_lowers_array_of_tuples_static_index_projection ... ok -test tests::compile_lowers_block_tail_if_expressions ... ok -test tests::compile_lowers_assert_invariant_into_fail_closed_cfg ... ok -test tests::compile_lowers_bounded_vec_literal_to_stack_collection ... ok -test tests::compile_lowers_byte_string_literals_with_expected_array_type ... ok -test tests::compile_lowers_consumed_input_field_access_through_loaded_cell_bytes ... ok -test tests::compile_lowers_exhaustive_enum_match_without_wildcard ... ok -test tests::compile_lowers_ckb_group_source_large_immediate_to_riscv_elf ... ok -test tests::compile_lowers_for_range_into_counted_loop_cfg ... ok -test tests::compile_lowers_if_expression_fixed_byte_const_join_move ... ok -test tests::compile_lowers_fixed_byte_schema_field_comparison ... ok -test tests::compile_lowers_if_expression_with_join_move ... ok -test tests::compile_lowers_if_statement_into_basic_blocks ... ok -test tests::compile_lowers_len_method_to_length_instruction ... ok -test tests::compile_lowers_local_struct_field_reads_and_writes ... ok -test tests::compile_lowers_local_fixed_array_static_index_reads_and_writes ... ok -test tests::compile_lowers_local_constants_into_real_operands ... ok -test tests::compile_lowers_local_tuple_destructuring_to_field_slots ... ok -test tests::compile_lowers_local_tuple_static_field_reads_and_writes ... ok -test tests::compile_keeps_unchecked_transition_field_runtime_required ... ok -test tests::compile_lowers_numeric_cast_without_zero_fallback ... ok -test tests::compile_lowers_match_expression_into_branch_cfg ... ok -test tests::compile_lowers_mutable_assignments_in_loop_bodies ... ok -test tests::compile_lowers_pure_function_assert_failure_to_abort ... ok -test tests::compile_lowers_packed_bool_and_u32_schema_fields_without_aligned_loads ... ok -test tests::compile_lowers_read_ref_schema_field_to_ckb_runtime_assembly ... ok -test tests::compile_lowers_stack_vec_clear_and_is_empty ... ok -test tests::compile_lowers_stack_vec_extend_from_fixed_bytes ... ok -test tests::compile_lowers_read_ref_schema_field_to_ckb_runtime_elf ... ok -test tests::compile_lowers_stack_vec_fixed_byte_capacity ... ok -test tests::compile_lowers_schema_backed_parameter_field_access_to_elf ... ok -test tests::compile_lowers_stack_vec_fixed_byte_pop ... ok -test tests::compile_lowers_stack_vec_fixed_byte_contains ... ok -test tests::compile_lowers_stack_vec_fixed_byte_first_last ... ok -test tests::compile_lowers_stack_vec_fixed_byte_insert ... ok -test tests::compile_lowers_stack_vec_fixed_byte_runtime_push_index ... ok -test tests::compile_lowers_stack_vec_fixed_byte_remove ... ok -test tests::compile_lowers_stack_vec_fixed_byte_reverse ... ok -test tests::compile_lowers_stack_vec_scalar_capacity ... ok -test tests::bundled_token_example_strict_ckb_compile_is_admitted ... ok -test tests::compile_lowers_stack_vec_fixed_byte_swap ... ok -test tests::compile_lowers_stack_vec_scalar_first_last ... ok -test tests::compile_lowers_stack_vec_scalar_contains ... ok -test tests::compile_lowers_stack_vec_fixed_byte_truncate ... ok -test tests::compile_lowers_stack_vec_scalar_insert ... ok -test tests::compile_lowers_stack_vec_scalar_pop ... ok -test tests::compile_lowers_stack_vec_fixed_byte_set ... ok -test tests::compile_lowers_stack_vec_scalar_set ... ok -test tests::compile_lowers_stack_vec_scalar_runtime_push_len_index ... ok -test tests::compile_lowers_stack_vec_scalar_remove ... ok -test tests::compile_lowers_tail_expr_as_action_return ... ok -test tests::compile_lowers_stack_vec_scalar_reverse ... ok -test tests::compile_lowers_stack_vec_scalar_truncate ... ok -test tests::compile_lowers_stack_vec_scalar_swap ... ok -test tests::compile_lowers_tail_if_as_action_return ... ok -test tests::compile_lowers_u128_equality_as_fixed_byte_comparison ... ok -test tests::compile_lowers_type_hash_without_generic_call ... ok -test tests::compile_lowers_vec_builtins_without_generic_calls ... ok -test tests::compile_lowers_while_statement_into_loop_cfg ... ok -test tests::compile_lowers_vec_with_capacity_to_stack_collection_new ... ok -test tests::compile_merges_if_branch_linear_states_conservatively ... ok -test tests::compile_lowers_zero_builtin_without_generic_call ... ok -test tests::compile_lowers_u128_mutate_delta_with_carry_arithmetic ... ok -test tests::compile_merges_linear_transfers_inside_if_expressions ... ok -test tests::compile_merges_linear_transfers_inside_block_tail_if_expressions ... ok -test tests::compile_marks_cell_backed_vec_runtime_features ... ok -test tests::compile_merges_linear_transfers_inside_match_expressions ... ok -test tests::compile_metadata_exposes_ckb_type_id_create_output_plan_under_ckb_profile ... ok -test tests::compile_metadata_exposes_aggregate_invariant_primitives_in_proof_plan ... ok -test tests::compile_metadata_exposes_declared_invariant_proof_plan ... ok -test codegen::tests::codegen_rejects_generated_far_jump_scratch_relaxation ... ok -test tests::compile_materializes_local_fixed_byte_constants_into_rodata ... ok -test tests::compile_metadata_exposes_transaction_and_selected_cell_aggregate_invariants ... ok -test tests::compile_metadata_with_options_rejects_strict_legacy_capabilities ... ok -test tests::compile_classifies_hash_committed_output_field_as_guarded ... ok -test tests::compile_metadata_exposes_lock_group_proof_plan_for_lock_entry ... ok -test tests::compile_merges_linear_transfers_inside_block_expressions ... ok -test tests::compile_metadata_declares_molecule_vm_abi ... ok -test tests::compile_metadata_reports_parameterless_action_entrypoint_selection ... ok -test tests::compile_metadata_warns_for_lock_group_transaction_invariant_scope ... ok -test tests::compile_metadata_exposes_covenant_proof_plan_for_transfer ... ok -test tests::compile_path_rejects_missing_path_dependency_manifest ... ok -test tests::compile_path_rejects_missing_configured_source_root ... ok -test tests::compile_path_rejects_duplicate_modules_across_source_roots ... ok -test tests::compile_metadata_proof_plan_preserves_lock_args_source ... ok -test tests::compile_metadata_with_options_uses_ast_optimizer_for_nonzero_levels ... ok -test tests::compile_normalizes_same_module_qualified_helper_calls ... ok -test tests::compile_path_ignores_examples_outside_package_source_roots ... ok -test tests::compile_path_accepts_package_root ... ok -test tests::compile_path_rejects_path_dependency_traversal ... ok -test tests::compile_path_rejects_non_path_dependencies ... ok -test tests::compile_path_rejects_path_dependency_cycles ... ok -test tests::compile_package_import_alias_emits_matching_external_callable ... ok -test tests::compile_prefers_no_arg_main_for_entry_wrapper ... ok -test tests::compile_preserves_if_array_aggregate_slots ... ok -test tests::compile_path_supports_configured_source_roots_without_src ... ok -test tests::compile_preserves_if_tuple_aggregate_slots ... ok -test tests::compile_path_supports_custom_entry_directory_modules ... ok -test tests::compile_preserves_index_and_tuple_projection_in_assembly ... ok -test tests::compile_preserves_create_instructions_in_assembly ... ok -test tests::compile_rejects_aggregate_invariant_non_fixed_field ... ok -test tests::compile_rejects_assert_delta_argument_from_cell_read ... ok -test tests::compile_rejects_assert_invariant_as_tail_return_value ... ok -test tests::compile_rejects_assignment_through_read_only_references ... ok -test tests::compile_rejects_assignment_to_immutable_array_element ... ok -test tests::compile_preserves_consume_and_destroy_instructions_in_assembly ... ok -test tests::compile_rejects_assignment_to_immutable_tuple_field ... ok -test tests::compile_preserves_match_tuple_aggregate_slots ... ok -test tests::compile_rejects_assignment_to_temporary_field_targets ... ok -test tests::compile_rejects_asymmetric_where_branch_output_constraints ... ok -test tests::compile_rejects_bad_flow_state_field_type_on_main_path ... ok -test tests::compile_rejects_bare_return_from_value_actions ... ok -test tests::compile_rejects_binding_assert_invariant_results ... ok -test tests::compile_rejects_binding_unit_function_results ... ok -test tests::compile_rejects_bounded_vec_literal_type_mismatch ... ok -test tests::compile_rejects_builtin_call_argument_mismatches ... ok -test tests::compile_rejects_core_state_transition_edge_not_in_graph ... ok -test tests::compile_rejects_cell_metadata_stdlib_on_non_cell_args ... ok -test tests::compile_rejects_duplicate_flow_for_same_state_field ... ok -test tests::compile_rejects_destroy_without_destroy_capability ... ok -test tests::compile_rejects_duplicate_stable_type_ids ... ok -test tests::compile_rejects_duplicate_top_level_symbols ... ok -test tests::compile_rejects_dynamic_assert_invariant_messages ... ok -test tests::compile_rejects_dynamic_require_messages ... ok -test tests::compile_rejects_dynamic_initial_flow_create_state ... ok -test tests::compile_rejects_empty_array_length_mismatch ... ok -test tests::compile_rejects_dynamic_unique_identity_field ... ok -test tests::compile_rejects_empty_literal_in_non_vec_context ... ok -test tests::compile_rejects_flow_by_action_when_explicit_move_uses_different_edge ... ok -test tests::compile_rejects_enum_payload_variants_until_lowering_exists ... ok -test tests::compile_rejects_flow_on_plain_struct ... ok -test tests::compile_rejects_flow_by_action_without_exact_move_clause ... ok -test tests::compile_rejects_flow_payload_enum_state_field ... ok -test tests::compile_rejects_flow_receipt_without_state_field ... ok -test tests::compile_rejects_function_call_argument_mismatches ... ok -test tests::compile_rejects_forbidden_unwrap_helpers ... ok -test tests::compile_rejects_helper_functions_that_indirectly_call_impure_actions ... ok -test tests::compile_rejects_heterogeneous_array_literals ... ok -test tests::compile_rejects_if_expression_branch_type_mismatch ... ok -test tests::compile_rejects_impure_helper_functions ... ok -test tests::compile_rejects_incomplete_branch_return_paths ... ok -test tests::compile_rejects_input_source_outside_action_cell_params ... ok -test tests::compile_preserves_schema_backed_parameter_field_access_in_assembly ... ok -test tests::compile_rejects_invalid_destroy_policy_shapes ... ok -test tests::compile_rejects_invalid_enum_match_patterns ... ok -test tests::compile_rejects_invalid_invariant_assert_expression ... ok -test tests::compile_rejects_invariant_without_explicit_trigger_and_scope ... ok -test tests::compile_rejects_invalid_create_field_initializers ... ok -test tests::compile_rejects_invariant_assert_runtime_operation ... ok -test tests::compile_rejects_linear_state_changes_hidden_inside_loops ... ok -test tests::compile_rejects_local_binding_name_reuse ... ok -test tests::compile_rejects_local_fixed_array_static_oob_write ... ok -test tests::compile_rejects_local_fixed_array_static_oob_read ... ok -test tests::compile_rejects_local_mutable_reference_aliases ... ok -test tests::compile_rejects_missing_action_return_paths ... ok -test tests::compile_rejects_missing_flow_state_create_on_main_path ... ok -test tests::compile_rejects_missing_function_return_paths ... ok -test tests::compile_rejects_non_bool_lock_definitions ... ok -test tests::compile_rejects_non_bool_assert_condition ... ok -test tests::compile_rejects_noop_flow_transition_on_main_path ... ok -test tests::compile_rejects_out_of_range_flow_state_create_on_main_path ... ok -test tests::compile_rejects_owned_linear_field_assignment ... ok -test tests::compile_rejects_pure_functions_that_call_ckb_header_runtime_builtins ... ok -test tests::compile_rejects_pure_functions_that_call_env_runtime_builtins ... ok -test tests::compile_rejects_payload_or_unknown_enum_variant_values ... ok -test tests::compile_produces_non_empty_riscv_assembly ... ok -test tests::compile_rejects_pure_functions_that_call_locks ... ok -test tests::compile_rejects_read_ref_for_non_cell_backed_types ... ok -test tests::compile_rejects_pure_functions_that_call_type_hash_runtime_builtin ... ok -test tests::compile_rejects_local_references_to_linear_roots ... ok -test tests::compile_rejects_returning_unit_function_results ... ok -test tests::compile_rejects_return_values_from_unit_actions ... ok -test tests::compile_rejects_state_edge_that_does_not_consume_binding ... ok -test tests::compile_rejects_reference_escape_boundaries ... ok -test tests::compile_rejects_stateful_operations_without_named_linear_cell_operands ... ok -test tests::compile_rejects_string_literals_as_runtime_values ... ok -test tests::compile_preserves_dynamic_witness_cursor_after_lock_args ... ok -test tests::compile_rejects_unbound_assert_delta_argument ... ok -test tests::compile_rejects_undeclared_action_state_edge ... ok -test tests::compile_rejects_underdeclared_effect_annotations ... ok -test tests::compile_rejects_unknown_functions ... ok -test tests::compile_rejects_underdeclared_effects_through_qualified_calls ... ok -test tests::compile_rejects_unknown_struct_fields ... ok -test tests::compile_rejects_underdeclared_effects_through_calls ... ok -test tests::compile_rejects_unknown_target_during_option_validation ... ok -test tests::compile_rejects_unknown_or_reserved_named_types ... ok -test tests::compile_rejects_unknown_target_profile ... ok -test tests::compile_rejects_unreachable_statements_after_return ... ok -test tests::compile_rejects_unreachable_statements_after_complete_branch_return ... ok -test tests::compile_rejects_unstable_callable_parameter_names ... ok -test tests::compile_rejects_unsupported_optimization_level ... ok -test tests::compile_rejects_unstable_schema_field_names ... ok -test tests::compile_rejects_untyped_empty_array_literals ... ok -test tests::compile_rejects_unsound_mutable_parameter_forms ... ok -test tests::compile_preserves_read_ref_instructions_in_assembly ... ok -test tests::compile_rejects_wrong_qualified_flow_state_field_initializer ... ok -test tests::compile_produces_ckb_elf_without_vm_abi_trailer ... ok -test tests::compile_rejects_unsupported_vec_helper_type_combinations ... ok -test tests::compile_produces_non_empty_riscv_elf ... ok -test tests::compile_result_exposes_nested_fixed_molecule_schema_metadata ... ok -test tests::compile_rejects_state_transitions_inside_locks ... ok -test tests::compile_lowers_ckb_hash_commitment_comparison_without_fixed_byte_fail_closed ... ok -test tests::compile_rejects_lock_boundary_sources_outside_supported_scope ... ok -test tests::compile_result_exposes_schema_layout_metadata ... ok -test tests::compile_result_validation_rejects_constraints_artifact_format_mismatch ... ok -test tests::compile_result_validation_rejects_compiler_version_mismatch ... ok -test tests::compile_replace_unique_field_identity_compares_input_and_output ... ok -test tests::compile_result_validation_rejects_assembly_with_vm_abi_trailer ... ok -test tests::compile_lowers_stack_vec_fixed_schema_values ... ok -test tests::compile_result_exposes_scheduler_metadata_sidecar ... ok -test tests::compile_result_validation_rejects_metadata_artifact_format_mismatch ... ok -test tests::compile_result_validation_rejects_constraints_artifact_size_mismatch ... ok -test tests::compile_result_validation_rejects_metadata_artifact_hash_mismatch ... ok -test tests::compile_result_validation_rejects_mismatched_ckb_output_data_binding ... ok -test tests::compile_result_validation_rejects_metadata_schema_version_mismatch ... ok -test tests::compile_result_validation_rejects_metadata_schema_downgrade ... ok -test tests::compile_result_validation_rejects_metadata_artifact_size_mismatch ... ok -test tests::compile_reports_equivalent_state_transition_obligation_for_sugar_and_core_forms ... ok -test tests::compile_result_validation_rejects_mismatched_ckb_type_id_create_output_plan ... ok -test tests::compile_result_validation_rejects_metadata_source_content_hash_mismatch ... ok -test tests::compile_result_validation_rejects_metadata_source_hash_mismatch ... ok -test tests::compile_result_validation_accepts_current_outputs ... ok -test tests::compile_result_validation_rejects_metadata_target_profile_mismatch ... ok -test tests::compile_result_validation_rejects_metadata_target_profile_v0_14_abi_mismatch ... ok -test tests::compile_result_validation_rejects_type_id_hash_mismatch ... ok -test tests::compile_result_validation_rejects_missing_metadata_artifact_size ... ok -test tests::compile_result_validation_rejects_tampered_artifact_hash ... ok -test tests::compile_result_writes_artifact_to_disk ... ok -test tests::compile_result_validation_rejects_noncanonical_source_unit_hash ... ok -test tests::compile_result_validation_rejects_molecule_schema_hash_mismatch ... ok -test tests::compile_riscv_elf_accepts_full_width_u64_literals ... ok -test tests::compile_supports_typed_empty_array_literals ... ok -test tests::compile_tracks_linear_values_returned_from_complete_branches ... ok -test tests::compile_spills_parameters_and_returns_computed_value ... ok -test tests::compile_tracks_linear_values_returned_from_tail_if_branches ... ok -test tests::compile_surfaces_type_level_hash_type_dsl_metadata ... ok -test tests::compile_unrolls_local_array_of_tuples_foreach_destructuring ... ok -test tests::compile_tracks_linear_values_inside_aggregate_bindings ... ok -test tests::compile_unrolls_fixed_param_array_foreach_with_pointer_abi ... ok -test tests::compile_unrolls_local_fixed_array_foreach_without_runtime_indexing ... ok -test tests::compile_verifies_create_output_against_computed_scalar_stack_value ... ok -test tests::compile_uses_ast_optimizer_for_nonzero_optimization_levels ... ok -test tests::compile_verifies_create_output_against_consumed_input_field_alias ... ok -test tests::default_output_path_for_package_input_uses_build_dir ... ok -test tests::default_output_path_for_package_input_uses_manifest_out_dir ... ok -test tests::compile_verifies_created_output_bool_and_u32_fields ... ok -test tests::compile_verifies_constructed_fixed_width_vec_output ... ok -test tests::compile_verifies_created_scalar_fields_against_consumed_input_aliases ... ok -test tests::create_output_verifier_accepts_const_lock_hash ... ok -test tests::compiled_riscv_elf_contains_exit_trampoline ... ok -test tests::create_output_verifier_accepts_fixed_byte_params_and_consts ... ok -test tests::entry_abi_constraints_mark_extreme_slot_counts_unsupported ... ok -test tests::compile_verifies_large_output_field_requirements_without_partial_fallback ... ok -test tests::compile_riscv_elf_accepts_large_schema_field_offsets ... ok -test tests::entry_witness_encoder_matches_u64_wrapper_abi ... ok -test tests::dynamic_mutable_schema_transitions_are_checked_after_table_decoding ... ok -test tests::compile_unique_script_args_and_singleton_identity_emit_hash_checks ... ok -test tests::dynamic_schema_fixed_vec_length_is_table_decoded ... ok -test tests::entry_witness_bool_params_are_canonicalized ... ok -test tests::dynamic_schema_fixed_field_access_is_table_decoded ... ok -test tests::entry_witness_encoder_includes_schema_backed_params_as_length_prefixed_bytes ... ok -test tests::entry_witness_encoder_supports_fixed_byte_params ... ok -test tests::ir_carries_flow_rules ... ok -test tests::ir_lowers_unit_function_calls_without_result_destinations ... ok -test tests::ir_preserves_function_call_return_types ... ok -test tests::ir_rejects_unknown_call_return_types_without_u64_fallback ... ok -test tests::ir_summary_captures_cell_runtime_accesses ... ok -test tests::dynamic_named_output_constraints_are_proven_in_where_block ... ok -test tests::dynamic_schema_fixed_vec_iteration_is_table_decoded ... ok -test tests::load_modules_for_input_collects_package_source_roots ... ok -test tests::fixed_enum_fields_have_molecule_schema_metadata ... ok -test tests::generated_outgoing_stack_reservations_are_psabi_aligned ... ok -test tests::internal_calls_keep_outgoing_stack_area_abi_aligned ... ok -test tests::package_entry_must_stay_inside_package_root ... ok -test tests::package_out_dir_must_stay_inside_package_root ... ok -test tests::package_source_roots_must_stay_inside_package_root ... ok -test tests::loaded_artifact_validation_rejects_metadata_artifact_size_mismatch ... ok -test tests::primitive_compat_predicates_match_validator_modes ... ok -test tests::compile_riscv_elf_accepts_large_stack_offsets ... ok -test tests::generic_shared_mutation_does_not_emit_pool_pattern_metadata ... ok -test tests::resolve_input_path_accepts_package_root_and_manifest ... ok -test tests::entry_witness_wrapper_supports_scalar_stack_args ... ok -test tests::fixed_byte_mutable_state_set_transition_is_checked_under_ckb_profile ... ok -test tests::scheduler_witness_hex_decode_rejects_invalid_metadata_hex ... ok -test tests::proof_plan_checked_static_excluded_from_on_chain_checked_obligations ... ok -test tests::named_action_output_create_binding_reuses_declared_output_index ... ok -test tests::tuple_return_abi_rejects_more_than_eight_fields ... ok -test tests::parameterized_entrypoint_emits_witness_entry_wrapper ... ok -test tests::proof_plan_cross_references_matching_action_obligation_for_invariant ... ok -test tests::vm_abi_trailer_detection_requires_complete_zero_reserved_trailer ... ok -test types::tests::block_expression_merges_existing_vec_refinements ... ok -test types::tests::branch_local_consume_is_rejected_until_lifecycle_effects_are_cfg_aware ... ok -test tests::source_unit_disk_verification_accepts_paths_inside_trusted_root ... ok -test types::tests::branch_local_create_is_rejected_until_lifecycle_effects_are_cfg_aware ... ok -test types::tests::byte_string_literal_type_uses_actual_length ... ok -test types::tests::call_arguments_do_not_coerce_mut_ref_to_ref ... ok -test types::tests::check_without_resolver_rejects_imports ... ok -test types::tests::compound_assign_rejects_implicit_narrowing ... ok -test types::tests::compound_assign_uses_numeric_binary_rules ... ok -test types::tests::const_initializers_allow_supported_literals ... ok -test types::tests::const_initializers_reject_cell_backed_types ... ok -test types::tests::const_initializers_reject_cell_lifecycle_expressions ... ok -test tests::proof_plan_marks_invariant_action_evidence_as_non_exhaustive ... ok -test types::tests::const_initializers_reject_computed_expressions ... ok -test types::tests::cyclic_schema_type_dependencies_are_rejected ... ok -test types::tests::constant_narrowing_casts_must_fit ... ok -test types::tests::contextual_integer_literals_fit_declared_widths ... ok -test types::tests::duplicate_lifecycle_binding_is_rejected_until_effects_are_cfg_aware ... ok -test types::tests::expected_type_does_not_widen_non_literal_abi_arg_boundary ... ok -test types::tests::expected_type_does_not_widen_non_literal_field_boundary ... ok -test types::tests::expected_type_does_not_widen_non_literal_let_boundary ... ok -test tests::source_unit_disk_verification_rejects_paths_outside_trusted_root ... ok -test types::tests::expected_type_does_not_widen_non_literal_return_boundary ... ok -test types::tests::expression_branch_unreachable_code_is_rejected_by_typechecker ... ok -test types::tests::generic_reference_detection_uses_type_structure ... ok -test types::tests::explicit_cast_can_cross_integer_width_boundary ... ok -test types::tests::if_statement_merges_matching_vec_refinements ... ok -test types::tests::if_expression_preserves_typed_vec_result_with_empty_constructor_branch ... ok -test types::tests::if_statement_rejects_one_sided_vec_refinement ... ok -test types::tests::if_statement_rejects_divergent_vec_refinements ... ok -test types::tests::imported_and_qualified_names_compare_as_same_type ... ok -test types::tests::imported_type_ids_must_not_collide_in_visible_module_scope ... ok -test types::tests::invalid_schema_field_types_are_not_registered_as_valid_fields ... ok -test types::tests::imported_token_type_is_treated_as_linear ... ok -test tests::strict_audit_codegen_emits_only_aligned_stack_pointer_deltas ... ok -test types::tests::match_requires_enum_scrutinee ... ok -test types::tests::lifecycle_capability_gates_reject_undeclared_kernel_effects ... ok -test types::tests::non_tail_linear_expression_statements_are_rejected ... ok -test types::tests::numeric_named_type_equality_is_commutative ... ok -test types::tests::mixed_width_arithmetic_and_ordering_are_rejected ... ok -test types::tests::numeric_type_equality_respects_width ... ok -test types::tests::preserve_rejects_mismatched_field_types ... ok -test types::tests::qualified_identifier_must_resolve_to_value ... ok -test types::tests::recursive_enum_payloads_are_rejected ... ok -test types::tests::imported_linear_argument_is_marked_consumed_after_call ... ok -test types::tests::require_block_rejects_assignment_expression ... ok -test types::tests::require_block_rejects_lifecycle_stdlib_call ... ok -test types::tests::statically_visible_division_by_zero_is_rejected ... ok -test types::tests::require_rejects_nested_cell_operation ... ok -test types::tests::stdlib_claim_rejects_declared_output_type_mismatch ... ok -test types::tests::stdlib_claim_output_requires_declared_claim_output_type ... ok -test tests::v014_runtime_helpers_fail_closed_when_not_executable ... ok -test types::tests::stdlib_claim_output_requires_complete_field_coverage ... ok -test types::tests::stdlib_claim_rejects_extra_arguments ... ok -test types::tests::stdlib_transfer_rejects_extra_arguments ... ok -test types::tests::stdlib_claim_requires_explicit_output_and_lock_arguments ... ok -test types::tests::strict_mode_rejects_imported_legacy_capabilities ... ok -test tests::ordered_named_output_create_constraints_are_checked_in_body_order ... ok -test types::tests::stdlib_settle_requires_explicit_output_and_lock_arguments ... ok -test types::tests::stdlib_claim_rejects_non_receipt_input ... ok -test types::tests::stdlib_transfer_output_requires_complete_field_coverage ... ok -test types::tests::tail_match_expressions_are_valid_return_values ... ok -test types::tests::unsigned_integer_negation_is_rejected ... ok -test types::tests::typed_vec_with_capacity_uses_declared_element_type ... ok -test types::tests::u128_ordering_and_arithmetic_still_rejected_on_widening ... ok -test types::tests::vec_type_arguments_are_validated ... ok -test wasm::tests::wasm_audit_reports_audit_only_for_type_only_module ... ok -test wasm::tests::wasm_compiler_rejects_pure_action_modules ... ok -test wasm::tests::wasm_encoder_emits_magic_version_and_status_custom_section ... ok -test wasm::tests::wasm_runtime_instantiates_metadata_module_but_refuses_calls ... ok -test types::tests::launch_module_type_checks_with_registered_imports ... ok -test types::tests::unsupported_u128_arithmetic_is_rejected ... ok -test types::tests::widening_boundary_matrix ... ok -test tests::u128_mutable_state_transition_with_u64_delta_is_checked ... ok -test tests::payload_enum_fields_use_dynamic_molecule_schema_metadata ... ok -test tests::optimized_entry_lock_keeps_inlined_schema_pointer_field_access_checked ... ok -test codegen::tests::internal_assembler_relaxes_far_conditional_branch_with_long_jump ... ok -test codegen::tests::internal_assembler_encodes_far_unconditional_jump ... ok -test codegen::tests::bundled_example_codegen_mnemonics_are_declared ... ok - -test result: ok. 776 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.56s - - -running 7 tests -test adversarial_parser_preserves_operator_precedence_in_ambiguous_sequences ... ok -test adversarial_0_13_rejects_invalid_hash_type_dsl ... ok -test adversarial_parser_binds_else_to_nearest_if ... ok -test adversarial_parser_rejects_deep_unary_expression_without_panicking ... ok -test adversarial_parser_rejects_deep_nested_control_flow_without_panicking ... ok -test adversarial_integer_literals_fail_closed_on_lexical_and_contextual_overflow ... ok -test adversarial_0_13_rejects_unsupported_generic_collection_surfaces ... ok - -test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s - - -running 11 tests -test runtime_u64_helpers_fail_closed_before_value_use ... ok -test snapshot_simple_action_assembly ... ok -test runtime_void_helpers_fail_closed_before_continuing ... ok -test snapshot_type_id_create_output_assembly ... ok -test snapshot_lock_args_assembly ... ok -test snapshot_spawn_ipc_executable_status_checked_assembly ... ok -test runtime_witness_helpers_fail_closed_before_pointer_use ... ok -test snapshot_witness_schema_syscall_assembly ... ok -test snapshot_collection_lowering_assembly ... ok -test snapshot_blake2b_helper_assembly ... ok -test snapshot_assemblies_contain_no_leaked_overflow_diagnostics ... ok - -test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.22s - - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s - - -running 86 tests -test cellc_add_and_remove_subcommands_honor_dev_path_and_json ... ok -test cellc_add_git_requires_full_rev_and_records_pin ... ok -Check succeeded - Target profile: ckb - Checked: package default (RISC-V assembly) -test cellc_check_denies_metadata_only_declared_invariant ... ok -test cellc_check_accepts_ckb_profile_timepoint ... ok -test cellc_check_accepts_pure_ckb_target_profile ... ok -test cellc_abi_subcommand_explains_entry_witness_layout ... ok -test cellc_build_uses_manifest_policy_before_writing_artifacts ... ok -test cellc_action_build_emits_builder_plan_json ... ok -Build complete - Artifact format: RISC-V assembly - Target profile: ckb - Output: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp7uhCRB/build/main.s - Metadata: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp7uhCRB/build/main.s.meta.json -test cellc_check_production_rejects_fail_closed_runtime_paths ... ok -test cellc_check_production_rejects_incomplete_output_verification ... ok -test cellc_build_accepts_pure_ckb_target_profile_without_vm_abi_trailer ... ok -test cellc_check_can_reject_runtime_required_obligations ... ok -test cellc_build_and_check_subcommands_use_package_flow ... ok -test cellc_check_reports_claim_source_predicate_blocker_class ... ok -test cellc_clean_subcommand_supports_json_summary ... ok -test cellc_check_uses_manifest_policy_defaults ... ok -test cellc_ckb_hash_emits_default_blake2b_vector ... ok -test cellc_check_all_targets_checks_asm_and_elf_without_writing_artifacts ... ok -test cellc_check_denies_checked_partial_proof_plan_gap ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [5d, 8b, d, ed, a5, 24, 99, 9f, c3, fe, 29, 67, 78, 19, 2a, 46, 8f, a3, b5, 44, cb, 36, cf, cc, e2, 10, 50, 24, 59, 4b, 5b, 37] - Output: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpw5jKIp/artifacts/main.s - Metadata: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpw5jKIp/artifacts/main.s.meta.json -test cellc_check_reports_linear_collection_ownership_blocker_class ... ok -test cellc_cli_target_overrides_manifest_build_target ... ok -test cellc_check_reports_resource_conservation_blocker_class ... ok -test cellc_check_accepts_u128_mutable_state_transition_with_u64_delta ... ok -test cellc_check_reports_settle_finalization_blocker_class ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [9f, aa, 3c, b9, 5, 1b, a7, 19, e4, ea, e4, 1, 79, 7, 11, 89, 7f, 40, ba, 26, 7e, 86, ba, 8c, d5, a3, a, 4d, 3, 45, eb, 54] - Output: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp54oy11/app_pkg/build/main.s - Metadata: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp54oy11/app_pkg/build/main.s.meta.json -test cellc_entry_witness_subcommand_emits_parameterized_witness_json ... ok -test cellc_constraints_subcommand_surfaces_ckb_deployment_manifest ... ok -test cellc_doc_subcommand_generates_markdown_docs ... ok -test cellc_compiles_package_with_local_path_dependency ... ok -test cellc_explain_profile_reports_ckb_v0_14_contract ... ok -test cellc_check_reports_explicit_output_binding_without_mutable_state_blockers ... ok -test cellc_entry_witness_subcommand_encodes_schema_backed_params ... ok -test cellc_entry_witness_subcommand_rejects_wrong_width_fixed_bytes ... ok -test cellc_explain_proof_reports_declared_invariant ... ok -test cellc_explain_subcommand_reports_runtime_error ... ok -test cellc_explain_proof_warns_for_lock_group_transaction_scope ... ok -test cellc_info_subcommand_supports_json_summary ... ok -test cellc_explain_proof_human_reports_macro_provenance ... ok -test cellc_errors_include_runtime_ecode_when_policy_failure_maps_to_runtime_registry ... ok -test cellc_init_subcommand_supports_json_summary ... ok -test cellc_lsp_flag_rejects_trailing_arguments ... ok -Formatting complete - Updated 1 file(s) -test cellc_explain_proof_reports_covenant_proof_plan ... ok -test cellc_explain_proof_reports_invariant_action_coverage_match ... ok -test cellc_new_subcommand_supports_json_summary_and_vcs_none ... ok -test cellc_fmt_subcommand_formats_sources ... ok -test cellc_rejects_registry_package_dependencies_fail_closed ... ok -test cellc_rejects_external_dependency_function_calls_until_linking_exists ... ok -test cellc_run_subcommand_without_vm_runner_degrades_gracefully ... ok -test cellc_install_path_updates_lockfile_and_remove_prunes_it ... ok -test cellc_explain_proof_summary_reports_fail_closed_diagnostics ... ok -test cellc_test_subcommand_rejects_conflicting_expectations ... ok -test cellc_rejects_underdeclared_effects_from_path_dependency_calls ... ok -test cellc_test_subcommand_rejects_empty_expected_error_line_text ... ok -test cellc_metadata_subcommand_emits_lowering_runtime_json ... ok -test cellc_test_subcommand_rejects_unknown_directives ... ok -test cellc_test_subcommand_rejects_missing_expected_error_text ... ok -test cellc_test_subcommand_rejects_wrong_expected_error_line ... ok -test cellc_test_subcommand_rejects_missing_entrypoint_metadata ... ok -test cellc_check_reports_pool_invariant_policy_families ... ok -test cellc_check_reports_transaction_invariant_checked_subconditions ... ok -test cellc_test_subcommand_supports_expected_compile_failures ... ok -test cellc_test_subcommand_rejects_missing_runtime_metadata ... ok -test cellc_test_subcommand_compiles_test_sources ... ok -test cellc_opt_report_compares_all_optimization_levels ... ok -test cellc_test_subcommand_supports_expected_error_line_directive ... ok -test cellc_scheduler_plan_consumes_shared_touch_hints ... ok -test cellc_test_subcommand_supports_entrypoint_metadata_directives ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpx8S472/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpx8S472/sample.s.meta.json -test cellc_new_subcommand_initializes_git_by_default ... ok -test cellc_top_level_primitive_strict_rejects_legacy_capabilities ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [5d, 8b, d, ed, a5, 24, 99, 9f, c3, fe, 29, 67, 78, 19, 2a, 46, 8f, a3, b5, 44, cb, 36, cf, cc, e2, 10, 50, 24, 59, 4b, 5b, 37] - Output: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpALgaoa/artifacts/main.s - Metadata: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpALgaoa/artifacts/main.s.meta.json -test cellc_uses_manifest_build_out_dir_for_package_input ... ok -test cellc_test_subcommand_supports_policy_directives ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpdRT70K/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpdRT70K/sample.s.meta.json -success: compiled successfully - Artifact format: RISC-V ELF - Target profile: ckb - Artifact hash: [cf, 7, cf, ac, d0, a, 43, a3, a8, cc, 8b, 6e, 66, e1, 29, b2, 32, 60, 2f, 76, a3, 55, 4d, 52, d5, 38, 51, 1f, c8, b, 49, 2] - Output: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpMYbYm6/artifacts/main.elf - Metadata: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpMYbYm6/artifacts/main.elf.meta.json -test cellc_test_subcommand_supports_runtime_metadata_directives ... ok -test cellc_uses_manifest_build_target_by_default ... ok -test cellc_test_subcommand_supports_target_directive ... ok -test cellc_top_level_accepts_primitive_strict_for_kernel_effect_capabilities ... ok -test cellc_verify_artifact_accepts_matching_sidecar ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [a7, d4, 78, cc, c3, f5, cd, 81, cd, de, 51, 44, ee, 83, 4d, 64, 46, df, bd, 40, 58, 5f, 51, 6c, d1, 56, 6b, b7, 44, 9d, 96, 8d] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpsHXIhT/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpsHXIhT/sample.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpVfZ9D4/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpVfZ9D4/sample.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpEQdza8/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpEQdza8/sample.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp7cEcEJ/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp7cEcEJ/sample.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmphZ9GHw/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmphZ9GHw/sample.s.meta.json -test cellc_verify_artifact_enforces_policy_flags ... ok -test cellc_verify_artifact_rejects_noncanonical_source_unit_hash ... ok -test cellc_verify_artifact_rejects_tampered_artifact ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpWyguP9/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpWyguP9/sample.s.meta.json -test cellc_verify_artifact_rejects_metadata_schema_downgrade ... ok -test cellc_writes_requested_output_file ... ok -test cellc_verify_artifact_rejects_tampered_source_when_requested ... ok -test cellc_verify_artifact_primitive_strict_rechecks_disk_sources ... ok -test cellc_verify_artifact_enforces_expected_hashes ... ok -test cellc_explain_generics_reports_checked_vec_instantiations ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [79, a1, 5d, 7e, f7, 1f, 9a, 64, 89, da, 9e, 8b, a8, 90, b6, 15, f0, b5, 61, d1, 80, 6b, 39, 9f, f0, 5a, a4, 4d, 0, 5, 41, 3c] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp8xEgYv/amm_pool.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp8xEgYv/amm_pool.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [2a, 2, ca, c2, fd, b6, 4a, 53, 9e, 26, cb, a1, 31, 69, ab, f3, 1d, c1, 42, d, 18, d3, fd, 1d, 92, b7, a, 55, c6, d, 87, df] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp8xEgYv/launch.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp8xEgYv/launch.s.meta.json -test cellc_check_reports_checked_pool_invariant_families_without_runtime_blockers ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [8b, d7, 59, d4, b6, d1, 6, 8b, 97, 0, fd, e5, df, 72, ec, a6, 99, bf, 20, 34, 90, 55, b2, 17, 6d, 48, 55, 9e, ec, ed, 11, 2b] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp8xEgYv/multisig.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp8xEgYv/multisig.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [cd, d0, f5, 74, b7, 9d, 8e, 7d, 79, 50, 6a, cf, 3e, 13, b, 53, 5c, b9, 7f, 8c, d1, 1f, 88, bf, 1b, 8a, 3c, 3b, 34, 45, c6, 4e] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp8xEgYv/nft.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp8xEgYv/nft.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [fd, 14, d8, d9, e, 2c, e8, 71, 98, aa, e2, b6, b4, fc, b8, 93, aa, 84, 66, 2f, 21, 2e, 9d, 26, 5, 63, 5d, 74, 55, fd, 56, 87] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp8xEgYv/timelock.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp8xEgYv/timelock.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [3c, bd, 90, ed, de, 2c, 8d, 8c, 97, 1, a4, d9, 9, dc, 3d, bd, 22, 6b, 5b, 39, e7, 3e, 59, 9a, 5d, e1, 2c, 13, 61, 4d, 32, 46] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp8xEgYv/token.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp8xEgYv/token.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [2a, 7, 99, 55, e2, cb, e6, 1b, 39, 63, db, fc, 1, 63, fd, 57, 38, 6, a4, 7, ad, b2, 5d, 5c, f1, de, 41, e5, 2a, 29, 1, e5] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp8xEgYv/vesting.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp8xEgYv/vesting.s.meta.json -test cellc_compiles_bundled_examples_to_requested_outputs ... ok - -test result: ok. 86 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.86s - - -running 26 tests -test ckb_scoped_entry_keeps_called_action_helpers ... ok -test launch_seed_pool_composition_is_scheduler_visible ... ok -test registry_example_uses_bounded_local_vec_helpers_without_collection_debt ... ok -test amm_pool_input_output_params_are_scheduler_visible ... ok -test release_examples_are_free_of_placeholder_hashes_and_formatter_artifacts ... ok -test registry_example_with_insert_contains_compiles_to_elf ... ok -test nft_core_actions_expose_action_specific_builder_metadata ... ok -test order_book_language_example_uses_local_vec_helpers_without_collection_debt ... ok -test token_cell_invariant_appears_in_proof_plan ... ok -test stdlib_language_example_compiles_with_all_patterns ... ok -test token_mint_authority_input_output_binding_is_explicit ... ok -test v0_15_identity_lifecycle_example_compiles_and_produces_proof_plan ... ok -test v0_15_scoped_invariant_example_compiles_and_produces_proof_plan ... ok -test vesting_phase2_remaining_obligations_are_explicit ... ok -test vesting_read_ref_params_are_scheduler_visible ... ok -test multisig_core_actions_expose_threshold_flow_metadata ... ok -test timelock_core_actions_expose_time_and_release_metadata ... ok -test canonical_examples_compile_under_primitive_strict_015 ... ok -test bundled_examples_compile_to_non_empty_assembly ... ok -test canonical_examples_are_the_single_checked_in_business_source ... ok -test bundled_examples_emit_molecule_schema_manifest_report ... ok -test bundled_examples_stay_within_backend_shape_budgets ... ok -test bundled_examples_stay_near_backend_shape_release_baseline ... ok -test bundled_examples_backend_shape_report_serializes ... ok -test all_checked_in_cell_examples_compile ... ok -test bundled_examples_compile_to_elf ... ok - -test result: ok. 26 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 10.53s - - -running 7 tests -test fuzzy_oversized_static_widths_are_controlled_errors ... ok -test fuzzy_metadata_tampering_never_panics ... ok -test fuzzy_unicode_hex_inputs_are_controlled_errors ... ok -test fuzzy_entry_witness_encoding_never_panics ... ok -test fuzzy_lsp_incremental_edits_never_panic ... ok -test fuzzy_mutated_sources_never_panic ... ok -test fuzzy_semantic_codegen_mutations_reach_assembly ... ok - -test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.54s - - -running 4 tests -test ickb_diff_matrix_is_partial_and_consistent_with_model_fixtures ... ok -test ickb_positive_fixtures_pass_model_verifier ... ok -test ickb_negative_fixtures_fail_for_expected_invariant ... ok -test ickb_benchmark_specs_compile_and_expose_expected_entries ... ok - -test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.29s - - -running 1 test -test syntax_combo_quick_matrix_is_cargo_test_visible ... FAILED - -failures: - ----- syntax_combo_quick_matrix_is_cargo_test_visible stdout ---- - -thread 'syntax_combo_quick_matrix_is_cargo_test_visible' (8211111) panicked at tests/syntax_combo.rs:15:5: -syntax combo quick runner failed -status: exit status: 1 -stdout: - -stderr: -Traceback (most recent call last): - File "/Users/arthur/RustroverProjects/CellScript/scripts/cellscript_syntax_combo_audit.py", line 20, in - import tomllib -ModuleNotFoundError: No module named 'tomllib' - -note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace - - -failures: - syntax_combo_quick_matrix_is_cargo_test_visible - -test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s - - -== stderr == - Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s - Finished `test` profile [unoptimized + debuginfo] target(s) in 0.04s - Running unittests src/lib.rs (target/debug/deps/cellscript-198f0ba9a296fb91) - Running tests/adversarial_0_13.rs (target/debug/deps/adversarial_0_13-87ca0b7751a0cf60) - Running tests/assembly_snapshots.rs (target/debug/deps/assembly_snapshots-a1b0ee4291a50be7) - Running tests/ckb_acceptance.rs (target/debug/deps/ckb_acceptance-546e6523e51ab114) - Running tests/cli.rs (target/debug/deps/cli-cd9b4a3e7ed668b4) - Running tests/examples.rs (target/debug/deps/examples-885631b36bce043e) - Running tests/fuzzy_debug.rs (target/debug/deps/fuzzy_debug-a3c500396cf14cf4) - Running tests/ickb_benchmark.rs (target/debug/deps/ickb_benchmark-d0fd214d43bb34ab) - Running tests/syntax_combo.rs (target/debug/deps/syntax_combo-b9abeffd268b17da) -error: test failed, to rerun pass `-p cellscript --test syntax_combo` diff --git a/.cap/logs/1780406387-71041.log b/.cap/logs/1780406387-71041.log deleted file mode 100644 index 2f0d290d..00000000 --- a/.cap/logs/1780406387-71041.log +++ /dev/null @@ -1,11 +0,0 @@ -== stdout == - -running 1 test -test syntax_combo_quick_matrix_is_cargo_test_visible ... ok - -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.41s - - -== stderr == - Finished `test` profile [unoptimized + debuginfo] target(s) in 0.05s - Running tests/syntax_combo.rs (target/debug/deps/syntax_combo-b9abeffd268b17da) diff --git a/.cap/logs/1780406397-71653.log b/.cap/logs/1780406397-71653.log deleted file mode 100644 index 90dd6337..00000000 --- a/.cap/logs/1780406397-71653.log +++ /dev/null @@ -1,1125 +0,0 @@ -== stdout == - -running 776 tests -test ckb_hash_tests::ckb_blake2b256_matches_blank_hash_vector ... ok -test cli::commands::tests::test_command_execution ... ok -test cli::commands::tests::invalid_parser_mapping_returns_error_instead_of_panicking ... ok -test cli::commands::tests::production_policy_finds_evidence_less_checked_runtime_proof_plan_gap ... ok -test cli::commands::tests::production_policy_finds_evidence_less_on_chain_checked_proof_plan_gap ... ok -test cli::commands::tests::expected_metadata_hash_comparison_is_case_sensitive ... ok -test codegen::assembler::tests::strict_audit_internal_assembler_oracle_for_core_instruction_bytes ... ok -test codegen::assembler::tests::strict_audit_li_split_handles_negative_32_bit_boundaries ... ok -test codegen::assembler::tests::strict_audit_riscv_immediate_boundaries_are_enforced ... ok -test codegen::calls::tests::canonical_type_names_strip_reference_wrappers ... ok -test codegen::calls::tests::fixed_u64_le_width_accepts_hashes_and_byte_arrays ... ok -test codegen::cell_ops::tests::consumed_operand_var_accepts_named_cell_operands_only ... ok -test codegen::calls::tests::packed_hash_width_uses_codegen_fixed_byte_type_rules ... ok -test codegen::cell_ops::tests::destroy_absence_scan_is_limited_to_singleton_and_type_id_unique_policies ... ok -test codegen::cell_ops::tests::identity_and_destruction_policy_labels_are_stable ... ok -test codegen::expr::tests::divisor_nonzero_guard_fails_closed_on_zero ... ok -test codegen::frame::tests::large_addi_materializes_out_of_range_immediates ... ok -test codegen::assembler::tests::strict_audit_elf_header_and_segments_are_internally_consistent ... ok -test codegen::expr::tests::bool_canonical_check_emits_zero_one_guard ... ok -test codegen::frame::tests::large_addi_uses_single_addi_for_small_immediates ... ok -test codegen::runtime::tests::checked_runtime_status_register_defaults_to_a1_for_unknown_helpers ... ok -test codegen::runtime::tests::ckb_runtime_syscall_abi_matches_declared_constants ... ok -test codegen::frame::tests::stack_access_helpers_emit_sp_relative_instructions ... ok -test cli::commands::tests::ckb_hash_file_rejects_inputs_above_limit ... ok -test codegen::schema::tests::aggregate_field_layouts_track_tuple_offsets ... ok -test codegen::runtime::tests::runtime_helper_classification_tracks_checked_and_hash_helpers ... ok -test codegen::schema::tests::fixed_byte_constants_materialize_little_endian_bytes ... ok -test codegen::schema::tests::fixed_width_helpers_classify_scalar_and_byte_storage ... ok -test codegen::tests::consumed_schema_params_use_loaded_cell_size_for_field_checks ... ok -test codegen::tests::cell_operation_identity_helpers_stay_in_cell_ops ... ok -test codegen::tests::dynamic_syscall_index_is_copied_before_large_stack_staging ... ok -test codegen::tests::generated_large_offsets_are_normalized_before_assembly ... ok -test codegen::tests::generated_collection_assembly_is_internal_assembler_clean ... ok -test codegen::tests::generated_public_assembly_mnemonics_are_declared ... ok -test codegen::tests::explicit_external_toolchain_paths_are_strict ... ok -test codegen::tests::internal_assembler_encodes_emitted_instruction_surface ... ok -test codegen::tests::internal_assembler_encodes_full_width_li_literals ... ok -test codegen::tests::internal_assembler_keeps_near_unconditional_jump_compact ... ok -test codegen::tests::generated_functions_use_shared_epilogue_tail ... ok -test codegen::tests::internal_assembler_encodes_register_conditional_branches ... ok -test codegen::tests::internal_assembler_rejects_unresolved_call_targets ... ok -test codegen::tests::internal_assembler_rejects_intentionally_unsupported_mnemonics ... ok -test codegen::tests::generated_stdlib_assembly_is_internal_assembler_clean ... ok -test codegen::tests::large_addi_avoids_clobbering_source_register ... ok -test codegen::tests::binary_codegen_materializes_narrow_integer_constants ... ok -test codegen::tests::machine_cfg_tracks_call_edges_to_local_helpers ... ok -test codegen::tests::machine_layout_plan_builds_explicit_machine_blocks ... ok -test codegen::tests::machine_layout_order_rejects_missing_duplicate_or_unknown_blocks ... ok -test codegen::tests::machine_layout_plan_builds_register_conditional_branch_blocks ... ok -test codegen::tests::machine_layout_plan_rejects_branch_target_outside_text ... ok -test codegen::tests::division_codegen_guards_zero_divisors ... ok -test codegen::tests::machine_reachability_uses_entry_label_not_every_global ... ok -test codegen::tests::outgoing_stack_arg_area_is_16_byte_aligned_at_call_boundaries ... ok -test codegen::tests::read_ref_runtime_fallback_records_cell_buffer_state ... ok -test codegen::assembler::tests::strict_audit_relaxed_conditional_branch_within_jal_range_preserves_registers ... ok -test codegen::tests::register_contract_allows_only_entry_wrapper_writes_to_direct_registers ... ok -test codegen::tests::dynamic_molecule_fixed_field_codegen_checks_full_header_and_exact_span ... ok -test codegen::tests::rv64_li_boundary_values_materialize_correct_bits ... ok -test codegen::tests::dynamic_molecule_vector_field_access_validates_full_table_offsets ... ok -test codegen::tests::semantic_molecule_field_access_uses_validated_api_gate ... ok -test codegen::tests::sp_addi_large_offsets_clobber_only_destination_register ... ok -test codegen::tests::state_transition_edges_use_explicit_consumed_binding ... ok -test codegen::tests::strict_audit_outgoing_stack_args_are_staged_inside_current_frame ... ok -test codegen::tests::type_hash_missing_output_buffer_slots_report_compile_error ... ok -test codegen::tests::narrow_arithmetic_codegen_truncates_to_declared_width ... ok -test codegen::tests::type_hash_missing_param_slots_report_compile_error ... ok -test codegen::tests::u128_const_without_fixed_storage_reports_compile_error ... ok -test codegen::tests::unaligned_scalar_load_large_offsets_preserve_live_accumulator ... ok -test codegen::tests::machine_layout_plan_reports_branch_relaxation_metrics ... ok -test codegen::tests::unrepresentable_memory_load_offsets_report_compile_error ... ok -test codegen::tests::unrepresentable_stack_offsets_report_compile_error ... ok -test debug::tests::test_debug_info_generator ... ok -test codegen::tests::schema_ref_call_preserves_schema_abi_length ... ok -test debug::tests::test_dwarf_generation ... ok -test debug::tests::test_line_table ... ok -test debug::tests::test_type_registration ... ok -test codegen::tests::runtime_cast_codegen_checks_narrowing_and_bool_canonicality ... ok -test docgen::tests::docgen_emits_flat_pool_runtime_input_requirements ... ok -test docgen::tests::docgen_emits_transaction_invariant_checked_subconditions ... ok -test docgen::tests::docgen_emits_markdown_for_action ... ok -test docgen::tests::docgen_html_escapes_module_and_item_text ... ok -test error::tests::caret_padding_starts_at_span_column ... ok -test error::tests::caret_width_counts_characters_not_bytes ... ok -test flow::tests::consumed_flow_tracking_follows_expression_aliases ... ok -test fmt::tests::format_action_transition_block_for_multiple_edges ... ok -test codegen::tests::internal_assembler_relaxes_out_of_range_conditional_branch ... ok -test fmt::tests::format_indents_preserve_fields_inside_expression_block ... ok -test fmt::tests::format_preserves_single_element_tuple_expression ... ok -test fmt::tests::format_preserves_type_policy_metadata ... ok -test fmt::tests::format_round_trips_inline_if_tuple_expression ... ok -test fmt::tests::format_round_trips_multiline_expression_block ... ok -test fmt::tests::format_round_trips_preserve_block ... ok -test fmt::tests::format_round_trips_require_block ... ok -test fmt::tests::format_round_trips_simple_module ... ok -test fmt::tests::format_single_expr_require_block_uses_compact_form ... ok -test fmt::tests::format_round_trips_stdlib_lifecycle_field_block ... ok -test fmt::tests::format_uses_canonical_assert_and_no_const_semicolon ... ok -test fmt::tests::format_uses_field_shorthand_when_value_matches_name ... ok -test codegen::tests::stack_pointer_offsets_are_emitted_through_helpers ... ok -test incremental::tests::clean_cache_rejects_overflowing_max_age ... ok -test incremental::tests::test_dependency_graph ... ok -test incremental::tests::clean_cache_skips_output_paths_outside_trusted_root ... ok -test incremental::tests::test_change_detector ... ok -test incremental::tests::load_cache_drops_units_with_paths_outside_trusted_root ... ok -test ir::tests::assert_in_pure_function_lowers_failure_to_abort_terminator ... ok -test ir::tests::all_diverging_match_expression_does_not_leave_unreachable_join ... ok -test ir::tests::constant_cast_rejects_out_of_range_u128_narrowing ... ok -test incremental::tests::test_incremental_compiler ... ok -test ir::tests::binary_arithmetic_result_type_preserves_left_operand_width ... ok -test ir::tests::contextual_integer_binary_operands_lower_to_peer_width ... ok -test ir::tests::ir_generation_aggregates_lowering_errors_with_source_spans ... ok -test ir::tests::exhaustive_enum_match_unmatched_path_lowers_to_abort_terminator ... ok -test ir::tests::ir_straight_line_lifecycle_certificate_rejects_duplicate_consume_without_typecheck ... ok -test ir::tests::ir_type_value_kind_never_derives_status_kinds ... ok -test ir::tests::ir_straight_line_lifecycle_certificate_rejects_branch_local_create_without_typecheck ... ok -test ir::tests::poison_lowering_keeps_value_invalid_while_block_stays_live ... ok -test ir::tests::mixed_width_expression_local_widening_lowers_as_explicit_casts ... ok -test ir::tests::logical_operators_lower_as_short_circuit_control_flow ... ok -test ir::tests::reference_and_deref_unary_result_types_match_ast_types ... ok -test ir::tests::require_block_lowers_to_atomic_requires ... ok -test ir::tests::status_boundary_ir_verifier_allows_domain_u64_return_tuple_and_call_argument ... ok -test ir::tests::runtime_narrowing_cast_lowers_as_cast_instruction ... ok -test ir::tests::status_boundary_ir_verifier_allows_unit_runtime_helper_when_status_is_checked_by_codegen_boundary ... ok -test ir::tests::status_boundary_ir_verifier_rejects_dropped_raw_syscall_status ... ok -test ir::tests::status_boundary_ir_verifier_rejects_raw_syscall_status_as_domain_call_argument ... ok -test ir::tests::preserve_sugar_populates_preserved_fields ... ok -test ir::tests::status_boundary_ir_verifier_rejects_raw_syscall_status_in_tuple_field ... ok -test ir::tests::status_boundary_ir_verifier_rejects_raw_syscall_status_produced_without_checked_consumer ... ok -test ir::tests::status_boundary_ir_verifier_rejects_raw_syscall_status_returned_as_domain_u64 ... ok -test ir::tests::status_boundary_ir_verifier_rejects_raw_syscall_status_stored_as_dsl_local ... ok -test ir::tests::status_boundary_ir_verifier_rejects_unit_runtime_helper_status_stored_as_domain_u64 ... ok -test ir::tests::stdlib_claim_lowers_to_consumed_receipt_and_locked_declared_output ... ok -test ir::tests::stdlib_settle_lowers_to_consumed_input_and_locked_output ... ok -test ir::tests::strict_audit_ir_lowering_records_instruction_level_provenance ... ok -test ir::tests::strict_audit_ir_verifier_rejects_constant_destination_width_mismatch ... ok -test ir::tests::strict_audit_ir_verifier_rejects_empty_body_blocks ... ok -test ir::tests::strict_audit_ir_verifier_rejects_extra_consume_set_metadata ... ok -test ir::tests::stdlib_transfer_lowers_to_single_consumed_input_and_locked_output ... ok -test ir::tests::strict_audit_ir_verifier_rejects_poisoned_load_const ... ok -test ir::tests::strict_audit_ir_verifier_rejects_poisoned_lowering_module ... ok -test ir::tests::strict_audit_ir_verifier_rejects_missing_terminator_target ... ok -test ir::tests::strict_audit_ir_verifier_rejects_poisoned_lowering_operand ... ok -test ir::tests::strict_audit_ir_verifier_rejects_missing_create_set_metadata ... ok -test ir::tests::strict_audit_ir_verifier_rejects_use_not_defined_on_all_paths ... ok -test ir::tests::strict_audit_ir_verifier_rejects_stale_write_intents_metadata ... ok -test ir::tests::strict_audit_ir_verifier_reports_instruction_provenance ... ok -test ir::tests::strict_audit_schema_field_accesses_are_rematerialized_per_cfg_path ... ok -test lexer::tests::test_byte_string ... ok -test lexer::tests::test_comment ... ok -test codegen::tests::vm2_syscall_helpers_emit_executable_status_checked_wrappers ... ok -test lexer::tests::test_identifiers ... ok -test lexer::tests::test_keywords ... ok -test lexer::tests::test_numbers ... ok -test lexer::tests::rejects_oversized_identifier ... ok -test lexer::tests::test_operators ... ok -test lexer::tests::test_punctuation ... ok -test lexer::tests::test_string ... ok -test lexer::tests::test_unterminated_string_errors ... ok -test lexer::tests::test_unterminated_byte_string_errors ... ok -test docgen::tests::docgen_emits_invariant_coverage_summary ... ok -test codegen::tests::u128_delta_arithmetic_codegen_uses_fixed_byte_storage ... ok -test lsp::tests::lsp_position_conversion_treats_crlf_as_single_line_ending ... ok -test lsp::tests::lsp_position_incremental_change_applies_crlf_ranges ... ok -test lsp::tests::goto_definition_prefers_local_scope_over_top_level_symbol ... ok -test lsp::tests::lsp_primitive_strict_rejects_legacy_capabilities ... ok -test lsp::tests::lsp_rejects_oversized_documents ... ok -test lsp::tests::lsp_rejects_document_count_over_limit ... ok -test lsp::tests::find_references_for_locals_stays_in_enclosing_callable_scope ... ok -test lsp::tests::test_ckb_namespace_completions ... ok -test lsp::tests::lsp_reads_primitive_strict_from_manifest ... ok -test lsp::tests::test_flow_namespace_completions ... ok -test codegen::tests::entry_dynamic_witness_stack_arg_staging_preserves_cursor_register ... ok -test lsp::tests::test_goto_definition_and_references ... ok -test lsp::tests::test_code_actions_for_lowering_diagnostics ... ok -test lsp::tests::test_incremental_change_applies_utf16_ranges_after_non_bmp_text ... ok -test lsp::tests::test_format_document ... ok -test lsp::tests::test_incremental_change_ignores_invalid_utf16_ranges ... ok -test lsp::tests::test_keyword_completions ... ok -test lsp::tests::test_lsp_position_conversion_uses_utf16_columns ... ok -test lsp::tests::test_flow_u8_namespace_completions ... ok -test lsp::tests::test_action_hover_includes_lowering_metadata ... ok -test lsp::tests::test_parse_errors_become_diagnostics ... ok -test lsp::tests::test_hover ... ok -test lsp::tests::test_lsp_server ... ok -test lsp::tests::test_vec_member_completions_match_supported_helpers ... ok -test lsp::tests::test_selection_range_orders_child_before_parent ... ok -test lsp::tests::test_lowering_diagnostics_warn_for_fail_closed_runtime_actions ... ok -test lsp::tests::test_workspace_diagnostics_check_imported_type_id_collisions ... ok -test lsp::tests::test_workspace_rename_is_disabled_until_symbol_scoped ... ok -test optimize::tests::does_not_inline_block_bodies_that_can_capture_call_site_names ... ok -test lsp::tests::test_workspace_goto_definition_across_modules ... ok -test optimize::tests::folds_boolean_expressions ... ok -test optimize::tests::folds_integer_arithmetic ... ok -test optimize::tests::folds_unsigned_high_bit_integer_operations ... ok -test lsp::tests::test_workspace_references_across_modules ... ok -test optimize::tests::folds_literal_if_statements_without_touching_cell_ops ... ok -test optimize::tests::propagates_constants_inlines_small_functions_and_removes_dead_code ... ok -test optimize::tests::unused_let_elimination_preserves_calls_and_stdlib_constraints ... ok -test package::tests::git_cache_entry_name_is_hash_only ... ok -test package::tests::lockfile_consistency_allows_resolved_transitive_path_dependencies ... ok -test package::tests::lockfile_consistency_requires_exact_git_revision_match ... ok -test package::tests::lockfile_replace_with_resolved_prunes_removed_dependencies ... ok -test package::tests::lockfile_consistency_reports_stale_and_mismatched_path_sources ... ok -test package::tests::package_manager_accepts_allowed_git_url_transports ... ok -test package::tests::package_manager_git_checkout_revalidates_full_commit_refs ... ok -test package::tests::package_manager_git_commands_separate_user_controlled_ref_arguments ... ok -test package::tests::lockfile_read_from_root_rejects_malformed_lockfiles ... ok -test package::tests::git_cache_child_check_rejects_path_escape ... ok -test package::tests::package_manager_allows_path_dependency_without_version ... ok -test package::tests::package_manager_rejects_branch_or_tag_git_dependency_before_fetch ... ok -test package::tests::package_manager_rejects_registry_dependencies_fail_closed ... ok -test package::tests::package_manager_rejects_local_path_dependency_traversal ... ok -test lexer::tests::rejects_oversized_string_literal ... ok -test package::tests::package_manager_rejects_unpinned_git_dependency_before_fetch ... ok -test package::tests::package_manager_rejects_transitive_path_dependency_cycles ... ok -test package::tests::package_manager_rejects_unsafe_git_url_transports ... ok -test package::tests::test_dependency_graph ... ok -test package::tests::package_manager_resolves_local_path_dependencies ... ok -test package::tests::test_manifest_serialization ... ok -test package::tests::test_version_compatibility ... ok -test parser::tests::action_where_block_allows_indented_following_top_level_item ... ok -test parser::tests::action_where_block_keeps_indented_keyword_like_binding_in_body ... ok -test parser::tests::array_size_uses_checked_target_width_conversion ... ok -test parser::tests::assignment_range_and_cast_spans_cover_full_expression ... ok -test parser::tests::generic_type_arguments_allow_newlines ... ok -test parser::tests::hex_literal_exprs_parse_as_integers ... ok -test parser::tests::binary_expr_spans_cover_full_expression ... ok -test parser::tests::identity_policy_diagnostic_uses_bad_policy_span ... ok -test parser::tests::named_arg_diagnostic_uses_bad_name_span ... ok -test package::tests::package_manager_resolves_transitive_local_path_dependencies ... ok -test parser::tests::parser_empty_token_slice_returns_controlled_error ... ok -test parser::tests::parser_rejects_bang_assert_syntax ... ok -test parser::tests::parser_rejects_deep_unary_expression_before_stack_overflow ... ok -test parser::tests::postfix_expr_spans_cover_the_full_postfix_chain ... ok -test parser::tests::postfix_exprs_cover_the_consumed_source_range ... ok -test parser::tests::parser_rejects_deep_if_expression_before_stack_overflow ... ok -test parser::tests::struct_init_span_covers_type_name_and_body ... ok -test parser::tests::primitive_and_container_exprs_keep_source_spans ... ok -test parser::tests::test_action_where_column_one_flow_identifier_stays_in_body ... ok -test parser::tests::test_parse_action ... ok -test parser::tests::test_launch_expression_is_reserved_until_lowering_exists ... ok -test parser::tests::test_parse_aggregate_invariant_primitives ... ok -test parser::tests::test_parse_create_field_shorthand ... ok -test parser::tests::test_parse_expression ... ok -test parser::tests::test_parse_action_transition_block ... ok -test parser::tests::test_parse_flow_and_action_transition_clause ... ok -test parser::tests::test_parse_grouped_use_imports ... ok -test parser::tests::test_parse_invariant ... ok -test lsp::tests::test_receipt_hover_includes_flow_metadata ... ok -test parser::tests::test_parse_invariant_assert_statement ... ok -test parser::tests::test_parse_merges_attribute_and_inline_capabilities ... ok -test parser::tests::test_parse_prefix_source_before_keyword_like_name ... ok -test parser::tests::test_parse_prefix_source_and_create_target ... ok -test parser::tests::test_parse_preserve_block ... ok -test parser::tests::test_parse_require_block ... ok -test parser::tests::test_parse_preserve_single_field ... ok -test parser::tests::test_parse_resource ... ok -test parser::tests::test_parse_type_id_attribute ... ok -test parser::tests::test_postfix_does_not_cross_statement_newline ... ok -test parser::tests::test_reject_bare_preserve ... ok -test parser::tests::test_reject_empty_require_block ... ok -test parser::tests::test_reject_empty_preserve_block ... ok -test parser::tests::test_reject_preserve_except ... ok -test parser::tests::test_reject_preserve_wildcard ... ok -test parser::tests::test_reject_require_block_with_consume ... ok -test parser::tests::test_rejects_empty_transition_block ... ok -test parser::tests::test_rejects_generic_resource_definition ... ok -test parser::tests::test_rejects_action_brace_body ... ok -test parser::tests::test_reject_require_block_with_control_flow ... ok -test parser::tests::test_rejects_legacy_move_clause ... ok -test parser::tests::test_rejects_output_parameter_source_prefix ... ok -test parser::tests::test_rejects_read_ref_as_type_qualifier ... ok -test parser::tests::test_rejects_transition_clause_without_state_colons ... ok -test parser::tests::test_rejects_type_id_on_action ... ok -test parser::tests::test_rejects_typed_let_without_initializer ... ok -test parser::tests::test_rejects_use_as_without_alias ... ok -test proof_plan::soundness::tests::strict_pp0103_only_applies_to_checked_runtime_records ... ok -test parser::tests::test_rejects_unbraced_match_arms ... ok -test proof_plan::soundness::tests::strict_pp0201_only_applies_to_executing_script_args ... ok -test proof_plan::tests::checked_runtime_without_concrete_evidence_is_not_marked_covered ... ok -test proof_plan::tests::checked_static_detail_does_not_create_executable_runtime_evidence ... ok -test proof_plan::tests::replace_unique_features_are_transaction_scoped ... ok -test proof_plan::tests::metadata_only_invariant_proof_plan_has_no_executable_evidence ... ok -test proof_plan::tests::unique_lifecycle_features_have_specific_codegen_evidence_ids ... ok -test proof_plan::tests::checked_runtime_proof_plan_claims_include_executable_evidence ... ok -test repl::tests::repl_read_limited_line_accepts_bounded_input ... ok -test resolve::tests::test_global_type_resolution_rejects_ambiguous_symbol ... ok -test resolve::tests::test_grouped_use_resolves_multiple_symbols ... ok -test resolve::tests::rejects_cross_module_type_dependency_cycles ... ok -test resolve::tests::test_imported_type_resolution_uses_exact_module_path ... ok -test resolve::tests::test_module_resolver ... ok -test resolve::tests::test_path_resolver ... ok -test resolve::tests::test_register_module_rejects_deferred_missing_import_when_target_arrives ... ok -test resolve::tests::test_rejects_duplicate_local_symbols ... ok -test resolve::tests::test_register_module_rejects_missing_imported_symbol_when_target_is_loaded ... ok -test resolve::tests::test_rejects_import_alias_collisions ... ok -test runtime_errors::tests::diagnostic_messages_map_to_runtime_error_codes_where_possible ... ok -test runtime_errors::tests::runtime_error_docs_explain_ckb_code_overlap_channels ... ok -test runtime_errors::tests::runtime_error_registry_roundtrips_and_has_unique_codes ... ok -test runtime_errors::tests::runtime_error_docs_cover_every_registered_code ... ok -test simulate::tests::array_size_simulator_uses_checked_target_width_for_indices ... ok -test simulate::tests::simulate_if_branch ... ok -test simulate::tests::simulate_cell_operation_traces ... ok -test simulate::tests::simulate_pure_arithmetic_action ... ok -test simulate::tests::simulate_read_ref_traces ... ok -test simulate::tests::simulate_rejects_wrong_action_arity ... ok -test simulate::tests::simulate_step_limit ... ok -test stdlib::collections::tests::collection_assembly_has_no_raw_syscalls_or_unclassified_helpers ... ok -test simulate::tests::simulate_unsigned_high_bit_integer_operations ... ok -test stdlib::collections::tests::test_collection_functions ... ok -test stdlib::collections::tests::collection_public_helpers_do_not_dereference_raw_a0_handles ... ok -test repl::tests::repl_read_limited_line_rejects_oversized_input ... ok -test stdlib::tests::generated_stdlib_has_no_raw_syscall_wrapper_symbols ... ok -test stdlib::collections::tests::test_generate_assembly ... ok -test stdlib::tests::generated_stdlib_omits_raw_syscall_wrappers ... ok -test stdlib::tests::test_get_function ... ok -test stdlib::tests::test_generate_assembly ... ok -test stdlib::tests::test_scheduler_metadata_generate_molecule_uses_table_layout ... ok -test stdlib::tests::test_std_functions ... ok -test syscalls::tests::ckb_debug_syscall_is_not_a_production_inventory_surface ... ok -test stdlib::tests::test_generate_ckb_assembly_uses_checked_env_helpers ... ok -test syscalls::tests::emitted_manual_runtime_and_stdlib_helpers_are_classified ... ok -test syscalls::tests::every_low_level_syscall_spec_is_inventoried ... ok -test tests::action_scheduler_witness_bytes_rejects_conflicting_molecule_alias ... ok -test syscalls::tests::helper_inventory_has_no_duplicate_symbols ... ok -test syscalls::tests::ckb_syscall_abi_matches_checked_baseline ... ok -test tests::ckb_capacity_calculation_saturates_on_extreme_sizes ... ok -test tests::branch_local_anonymous_creates_are_rejected_until_effects_are_cfg_aware ... ok -test tests::ckb_deploy_manifest_rejects_conflicting_cell_dep_locations ... ok -test tests::ckb_constraints_surface_capacity_planning_for_created_outputs ... ok -test runtime_errors::tests::codegen_does_not_emit_unregistered_numeric_fail_literals ... ok -test tests::ckb_deploy_manifest_rejects_incomplete_split_cell_dep_location ... ok -test package::tests::package_manager_git_dependency_fails_for_invalid_url ... ok -test tests::ckb_deploy_manifest_rejects_invalid_dep_type ... ok -test tests::ckb_deploy_manifest_rejects_invalid_hash_type ... ok -test tests::ckb_deploy_manifest_surfaces_hash_type_and_dep_group_policy ... ok -test package::tests::package_manager_git_update_fails_closed_on_fetch_error ... ok -test lsp::tests::lsp_loads_sibling_modules_for_standalone_example_imports ... ok -test tests::ckb_target_profile_has_no_policy_exception ... ok -test tests::ckb_dynamic_vector_len_can_drive_mutate_transition ... ok -test tests::collection_fail_closed_feature_names_are_stable ... ok -test tests::ckb_lock_false_return_lowers_to_script_failure ... ok -test tests::ckb_entry_lock_scope_selects_lock_entrypoint ... ok -test tests::ckb_u64_syscall_helpers_check_return_code_and_size ... ok -test tests::compile_accepts_chain_neutral_timepoint_under_ckb_profile ... ok -test codegen::tests::internal_assembler_relaxes_out_of_range_register_conditional_branch ... ok -test tests::compile_accepts_action_witness_source_qualifier ... ok -test tests::compile_accepts_ckb_target_profile_timepoint ... ok -test tests::ckb_entry_scope_keeps_vec_element_schema_dependencies ... ok -test tests::compile_accepts_ckb_header_epoch_api_only_for_ckb_profile ... ok -test tests::compile_accepts_complete_branch_return_paths ... ok -test tests::compile_accepts_ckb_shared_create_when_verifier_covered ... ok -test tests::compile_accepts_empty_vec_literal_with_declared_type ... ok -test tests::ckb_entry_action_scope_excludes_unselected_unsupported_code ... ok -test tests::compile_accepts_create_field_shorthand ... ok -test tests::compile_accepts_flow_initial_create_at_any_declared_state ... ok -test tests::compile_accepts_flow_state_name_initializers ... ok -test tests::compile_accepts_core_input_output_state_transition_edges ... ok -test codegen::tests::emitted_runtime_helper_symbols_are_classified_in_syscall_inventory ... ok -test lexer::tests::rejects_oversized_block_comment ... ok -test tests::compile_accepts_explicit_flow_action_edges ... ok -test tests::compile_accepts_flow_edge_returning_to_first_state ... ok -test tests::compile_accepts_flow_on_custom_state_field ... ok -test tests::compile_accepts_kernel_effect_capabilities_for_destroy ... ok -test tests::compile_accepts_pure_ckb_target_profile ... ok -test tests::compile_accepts_non_initial_flow_create_without_consumed_prior_state ... ok -test tests::compile_accepts_lock_args_script_args_binding ... ok -test tests::compile_accepts_kernel_effect_capabilities_for_transfer ... ok -test tests::compile_accepts_lock_boundary_param_sources_and_require ... ok -test tests::compile_accepts_named_action_output_and_create_binding ... ok -test tests::compile_accepts_qualified_flow_state_names ... ok -test tests::compile_accepts_prefix_read_params_as_cell_dep_bindings ... ok -test tests::compile_allows_struct_type_id_under_ckb_profile ... ok -test tests::compile_accepts_static_flow_update_to_non_initial_state ... ok -test tests::compile_accepts_vec_literals_in_create_fields ... ok -test tests::compile_allows_actions_and_locks_to_call_pure_functions ... ok -test tests::compile_binds_duplicate_read_refs_by_order_not_name ... ok -test tests::compile_accepts_symmetric_where_branch_output_constraints ... ok -test tests::compile_allows_unit_function_calls_as_statements ... ok -test tests::compile_allows_flow_update_to_declared_initial_state_at_type_check ... ok -test tests::compile_binds_read_action_schema_params_to_cell_deps ... ok -test tests::compile_create_unique_field_identity_emits_runtime_anchor ... ok -test tests::compile_binds_read_ref_entry_params_to_cell_deps ... ok -test tests::compile_classifies_resource_merge_amount_sum_as_checked_runtime ... ok -test tests::compile_classifies_resource_split_amount_subtraction_as_checked_runtime ... ok -test tests::compile_emits_create_output_field_verification_for_fixed_u64_fields ... ok -test tests::compile_emits_direct_user_function_calls ... ok -test tests::compile_emits_ckb_style_load_cell_abi_for_cell_runtime_summary ... ok -test tests::compile_exposes_ckb_type_id_contract_under_ckb_profile ... ok -test tests::compile_classifies_guarded_identity_field_merge_as_checked_runtime ... ok -test tests::compile_classifies_protocol_agnostic_guarded_transition_as_checked_runtime ... ok -test tests::compile_destroy_policies_are_policy_aware ... ok -test tests::compile_entry_witness_rejects_payloads_larger_than_buffer ... ok -test tests::compile_file_explicit_target_overrides_manifest_build_target ... ok -test tests::compile_emits_protocol_agnostic_guard_equality_proofplan_records ... ok -test tests::compile_folds_local_fixed_array_len_to_constant ... ok -test tests::compile_file_loads_local_path_dependencies_from_cell_manifest ... ok -test tests::compile_file_uses_manifest_ckb_target_profile ... ok -test tests::compile_identity_ckb_type_id_emits_metadata ... ok -test tests::compile_file_uses_manifest_build_target_by_default ... ok -test tests::compile_identity_field_emits_path ... ok -test tests::compile_ignores_trivial_self_equality_guard_records ... ok -test tests::compile_identity_script_args_emits_metadata ... ok -test tests::compile_identity_none_is_default_and_hidden ... ok -test tests::compile_identity_singleton_type_emits_metadata ... ok -test tests::compile_file_source_content_hash_is_path_independent ... ok -test tests::bundled_token_example_strict_ckb_compile_is_admitted ... ok -test tests::compile_infers_and_validates_read_only_effects ... ok -test tests::compile_lowers_array_of_tuples_static_index_projection ... ok -test tests::compile_lowers_assert_invariant_into_fail_closed_cfg ... ok -test tests::compile_lowers_block_tail_if_expressions ... ok -test tests::compile_lowers_bounded_vec_literal_to_stack_collection ... ok -test tests::compile_lowers_byte_string_literals_with_expected_array_type ... ok -test tests::compile_lowers_consumed_input_field_access_through_loaded_cell_bytes ... ok -test tests::compile_lowers_for_range_into_counted_loop_cfg ... ok -test tests::compile_lowers_exhaustive_enum_match_without_wildcard ... ok -test tests::compile_lowers_if_expression_fixed_byte_const_join_move ... ok -test tests::compile_lowers_fixed_byte_schema_field_comparison ... ok -test tests::compile_lowers_if_expression_with_join_move ... ok -test tests::compile_lowers_ckb_group_source_large_immediate_to_riscv_elf ... ok -test tests::compile_lowers_len_method_to_length_instruction ... ok -test tests::compile_lowers_local_constants_into_real_operands ... ok -test tests::compile_lowers_if_statement_into_basic_blocks ... ok -test tests::compile_keeps_unchecked_transition_field_runtime_required ... ok -test tests::compile_lowers_local_struct_field_reads_and_writes ... ok -test tests::compile_lowers_local_tuple_destructuring_to_field_slots ... ok -test tests::compile_lowers_local_fixed_array_static_index_reads_and_writes ... ok -test tests::compile_lowers_local_tuple_static_field_reads_and_writes ... ok -test tests::compile_lowers_numeric_cast_without_zero_fallback ... ok -test tests::compile_lowers_packed_bool_and_u32_schema_fields_without_aligned_loads ... ok -test tests::compile_lowers_read_ref_schema_field_to_ckb_runtime_assembly ... ok -test tests::compile_lowers_mutable_assignments_in_loop_bodies ... ok -test tests::compile_lowers_match_expression_into_branch_cfg ... ok -test tests::compile_lowers_pure_function_assert_failure_to_abort ... ok -test tests::compile_lowers_stack_vec_clear_and_is_empty ... ok -test tests::compile_lowers_stack_vec_extend_from_fixed_bytes ... ok -test tests::compile_lowers_stack_vec_fixed_byte_capacity ... ok -test tests::compile_lowers_read_ref_schema_field_to_ckb_runtime_elf ... ok -test tests::compile_lowers_schema_backed_parameter_field_access_to_elf ... ok -test tests::compile_lowers_stack_vec_fixed_byte_pop ... ok -test tests::compile_lowers_stack_vec_fixed_byte_first_last ... ok -test tests::compile_lowers_stack_vec_fixed_byte_contains ... ok -test tests::compile_lowers_stack_vec_fixed_byte_insert ... ok -test tests::compile_lowers_stack_vec_fixed_byte_runtime_push_index ... ok -test tests::compile_lowers_stack_vec_scalar_capacity ... ok -test tests::compile_lowers_stack_vec_fixed_byte_remove ... ok -test tests::compile_lowers_stack_vec_scalar_contains ... ok -test tests::compile_lowers_stack_vec_fixed_byte_truncate ... ok -test tests::compile_lowers_stack_vec_fixed_byte_reverse ... ok -test tests::compile_lowers_stack_vec_scalar_insert ... ok -test tests::compile_lowers_stack_vec_scalar_first_last ... ok -test tests::compile_lowers_stack_vec_fixed_byte_set ... ok -test tests::compile_lowers_stack_vec_fixed_byte_swap ... ok -test tests::compile_lowers_stack_vec_scalar_remove ... ok -test tests::compile_lowers_stack_vec_scalar_pop ... ok -test tests::compile_lowers_stack_vec_scalar_set ... ok -test tests::compile_lowers_stack_vec_scalar_runtime_push_len_index ... ok -test tests::compile_lowers_stack_vec_scalar_swap ... ok -test tests::compile_lowers_stack_vec_scalar_reverse ... ok -test tests::compile_lowers_tail_expr_as_action_return ... ok -test tests::compile_lowers_stack_vec_scalar_truncate ... ok -test tests::compile_lowers_tail_if_as_action_return ... ok -test tests::compile_lowers_u128_equality_as_fixed_byte_comparison ... ok -test tests::compile_lowers_type_hash_without_generic_call ... ok -test tests::compile_lowers_vec_with_capacity_to_stack_collection_new ... ok -test tests::compile_merges_if_branch_linear_states_conservatively ... ok -test tests::compile_lowers_vec_builtins_without_generic_calls ... ok -test tests::compile_lowers_while_statement_into_loop_cfg ... ok -test tests::compile_lowers_zero_builtin_without_generic_call ... ok -test tests::compile_marks_cell_backed_vec_runtime_features ... ok -test tests::compile_merges_linear_transfers_inside_block_tail_if_expressions ... ok -test tests::compile_lowers_u128_mutate_delta_with_carry_arithmetic ... ok -test tests::compile_merges_linear_transfers_inside_if_expressions ... ok -test tests::compile_classifies_hash_committed_output_field_as_guarded ... ok -test tests::compile_merges_linear_transfers_inside_match_expressions ... ok -test tests::compile_metadata_exposes_ckb_type_id_create_output_plan_under_ckb_profile ... ok -test tests::compile_metadata_exposes_aggregate_invariant_primitives_in_proof_plan ... ok -test tests::compile_metadata_exposes_declared_invariant_proof_plan ... ok -test tests::compile_materializes_local_fixed_byte_constants_into_rodata ... ok -test tests::compile_metadata_exposes_lock_group_proof_plan_for_lock_entry ... ok -test tests::compile_metadata_with_options_rejects_strict_legacy_capabilities ... ok -test tests::compile_metadata_exposes_transaction_and_selected_cell_aggregate_invariants ... ok -test tests::compile_metadata_reports_parameterless_action_entrypoint_selection ... ok -test tests::compile_metadata_warns_for_lock_group_transaction_invariant_scope ... ok -test tests::compile_merges_linear_transfers_inside_block_expressions ... ok -test tests::compile_metadata_declares_molecule_vm_abi ... ok -test tests::compile_metadata_exposes_covenant_proof_plan_for_transfer ... ok -test tests::compile_path_rejects_duplicate_modules_across_source_roots ... ok -test tests::compile_metadata_proof_plan_preserves_lock_args_source ... ok -test tests::compile_path_rejects_missing_configured_source_root ... ok -test tests::compile_normalizes_same_module_qualified_helper_calls ... ok -test tests::compile_path_rejects_non_path_dependencies ... ok -test tests::compile_path_rejects_missing_path_dependency_manifest ... ok -test tests::compile_path_accepts_package_root ... ok -test tests::compile_path_ignores_examples_outside_package_source_roots ... ok -test tests::compile_path_rejects_path_dependency_cycles ... ok -test tests::compile_path_rejects_path_dependency_traversal ... ok -test tests::compile_metadata_with_options_uses_ast_optimizer_for_nonzero_levels ... ok -test tests::compile_package_import_alias_emits_matching_external_callable ... ok -test tests::compile_prefers_no_arg_main_for_entry_wrapper ... ok -test tests::compile_path_supports_custom_entry_directory_modules ... ok -test tests::compile_path_supports_configured_source_roots_without_src ... ok -test tests::compile_preserves_if_array_aggregate_slots ... ok -test tests::compile_preserves_create_instructions_in_assembly ... ok -test tests::compile_preserves_index_and_tuple_projection_in_assembly ... ok -test tests::compile_preserves_consume_and_destroy_instructions_in_assembly ... ok -test tests::compile_preserves_if_tuple_aggregate_slots ... ok -test tests::compile_preserves_match_tuple_aggregate_slots ... ok -test tests::compile_rejects_assert_delta_argument_from_cell_read ... ok -test tests::compile_rejects_aggregate_invariant_non_fixed_field ... ok -test tests::compile_rejects_assert_invariant_as_tail_return_value ... ok -test tests::compile_rejects_assignment_through_read_only_references ... ok -test tests::compile_rejects_assignment_to_immutable_array_element ... ok -test tests::compile_rejects_assignment_to_immutable_tuple_field ... ok -test tests::compile_rejects_assignment_to_temporary_field_targets ... ok -test tests::compile_rejects_asymmetric_where_branch_output_constraints ... ok -test tests::compile_rejects_bare_return_from_value_actions ... ok -test tests::compile_rejects_binding_assert_invariant_results ... ok -test tests::compile_rejects_bad_flow_state_field_type_on_main_path ... ok -test tests::compile_rejects_binding_unit_function_results ... ok -test tests::compile_rejects_bounded_vec_literal_type_mismatch ... ok -test tests::compile_rejects_builtin_call_argument_mismatches ... ok -test tests::compile_rejects_cell_metadata_stdlib_on_non_cell_args ... ok -test tests::compile_rejects_core_state_transition_edge_not_in_graph ... ok -test tests::compile_preserves_schema_backed_parameter_field_access_in_assembly ... ok -test tests::compile_rejects_destroy_without_destroy_capability ... ok -test tests::compile_rejects_duplicate_stable_type_ids ... ok -test tests::compile_rejects_duplicate_flow_for_same_state_field ... ok -test tests::compile_rejects_duplicate_top_level_symbols ... ok -test tests::compile_lowers_ckb_hash_commitment_comparison_without_fixed_byte_fail_closed ... ok -test tests::compile_preserves_dynamic_witness_cursor_after_lock_args ... ok -test tests::compile_rejects_dynamic_assert_invariant_messages ... ok -test tests::compile_rejects_dynamic_require_messages ... ok -test tests::compile_rejects_dynamic_initial_flow_create_state ... ok -test tests::compile_rejects_empty_array_length_mismatch ... ok -test tests::compile_rejects_dynamic_unique_identity_field ... ok -test tests::compile_rejects_empty_literal_in_non_vec_context ... ok -test tests::compile_rejects_enum_payload_variants_until_lowering_exists ... ok -test tests::compile_rejects_flow_by_action_when_explicit_move_uses_different_edge ... ok -test tests::compile_rejects_flow_on_plain_struct ... ok -test tests::compile_rejects_flow_by_action_without_exact_move_clause ... ok -test tests::compile_rejects_flow_receipt_without_state_field ... ok -test tests::compile_rejects_flow_payload_enum_state_field ... ok -test tests::compile_rejects_forbidden_unwrap_helpers ... ok -test tests::compile_rejects_helper_functions_that_indirectly_call_impure_actions ... ok -test tests::compile_rejects_function_call_argument_mismatches ... ok -test tests::compile_rejects_heterogeneous_array_literals ... ok -test tests::compile_rejects_if_expression_branch_type_mismatch ... ok -test tests::compile_rejects_impure_helper_functions ... ok -test tests::compile_rejects_incomplete_branch_return_paths ... ok -test tests::compile_rejects_input_source_outside_action_cell_params ... ok -test tests::compile_rejects_invalid_enum_match_patterns ... ok -test tests::compile_rejects_invalid_create_field_initializers ... ok -test tests::compile_rejects_invariant_assert_runtime_operation ... ok -test tests::compile_rejects_invalid_invariant_assert_expression ... ok -test tests::compile_rejects_invalid_destroy_policy_shapes ... ok -test tests::compile_rejects_invariant_without_explicit_trigger_and_scope ... ok -test tests::compile_rejects_local_binding_name_reuse ... ok -test tests::compile_rejects_local_fixed_array_static_oob_read ... ok -test tests::compile_rejects_local_fixed_array_static_oob_write ... ok -test tests::compile_rejects_linear_state_changes_hidden_inside_loops ... ok -test tests::compile_rejects_local_mutable_reference_aliases ... ok -test tests::compile_rejects_missing_action_return_paths ... ok -test tests::compile_rejects_missing_flow_state_create_on_main_path ... ok -test tests::compile_rejects_missing_function_return_paths ... ok -test tests::compile_rejects_non_bool_lock_definitions ... ok -test tests::compile_rejects_non_bool_assert_condition ... ok -test tests::compile_rejects_local_references_to_linear_roots ... ok -test tests::compile_rejects_noop_flow_transition_on_main_path ... ok -test tests::compile_rejects_owned_linear_field_assignment ... ok -test tests::compile_rejects_payload_or_unknown_enum_variant_values ... ok -test tests::compile_rejects_pure_functions_that_call_ckb_header_runtime_builtins ... ok -test tests::compile_rejects_out_of_range_flow_state_create_on_main_path ... ok -test tests::compile_rejects_pure_functions_that_call_env_runtime_builtins ... ok -test tests::compile_rejects_pure_functions_that_call_locks ... ok -test tests::compile_rejects_read_ref_for_non_cell_backed_types ... ok -test tests::compile_rejects_pure_functions_that_call_type_hash_runtime_builtin ... ok -test tests::compile_produces_non_empty_riscv_assembly ... ok -test tests::compile_rejects_return_values_from_unit_actions ... ok -test tests::compile_rejects_returning_unit_function_results ... ok -test tests::compile_rejects_state_edge_that_does_not_consume_binding ... ok -test tests::compile_rejects_stateful_operations_without_named_linear_cell_operands ... ok -test tests::compile_rejects_string_literals_as_runtime_values ... ok -test tests::compile_rejects_unbound_assert_delta_argument ... ok -test tests::compile_rejects_undeclared_action_state_edge ... ok -test tests::compile_rejects_underdeclared_effect_annotations ... ok -test tests::compile_rejects_reference_escape_boundaries ... ok -test tests::compile_rejects_underdeclared_effects_through_calls ... ok -test tests::compile_rejects_unknown_functions ... ok -test tests::compile_rejects_underdeclared_effects_through_qualified_calls ... ok -test tests::compile_rejects_unknown_target_profile ... ok -test tests::compile_rejects_unknown_target_during_option_validation ... ok -test tests::compile_rejects_unknown_struct_fields ... ok -test tests::compile_rejects_unknown_or_reserved_named_types ... ok -test tests::compile_rejects_unreachable_statements_after_return ... ok -test tests::compile_rejects_unreachable_statements_after_complete_branch_return ... ok -test tests::compile_rejects_unstable_callable_parameter_names ... ok -test tests::compile_rejects_unsupported_optimization_level ... ok -test tests::compile_rejects_unstable_schema_field_names ... ok -test tests::compile_rejects_unsound_mutable_parameter_forms ... ok -test tests::compile_rejects_untyped_empty_array_literals ... ok -test tests::compile_rejects_wrong_qualified_flow_state_field_initializer ... ok -test tests::compile_preserves_read_ref_instructions_in_assembly ... ok -test tests::compile_rejects_unsupported_vec_helper_type_combinations ... ok -test tests::compile_result_exposes_nested_fixed_molecule_schema_metadata ... ok -test tests::compile_produces_non_empty_riscv_elf ... ok -test tests::compile_rejects_state_transitions_inside_locks ... ok -test tests::compile_produces_ckb_elf_without_vm_abi_trailer ... ok -test tests::compile_result_exposes_schema_layout_metadata ... ok -test tests::compile_rejects_lock_boundary_sources_outside_supported_scope ... ok -test tests::compile_result_validation_rejects_compiler_version_mismatch ... ok -test tests::compile_result_exposes_scheduler_metadata_sidecar ... ok -test tests::compile_replace_unique_field_identity_compares_input_and_output ... ok -test tests::compile_result_validation_rejects_constraints_artifact_format_mismatch ... ok -test tests::compile_result_validation_rejects_assembly_with_vm_abi_trailer ... ok -test tests::compile_result_validation_rejects_metadata_artifact_format_mismatch ... ok -test tests::compile_result_validation_rejects_metadata_artifact_hash_mismatch ... ok -test tests::compile_result_validation_rejects_constraints_artifact_size_mismatch ... ok -test tests::compile_result_validation_accepts_current_outputs ... ok -test tests::compile_result_validation_rejects_metadata_artifact_size_mismatch ... ok -test tests::compile_result_validation_rejects_metadata_schema_version_mismatch ... ok -test tests::compile_result_validation_rejects_mismatched_ckb_type_id_create_output_plan ... ok -test tests::compile_result_validation_rejects_mismatched_ckb_output_data_binding ... ok -test tests::compile_result_validation_rejects_metadata_schema_downgrade ... ok -test tests::compile_result_validation_rejects_metadata_source_content_hash_mismatch ... ok -test tests::compile_result_validation_rejects_metadata_target_profile_v0_14_abi_mismatch ... ok -test tests::compile_reports_equivalent_state_transition_obligation_for_sugar_and_core_forms ... ok -test tests::compile_result_validation_rejects_metadata_source_hash_mismatch ... ok -test tests::compile_result_validation_rejects_metadata_target_profile_mismatch ... ok -test tests::compile_result_validation_rejects_type_id_hash_mismatch ... ok -test tests::compile_result_validation_rejects_tampered_artifact_hash ... ok -test tests::compile_result_validation_rejects_missing_metadata_artifact_size ... ok -test tests::compile_result_validation_rejects_molecule_schema_hash_mismatch ... ok -test tests::compile_result_validation_rejects_noncanonical_source_unit_hash ... ok -test tests::compile_spills_parameters_and_returns_computed_value ... ok -test tests::compile_supports_typed_empty_array_literals ... ok -test tests::compile_result_writes_artifact_to_disk ... ok -test tests::compile_lowers_stack_vec_fixed_schema_values ... ok -test tests::compile_surfaces_type_level_hash_type_dsl_metadata ... ok -test tests::compile_riscv_elf_accepts_full_width_u64_literals ... ok -test tests::compile_tracks_linear_values_returned_from_complete_branches ... ok -test tests::compile_tracks_linear_values_returned_from_tail_if_branches ... ok -test tests::compile_unrolls_local_array_of_tuples_foreach_destructuring ... ok -test tests::compile_unrolls_local_fixed_array_foreach_without_runtime_indexing ... ok -test tests::compile_unrolls_fixed_param_array_foreach_with_pointer_abi ... ok -test codegen::tests::codegen_rejects_generated_far_jump_scratch_relaxation ... ok -test tests::compile_uses_ast_optimizer_for_nonzero_optimization_levels ... ok -test tests::compile_verifies_create_output_against_computed_scalar_stack_value ... ok -test tests::compile_tracks_linear_values_inside_aggregate_bindings ... ok -test tests::compile_verifies_create_output_against_consumed_input_field_alias ... ok -test tests::compile_verifies_created_output_bool_and_u32_fields ... ok -test tests::compile_verifies_constructed_fixed_width_vec_output ... ok -test tests::default_output_path_for_package_input_uses_build_dir ... ok -test tests::default_output_path_for_package_input_uses_manifest_out_dir ... ok -test tests::compile_verifies_created_scalar_fields_against_consumed_input_aliases ... ok -test tests::compiled_riscv_elf_contains_exit_trampoline ... ok -test tests::create_output_verifier_accepts_const_lock_hash ... ok -test tests::compile_verifies_large_output_field_requirements_without_partial_fallback ... ok -test tests::create_output_verifier_accepts_fixed_byte_params_and_consts ... ok -test tests::entry_abi_constraints_mark_extreme_slot_counts_unsupported ... ok -test tests::entry_witness_encoder_includes_schema_backed_params_as_length_prefixed_bytes ... ok -test tests::entry_witness_bool_params_are_canonicalized ... ok -test tests::entry_witness_encoder_matches_u64_wrapper_abi ... ok -test tests::compile_riscv_elf_accepts_large_schema_field_offsets ... ok -test tests::dynamic_schema_fixed_vec_length_is_table_decoded ... ok -test tests::dynamic_schema_fixed_field_access_is_table_decoded ... ok -test tests::compile_unique_script_args_and_singleton_identity_emit_hash_checks ... ok -test tests::entry_witness_encoder_supports_fixed_byte_params ... ok -test tests::ir_carries_flow_rules ... ok -test tests::dynamic_mutable_schema_transitions_are_checked_after_table_decoding ... ok -test tests::ir_lowers_unit_function_calls_without_result_destinations ... ok -test tests::ir_rejects_unknown_call_return_types_without_u64_fallback ... ok -test tests::ir_preserves_function_call_return_types ... ok -test tests::ir_summary_captures_cell_runtime_accesses ... ok -test tests::load_modules_for_input_collects_package_source_roots ... ok -test tests::fixed_enum_fields_have_molecule_schema_metadata ... ok -test tests::dynamic_schema_fixed_vec_iteration_is_table_decoded ... ok -test tests::internal_calls_keep_outgoing_stack_area_abi_aligned ... ok -test tests::generated_outgoing_stack_reservations_are_psabi_aligned ... ok -test tests::dynamic_named_output_constraints_are_proven_in_where_block ... ok -test tests::package_entry_must_stay_inside_package_root ... ok -test tests::package_out_dir_must_stay_inside_package_root ... ok -test tests::package_source_roots_must_stay_inside_package_root ... ok -test tests::primitive_compat_predicates_match_validator_modes ... ok -test tests::loaded_artifact_validation_rejects_metadata_artifact_size_mismatch ... ok -test tests::generic_shared_mutation_does_not_emit_pool_pattern_metadata ... ok -test tests::entry_witness_wrapper_supports_scalar_stack_args ... ok -test tests::fixed_byte_mutable_state_set_transition_is_checked_under_ckb_profile ... ok -test tests::scheduler_witness_hex_decode_rejects_invalid_metadata_hex ... ok -test tests::resolve_input_path_accepts_package_root_and_manifest ... ok -test tests::proof_plan_checked_static_excluded_from_on_chain_checked_obligations ... ok -test tests::proof_plan_cross_references_matching_action_obligation_for_invariant ... ok -test tests::named_action_output_create_binding_reuses_declared_output_index ... ok -test tests::compile_riscv_elf_accepts_large_stack_offsets ... ok -test tests::tuple_return_abi_rejects_more_than_eight_fields ... ok -test tests::vm_abi_trailer_detection_requires_complete_zero_reserved_trailer ... ok -test types::tests::block_expression_merges_existing_vec_refinements ... ok -test types::tests::branch_local_consume_is_rejected_until_lifecycle_effects_are_cfg_aware ... ok -test types::tests::branch_local_create_is_rejected_until_lifecycle_effects_are_cfg_aware ... ok -test types::tests::byte_string_literal_type_uses_actual_length ... ok -test types::tests::call_arguments_do_not_coerce_mut_ref_to_ref ... ok -test types::tests::check_without_resolver_rejects_imports ... ok -test types::tests::compound_assign_rejects_implicit_narrowing ... ok -test tests::source_unit_disk_verification_accepts_paths_inside_trusted_root ... ok -test types::tests::const_initializers_allow_supported_literals ... ok -test types::tests::const_initializers_reject_cell_backed_types ... ok -test types::tests::compound_assign_uses_numeric_binary_rules ... ok -test types::tests::const_initializers_reject_cell_lifecycle_expressions ... ok -test types::tests::const_initializers_reject_computed_expressions ... ok -test types::tests::constant_narrowing_casts_must_fit ... ok -test types::tests::contextual_integer_literals_fit_declared_widths ... ok -test types::tests::cyclic_schema_type_dependencies_are_rejected ... ok -test types::tests::duplicate_lifecycle_binding_is_rejected_until_effects_are_cfg_aware ... ok -test types::tests::expected_type_does_not_widen_non_literal_abi_arg_boundary ... ok -test types::tests::expected_type_does_not_widen_non_literal_field_boundary ... ok -test types::tests::expected_type_does_not_widen_non_literal_let_boundary ... ok -test types::tests::expected_type_does_not_widen_non_literal_return_boundary ... ok -test types::tests::expression_branch_unreachable_code_is_rejected_by_typechecker ... ok -test types::tests::explicit_cast_can_cross_integer_width_boundary ... ok -test tests::source_unit_disk_verification_rejects_paths_outside_trusted_root ... ok -test types::tests::generic_reference_detection_uses_type_structure ... ok -test types::tests::if_expression_preserves_typed_vec_result_with_empty_constructor_branch ... ok -test types::tests::if_statement_merges_matching_vec_refinements ... ok -test types::tests::if_statement_rejects_divergent_vec_refinements ... ok -test types::tests::imported_and_qualified_names_compare_as_same_type ... ok -test types::tests::if_statement_rejects_one_sided_vec_refinement ... ok -test types::tests::imported_type_ids_must_not_collide_in_visible_module_scope ... ok -test tests::parameterized_entrypoint_emits_witness_entry_wrapper ... ok -test types::tests::invalid_schema_field_types_are_not_registered_as_valid_fields ... ok -test types::tests::imported_token_type_is_treated_as_linear ... ok -test types::tests::lifecycle_capability_gates_reject_undeclared_kernel_effects ... ok -test types::tests::match_requires_enum_scrutinee ... ok -test types::tests::imported_linear_argument_is_marked_consumed_after_call ... ok -test types::tests::mixed_width_arithmetic_and_ordering_are_rejected ... ok -test types::tests::numeric_type_equality_respects_width ... ok -test types::tests::numeric_named_type_equality_is_commutative ... ok -test tests::proof_plan_marks_invariant_action_evidence_as_non_exhaustive ... ok -test types::tests::non_tail_linear_expression_statements_are_rejected ... ok -test types::tests::preserve_rejects_mismatched_field_types ... ok -test types::tests::require_block_rejects_assignment_expression ... ok -test types::tests::qualified_identifier_must_resolve_to_value ... ok -test tests::v014_runtime_helpers_fail_closed_when_not_executable ... ok -test types::tests::recursive_enum_payloads_are_rejected ... ok -test types::tests::require_block_rejects_lifecycle_stdlib_call ... ok -test types::tests::require_rejects_nested_cell_operation ... ok -test types::tests::statically_visible_division_by_zero_is_rejected ... ok -test types::tests::stdlib_claim_output_requires_complete_field_coverage ... ok -test types::tests::stdlib_claim_rejects_extra_arguments ... ok -test types::tests::stdlib_claim_output_requires_declared_claim_output_type ... ok -test types::tests::stdlib_claim_rejects_non_receipt_input ... ok -test types::tests::stdlib_claim_requires_explicit_output_and_lock_arguments ... ok -test types::tests::stdlib_claim_rejects_declared_output_type_mismatch ... ok -test types::tests::launch_module_type_checks_with_registered_imports ... ok -test types::tests::strict_mode_rejects_imported_legacy_capabilities ... ok -test types::tests::stdlib_transfer_rejects_extra_arguments ... ok -test types::tests::typed_vec_with_capacity_uses_declared_element_type ... ok -test types::tests::stdlib_settle_requires_explicit_output_and_lock_arguments ... ok -test types::tests::stdlib_transfer_output_requires_complete_field_coverage ... ok -test types::tests::tail_match_expressions_are_valid_return_values ... ok -test types::tests::unsigned_integer_negation_is_rejected ... ok -test wasm::tests::wasm_audit_reports_audit_only_for_type_only_module ... ok -test wasm::tests::wasm_compiler_rejects_pure_action_modules ... ok -test types::tests::vec_type_arguments_are_validated ... ok -test types::tests::unsupported_u128_arithmetic_is_rejected ... ok -test types::tests::u128_ordering_and_arithmetic_still_rejected_on_widening ... ok -test wasm::tests::wasm_encoder_emits_magic_version_and_status_custom_section ... ok -test wasm::tests::wasm_runtime_instantiates_metadata_module_but_refuses_calls ... ok -test types::tests::widening_boundary_matrix ... ok -test tests::strict_audit_codegen_emits_only_aligned_stack_pointer_deltas ... ok -test tests::ordered_named_output_create_constraints_are_checked_in_body_order ... ok -test tests::u128_mutable_state_transition_with_u64_delta_is_checked ... ok -test tests::payload_enum_fields_use_dynamic_molecule_schema_metadata ... ok -test tests::optimized_entry_lock_keeps_inlined_schema_pointer_field_access_checked ... ok -test codegen::tests::internal_assembler_relaxes_far_conditional_branch_with_long_jump ... ok -test codegen::tests::internal_assembler_encodes_far_unconditional_jump ... ok -test codegen::tests::bundled_example_codegen_mnemonics_are_declared ... ok - -test result: ok. 776 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.56s - - -running 7 tests -test adversarial_0_13_rejects_invalid_hash_type_dsl ... ok -test adversarial_parser_preserves_operator_precedence_in_ambiguous_sequences ... ok -test adversarial_parser_binds_else_to_nearest_if ... ok -test adversarial_parser_rejects_deep_unary_expression_without_panicking ... ok -test adversarial_integer_literals_fail_closed_on_lexical_and_contextual_overflow ... ok -test adversarial_parser_rejects_deep_nested_control_flow_without_panicking ... ok -test adversarial_0_13_rejects_unsupported_generic_collection_surfaces ... ok - -test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s - - -running 11 tests -test snapshot_simple_action_assembly ... ok -test runtime_u64_helpers_fail_closed_before_value_use ... ok -test runtime_void_helpers_fail_closed_before_continuing ... ok -test snapshot_lock_args_assembly ... ok -test snapshot_type_id_create_output_assembly ... ok -test snapshot_spawn_ipc_executable_status_checked_assembly ... ok -test runtime_witness_helpers_fail_closed_before_pointer_use ... ok -test snapshot_witness_schema_syscall_assembly ... ok -test snapshot_collection_lowering_assembly ... ok -test snapshot_blake2b_helper_assembly ... ok -test snapshot_assemblies_contain_no_leaked_overflow_diagnostics ... ok - -test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.23s - - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s - - -running 86 tests -test cellc_add_git_requires_full_rev_and_records_pin ... ok -test cellc_add_and_remove_subcommands_honor_dev_path_and_json ... ok -Check succeeded - Target profile: ckb - Checked: package default (RISC-V assembly) -test cellc_check_accepts_ckb_profile_timepoint ... ok -test cellc_check_denies_metadata_only_declared_invariant ... ok -test cellc_abi_subcommand_explains_entry_witness_layout ... ok -test cellc_check_accepts_pure_ckb_target_profile ... ok -test cellc_build_uses_manifest_policy_before_writing_artifacts ... ok -test cellc_action_build_emits_builder_plan_json ... ok -test cellc_check_production_rejects_fail_closed_runtime_paths ... ok -Build complete - Artifact format: RISC-V assembly - Target profile: ckb - Output: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpLeWH4w/build/main.s - Metadata: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpLeWH4w/build/main.s.meta.json -test cellc_check_production_rejects_incomplete_output_verification ... ok -test cellc_build_accepts_pure_ckb_target_profile_without_vm_abi_trailer ... ok -test cellc_check_reports_claim_source_predicate_blocker_class ... ok -test cellc_build_and_check_subcommands_use_package_flow ... ok -test cellc_check_can_reject_runtime_required_obligations ... ok -test cellc_check_denies_checked_partial_proof_plan_gap ... ok -test cellc_clean_subcommand_supports_json_summary ... ok -test cellc_ckb_hash_emits_default_blake2b_vector ... ok -test cellc_check_all_targets_checks_asm_and_elf_without_writing_artifacts ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [5d, 8b, d, ed, a5, 24, 99, 9f, c3, fe, 29, 67, 78, 19, 2a, 46, 8f, a3, b5, 44, cb, 36, cf, cc, e2, 10, 50, 24, 59, 4b, 5b, 37] - Output: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpdSbrBT/artifacts/main.s - Metadata: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpdSbrBT/artifacts/main.s.meta.json -test cellc_check_uses_manifest_policy_defaults ... ok -test cellc_check_accepts_u128_mutable_state_transition_with_u64_delta ... ok -test cellc_check_reports_linear_collection_ownership_blocker_class ... ok -test cellc_check_reports_resource_conservation_blocker_class ... ok -test cellc_cli_target_overrides_manifest_build_target ... ok -test cellc_check_reports_settle_finalization_blocker_class ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [9f, aa, 3c, b9, 5, 1b, a7, 19, e4, ea, e4, 1, 79, 7, 11, 89, 7f, 40, ba, 26, 7e, 86, ba, 8c, d5, a3, a, 4d, 3, 45, eb, 54] - Output: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpRDoIpR/app_pkg/build/main.s - Metadata: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpRDoIpR/app_pkg/build/main.s.meta.json -test cellc_check_reports_explicit_output_binding_without_mutable_state_blockers ... ok -test cellc_compiles_package_with_local_path_dependency ... ok -test cellc_doc_subcommand_generates_markdown_docs ... ok -test cellc_constraints_subcommand_surfaces_ckb_deployment_manifest ... ok -test cellc_explain_profile_reports_ckb_v0_14_contract ... ok -test cellc_entry_witness_subcommand_rejects_wrong_width_fixed_bytes ... ok -test cellc_entry_witness_subcommand_encodes_schema_backed_params ... ok -test cellc_errors_include_runtime_ecode_when_policy_failure_maps_to_runtime_registry ... ok -test cellc_entry_witness_subcommand_emits_parameterized_witness_json ... ok -test cellc_explain_subcommand_reports_runtime_error ... ok -test cellc_explain_proof_reports_declared_invariant ... ok -test cellc_info_subcommand_supports_json_summary ... ok -test cellc_init_subcommand_supports_json_summary ... ok -test cellc_explain_proof_warns_for_lock_group_transaction_scope ... ok -test cellc_lsp_flag_rejects_trailing_arguments ... ok -test cellc_explain_proof_reports_invariant_action_coverage_match ... ok -Formatting complete - Updated 1 file(s) -test cellc_explain_proof_reports_covenant_proof_plan ... ok -test cellc_explain_proof_human_reports_macro_provenance ... ok -test cellc_new_subcommand_supports_json_summary_and_vcs_none ... ok -test cellc_fmt_subcommand_formats_sources ... ok -test cellc_explain_proof_summary_reports_fail_closed_diagnostics ... ok -test cellc_run_subcommand_without_vm_runner_degrades_gracefully ... ok -test cellc_rejects_registry_package_dependencies_fail_closed ... ok -test cellc_rejects_external_dependency_function_calls_until_linking_exists ... ok -test cellc_install_path_updates_lockfile_and_remove_prunes_it ... ok -test cellc_test_subcommand_rejects_conflicting_expectations ... ok -test cellc_rejects_underdeclared_effects_from_path_dependency_calls ... ok -test cellc_test_subcommand_rejects_empty_expected_error_line_text ... ok -test cellc_test_subcommand_rejects_missing_expected_error_text ... ok -test cellc_metadata_subcommand_emits_lowering_runtime_json ... ok -test cellc_test_subcommand_rejects_unknown_directives ... ok -test cellc_test_subcommand_rejects_wrong_expected_error_line ... ok -test cellc_test_subcommand_rejects_missing_entrypoint_metadata ... ok -test cellc_test_subcommand_rejects_missing_runtime_metadata ... ok -test cellc_test_subcommand_supports_expected_compile_failures ... ok -test cellc_test_subcommand_supports_expected_error_line_directive ... ok -test cellc_test_subcommand_compiles_test_sources ... ok -test cellc_test_subcommand_supports_entrypoint_metadata_directives ... ok -test cellc_new_subcommand_initializes_git_by_default ... ok -test cellc_test_subcommand_supports_runtime_metadata_directives ... ok -test cellc_opt_report_compares_all_optimization_levels ... ok -test cellc_top_level_primitive_strict_rejects_legacy_capabilities ... ok -test cellc_scheduler_plan_consumes_shared_touch_hints ... ok -test cellc_check_reports_transaction_invariant_checked_subconditions ... ok -test cellc_test_subcommand_supports_target_directive ... ok -test cellc_test_subcommand_supports_policy_directives ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [5d, 8b, d, ed, a5, 24, 99, 9f, c3, fe, 29, 67, 78, 19, 2a, 46, 8f, a3, b5, 44, cb, 36, cf, cc, e2, 10, 50, 24, 59, 4b, 5b, 37] - Output: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpA5Mqvw/artifacts/main.s - Metadata: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpA5Mqvw/artifacts/main.s.meta.json -test cellc_uses_manifest_build_out_dir_for_package_input ... ok -test cellc_top_level_accepts_primitive_strict_for_kernel_effect_capabilities ... ok -success: compiled successfully - Artifact format: RISC-V ELF - Target profile: ckb - Artifact hash: [cf, 7, cf, ac, d0, a, 43, a3, a8, cc, 8b, 6e, 66, e1, 29, b2, 32, 60, 2f, 76, a3, 55, 4d, 52, d5, 38, 51, 1f, c8, b, 49, 2] - Output: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpEPDDUG/artifacts/main.elf - Metadata: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpEPDDUG/artifacts/main.elf.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp5nuyD3/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp5nuyD3/sample.s.meta.json -test cellc_check_reports_pool_invariant_policy_families ... ok -test cellc_uses_manifest_build_target_by_default ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpHdp8aG/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpHdp8aG/sample.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [a7, d4, 78, cc, c3, f5, cd, 81, cd, de, 51, 44, ee, 83, 4d, 64, 46, df, bd, 40, 58, 5f, 51, 6c, d1, 56, 6b, b7, 44, 9d, 96, 8d] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpVABuXl/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpVABuXl/sample.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp7Y5SaD/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp7Y5SaD/sample.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp41919d/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp41919d/sample.s.meta.json -test cellc_verify_artifact_enforces_policy_flags ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpVjkaXz/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpVjkaXz/sample.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpzDHwMo/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpzDHwMo/sample.s.meta.json -test cellc_verify_artifact_accepts_matching_sidecar ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmppVowcH/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmppVowcH/sample.s.meta.json -test cellc_verify_artifact_primitive_strict_rechecks_disk_sources ... ok -test cellc_verify_artifact_rejects_tampered_artifact ... ok -test cellc_verify_artifact_rejects_noncanonical_source_unit_hash ... ok -test cellc_writes_requested_output_file ... ok -test cellc_verify_artifact_rejects_tampered_source_when_requested ... ok -test cellc_verify_artifact_rejects_metadata_schema_downgrade ... ok -test cellc_verify_artifact_enforces_expected_hashes ... ok -test cellc_explain_generics_reports_checked_vec_instantiations ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [79, a1, 5d, 7e, f7, 1f, 9a, 64, 89, da, 9e, 8b, a8, 90, b6, 15, f0, b5, 61, d1, 80, 6b, 39, 9f, f0, 5a, a4, 4d, 0, 5, 41, 3c] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpwTleoZ/amm_pool.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpwTleoZ/amm_pool.s.meta.json -test cellc_check_reports_checked_pool_invariant_families_without_runtime_blockers ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [2a, 2, ca, c2, fd, b6, 4a, 53, 9e, 26, cb, a1, 31, 69, ab, f3, 1d, c1, 42, d, 18, d3, fd, 1d, 92, b7, a, 55, c6, d, 87, df] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpwTleoZ/launch.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpwTleoZ/launch.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [8b, d7, 59, d4, b6, d1, 6, 8b, 97, 0, fd, e5, df, 72, ec, a6, 99, bf, 20, 34, 90, 55, b2, 17, 6d, 48, 55, 9e, ec, ed, 11, 2b] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpwTleoZ/multisig.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpwTleoZ/multisig.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [cd, d0, f5, 74, b7, 9d, 8e, 7d, 79, 50, 6a, cf, 3e, 13, b, 53, 5c, b9, 7f, 8c, d1, 1f, 88, bf, 1b, 8a, 3c, 3b, 34, 45, c6, 4e] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpwTleoZ/nft.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpwTleoZ/nft.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [fd, 14, d8, d9, e, 2c, e8, 71, 98, aa, e2, b6, b4, fc, b8, 93, aa, 84, 66, 2f, 21, 2e, 9d, 26, 5, 63, 5d, 74, 55, fd, 56, 87] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpwTleoZ/timelock.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpwTleoZ/timelock.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [3c, bd, 90, ed, de, 2c, 8d, 8c, 97, 1, a4, d9, 9, dc, 3d, bd, 22, 6b, 5b, 39, e7, 3e, 59, 9a, 5d, e1, 2c, 13, 61, 4d, 32, 46] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpwTleoZ/token.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpwTleoZ/token.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [2a, 7, 99, 55, e2, cb, e6, 1b, 39, 63, db, fc, 1, 63, fd, 57, 38, 6, a4, 7, ad, b2, 5d, 5c, f1, de, 41, e5, 2a, 29, 1, e5] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpwTleoZ/vesting.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpwTleoZ/vesting.s.meta.json -test cellc_compiles_bundled_examples_to_requested_outputs ... ok - -test result: ok. 86 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.90s - - -running 26 tests -test ckb_scoped_entry_keeps_called_action_helpers ... ok -test launch_seed_pool_composition_is_scheduler_visible ... ok -test registry_example_uses_bounded_local_vec_helpers_without_collection_debt ... ok -test amm_pool_input_output_params_are_scheduler_visible ... ok -test release_examples_are_free_of_placeholder_hashes_and_formatter_artifacts ... ok -test registry_example_with_insert_contains_compiles_to_elf ... ok -test nft_core_actions_expose_action_specific_builder_metadata ... ok -test token_cell_invariant_appears_in_proof_plan ... ok -test order_book_language_example_uses_local_vec_helpers_without_collection_debt ... ok -test stdlib_language_example_compiles_with_all_patterns ... ok -test v0_15_scoped_invariant_example_compiles_and_produces_proof_plan ... ok -test token_mint_authority_input_output_binding_is_explicit ... ok -test v0_15_identity_lifecycle_example_compiles_and_produces_proof_plan ... ok -test vesting_phase2_remaining_obligations_are_explicit ... ok -test vesting_read_ref_params_are_scheduler_visible ... ok -test multisig_core_actions_expose_threshold_flow_metadata ... ok -test timelock_core_actions_expose_time_and_release_metadata ... ok -test bundled_examples_emit_molecule_schema_manifest_report ... ok -test canonical_examples_are_the_single_checked_in_business_source ... ok -test bundled_examples_compile_to_non_empty_assembly ... ok -test canonical_examples_compile_under_primitive_strict_015 ... ok -test bundled_examples_stay_within_backend_shape_budgets ... ok -test bundled_examples_backend_shape_report_serializes ... ok -test bundled_examples_stay_near_backend_shape_release_baseline ... ok -test all_checked_in_cell_examples_compile ... ok -test bundled_examples_compile_to_elf ... ok - -test result: ok. 26 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.91s - - -running 7 tests -test fuzzy_oversized_static_widths_are_controlled_errors ... ok -test fuzzy_metadata_tampering_never_panics ... ok -test fuzzy_unicode_hex_inputs_are_controlled_errors ... ok -test fuzzy_entry_witness_encoding_never_panics ... ok -test fuzzy_lsp_incremental_edits_never_panic ... ok -test fuzzy_mutated_sources_never_panic ... ok -test fuzzy_semantic_codegen_mutations_reach_assembly ... ok - -test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.50s - - -running 4 tests -test ickb_diff_matrix_is_partial_and_consistent_with_model_fixtures ... ok -test ickb_positive_fixtures_pass_model_verifier ... ok -test ickb_negative_fixtures_fail_for_expected_invariant ... ok -test ickb_benchmark_specs_compile_and_expose_expected_entries ... ok - -test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.28s - - -running 1 test -test syntax_combo_quick_matrix_is_cargo_test_visible ... FAILED - -failures: - ----- syntax_combo_quick_matrix_is_cargo_test_visible stdout ---- - -thread 'syntax_combo_quick_matrix_is_cargo_test_visible' (8226730) panicked at tests/syntax_combo.rs:15:5: -syntax combo quick runner failed -status: exit status: 1 -stdout: - -stderr: -Traceback (most recent call last): - File "/Users/arthur/RustroverProjects/CellScript/scripts/cellscript_syntax_combo_audit.py", line 1482, in - raise SystemExit(main(sys.argv[1:])) - File "/Users/arthur/RustroverProjects/CellScript/scripts/cellscript_syntax_combo_audit.py", line 1414, in main - timestamp = dt.datetime.now(dt.UTC).strftime("%Y%m%d-%H%M%S") -AttributeError: module 'datetime' has no attribute 'UTC' - -note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace - - -failures: - syntax_combo_quick_matrix_is_cargo_test_visible - -test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.18s - - -== stderr == - Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.04s - Finished `test` profile [unoptimized + debuginfo] target(s) in 0.04s - Running unittests src/lib.rs (target/debug/deps/cellscript-198f0ba9a296fb91) - Running tests/adversarial_0_13.rs (target/debug/deps/adversarial_0_13-87ca0b7751a0cf60) - Running tests/assembly_snapshots.rs (target/debug/deps/assembly_snapshots-a1b0ee4291a50be7) - Running tests/ckb_acceptance.rs (target/debug/deps/ckb_acceptance-546e6523e51ab114) - Running tests/cli.rs (target/debug/deps/cli-cd9b4a3e7ed668b4) - Running tests/examples.rs (target/debug/deps/examples-885631b36bce043e) - Running tests/fuzzy_debug.rs (target/debug/deps/fuzzy_debug-a3c500396cf14cf4) - Running tests/ickb_benchmark.rs (target/debug/deps/ickb_benchmark-d0fd214d43bb34ab) - Running tests/syntax_combo.rs (target/debug/deps/syntax_combo-b9abeffd268b17da) -error: test failed, to rerun pass `-p cellscript --test syntax_combo` diff --git a/.cap/logs/1780406425-73610.log b/.cap/logs/1780406425-73610.log deleted file mode 100644 index aec64533..00000000 --- a/.cap/logs/1780406425-73610.log +++ /dev/null @@ -1,11 +0,0 @@ -== stdout == - -running 1 test -test syntax_combo_quick_matrix_is_cargo_test_visible ... ok - -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.42s - - -== stderr == - Finished `test` profile [unoptimized + debuginfo] target(s) in 0.05s - Running tests/syntax_combo.rs (target/debug/deps/syntax_combo-b9abeffd268b17da) diff --git a/.cap/logs/1780406434-74725.log b/.cap/logs/1780406434-74725.log deleted file mode 100644 index 26b98e82..00000000 --- a/.cap/logs/1780406434-74725.log +++ /dev/null @@ -1,1191 +0,0 @@ -== stdout == - -running 776 tests -test cli::commands::tests::test_command_execution ... ok -test ckb_hash_tests::ckb_blake2b256_matches_blank_hash_vector ... ok -test codegen::assembler::tests::strict_audit_internal_assembler_oracle_for_core_instruction_bytes ... ok -test cli::commands::tests::invalid_parser_mapping_returns_error_instead_of_panicking ... ok -test codegen::assembler::tests::strict_audit_riscv_immediate_boundaries_are_enforced ... ok -test cli::commands::tests::expected_metadata_hash_comparison_is_case_sensitive ... ok -test codegen::assembler::tests::strict_audit_li_split_handles_negative_32_bit_boundaries ... ok -test cli::commands::tests::production_policy_finds_evidence_less_checked_runtime_proof_plan_gap ... ok -test cli::commands::tests::production_policy_finds_evidence_less_on_chain_checked_proof_plan_gap ... ok -test codegen::calls::tests::fixed_u64_le_width_accepts_hashes_and_byte_arrays ... ok -test codegen::calls::tests::canonical_type_names_strip_reference_wrappers ... ok -test codegen::cell_ops::tests::consumed_operand_var_accepts_named_cell_operands_only ... ok -test codegen::cell_ops::tests::destroy_absence_scan_is_limited_to_singleton_and_type_id_unique_policies ... ok -test codegen::calls::tests::packed_hash_width_uses_codegen_fixed_byte_type_rules ... ok -test codegen::cell_ops::tests::identity_and_destruction_policy_labels_are_stable ... ok -test codegen::expr::tests::divisor_nonzero_guard_fails_closed_on_zero ... ok -test codegen::frame::tests::large_addi_materializes_out_of_range_immediates ... ok -test codegen::frame::tests::large_addi_uses_single_addi_for_small_immediates ... ok -test codegen::frame::tests::stack_access_helpers_emit_sp_relative_instructions ... ok -test codegen::runtime::tests::checked_runtime_status_register_defaults_to_a1_for_unknown_helpers ... ok -test codegen::expr::tests::bool_canonical_check_emits_zero_one_guard ... ok -test codegen::runtime::tests::ckb_runtime_syscall_abi_matches_declared_constants ... ok -test codegen::runtime::tests::runtime_helper_classification_tracks_checked_and_hash_helpers ... ok -test codegen::schema::tests::aggregate_field_layouts_track_tuple_offsets ... ok -test codegen::assembler::tests::strict_audit_elf_header_and_segments_are_internally_consistent ... ok -test codegen::schema::tests::fixed_byte_constants_materialize_little_endian_bytes ... ok -test codegen::schema::tests::fixed_width_helpers_classify_scalar_and_byte_storage ... ok -test codegen::tests::consumed_schema_params_use_loaded_cell_size_for_field_checks ... ok -test codegen::tests::dynamic_syscall_index_is_copied_before_large_stack_staging ... ok -test codegen::tests::explicit_external_toolchain_paths_are_strict ... ok -test codegen::tests::cell_operation_identity_helpers_stay_in_cell_ops ... ok -test cli::commands::tests::ckb_hash_file_rejects_inputs_above_limit ... ok -test codegen::tests::generated_large_offsets_are_normalized_before_assembly ... ok -test codegen::tests::generated_public_assembly_mnemonics_are_declared ... ok -test codegen::tests::generated_collection_assembly_is_internal_assembler_clean ... ok -test codegen::tests::generated_stdlib_assembly_is_internal_assembler_clean ... ok -test codegen::tests::internal_assembler_encodes_emitted_instruction_surface ... ok -test codegen::tests::internal_assembler_encodes_full_width_li_literals ... ok -test codegen::tests::internal_assembler_keeps_near_unconditional_jump_compact ... ok -test codegen::tests::internal_assembler_rejects_unresolved_call_targets ... ok -test codegen::tests::internal_assembler_encodes_register_conditional_branches ... ok -test codegen::tests::internal_assembler_rejects_intentionally_unsupported_mnemonics ... ok -test codegen::tests::generated_functions_use_shared_epilogue_tail ... ok -test codegen::tests::large_addi_avoids_clobbering_source_register ... ok -test codegen::tests::machine_cfg_tracks_call_edges_to_local_helpers ... ok -test codegen::tests::machine_layout_order_rejects_missing_duplicate_or_unknown_blocks ... ok -test codegen::assembler::tests::strict_audit_relaxed_conditional_branch_within_jal_range_preserves_registers ... ok -test codegen::tests::machine_layout_plan_builds_explicit_machine_blocks ... ok -test codegen::tests::machine_layout_plan_builds_register_conditional_branch_blocks ... ok -test codegen::tests::machine_layout_plan_rejects_branch_target_outside_text ... ok -test codegen::tests::dynamic_molecule_vector_field_access_validates_full_table_offsets ... ok -test codegen::tests::binary_codegen_materializes_narrow_integer_constants ... ok -test codegen::tests::machine_reachability_uses_entry_label_not_every_global ... ok -test codegen::tests::outgoing_stack_arg_area_is_16_byte_aligned_at_call_boundaries ... ok -test codegen::tests::division_codegen_guards_zero_divisors ... ok -test codegen::tests::read_ref_runtime_fallback_records_cell_buffer_state ... ok -test codegen::tests::register_contract_allows_only_entry_wrapper_writes_to_direct_registers ... ok -test codegen::tests::rv64_li_boundary_values_materialize_correct_bits ... ok -test codegen::tests::semantic_molecule_field_access_uses_validated_api_gate ... ok -test codegen::tests::sp_addi_large_offsets_clobber_only_destination_register ... ok -test codegen::tests::dynamic_molecule_fixed_field_codegen_checks_full_header_and_exact_span ... ok -test codegen::tests::state_transition_edges_use_explicit_consumed_binding ... ok -test codegen::tests::strict_audit_outgoing_stack_args_are_staged_inside_current_frame ... ok -test codegen::tests::type_hash_missing_output_buffer_slots_report_compile_error ... ok -test codegen::tests::type_hash_missing_param_slots_report_compile_error ... ok -test codegen::tests::u128_const_without_fixed_storage_reports_compile_error ... ok -test codegen::tests::narrow_arithmetic_codegen_truncates_to_declared_width ... ok -test codegen::tests::unaligned_scalar_load_large_offsets_preserve_live_accumulator ... ok -test codegen::tests::unrepresentable_memory_load_offsets_report_compile_error ... ok -test codegen::tests::machine_layout_plan_reports_branch_relaxation_metrics ... ok -test codegen::tests::runtime_cast_codegen_checks_narrowing_and_bool_canonicality ... ok -test codegen::tests::unrepresentable_stack_offsets_report_compile_error ... ok -test debug::tests::test_debug_info_generator ... ok -test debug::tests::test_dwarf_generation ... ok -test debug::tests::test_line_table ... ok -test debug::tests::test_type_registration ... ok -test docgen::tests::docgen_emits_flat_pool_runtime_input_requirements ... ok -test docgen::tests::docgen_emits_markdown_for_action ... ok -test codegen::tests::internal_assembler_relaxes_out_of_range_conditional_branch ... ok -test docgen::tests::docgen_emits_transaction_invariant_checked_subconditions ... ok -test docgen::tests::docgen_html_escapes_module_and_item_text ... ok -test error::tests::caret_padding_starts_at_span_column ... ok -test error::tests::caret_width_counts_characters_not_bytes ... ok -test flow::tests::consumed_flow_tracking_follows_expression_aliases ... ok -test fmt::tests::format_action_transition_block_for_multiple_edges ... ok -test fmt::tests::format_indents_preserve_fields_inside_expression_block ... ok -test fmt::tests::format_preserves_single_element_tuple_expression ... ok -test fmt::tests::format_preserves_type_policy_metadata ... ok -test fmt::tests::format_round_trips_inline_if_tuple_expression ... ok -test codegen::tests::schema_ref_call_preserves_schema_abi_length ... ok -test fmt::tests::format_round_trips_multiline_expression_block ... ok -test fmt::tests::format_round_trips_preserve_block ... ok -test fmt::tests::format_round_trips_require_block ... ok -test fmt::tests::format_round_trips_simple_module ... ok -test fmt::tests::format_round_trips_stdlib_lifecycle_field_block ... ok -test fmt::tests::format_single_expr_require_block_uses_compact_form ... ok -test fmt::tests::format_uses_canonical_assert_and_no_const_semicolon ... ok -test fmt::tests::format_uses_field_shorthand_when_value_matches_name ... ok -test incremental::tests::clean_cache_rejects_overflowing_max_age ... ok -test codegen::tests::stack_pointer_offsets_are_emitted_through_helpers ... ok -test incremental::tests::test_dependency_graph ... ok -test incremental::tests::test_change_detector ... ok -test incremental::tests::load_cache_drops_units_with_paths_outside_trusted_root ... ok -test incremental::tests::clean_cache_skips_output_paths_outside_trusted_root ... ok -test incremental::tests::test_incremental_compiler ... ok -test codegen::tests::vm2_syscall_helpers_emit_executable_status_checked_wrappers ... ok -test ir::tests::assert_in_pure_function_lowers_failure_to_abort_terminator ... ok -test ir::tests::constant_cast_rejects_out_of_range_u128_narrowing ... ok -test ir::tests::binary_arithmetic_result_type_preserves_left_operand_width ... ok -test ir::tests::contextual_integer_binary_operands_lower_to_peer_width ... ok -test ir::tests::ir_generation_aggregates_lowering_errors_with_source_spans ... ok -test ir::tests::all_diverging_match_expression_does_not_leave_unreachable_join ... ok -test ir::tests::ir_straight_line_lifecycle_certificate_rejects_branch_local_create_without_typecheck ... ok -test ir::tests::exhaustive_enum_match_unmatched_path_lowers_to_abort_terminator ... ok -test ir::tests::ir_type_value_kind_never_derives_status_kinds ... ok -test ir::tests::ir_straight_line_lifecycle_certificate_rejects_duplicate_consume_without_typecheck ... ok -test ir::tests::poison_lowering_keeps_value_invalid_while_block_stays_live ... ok -test ir::tests::logical_operators_lower_as_short_circuit_control_flow ... ok -test ir::tests::reference_and_deref_unary_result_types_match_ast_types ... ok -test ir::tests::mixed_width_expression_local_widening_lowers_as_explicit_casts ... ok -test ir::tests::require_block_lowers_to_atomic_requires ... ok -test ir::tests::runtime_narrowing_cast_lowers_as_cast_instruction ... ok -test ir::tests::preserve_sugar_populates_preserved_fields ... ok -test ir::tests::status_boundary_ir_verifier_allows_unit_runtime_helper_when_status_is_checked_by_codegen_boundary ... ok -test ir::tests::status_boundary_ir_verifier_allows_domain_u64_return_tuple_and_call_argument ... ok -test ir::tests::status_boundary_ir_verifier_rejects_dropped_raw_syscall_status ... ok -test ir::tests::status_boundary_ir_verifier_rejects_raw_syscall_status_as_domain_call_argument ... ok -test ir::tests::status_boundary_ir_verifier_rejects_raw_syscall_status_in_tuple_field ... ok -test docgen::tests::docgen_emits_invariant_coverage_summary ... ok -test ir::tests::status_boundary_ir_verifier_rejects_raw_syscall_status_produced_without_checked_consumer ... ok -test ir::tests::status_boundary_ir_verifier_rejects_raw_syscall_status_returned_as_domain_u64 ... ok -test ir::tests::status_boundary_ir_verifier_rejects_raw_syscall_status_stored_as_dsl_local ... ok -test ir::tests::status_boundary_ir_verifier_rejects_unit_runtime_helper_status_stored_as_domain_u64 ... ok -test ir::tests::stdlib_claim_lowers_to_consumed_receipt_and_locked_declared_output ... ok -test ir::tests::strict_audit_ir_verifier_rejects_constant_destination_width_mismatch ... ok -test ir::tests::strict_audit_ir_verifier_rejects_empty_body_blocks ... ok -test ir::tests::strict_audit_ir_lowering_records_instruction_level_provenance ... ok -test ir::tests::strict_audit_ir_verifier_rejects_missing_terminator_target ... ok -test ir::tests::stdlib_transfer_lowers_to_single_consumed_input_and_locked_output ... ok -test ir::tests::strict_audit_ir_verifier_rejects_poisoned_load_const ... ok -test ir::tests::strict_audit_ir_verifier_rejects_extra_consume_set_metadata ... ok -test ir::tests::strict_audit_ir_verifier_rejects_poisoned_lowering_module ... ok -test ir::tests::strict_audit_ir_verifier_rejects_missing_create_set_metadata ... ok -test ir::tests::strict_audit_ir_verifier_rejects_poisoned_lowering_operand ... ok -test ir::tests::strict_audit_ir_verifier_rejects_use_not_defined_on_all_paths ... ok -test ir::tests::stdlib_settle_lowers_to_consumed_input_and_locked_output ... ok -test ir::tests::strict_audit_ir_verifier_rejects_stale_write_intents_metadata ... ok -test ir::tests::strict_audit_ir_verifier_reports_instruction_provenance ... ok -test ir::tests::strict_audit_schema_field_accesses_are_rematerialized_per_cfg_path ... ok -test lexer::tests::test_byte_string ... ok -test codegen::tests::u128_delta_arithmetic_codegen_uses_fixed_byte_storage ... ok -test lexer::tests::test_comment ... ok -test lexer::tests::test_identifiers ... ok -test lexer::tests::test_keywords ... ok -test lexer::tests::test_numbers ... ok -test lexer::tests::test_operators ... ok -test lexer::tests::test_punctuation ... ok -test lexer::tests::test_string ... ok -test lexer::tests::test_unterminated_byte_string_errors ... ok -test lexer::tests::test_unterminated_string_errors ... ok -test lsp::tests::lsp_position_conversion_treats_crlf_as_single_line_ending ... ok -test lexer::tests::rejects_oversized_identifier ... ok -test lsp::tests::lsp_position_incremental_change_applies_crlf_ranges ... ok -test lsp::tests::lsp_primitive_strict_rejects_legacy_capabilities ... ok -test lsp::tests::lsp_reads_primitive_strict_from_manifest ... ok -test lsp::tests::lsp_rejects_document_count_over_limit ... ok -test lsp::tests::goto_definition_prefers_local_scope_over_top_level_symbol ... ok -test lsp::tests::lsp_rejects_oversized_documents ... ok -test lsp::tests::test_ckb_namespace_completions ... ok -test lsp::tests::find_references_for_locals_stays_in_enclosing_callable_scope ... ok -test lsp::tests::test_flow_namespace_completions ... ok -test lsp::tests::test_format_document ... ok -test lsp::tests::test_code_actions_for_lowering_diagnostics ... ok -test lsp::tests::test_goto_definition_and_references ... ok -test lsp::tests::test_incremental_change_applies_utf16_ranges_after_non_bmp_text ... ok -test lsp::tests::test_incremental_change_ignores_invalid_utf16_ranges ... ok -test lsp::tests::test_keyword_completions ... ok -test lsp::tests::test_flow_u8_namespace_completions ... ok -test lsp::tests::test_lsp_position_conversion_uses_utf16_columns ... ok -test lsp::tests::test_hover ... ok -test lsp::tests::test_action_hover_includes_lowering_metadata ... ok -test lsp::tests::test_parse_errors_become_diagnostics ... ok -test codegen::tests::entry_dynamic_witness_stack_arg_staging_preserves_cursor_register ... ok -test lsp::tests::test_lsp_server ... ok -test lsp::tests::test_vec_member_completions_match_supported_helpers ... ok -test lsp::tests::test_workspace_diagnostics_check_imported_type_id_collisions ... ok -test lsp::tests::test_workspace_goto_definition_across_modules ... ok -test lsp::tests::test_selection_range_orders_child_before_parent ... ok -test lsp::tests::test_workspace_references_across_modules ... ok -test optimize::tests::does_not_inline_block_bodies_that_can_capture_call_site_names ... ok -test optimize::tests::folds_boolean_expressions ... ok -test optimize::tests::folds_integer_arithmetic ... ok -test optimize::tests::folds_literal_if_statements_without_touching_cell_ops ... ok -test optimize::tests::folds_unsigned_high_bit_integer_operations ... ok -test optimize::tests::propagates_constants_inlines_small_functions_and_removes_dead_code ... ok -test optimize::tests::unused_let_elimination_preserves_calls_and_stdlib_constraints ... ok -test lsp::tests::test_workspace_rename_is_disabled_until_symbol_scoped ... ok -test package::tests::git_cache_entry_name_is_hash_only ... ok -test package::tests::lockfile_consistency_allows_resolved_transitive_path_dependencies ... ok -test package::tests::git_cache_child_check_rejects_path_escape ... ok -test package::tests::lockfile_consistency_reports_stale_and_mismatched_path_sources ... ok -test package::tests::lockfile_consistency_requires_exact_git_revision_match ... ok -test package::tests::lockfile_replace_with_resolved_prunes_removed_dependencies ... ok -test package::tests::package_manager_accepts_allowed_git_url_transports ... ok -test package::tests::lockfile_read_from_root_rejects_malformed_lockfiles ... ok -test package::tests::package_manager_git_checkout_revalidates_full_commit_refs ... ok -test package::tests::package_manager_git_commands_separate_user_controlled_ref_arguments ... ok -test lsp::tests::test_lowering_diagnostics_warn_for_fail_closed_runtime_actions ... ok -test package::tests::package_manager_allows_path_dependency_without_version ... ok -test package::tests::package_manager_rejects_branch_or_tag_git_dependency_before_fetch ... ok -test package::tests::package_manager_rejects_registry_dependencies_fail_closed ... ok -test package::tests::package_manager_rejects_local_path_dependency_traversal ... ok -test package::tests::package_manager_rejects_unpinned_git_dependency_before_fetch ... ok -test package::tests::package_manager_rejects_unsafe_git_url_transports ... ok -test package::tests::package_manager_rejects_transitive_path_dependency_cycles ... ok -test package::tests::package_manager_resolves_local_path_dependencies ... ok -test package::tests::test_dependency_graph ... ok -test package::tests::package_manager_resolves_transitive_local_path_dependencies ... ok -test package::tests::test_manifest_serialization ... ok -test package::tests::test_version_compatibility ... ok -test parser::tests::action_where_block_allows_indented_following_top_level_item ... ok -test parser::tests::action_where_block_keeps_indented_keyword_like_binding_in_body ... ok -test parser::tests::assignment_range_and_cast_spans_cover_full_expression ... ok -test parser::tests::array_size_uses_checked_target_width_conversion ... ok -test parser::tests::binary_expr_spans_cover_full_expression ... ok -test parser::tests::generic_type_arguments_allow_newlines ... ok -test parser::tests::hex_literal_exprs_parse_as_integers ... ok -test parser::tests::identity_policy_diagnostic_uses_bad_policy_span ... ok -test parser::tests::named_arg_diagnostic_uses_bad_name_span ... ok -test parser::tests::parser_empty_token_slice_returns_controlled_error ... ok -test lsp::tests::test_receipt_hover_includes_flow_metadata ... ok -test parser::tests::parser_rejects_bang_assert_syntax ... ok -test parser::tests::postfix_expr_spans_cover_the_full_postfix_chain ... ok -test parser::tests::parser_rejects_deep_unary_expression_before_stack_overflow ... ok -test parser::tests::postfix_exprs_cover_the_consumed_source_range ... ok -test parser::tests::primitive_and_container_exprs_keep_source_spans ... ok -test parser::tests::test_action_where_column_one_flow_identifier_stays_in_body ... ok -test parser::tests::struct_init_span_covers_type_name_and_body ... ok -test parser::tests::parser_rejects_deep_if_expression_before_stack_overflow ... ok -test parser::tests::test_launch_expression_is_reserved_until_lowering_exists ... ok -test parser::tests::test_parse_action_transition_block ... ok -test parser::tests::test_parse_action ... ok -test parser::tests::test_parse_aggregate_invariant_primitives ... ok -test parser::tests::test_parse_create_field_shorthand ... ok -test parser::tests::test_parse_expression ... ok -test parser::tests::test_parse_grouped_use_imports ... ok -test parser::tests::test_parse_flow_and_action_transition_clause ... ok -test parser::tests::test_parse_invariant ... ok -test parser::tests::test_parse_invariant_assert_statement ... ok -test lexer::tests::rejects_oversized_string_literal ... ok -test parser::tests::test_parse_merges_attribute_and_inline_capabilities ... ok -test parser::tests::test_parse_prefix_source_before_keyword_like_name ... ok -test parser::tests::test_parse_prefix_source_and_create_target ... ok -test parser::tests::test_parse_preserve_block ... ok -test parser::tests::test_parse_preserve_single_field ... ok -test parser::tests::test_parse_resource ... ok -test parser::tests::test_parse_require_block ... ok -test parser::tests::test_parse_type_id_attribute ... ok -test parser::tests::test_postfix_does_not_cross_statement_newline ... ok -test parser::tests::test_reject_bare_preserve ... ok -test parser::tests::test_reject_empty_preserve_block ... ok -test parser::tests::test_reject_empty_require_block ... ok -test parser::tests::test_reject_preserve_except ... ok -test parser::tests::test_reject_preserve_wildcard ... ok -test parser::tests::test_reject_require_block_with_consume ... ok -test parser::tests::test_reject_require_block_with_control_flow ... ok -test parser::tests::test_rejects_action_brace_body ... ok -test parser::tests::test_rejects_empty_transition_block ... ok -test parser::tests::test_rejects_legacy_move_clause ... ok -test parser::tests::test_rejects_read_ref_as_type_qualifier ... ok -test parser::tests::test_rejects_generic_resource_definition ... ok -test parser::tests::test_rejects_output_parameter_source_prefix ... ok -test parser::tests::test_rejects_type_id_on_action ... ok -test parser::tests::test_rejects_transition_clause_without_state_colons ... ok -test parser::tests::test_rejects_typed_let_without_initializer ... ok -test parser::tests::test_rejects_use_as_without_alias ... ok -test parser::tests::test_rejects_unbraced_match_arms ... ok -test proof_plan::soundness::tests::strict_pp0103_only_applies_to_checked_runtime_records ... ok -test proof_plan::soundness::tests::strict_pp0201_only_applies_to_executing_script_args ... ok -test proof_plan::tests::checked_runtime_without_concrete_evidence_is_not_marked_covered ... ok -test proof_plan::tests::checked_static_detail_does_not_create_executable_runtime_evidence ... ok -test proof_plan::tests::checked_runtime_proof_plan_claims_include_executable_evidence ... ok -test proof_plan::tests::metadata_only_invariant_proof_plan_has_no_executable_evidence ... ok -test proof_plan::tests::replace_unique_features_are_transaction_scoped ... ok -test proof_plan::tests::unique_lifecycle_features_have_specific_codegen_evidence_ids ... ok -test repl::tests::repl_read_limited_line_accepts_bounded_input ... ok -test resolve::tests::test_global_type_resolution_rejects_ambiguous_symbol ... ok -test resolve::tests::rejects_cross_module_type_dependency_cycles ... ok -test resolve::tests::test_grouped_use_resolves_multiple_symbols ... ok -test resolve::tests::test_imported_type_resolution_uses_exact_module_path ... ok -test resolve::tests::test_module_resolver ... ok -test resolve::tests::test_path_resolver ... ok -test resolve::tests::test_register_module_rejects_deferred_missing_import_when_target_arrives ... ok -test resolve::tests::test_register_module_rejects_missing_imported_symbol_when_target_is_loaded ... ok -test resolve::tests::test_rejects_duplicate_local_symbols ... ok -test resolve::tests::test_rejects_import_alias_collisions ... ok -test runtime_errors::tests::diagnostic_messages_map_to_runtime_error_codes_where_possible ... ok -test runtime_errors::tests::runtime_error_docs_cover_every_registered_code ... ok -test runtime_errors::tests::runtime_error_docs_explain_ckb_code_overlap_channels ... ok -test runtime_errors::tests::runtime_error_registry_roundtrips_and_has_unique_codes ... ok -test simulate::tests::array_size_simulator_uses_checked_target_width_for_indices ... ok -test simulate::tests::simulate_cell_operation_traces ... ok -test simulate::tests::simulate_if_branch ... ok -test simulate::tests::simulate_pure_arithmetic_action ... ok -test simulate::tests::simulate_read_ref_traces ... ok -test simulate::tests::simulate_rejects_wrong_action_arity ... ok -test simulate::tests::simulate_step_limit ... ok -test simulate::tests::simulate_unsigned_high_bit_integer_operations ... ok -test stdlib::collections::tests::collection_assembly_has_no_raw_syscalls_or_unclassified_helpers ... ok -test stdlib::collections::tests::collection_public_helpers_do_not_dereference_raw_a0_handles ... ok -test stdlib::collections::tests::test_collection_functions ... ok -test repl::tests::repl_read_limited_line_rejects_oversized_input ... ok -test stdlib::tests::generated_stdlib_has_no_raw_syscall_wrapper_symbols ... ok -test stdlib::collections::tests::test_generate_assembly ... ok -test stdlib::tests::generated_stdlib_omits_raw_syscall_wrappers ... ok -test stdlib::tests::test_generate_assembly ... ok -test stdlib::tests::test_get_function ... ok -test stdlib::tests::test_generate_ckb_assembly_uses_checked_env_helpers ... ok -test stdlib::tests::test_std_functions ... ok -test stdlib::tests::test_scheduler_metadata_generate_molecule_uses_table_layout ... ok -test syscalls::tests::ckb_debug_syscall_is_not_a_production_inventory_surface ... ok -test syscalls::tests::emitted_manual_runtime_and_stdlib_helpers_are_classified ... ok -test syscalls::tests::ckb_syscall_abi_matches_checked_baseline ... ok -test syscalls::tests::every_low_level_syscall_spec_is_inventoried ... ok -test tests::action_scheduler_witness_bytes_rejects_conflicting_molecule_alias ... ok -test syscalls::tests::helper_inventory_has_no_duplicate_symbols ... ok -test tests::ckb_capacity_calculation_saturates_on_extreme_sizes ... ok -test tests::branch_local_anonymous_creates_are_rejected_until_effects_are_cfg_aware ... ok -test tests::ckb_constraints_surface_capacity_planning_for_created_outputs ... ok -test tests::ckb_deploy_manifest_rejects_conflicting_cell_dep_locations ... ok -test runtime_errors::tests::codegen_does_not_emit_unregistered_numeric_fail_literals ... ok -test tests::ckb_deploy_manifest_rejects_incomplete_split_cell_dep_location ... ok -test tests::ckb_deploy_manifest_rejects_invalid_dep_type ... ok -test codegen::tests::emitted_runtime_helper_symbols_are_classified_in_syscall_inventory ... ok -test tests::ckb_deploy_manifest_rejects_invalid_hash_type ... ok -test lexer::tests::rejects_oversized_block_comment ... ok -test codegen::tests::internal_assembler_relaxes_out_of_range_register_conditional_branch ... ok -test tests::ckb_deploy_manifest_surfaces_hash_type_and_dep_group_policy ... ok -test package::tests::package_manager_git_dependency_fails_for_invalid_url ... ok -test lsp::tests::lsp_loads_sibling_modules_for_standalone_example_imports ... ok -test tests::collection_fail_closed_feature_names_are_stable ... ok -test tests::ckb_lock_false_return_lowers_to_script_failure ... ok -test tests::ckb_target_profile_has_no_policy_exception ... ok -test tests::ckb_u64_syscall_helpers_check_return_code_and_size ... ok -test tests::ckb_dynamic_vector_len_can_drive_mutate_transition ... ok -test package::tests::package_manager_git_update_fails_closed_on_fetch_error ... ok -test tests::compile_accepts_chain_neutral_timepoint_under_ckb_profile ... ok -test tests::compile_accepts_ckb_target_profile_timepoint ... ok -test tests::compile_accepts_action_witness_source_qualifier ... ok -test tests::compile_accepts_ckb_header_epoch_api_only_for_ckb_profile ... ok -test tests::ckb_entry_lock_scope_selects_lock_entrypoint ... ok -test tests::compile_accepts_complete_branch_return_paths ... ok -test tests::compile_accepts_ckb_shared_create_when_verifier_covered ... ok -test tests::compile_accepts_empty_vec_literal_with_declared_type ... ok -test tests::ckb_entry_scope_keeps_vec_element_schema_dependencies ... ok -test tests::compile_accepts_flow_initial_create_at_any_declared_state ... ok -test tests::compile_accepts_create_field_shorthand ... ok -test tests::compile_accepts_flow_state_name_initializers ... ok -test tests::compile_accepts_flow_on_custom_state_field ... ok -test tests::compile_accepts_flow_edge_returning_to_first_state ... ok -test tests::compile_accepts_explicit_flow_action_edges ... ok -test tests::compile_accepts_kernel_effect_capabilities_for_destroy ... ok -test tests::compile_accepts_lock_args_script_args_binding ... ok -test tests::compile_accepts_pure_ckb_target_profile ... ok -test tests::compile_accepts_non_initial_flow_create_without_consumed_prior_state ... ok -test tests::compile_accepts_lock_boundary_param_sources_and_require ... ok -test tests::compile_accepts_named_action_output_and_create_binding ... ok -test tests::compile_accepts_core_input_output_state_transition_edges ... ok -test tests::compile_accepts_kernel_effect_capabilities_for_transfer ... ok -test tests::compile_accepts_qualified_flow_state_names ... ok -test tests::compile_accepts_prefix_read_params_as_cell_dep_bindings ... ok -test tests::compile_allows_struct_type_id_under_ckb_profile ... ok -test tests::compile_accepts_static_flow_update_to_non_initial_state ... ok -test tests::compile_allows_unit_function_calls_as_statements ... ok -test tests::compile_allows_actions_and_locks_to_call_pure_functions ... ok -test tests::compile_accepts_vec_literals_in_create_fields ... ok -test tests::compile_accepts_symmetric_where_branch_output_constraints ... ok -test tests::compile_binds_duplicate_read_refs_by_order_not_name ... ok -test tests::compile_allows_flow_update_to_declared_initial_state_at_type_check ... ok -test tests::compile_binds_read_action_schema_params_to_cell_deps ... ok -test tests::compile_binds_read_ref_entry_params_to_cell_deps ... ok -test tests::compile_create_unique_field_identity_emits_runtime_anchor ... ok -test tests::compile_classifies_resource_split_amount_subtraction_as_checked_runtime ... ok -test tests::compile_classifies_resource_merge_amount_sum_as_checked_runtime ... ok -test tests::compile_emits_create_output_field_verification_for_fixed_u64_fields ... ok -test tests::compile_emits_direct_user_function_calls ... ok -test tests::compile_classifies_protocol_agnostic_guarded_transition_as_checked_runtime ... ok -test tests::compile_exposes_ckb_type_id_contract_under_ckb_profile ... ok -test tests::compile_emits_ckb_style_load_cell_abi_for_cell_runtime_summary ... ok -test tests::compile_entry_witness_rejects_payloads_larger_than_buffer ... ok -test tests::compile_destroy_policies_are_policy_aware ... ok -test tests::compile_emits_protocol_agnostic_guard_equality_proofplan_records ... ok -test tests::compile_file_explicit_target_overrides_manifest_build_target ... ok -test tests::compile_file_uses_manifest_ckb_target_profile ... ok -test tests::compile_folds_local_fixed_array_len_to_constant ... ok -test tests::compile_file_loads_local_path_dependencies_from_cell_manifest ... ok -test tests::compile_file_uses_manifest_build_target_by_default ... ok -test tests::compile_identity_ckb_type_id_emits_metadata ... ok -test tests::compile_file_source_content_hash_is_path_independent ... ok -test tests::compile_identity_singleton_type_emits_metadata ... ok -test tests::compile_identity_none_is_default_and_hidden ... ok -test tests::compile_identity_field_emits_path ... ok -test tests::compile_identity_script_args_emits_metadata ... ok -test tests::compile_classifies_guarded_identity_field_merge_as_checked_runtime ... ok -test tests::compile_ignores_trivial_self_equality_guard_records ... ok -test tests::compile_lowers_array_of_tuples_static_index_projection ... ok -test tests::compile_infers_and_validates_read_only_effects ... ok -test tests::compile_lowers_assert_invariant_into_fail_closed_cfg ... ok -test tests::compile_lowers_block_tail_if_expressions ... ok -test tests::compile_lowers_bounded_vec_literal_to_stack_collection ... ok -test tests::compile_lowers_byte_string_literals_with_expected_array_type ... ok -test tests::compile_lowers_exhaustive_enum_match_without_wildcard ... ok -test tests::compile_lowers_consumed_input_field_access_through_loaded_cell_bytes ... ok -test tests::ckb_entry_action_scope_excludes_unselected_unsupported_code ... ok -test tests::compile_lowers_for_range_into_counted_loop_cfg ... ok -test tests::compile_lowers_ckb_group_source_large_immediate_to_riscv_elf ... ok -test tests::compile_lowers_if_expression_fixed_byte_const_join_move ... ok -test tests::compile_lowers_fixed_byte_schema_field_comparison ... ok -test tests::compile_lowers_len_method_to_length_instruction ... ok -test tests::compile_lowers_if_statement_into_basic_blocks ... ok -test tests::compile_lowers_if_expression_with_join_move ... ok -test tests::compile_keeps_unchecked_transition_field_runtime_required ... ok -test tests::compile_lowers_local_fixed_array_static_index_reads_and_writes ... ok -test tests::compile_lowers_local_struct_field_reads_and_writes ... ok -test tests::compile_lowers_local_constants_into_real_operands ... ok -test tests::compile_lowers_local_tuple_destructuring_to_field_slots ... ok -test tests::compile_lowers_local_tuple_static_field_reads_and_writes ... ok -test tests::compile_lowers_numeric_cast_without_zero_fallback ... ok -test tests::compile_lowers_match_expression_into_branch_cfg ... ok -test tests::compile_lowers_pure_function_assert_failure_to_abort ... ok -test tests::compile_lowers_packed_bool_and_u32_schema_fields_without_aligned_loads ... ok -test tests::compile_lowers_mutable_assignments_in_loop_bodies ... ok -test tests::compile_lowers_read_ref_schema_field_to_ckb_runtime_assembly ... ok -test tests::compile_lowers_stack_vec_clear_and_is_empty ... ok -test tests::compile_lowers_stack_vec_extend_from_fixed_bytes ... ok -test tests::compile_lowers_read_ref_schema_field_to_ckb_runtime_elf ... ok -test tests::compile_lowers_schema_backed_parameter_field_access_to_elf ... ok -test tests::compile_lowers_stack_vec_fixed_byte_capacity ... ok -test tests::compile_lowers_stack_vec_fixed_byte_contains ... ok -test tests::compile_lowers_stack_vec_fixed_byte_pop ... ok -test tests::compile_lowers_stack_vec_fixed_byte_first_last ... ok -test tests::compile_lowers_stack_vec_fixed_byte_runtime_push_index ... ok -test tests::compile_lowers_stack_vec_fixed_byte_insert ... ok -test tests::compile_lowers_stack_vec_fixed_byte_remove ... ok -test tests::compile_lowers_stack_vec_scalar_capacity ... ok -test tests::compile_lowers_stack_vec_fixed_byte_reverse ... ok -test tests::compile_lowers_stack_vec_fixed_byte_set ... ok -test tests::compile_lowers_stack_vec_scalar_contains ... ok -test tests::compile_lowers_stack_vec_fixed_byte_truncate ... ok -test tests::compile_lowers_stack_vec_fixed_byte_swap ... ok -test tests::compile_lowers_stack_vec_scalar_insert ... ok -test tests::compile_lowers_stack_vec_scalar_first_last ... ok -test tests::compile_lowers_stack_vec_scalar_pop ... ok -test tests::compile_lowers_stack_vec_scalar_remove ... ok -test tests::compile_lowers_stack_vec_scalar_reverse ... ok -test tests::compile_lowers_stack_vec_scalar_runtime_push_len_index ... ok -test tests::compile_lowers_stack_vec_scalar_set ... ok -test tests::compile_lowers_tail_expr_as_action_return ... ok -test tests::bundled_token_example_strict_ckb_compile_is_admitted ... ok -test tests::compile_lowers_stack_vec_scalar_truncate ... ok -test tests::compile_lowers_stack_vec_scalar_swap ... ok -test tests::compile_lowers_tail_if_as_action_return ... ok -test tests::compile_lowers_u128_equality_as_fixed_byte_comparison ... ok -test tests::compile_lowers_type_hash_without_generic_call ... ok -test tests::compile_lowers_vec_with_capacity_to_stack_collection_new ... ok -test tests::compile_lowers_vec_builtins_without_generic_calls ... ok -test tests::compile_lowers_while_statement_into_loop_cfg ... ok -test tests::compile_merges_if_branch_linear_states_conservatively ... ok -test tests::compile_lowers_zero_builtin_without_generic_call ... ok -test tests::compile_lowers_u128_mutate_delta_with_carry_arithmetic ... ok -test tests::compile_merges_linear_transfers_inside_match_expressions ... ok -test tests::compile_merges_linear_transfers_inside_if_expressions ... ok -test tests::compile_metadata_exposes_ckb_type_id_create_output_plan_under_ckb_profile ... ok -test tests::compile_merges_linear_transfers_inside_block_tail_if_expressions ... ok -test tests::compile_marks_cell_backed_vec_runtime_features ... ok -test tests::compile_metadata_exposes_aggregate_invariant_primitives_in_proof_plan ... ok -test tests::compile_metadata_exposes_declared_invariant_proof_plan ... ok -test tests::compile_materializes_local_fixed_byte_constants_into_rodata ... ok -test tests::compile_merges_linear_transfers_inside_block_expressions ... ok -test tests::compile_metadata_declares_molecule_vm_abi ... ok -test tests::compile_metadata_exposes_transaction_and_selected_cell_aggregate_invariants ... ok -test tests::compile_metadata_with_options_rejects_strict_legacy_capabilities ... ok -test tests::compile_classifies_hash_committed_output_field_as_guarded ... ok -test tests::compile_metadata_exposes_lock_group_proof_plan_for_lock_entry ... ok -test tests::compile_lowers_ckb_hash_commitment_comparison_without_fixed_byte_fail_closed ... ok -test tests::compile_metadata_reports_parameterless_action_entrypoint_selection ... ok -test tests::compile_metadata_exposes_covenant_proof_plan_for_transfer ... ok -test tests::compile_metadata_warns_for_lock_group_transaction_invariant_scope ... ok -test tests::compile_normalizes_same_module_qualified_helper_calls ... ok -test tests::compile_path_rejects_missing_configured_source_root ... ok -test tests::compile_path_rejects_duplicate_modules_across_source_roots ... ok -test tests::compile_metadata_with_options_uses_ast_optimizer_for_nonzero_levels ... ok -test codegen::tests::codegen_rejects_generated_far_jump_scratch_relaxation ... ok -test tests::compile_metadata_proof_plan_preserves_lock_args_source ... ok -test tests::compile_path_rejects_missing_path_dependency_manifest ... ok -test tests::compile_path_rejects_path_dependency_cycles ... ok -test tests::compile_path_rejects_non_path_dependencies ... ok -test tests::compile_path_ignores_examples_outside_package_source_roots ... ok -test tests::compile_path_rejects_path_dependency_traversal ... ok -test tests::compile_path_accepts_package_root ... ok -test tests::compile_package_import_alias_emits_matching_external_callable ... ok -test tests::compile_prefers_no_arg_main_for_entry_wrapper ... ok -test tests::compile_path_supports_custom_entry_directory_modules ... ok -test tests::compile_path_supports_configured_source_roots_without_src ... ok -test tests::compile_preserves_index_and_tuple_projection_in_assembly ... ok -test tests::compile_preserves_if_tuple_aggregate_slots ... ok -test tests::compile_preserves_if_array_aggregate_slots ... ok -test tests::compile_preserves_create_instructions_in_assembly ... ok -test tests::compile_rejects_aggregate_invariant_non_fixed_field ... ok -test tests::compile_rejects_assert_delta_argument_from_cell_read ... ok -test tests::compile_rejects_assert_invariant_as_tail_return_value ... ok -test tests::compile_preserves_schema_backed_parameter_field_access_in_assembly ... ok -test tests::compile_rejects_assignment_through_read_only_references ... ok -test tests::compile_rejects_assignment_to_immutable_array_element ... ok -test tests::compile_rejects_assignment_to_immutable_tuple_field ... ok -test tests::compile_rejects_assignment_to_temporary_field_targets ... ok -test tests::compile_rejects_bad_flow_state_field_type_on_main_path ... ok -test tests::compile_rejects_asymmetric_where_branch_output_constraints ... ok -test tests::compile_rejects_bare_return_from_value_actions ... ok -test tests::compile_rejects_binding_assert_invariant_results ... ok -test tests::compile_rejects_binding_unit_function_results ... ok -test tests::compile_preserves_match_tuple_aggregate_slots ... ok -test tests::compile_rejects_bounded_vec_literal_type_mismatch ... ok -test tests::compile_rejects_cell_metadata_stdlib_on_non_cell_args ... ok -test tests::compile_rejects_destroy_without_destroy_capability ... ok -test tests::compile_rejects_builtin_call_argument_mismatches ... ok -test tests::compile_rejects_core_state_transition_edge_not_in_graph ... ok -test tests::compile_rejects_duplicate_flow_for_same_state_field ... ok -test tests::compile_rejects_duplicate_stable_type_ids ... ok -test tests::compile_rejects_duplicate_top_level_symbols ... ok -test tests::compile_rejects_dynamic_assert_invariant_messages ... ok -test tests::compile_rejects_empty_array_length_mismatch ... ok -test tests::compile_rejects_dynamic_require_messages ... ok -test tests::compile_rejects_dynamic_initial_flow_create_state ... ok -test tests::compile_rejects_dynamic_unique_identity_field ... ok -test tests::compile_rejects_empty_literal_in_non_vec_context ... ok -test tests::compile_rejects_enum_payload_variants_until_lowering_exists ... ok -test tests::compile_produces_non_empty_riscv_assembly ... ok -test tests::compile_rejects_flow_by_action_when_explicit_move_uses_different_edge ... ok -test tests::compile_rejects_flow_on_plain_struct ... ok -test tests::compile_rejects_flow_payload_enum_state_field ... ok -test tests::compile_rejects_flow_by_action_without_exact_move_clause ... ok -test tests::compile_rejects_flow_receipt_without_state_field ... ok -test tests::compile_rejects_heterogeneous_array_literals ... ok -test tests::compile_rejects_function_call_argument_mismatches ... ok -test tests::compile_rejects_if_expression_branch_type_mismatch ... ok -test tests::compile_rejects_forbidden_unwrap_helpers ... ok -test tests::compile_rejects_helper_functions_that_indirectly_call_impure_actions ... ok -test tests::compile_preserves_dynamic_witness_cursor_after_lock_args ... ok -test tests::compile_rejects_incomplete_branch_return_paths ... ok -test tests::compile_rejects_impure_helper_functions ... ok -test tests::compile_rejects_input_source_outside_action_cell_params ... ok -test tests::compile_rejects_invalid_destroy_policy_shapes ... ok -test tests::compile_rejects_invariant_assert_runtime_operation ... ok -test tests::compile_rejects_invalid_invariant_assert_expression ... ok -test tests::compile_rejects_invalid_create_field_initializers ... ok -test tests::compile_rejects_invariant_without_explicit_trigger_and_scope ... ok -test tests::compile_rejects_linear_state_changes_hidden_inside_loops ... ok -test tests::compile_rejects_invalid_enum_match_patterns ... ok -test tests::compile_rejects_local_binding_name_reuse ... ok -test tests::compile_rejects_local_fixed_array_static_oob_read ... ok -test tests::compile_rejects_local_fixed_array_static_oob_write ... ok -test tests::compile_rejects_missing_action_return_paths ... ok -test tests::compile_rejects_local_mutable_reference_aliases ... ok -test tests::compile_rejects_missing_function_return_paths ... ok -test tests::compile_rejects_missing_flow_state_create_on_main_path ... ok -test tests::compile_rejects_non_bool_assert_condition ... ok -test tests::compile_rejects_non_bool_lock_definitions ... ok -test tests::compile_rejects_noop_flow_transition_on_main_path ... ok -test tests::compile_rejects_out_of_range_flow_state_create_on_main_path ... ok -test tests::compile_preserves_consume_and_destroy_instructions_in_assembly ... ok -test tests::compile_rejects_local_references_to_linear_roots ... ok -test tests::compile_rejects_pure_functions_that_call_ckb_header_runtime_builtins ... ok -test tests::compile_rejects_owned_linear_field_assignment ... ok -test tests::compile_rejects_pure_functions_that_call_env_runtime_builtins ... ok -test tests::compile_rejects_payload_or_unknown_enum_variant_values ... ok -test tests::compile_rejects_pure_functions_that_call_locks ... ok -test tests::compile_rejects_pure_functions_that_call_type_hash_runtime_builtin ... ok -test tests::compile_rejects_read_ref_for_non_cell_backed_types ... ok -test tests::compile_rejects_return_values_from_unit_actions ... ok -test tests::compile_rejects_state_edge_that_does_not_consume_binding ... ok -test tests::compile_rejects_returning_unit_function_results ... ok -test tests::compile_rejects_reference_escape_boundaries ... ok -test tests::compile_rejects_undeclared_action_state_edge ... ok -test tests::compile_rejects_stateful_operations_without_named_linear_cell_operands ... ok -test tests::compile_rejects_unbound_assert_delta_argument ... ok -test tests::compile_rejects_string_literals_as_runtime_values ... ok -test tests::compile_rejects_underdeclared_effects_through_calls ... ok -test tests::compile_preserves_read_ref_instructions_in_assembly ... ok -test tests::compile_rejects_underdeclared_effects_through_qualified_calls ... ok -test tests::compile_rejects_unknown_functions ... ok -test tests::compile_rejects_unknown_or_reserved_named_types ... ok -test tests::compile_rejects_underdeclared_effect_annotations ... ok -test tests::compile_rejects_unknown_struct_fields ... ok -test tests::compile_rejects_unknown_target_during_option_validation ... ok -test tests::compile_rejects_unknown_target_profile ... ok -test tests::compile_rejects_unreachable_statements_after_complete_branch_return ... ok -test tests::compile_rejects_unreachable_statements_after_return ... ok -test tests::compile_rejects_unstable_callable_parameter_names ... ok -test tests::compile_rejects_unsupported_optimization_level ... ok -test tests::compile_rejects_untyped_empty_array_literals ... ok -test tests::compile_rejects_unstable_schema_field_names ... ok -test tests::compile_rejects_wrong_qualified_flow_state_field_initializer ... ok -test tests::compile_rejects_unsupported_vec_helper_type_combinations ... ok -test tests::compile_rejects_unsound_mutable_parameter_forms ... ok -test tests::compile_produces_ckb_elf_without_vm_abi_trailer ... ok -test tests::compile_result_exposes_nested_fixed_molecule_schema_metadata ... ok -test tests::compile_produces_non_empty_riscv_elf ... ok -test tests::compile_rejects_state_transitions_inside_locks ... ok -test tests::compile_result_exposes_schema_layout_metadata ... ok -test tests::compile_result_validation_rejects_assembly_with_vm_abi_trailer ... ok -test tests::compile_rejects_lock_boundary_sources_outside_supported_scope ... ok -test tests::compile_replace_unique_field_identity_compares_input_and_output ... ok -test tests::compile_result_exposes_scheduler_metadata_sidecar ... ok -test tests::compile_lowers_stack_vec_fixed_schema_values ... ok -test tests::compile_result_validation_rejects_compiler_version_mismatch ... ok -test tests::compile_result_validation_rejects_metadata_artifact_hash_mismatch ... ok -test tests::compile_result_validation_rejects_constraints_artifact_format_mismatch ... ok -test tests::compile_result_validation_rejects_metadata_artifact_size_mismatch ... ok -test tests::compile_result_validation_rejects_constraints_artifact_size_mismatch ... ok -test tests::compile_result_validation_accepts_current_outputs ... ok -test tests::compile_result_validation_rejects_metadata_schema_downgrade ... ok -test tests::compile_result_validation_rejects_metadata_artifact_format_mismatch ... ok -test tests::compile_result_validation_rejects_metadata_schema_version_mismatch ... ok -test tests::compile_result_validation_rejects_mismatched_ckb_type_id_create_output_plan ... ok -test tests::compile_result_validation_rejects_metadata_source_content_hash_mismatch ... ok -test tests::compile_result_validation_rejects_mismatched_ckb_output_data_binding ... ok -test tests::compile_result_validation_rejects_metadata_source_hash_mismatch ... ok -test tests::compile_result_validation_rejects_metadata_target_profile_mismatch ... ok -test tests::compile_result_validation_rejects_type_id_hash_mismatch ... ok -test tests::compile_result_validation_rejects_metadata_target_profile_v0_14_abi_mismatch ... ok -test tests::compile_result_validation_rejects_missing_metadata_artifact_size ... ok -test tests::compile_result_validation_rejects_molecule_schema_hash_mismatch ... ok -test tests::compile_result_validation_rejects_noncanonical_source_unit_hash ... ok -test tests::compile_result_writes_artifact_to_disk ... ok -test tests::compile_result_validation_rejects_tampered_artifact_hash ... ok -test tests::compile_supports_typed_empty_array_literals ... ok -test tests::compile_riscv_elf_accepts_full_width_u64_literals ... ok -test tests::compile_spills_parameters_and_returns_computed_value ... ok -test tests::compile_surfaces_type_level_hash_type_dsl_metadata ... ok -test tests::compile_tracks_linear_values_returned_from_complete_branches ... ok -test tests::compile_tracks_linear_values_returned_from_tail_if_branches ... ok -test tests::compile_unrolls_fixed_param_array_foreach_with_pointer_abi ... ok -test tests::compile_unrolls_local_array_of_tuples_foreach_destructuring ... ok -test tests::compile_reports_equivalent_state_transition_obligation_for_sugar_and_core_forms ... ok -test tests::compile_unrolls_local_fixed_array_foreach_without_runtime_indexing ... ok -test tests::compile_tracks_linear_values_inside_aggregate_bindings ... ok -test tests::compile_uses_ast_optimizer_for_nonzero_optimization_levels ... ok -test tests::compile_verifies_create_output_against_computed_scalar_stack_value ... ok -test tests::compile_verifies_created_output_bool_and_u32_fields ... ok -test tests::compile_verifies_constructed_fixed_width_vec_output ... ok -test tests::default_output_path_for_package_input_uses_manifest_out_dir ... ok -test tests::default_output_path_for_package_input_uses_build_dir ... ok -test tests::compile_verifies_created_scalar_fields_against_consumed_input_aliases ... ok -test tests::create_output_verifier_accepts_const_lock_hash ... ok -test tests::compile_verifies_create_output_against_consumed_input_field_alias ... ok -test tests::create_output_verifier_accepts_fixed_byte_params_and_consts ... ok -test tests::compiled_riscv_elf_contains_exit_trampoline ... ok -test tests::entry_abi_constraints_mark_extreme_slot_counts_unsupported ... ok -test tests::compile_verifies_large_output_field_requirements_without_partial_fallback ... ok -test tests::entry_witness_bool_params_are_canonicalized ... ok -test tests::entry_witness_encoder_includes_schema_backed_params_as_length_prefixed_bytes ... ok -test tests::entry_witness_encoder_matches_u64_wrapper_abi ... ok -test tests::entry_witness_encoder_supports_fixed_byte_params ... ok -test tests::dynamic_schema_fixed_vec_length_is_table_decoded ... ok -test tests::dynamic_mutable_schema_transitions_are_checked_after_table_decoding ... ok -test tests::dynamic_schema_fixed_field_access_is_table_decoded ... ok -test tests::dynamic_named_output_constraints_are_proven_in_where_block ... ok -test tests::ir_carries_flow_rules ... ok -test tests::dynamic_schema_fixed_vec_iteration_is_table_decoded ... ok -test tests::ir_preserves_function_call_return_types ... ok -test tests::ir_lowers_unit_function_calls_without_result_destinations ... ok -test tests::ir_rejects_unknown_call_return_types_without_u64_fallback ... ok -test tests::generated_outgoing_stack_reservations_are_psabi_aligned ... ok -test tests::ir_summary_captures_cell_runtime_accesses ... ok -test tests::fixed_enum_fields_have_molecule_schema_metadata ... ok -test tests::load_modules_for_input_collects_package_source_roots ... ok -test tests::internal_calls_keep_outgoing_stack_area_abi_aligned ... ok -test tests::package_entry_must_stay_inside_package_root ... ok -test tests::package_out_dir_must_stay_inside_package_root ... ok -test tests::compile_unique_script_args_and_singleton_identity_emit_hash_checks ... ok -test tests::package_source_roots_must_stay_inside_package_root ... ok -test tests::loaded_artifact_validation_rejects_metadata_artifact_size_mismatch ... ok -test tests::primitive_compat_predicates_match_validator_modes ... ok -test tests::compile_riscv_elf_accepts_large_schema_field_offsets ... ok -test tests::generic_shared_mutation_does_not_emit_pool_pattern_metadata ... ok -test tests::fixed_byte_mutable_state_set_transition_is_checked_under_ckb_profile ... ok -test tests::resolve_input_path_accepts_package_root_and_manifest ... ok -test tests::scheduler_witness_hex_decode_rejects_invalid_metadata_hex ... ok -test tests::entry_witness_wrapper_supports_scalar_stack_args ... ok -test tests::named_action_output_create_binding_reuses_declared_output_index ... ok -test tests::proof_plan_cross_references_matching_action_obligation_for_invariant ... ok -test tests::parameterized_entrypoint_emits_witness_entry_wrapper ... ok -test tests::compile_riscv_elf_accepts_large_stack_offsets ... ok -test tests::source_unit_disk_verification_rejects_paths_outside_trusted_root ... ok -test tests::tuple_return_abi_rejects_more_than_eight_fields ... ok -test tests::vm_abi_trailer_detection_requires_complete_zero_reserved_trailer ... ok -test types::tests::block_expression_merges_existing_vec_refinements ... ok -test types::tests::branch_local_consume_is_rejected_until_lifecycle_effects_are_cfg_aware ... ok -test types::tests::branch_local_create_is_rejected_until_lifecycle_effects_are_cfg_aware ... ok -test types::tests::byte_string_literal_type_uses_actual_length ... ok -test types::tests::call_arguments_do_not_coerce_mut_ref_to_ref ... ok -test types::tests::check_without_resolver_rejects_imports ... ok -test types::tests::compound_assign_rejects_implicit_narrowing ... ok -test tests::source_unit_disk_verification_accepts_paths_inside_trusted_root ... ok -test types::tests::compound_assign_uses_numeric_binary_rules ... ok -test types::tests::const_initializers_reject_cell_backed_types ... ok -test types::tests::const_initializers_allow_supported_literals ... ok -test types::tests::const_initializers_reject_cell_lifecycle_expressions ... ok -test types::tests::const_initializers_reject_computed_expressions ... ok -test types::tests::contextual_integer_literals_fit_declared_widths ... ok -test types::tests::constant_narrowing_casts_must_fit ... ok -test types::tests::cyclic_schema_type_dependencies_are_rejected ... ok -test tests::proof_plan_checked_static_excluded_from_on_chain_checked_obligations ... ok -test types::tests::duplicate_lifecycle_binding_is_rejected_until_effects_are_cfg_aware ... ok -test types::tests::expected_type_does_not_widen_non_literal_field_boundary ... ok -test types::tests::expected_type_does_not_widen_non_literal_let_boundary ... ok -test types::tests::expected_type_does_not_widen_non_literal_abi_arg_boundary ... ok -test types::tests::expected_type_does_not_widen_non_literal_return_boundary ... ok -test types::tests::generic_reference_detection_uses_type_structure ... ok -test types::tests::explicit_cast_can_cross_integer_width_boundary ... ok -test types::tests::expression_branch_unreachable_code_is_rejected_by_typechecker ... ok -test types::tests::if_expression_preserves_typed_vec_result_with_empty_constructor_branch ... ok -test types::tests::if_statement_merges_matching_vec_refinements ... ok -test types::tests::if_statement_rejects_one_sided_vec_refinement ... ok -test types::tests::if_statement_rejects_divergent_vec_refinements ... ok -test types::tests::imported_and_qualified_names_compare_as_same_type ... ok -test types::tests::imported_type_ids_must_not_collide_in_visible_module_scope ... ok -test types::tests::invalid_schema_field_types_are_not_registered_as_valid_fields ... ok -test types::tests::imported_token_type_is_treated_as_linear ... ok -test types::tests::lifecycle_capability_gates_reject_undeclared_kernel_effects ... ok -test tests::proof_plan_marks_invariant_action_evidence_as_non_exhaustive ... ok -test types::tests::match_requires_enum_scrutinee ... ok -test types::tests::non_tail_linear_expression_statements_are_rejected ... ok -test types::tests::mixed_width_arithmetic_and_ordering_are_rejected ... ok -test types::tests::numeric_named_type_equality_is_commutative ... ok -test types::tests::numeric_type_equality_respects_width ... ok -test types::tests::qualified_identifier_must_resolve_to_value ... ok -test types::tests::preserve_rejects_mismatched_field_types ... ok -test types::tests::imported_linear_argument_is_marked_consumed_after_call ... ok -test types::tests::recursive_enum_payloads_are_rejected ... ok -test types::tests::require_block_rejects_assignment_expression ... ok -test types::tests::require_block_rejects_lifecycle_stdlib_call ... ok -test types::tests::statically_visible_division_by_zero_is_rejected ... ok -test types::tests::require_rejects_nested_cell_operation ... ok -test types::tests::stdlib_claim_output_requires_declared_claim_output_type ... ok -test types::tests::stdlib_claim_output_requires_complete_field_coverage ... ok -test types::tests::stdlib_claim_rejects_declared_output_type_mismatch ... ok -test types::tests::stdlib_claim_rejects_extra_arguments ... ok -test types::tests::stdlib_claim_rejects_non_receipt_input ... ok -test types::tests::launch_module_type_checks_with_registered_imports ... ok -test types::tests::stdlib_claim_requires_explicit_output_and_lock_arguments ... ok -test types::tests::stdlib_transfer_output_requires_complete_field_coverage ... ok -test types::tests::stdlib_settle_requires_explicit_output_and_lock_arguments ... ok -test types::tests::stdlib_transfer_rejects_extra_arguments ... ok -test types::tests::strict_mode_rejects_imported_legacy_capabilities ... ok -test types::tests::typed_vec_with_capacity_uses_declared_element_type ... ok -test types::tests::unsigned_integer_negation_is_rejected ... ok -test types::tests::tail_match_expressions_are_valid_return_values ... ok -test types::tests::vec_type_arguments_are_validated ... ok -test types::tests::u128_ordering_and_arithmetic_still_rejected_on_widening ... ok -test tests::strict_audit_codegen_emits_only_aligned_stack_pointer_deltas ... ok -test wasm::tests::wasm_audit_reports_audit_only_for_type_only_module ... ok -test wasm::tests::wasm_compiler_rejects_pure_action_modules ... ok -test wasm::tests::wasm_encoder_emits_magic_version_and_status_custom_section ... ok -test wasm::tests::wasm_runtime_instantiates_metadata_module_but_refuses_calls ... ok -test types::tests::unsupported_u128_arithmetic_is_rejected ... ok -test tests::v014_runtime_helpers_fail_closed_when_not_executable ... ok -test types::tests::widening_boundary_matrix ... ok -test tests::ordered_named_output_create_constraints_are_checked_in_body_order ... ok -test tests::u128_mutable_state_transition_with_u64_delta_is_checked ... ok -test tests::payload_enum_fields_use_dynamic_molecule_schema_metadata ... ok -test tests::optimized_entry_lock_keeps_inlined_schema_pointer_field_access_checked ... ok -test codegen::tests::internal_assembler_relaxes_far_conditional_branch_with_long_jump ... ok -test codegen::tests::internal_assembler_encodes_far_unconditional_jump ... ok -test codegen::tests::bundled_example_codegen_mnemonics_are_declared ... ok - -test result: ok. 776 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.64s - - -running 7 tests -test adversarial_parser_preserves_operator_precedence_in_ambiguous_sequences ... ok -test adversarial_0_13_rejects_invalid_hash_type_dsl ... ok -test adversarial_parser_binds_else_to_nearest_if ... ok -test adversarial_parser_rejects_deep_unary_expression_without_panicking ... ok -test adversarial_integer_literals_fail_closed_on_lexical_and_contextual_overflow ... ok -test adversarial_parser_rejects_deep_nested_control_flow_without_panicking ... ok -test adversarial_0_13_rejects_unsupported_generic_collection_surfaces ... ok - -test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s - - -running 11 tests -test snapshot_simple_action_assembly ... ok -test runtime_u64_helpers_fail_closed_before_value_use ... ok -test runtime_void_helpers_fail_closed_before_continuing ... ok -test snapshot_lock_args_assembly ... ok -test snapshot_type_id_create_output_assembly ... ok -test snapshot_spawn_ipc_executable_status_checked_assembly ... ok -test snapshot_witness_schema_syscall_assembly ... ok -test runtime_witness_helpers_fail_closed_before_pointer_use ... ok -test snapshot_collection_lowering_assembly ... ok -test snapshot_blake2b_helper_assembly ... ok -test snapshot_assemblies_contain_no_leaked_overflow_diagnostics ... ok - -test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.22s - - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s - - -running 86 tests -test cellc_add_git_requires_full_rev_and_records_pin ... ok -test cellc_add_and_remove_subcommands_honor_dev_path_and_json ... ok -test cellc_check_denies_metadata_only_declared_invariant ... ok -Check succeeded - Target profile: ckb - Checked: package default (RISC-V assembly) -test cellc_check_accepts_ckb_profile_timepoint ... ok -test cellc_abi_subcommand_explains_entry_witness_layout ... ok -test cellc_check_accepts_pure_ckb_target_profile ... ok -test cellc_action_build_emits_builder_plan_json ... ok -test cellc_build_uses_manifest_policy_before_writing_artifacts ... ok -Build complete - Artifact format: RISC-V assembly - Target profile: ckb - Output: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmphktJVg/build/main.s - Metadata: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmphktJVg/build/main.s.meta.json -test cellc_check_production_rejects_incomplete_output_verification ... ok -test cellc_check_production_rejects_fail_closed_runtime_paths ... ok -test cellc_build_accepts_pure_ckb_target_profile_without_vm_abi_trailer ... ok -test cellc_check_can_reject_runtime_required_obligations ... ok -test cellc_build_and_check_subcommands_use_package_flow ... ok -test cellc_ckb_hash_emits_default_blake2b_vector ... ok -test cellc_check_denies_checked_partial_proof_plan_gap ... ok -test cellc_clean_subcommand_supports_json_summary ... ok -test cellc_check_all_targets_checks_asm_and_elf_without_writing_artifacts ... ok -test cellc_check_uses_manifest_policy_defaults ... ok -test cellc_check_reports_claim_source_predicate_blocker_class ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [5d, 8b, d, ed, a5, 24, 99, 9f, c3, fe, 29, 67, 78, 19, 2a, 46, 8f, a3, b5, 44, cb, 36, cf, cc, e2, 10, 50, 24, 59, 4b, 5b, 37] - Output: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpZN7eUS/artifacts/main.s - Metadata: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpZN7eUS/artifacts/main.s.meta.json -test cellc_check_accepts_u128_mutable_state_transition_with_u64_delta ... ok -test cellc_cli_target_overrides_manifest_build_target ... ok -test cellc_doc_subcommand_generates_markdown_docs ... ok -test cellc_check_reports_linear_collection_ownership_blocker_class ... ok -test cellc_entry_witness_subcommand_emits_parameterized_witness_json ... ok -test cellc_check_reports_settle_finalization_blocker_class ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [9f, aa, 3c, b9, 5, 1b, a7, 19, e4, ea, e4, 1, 79, 7, 11, 89, 7f, 40, ba, 26, 7e, 86, ba, 8c, d5, a3, a, 4d, 3, 45, eb, 54] - Output: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmplIPUBc/app_pkg/build/main.s - Metadata: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmplIPUBc/app_pkg/build/main.s.meta.json -test cellc_compiles_package_with_local_path_dependency ... ok -test cellc_explain_profile_reports_ckb_v0_14_contract ... ok -test cellc_constraints_subcommand_surfaces_ckb_deployment_manifest ... ok -test cellc_check_reports_resource_conservation_blocker_class ... ok -test cellc_check_reports_explicit_output_binding_without_mutable_state_blockers ... ok -test cellc_entry_witness_subcommand_rejects_wrong_width_fixed_bytes ... ok -test cellc_entry_witness_subcommand_encodes_schema_backed_params ... ok -test cellc_errors_include_runtime_ecode_when_policy_failure_maps_to_runtime_registry ... ok -test cellc_explain_subcommand_reports_runtime_error ... ok -test cellc_explain_proof_reports_invariant_action_coverage_match ... ok -test cellc_explain_proof_human_reports_macro_provenance ... ok -test cellc_explain_proof_warns_for_lock_group_transaction_scope ... ok -test cellc_explain_proof_reports_declared_invariant ... ok -test cellc_explain_proof_reports_covenant_proof_plan ... ok -test cellc_info_subcommand_supports_json_summary ... ok -test cellc_explain_proof_summary_reports_fail_closed_diagnostics ... ok -test cellc_check_reports_pool_invariant_policy_families ... ok -test cellc_init_subcommand_supports_json_summary ... ok -test cellc_lsp_flag_rejects_trailing_arguments ... ok -Formatting complete - Updated 1 file(s) -test cellc_rejects_registry_package_dependencies_fail_closed ... ok -test cellc_new_subcommand_supports_json_summary_and_vcs_none ... ok -test cellc_run_subcommand_without_vm_runner_degrades_gracefully ... ok -test cellc_rejects_external_dependency_function_calls_until_linking_exists ... ok -test cellc_fmt_subcommand_formats_sources ... ok -test cellc_rejects_underdeclared_effects_from_path_dependency_calls ... ok -test cellc_test_subcommand_rejects_empty_expected_error_line_text ... ok -test cellc_install_path_updates_lockfile_and_remove_prunes_it ... ok -test cellc_test_subcommand_rejects_conflicting_expectations ... ok -test cellc_test_subcommand_rejects_unknown_directives ... ok -test cellc_test_subcommand_rejects_missing_expected_error_text ... ok -test cellc_test_subcommand_rejects_missing_entrypoint_metadata ... ok -test cellc_test_subcommand_rejects_wrong_expected_error_line ... ok -test cellc_test_subcommand_rejects_missing_runtime_metadata ... ok -test cellc_metadata_subcommand_emits_lowering_runtime_json ... ok -test cellc_test_subcommand_supports_expected_compile_failures ... ok -test cellc_test_subcommand_supports_expected_error_line_directive ... ok -test cellc_new_subcommand_initializes_git_by_default ... ok -test cellc_test_subcommand_compiles_test_sources ... ok -test cellc_scheduler_plan_consumes_shared_touch_hints ... ok -test cellc_test_subcommand_supports_entrypoint_metadata_directives ... ok -test cellc_opt_report_compares_all_optimization_levels ... ok -test cellc_top_level_primitive_strict_rejects_legacy_capabilities ... ok -test cellc_test_subcommand_supports_policy_directives ... ok -test cellc_test_subcommand_supports_runtime_metadata_directives ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpxIOTw9/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpxIOTw9/sample.s.meta.json -success: compiled successfully - Artifact format: RISC-V ELF - Target profile: ckb - Artifact hash: [cf, 7, cf, ac, d0, a, 43, a3, a8, cc, 8b, 6e, 66, e1, 29, b2, 32, 60, 2f, 76, a3, 55, 4d, 52, d5, 38, 51, 1f, c8, b, 49, 2] - Output: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpMnCqdX/artifacts/main.elf - Metadata: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpMnCqdX/artifacts/main.elf.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [5d, 8b, d, ed, a5, 24, 99, 9f, c3, fe, 29, 67, 78, 19, 2a, 46, 8f, a3, b5, 44, cb, 36, cf, cc, e2, 10, 50, 24, 59, 4b, 5b, 37] - Output: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpfZfgkz/artifacts/main.s - Metadata: /private/var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpfZfgkz/artifacts/main.s.meta.json -test cellc_check_reports_transaction_invariant_checked_subconditions ... ok -test cellc_test_subcommand_supports_target_directive ... ok -test cellc_uses_manifest_build_target_by_default ... ok -test cellc_uses_manifest_build_out_dir_for_package_input ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [a7, d4, 78, cc, c3, f5, cd, 81, cd, de, 51, 44, ee, 83, 4d, 64, 46, df, bd, 40, 58, 5f, 51, 6c, d1, 56, 6b, b7, 44, 9d, 96, 8d] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpy8gfiw/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpy8gfiw/sample.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp30zZTS/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp30zZTS/sample.s.meta.json -test cellc_top_level_accepts_primitive_strict_for_kernel_effect_capabilities ... ok -test cellc_verify_artifact_accepts_matching_sidecar ... ok -test cellc_verify_artifact_enforces_policy_flags ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpdNrwHe/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpdNrwHe/sample.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp2g2vYW/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp2g2vYW/sample.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp1rv5fc/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmp1rv5fc/sample.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpm5WZKi/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpm5WZKi/sample.s.meta.json -test cellc_writes_requested_output_file ... ok -test cellc_explain_generics_reports_checked_vec_instantiations ... ok -test cellc_verify_artifact_rejects_metadata_schema_downgrade ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [47, e2, e6, 55, 6c, 78, 48, d4, 45, 84, a8, 71, 67, a6, 5b, b9, c4, 8d, 69, 85, c0, 5e, 48, c7, e, 11, c0, 24, be, 95, 38, 76] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpJEBJq0/sample.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpJEBJq0/sample.s.meta.json -test cellc_verify_artifact_rejects_noncanonical_source_unit_hash ... ok -test cellc_verify_artifact_rejects_tampered_artifact ... ok -test cellc_verify_artifact_rejects_tampered_source_when_requested ... ok -test cellc_verify_artifact_enforces_expected_hashes ... ok -test cellc_verify_artifact_primitive_strict_rechecks_disk_sources ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [79, a1, 5d, 7e, f7, 1f, 9a, 64, 89, da, 9e, 8b, a8, 90, b6, 15, f0, b5, 61, d1, 80, 6b, 39, 9f, f0, 5a, a4, 4d, 0, 5, 41, 3c] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpQx0Gsc/amm_pool.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpQx0Gsc/amm_pool.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [2a, 2, ca, c2, fd, b6, 4a, 53, 9e, 26, cb, a1, 31, 69, ab, f3, 1d, c1, 42, d, 18, d3, fd, 1d, 92, b7, a, 55, c6, d, 87, df] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpQx0Gsc/launch.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpQx0Gsc/launch.s.meta.json -test cellc_check_reports_checked_pool_invariant_families_without_runtime_blockers ... ok -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [8b, d7, 59, d4, b6, d1, 6, 8b, 97, 0, fd, e5, df, 72, ec, a6, 99, bf, 20, 34, 90, 55, b2, 17, 6d, 48, 55, 9e, ec, ed, 11, 2b] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpQx0Gsc/multisig.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpQx0Gsc/multisig.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [cd, d0, f5, 74, b7, 9d, 8e, 7d, 79, 50, 6a, cf, 3e, 13, b, 53, 5c, b9, 7f, 8c, d1, 1f, 88, bf, 1b, 8a, 3c, 3b, 34, 45, c6, 4e] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpQx0Gsc/nft.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpQx0Gsc/nft.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [fd, 14, d8, d9, e, 2c, e8, 71, 98, aa, e2, b6, b4, fc, b8, 93, aa, 84, 66, 2f, 21, 2e, 9d, 26, 5, 63, 5d, 74, 55, fd, 56, 87] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpQx0Gsc/timelock.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpQx0Gsc/timelock.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [3c, bd, 90, ed, de, 2c, 8d, 8c, 97, 1, a4, d9, 9, dc, 3d, bd, 22, 6b, 5b, 39, e7, 3e, 59, 9a, 5d, e1, 2c, 13, 61, 4d, 32, 46] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpQx0Gsc/token.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpQx0Gsc/token.s.meta.json -success: compiled successfully - Artifact format: RISC-V assembly - Target profile: ckb - Artifact hash: [2a, 7, 99, 55, e2, cb, e6, 1b, 39, 63, db, fc, 1, 63, fd, 57, 38, 6, a4, 7, ad, b2, 5d, 5c, f1, de, 41, e5, 2a, 29, 1, e5] - Output: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpQx0Gsc/vesting.s - Metadata: /var/folders/kq/dz44fm994nz94zw2dfqnz_g00000gn/T/.tmpQx0Gsc/vesting.s.meta.json -test cellc_compiles_bundled_examples_to_requested_outputs ... ok - -test result: ok. 86 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.90s - - -running 26 tests -test ckb_scoped_entry_keeps_called_action_helpers ... ok -test launch_seed_pool_composition_is_scheduler_visible ... ok -test registry_example_uses_bounded_local_vec_helpers_without_collection_debt ... ok -test amm_pool_input_output_params_are_scheduler_visible ... ok -test release_examples_are_free_of_placeholder_hashes_and_formatter_artifacts ... ok -test nft_core_actions_expose_action_specific_builder_metadata ... ok -test registry_example_with_insert_contains_compiles_to_elf ... ok -test token_cell_invariant_appears_in_proof_plan ... ok -test order_book_language_example_uses_local_vec_helpers_without_collection_debt ... ok -test stdlib_language_example_compiles_with_all_patterns ... ok -test v0_15_scoped_invariant_example_compiles_and_produces_proof_plan ... ok -test token_mint_authority_input_output_binding_is_explicit ... ok -test v0_15_identity_lifecycle_example_compiles_and_produces_proof_plan ... ok -test vesting_phase2_remaining_obligations_are_explicit ... ok -test vesting_read_ref_params_are_scheduler_visible ... ok -test multisig_core_actions_expose_threshold_flow_metadata ... ok -test timelock_core_actions_expose_time_and_release_metadata ... ok -test bundled_examples_emit_molecule_schema_manifest_report ... ok -test canonical_examples_compile_under_primitive_strict_015 ... ok -test canonical_examples_are_the_single_checked_in_business_source ... ok -test bundled_examples_compile_to_non_empty_assembly ... ok -test bundled_examples_stay_near_backend_shape_release_baseline ... ok -test bundled_examples_stay_within_backend_shape_budgets ... ok -test bundled_examples_backend_shape_report_serializes ... ok -test all_checked_in_cell_examples_compile ... ok -test bundled_examples_compile_to_elf ... ok - -test result: ok. 26 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 8.13s - - -running 7 tests -test fuzzy_oversized_static_widths_are_controlled_errors ... ok -test fuzzy_metadata_tampering_never_panics ... ok -test fuzzy_unicode_hex_inputs_are_controlled_errors ... ok -test fuzzy_entry_witness_encoding_never_panics ... ok -test fuzzy_lsp_incremental_edits_never_panic ... ok -test fuzzy_mutated_sources_never_panic ... ok -test fuzzy_semantic_codegen_mutations_reach_assembly ... ok - -test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.53s - - -running 4 tests -test ickb_diff_matrix_is_partial_and_consistent_with_model_fixtures ... ok -test ickb_positive_fixtures_pass_model_verifier ... ok -test ickb_negative_fixtures_fail_for_expected_invariant ... ok -test ickb_benchmark_specs_compile_and_expose_expected_entries ... ok - -test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.26s - - -running 1 test -test syntax_combo_quick_matrix_is_cargo_test_visible ... ok - -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.11s - - -running 14 tests -test v0_14_wait_requires_child_pid ... ok -test v0_14_rejects_spawn_ipc_fd_double_close ... ok -test v0_14_rejects_blake2b_non_hash_input ... ok -test v0_14_rejects_spawn_ipc_fd_use_after_close ... ok -test v0_14_rejects_spawn_ipc_fd_leak ... ok -test v0_14_spawn_target_must_be_static ... ok -test v0_14_rejects_tampered_spawn_script_reference_metadata ... ok -test v0_14_exposes_type_id_create_output_plan_and_output_data_boundary ... ok -test v0_14_exposes_declarative_capacity_floor_metadata ... ok -test v0_14_rejects_tampered_type_id_output_data_and_script_reference_metadata ... ok -test v0_14_rejects_tampered_runtime_access_and_script_group_metadata ... ok -test v0_14_exposes_spawn_ipc_source_witness_time_capacity_metadata ... ok -test v0_14_language_examples_cover_spawn_pipeline_type_id_and_canonical_style ... ok -test v0_14_compiles_dynamic_blake2b_hash_helper ... ok - -test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.31s - - -running 17 tests -test ckb_stdlib_protocol_modules_exist_and_cover_required_suites ... ok -test standard_ckb_compat_manifest_covers_required_suites ... ok -test ckb_stdlib_protocol_functions_cover_core_operations ... ok -test standard_ckb_compat_fixture_files_parse_and_have_required_fields ... ok -test strict_0_16_rejects_unbound_spawn_target_cell_dep ... ok -test strict_0_16_rejects_metadata_only_proof_plan_gaps ... ok -test strict_0_16_rejects_spawn_target_manifest_binding_outside_cell_dep_zero ... ok -test strict_0_16_rejects_spawn_target_manifest_dep_group_binding ... ok -test proof_plan_soundness_rejects_group_cardinality_drift_after_optimization ... ok -test proof_plan_soundness_rejects_local_runtime_mismatches ... ok -test proof_plan_soundness_is_emitted_and_passes_for_checked_identity ... ok -test proof_plan_soundness_rejects_scoped_duplicate_obligation_deletion ... ok -test validate_tx_checks_builder_assumption_evidence ... ok -test cli_verify_deploy_rejects_tampered_plan_integrity ... ok -test strict_0_16_accepts_manifest_bound_spawn_target_cell_dep ... ok -test cli_explain_assumptions_and_validate_tx_are_machine_readable ... ok -test cli_v0_16_tooling_outputs_are_machine_readable_and_schema_bound ... ok - -test result: ok. 17 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s - - -running 3 tests -test btc_bip340_verifier_surface_rejects_wrong_argument_widths ... ok -test btc_bip340_verifier_surface_lowers_to_generic_spawn_ipc ... ok -test strict_0_16_accepts_manifest_bound_btc_bip340_verifier ... ok - -test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s - - -running 3 tests -test fixed_u64_le_rejects_dynamic_index_oob_window_and_non_fixed_input ... ok -test fixed_u64_le_lowers_fixed_byte_constants_and_parameters ... ok -test generic_verifier_envelope_compiles_all_words_after_spawn_with_fd ... ok - -test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s - - -running 7 tests -test hash_blake2b_packed_rejects_dynamic_values ... ok -test verifier_namespace_is_reserved_for_source_and_dependencies ... ok -test hash_and_byte32_equality_is_allowed_for_authority_binding ... ok -test hash_blake2b_packed_uses_canonical_type_domain_and_declared_field_order ... ok -test ckb_outpoint_capacity_and_lock_args_helpers_emit_checked_runtime_accesses ... ok -test nested_packed_receipt_hash_guards_resource_transition_in_strict_mode ... ok -test production_runtime_verifier_manifest_requires_full_non_placeholder_pin ... ok - -test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.14s - - -running 4 tests -test v0_16_spawn_with_fd_requires_static_target_and_open_fd ... ok -test v0_16_spawn_with_fd_exposes_spawn_target_metadata ... ok -test v0_16_spawn_with_fd_emits_vm2_spawnargs_with_single_inherited_fd ... ok -test strict_0_16_accepts_manifest_bound_spawn_with_fd_target_cell_dep ... ok - -test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s - - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s - - -== stderr == - Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s - Finished `test` profile [unoptimized + debuginfo] target(s) in 0.04s - Running unittests src/lib.rs (target/debug/deps/cellscript-198f0ba9a296fb91) - Running tests/adversarial_0_13.rs (target/debug/deps/adversarial_0_13-87ca0b7751a0cf60) - Running tests/assembly_snapshots.rs (target/debug/deps/assembly_snapshots-a1b0ee4291a50be7) - Running tests/ckb_acceptance.rs (target/debug/deps/ckb_acceptance-546e6523e51ab114) - Running tests/cli.rs (target/debug/deps/cli-cd9b4a3e7ed668b4) - Running tests/examples.rs (target/debug/deps/examples-885631b36bce043e) - Running tests/fuzzy_debug.rs (target/debug/deps/fuzzy_debug-a3c500396cf14cf4) - Running tests/ickb_benchmark.rs (target/debug/deps/ickb_benchmark-d0fd214d43bb34ab) - Running tests/syntax_combo.rs (target/debug/deps/syntax_combo-b9abeffd268b17da) - Running tests/v0_14.rs (target/debug/deps/v0_14-41fd99cdcf8b775d) - Running tests/v0_16.rs (target/debug/deps/v0_16-2adfb11c7d8743b5) - Running tests/v0_16_btc_bip340_verifier.rs (target/debug/deps/v0_16_btc_bip340_verifier-1690c4a1af4e82fe) - Running tests/v0_16_fixed_u64_le.rs (target/debug/deps/v0_16_fixed_u64_le-42b56ec94f2d9f31) - Running tests/v0_16_packed_hash_ckb_helpers.rs (target/debug/deps/v0_16_packed_hash_ckb_helpers-45b534a50db0d149) - Running tests/v0_16_spawn_with_fd.rs (target/debug/deps/v0_16_spawn_with_fd-1fc1dca0d6908655) - Doc-tests cellscript diff --git a/.cap/logs/1780406460-76478.log b/.cap/logs/1780406460-76478.log deleted file mode 100644 index 1af2e126..00000000 --- a/.cap/logs/1780406460-76478.log +++ /dev/null @@ -1,4 +0,0 @@ -== stdout == -wrote /Users/arthur/RustroverProjects/CellScript/target/novaseal-devnet-stateful-acceptance.json status=passed live_devnet_rpc_executed=True blockers=0 - -== stderr == diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..b8c67051 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +.git +.cap +.codex +.idea +.playwright-mcp +target +**/target +**/node_modules +website +proposals +audits +tests +docs +roadmap +editors +tools +*.png +*.log diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 956398ec..a1fa73e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,17 +20,41 @@ jobs: name: Test runs-on: ubuntu-latest timeout-minutes: 45 + services: + postgres: + image: postgres:17-alpine + env: + POSTGRES_DB: cellscript_registry_test + POSTGRES_USER: cellscript_test + POSTGRES_PASSWORD: cellscript_test_password + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U cellscript_test -d cellscript_registry_test" + --health-interval 5s + --health-timeout 5s + --health-retries 10 env: CARGO_INCREMENTAL: "0" CARGO_TARGET_DIR: /tmp/cellscript-ci-target CELLSCRIPT_BACKEND_SHAPE_REPORT: /tmp/cellscript-backend-shape/backend-shape-report.json CKB_SDK_RUST_REF: v5.1.0 + REGISTRY_TEST_DATABASE_URL: postgresql://cellscript_test:cellscript_test_password@127.0.0.1:5432/cellscript_registry_test steps: - name: Check out repository uses: actions/checkout@v4 with: submodules: recursive + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + cache-dependency-path: | + website/package-lock.json + services/registry-api/package-lock.json + - name: Check out ckb-sdk-rust path dependency run: | git clone --depth 1 --branch "$CKB_SDK_RUST_REF" \ @@ -40,7 +64,8 @@ jobs: - name: Install Rust toolchain run: | - rustup toolchain install 1.97.1 --profile minimal --component rustfmt --component clippy + rustup toolchain install 1.97.1 --profile minimal --component rustfmt --component clippy --component llvm-tools-preview + rustup target add --toolchain 1.97.1 riscv64imac-unknown-none-elf wasm32-unknown-unknown rustup default 1.97.1 rustc --version cargo --version diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7a9769bf..403adeb9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,6 +38,7 @@ jobs: cache: npm cache-dependency-path: | website/package-lock.json + services/registry-api/package-lock.json editors/vscode-cellscript/package-lock.json - name: Install Rust release toolchain @@ -77,12 +78,8 @@ jobs: - name: Install pinned CKB build toolchain run: | - CKB_TOOLCHAIN="$(python3 - <<'PY' - import tomllib - from pathlib import Path - print(tomllib.loads(Path('../ckb/rust-toolchain.toml').read_text(encoding='utf-8'))['toolchain']['channel']) - PY - )" + CKB_TOOLCHAIN="$(sed -n 's/^channel = "\(.*\)"$/\1/p' ../ckb/rust-toolchain.toml | head -n 1)" + test -n "$CKB_TOOLCHAIN" rustup toolchain install "$CKB_TOOLCHAIN" --profile minimal - name: Resolve release version diff --git a/.github/workflows/website-build.yml b/.github/workflows/website-build.yml index de96494e..17ad3711 100644 --- a/.github/workflows/website-build.yml +++ b/.github/workflows/website-build.yml @@ -17,11 +17,6 @@ jobs: with: submodules: recursive - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - name: Set up Node.js uses: actions/setup-node@v4 with: @@ -45,10 +40,7 @@ jobs: fi - name: Build website - run: | - cd website - npm exec -- astro check - npm exec -- astro build + run: npm --prefix website run build:ci - name: Upload website dist uses: actions/upload-artifact@v4 diff --git a/.gitignore b/.gitignore index 8b86e17a..9920a7f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ /target/ +services/registry-verifier/target/ +services/registry-artifact-verifier/target/ node_modules/ dist/ +dist-node/ services/registry-api/dist/ editors/vscode-cellscript/dist/ @@ -8,10 +11,10 @@ editors/vscode-cellscript/dist/ .idea/ .vscode/ .cap/ +.codex/ +.zcode/ .playwright-mcp/ .wrangler/ -__pycache__/ -*.py[cod] *.swp *.swo @@ -25,7 +28,6 @@ __pycache__/ *.meta.json .cell/ -Cell.lock proposals/* !proposals/novaseal/ @@ -55,14 +57,12 @@ proposals/novaseal/v0-mvp-skeleton/build/ proposals/novaseal/**/.cell/ proposals/novaseal/v0-mvp-skeleton/target/ proposals/novaseal/v0-mvp-skeleton/src/.cell/ -proposals/novaseal/v0-mvp-skeleton/scripts/__pycache__/ proposals/novaseal/v0-mvp-skeleton/verifier/**/target/ proposals/novaseal/v0-mvp-skeleton/harness/**/target/ proposals/novaseal/agreement-profile-v0/target/ proposals/novaseal/agreement-profile-v0/harness/**/target/ proposals/novaseal/agreement-profile-v0/src/.cell/ proposals/novaseal/agreement-profile-v0/harness/**/.cell/ -proposals/novaseal/agreement-profile-v0/scripts/__pycache__/ proposals/novaseal/fungible-xudt-profile-v0/target/ proposals/novaseal/fungible-xudt-profile-v0/src/.cell/ proposals/novaseal/rwa-receipt-profile-v0/target/ @@ -77,8 +77,6 @@ proposals/novaseal/fiber-candidate-profile-v0/target/ proposals/novaseal/fiber-candidate-profile-v0/src/.cell/ proposals/novaseal/**/.DS_Store proposals/novaseal/**/.cap/ -proposals/novaseal/**/__pycache__/ -proposals/novaseal/**/*.py[cod] proposals/novaseal/**/*.s proposals/novaseal/**/*.elf proposals/novaseal/**/*.meta.json @@ -86,9 +84,7 @@ proposals/evolving-dob/evolving-dob-profile-v1/build/ proposals/evolving-dob/evolving-dob-profile-v1/target/ proposals/evolving-dob/evolving-dob-profile-v1/.cell/ proposals/evolving-dob/evolving-dob-profile-v1/src/.cell/ -proposals/evolving-dob/evolving-dob-profile-v1/scripts/__pycache__/ proposals/evolving-dob/evolving-dob-profile-v1/.DS_Store -proposals/evolving-dob/evolving-dob-profile-v1/**/*.py[cod] proposals/evolving-dob/evolving-dob-profile-v1/**/*.s proposals/evolving-dob/evolving-dob-profile-v1/**/*.elf proposals/evolving-dob/evolving-dob-profile-v1/**/*.meta.json diff --git a/.playwright-mcp/console-2026-06-02T06-24-37-467Z.log b/.playwright-mcp/console-2026-06-02T06-24-37-467Z.log deleted file mode 100644 index 9d06a057..00000000 --- a/.playwright-mcp/console-2026-06-02T06-24-37-467Z.log +++ /dev/null @@ -1,5 +0,0 @@ -[ 247982ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://127.0.0.1:4321/CellScript/:0 -[ 247991ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://127.0.0.1:4321/favicon.ico:0 -[ 318343ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://127.0.0.1:4321/CellScript/:0 -[ 711210ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://127.0.0.1:4321/CellScript/:0 -[ 759754ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://127.0.0.1:4321/CellScript/:0 diff --git a/.playwright-mcp/console-2026-06-02T08-37-09-619Z.log b/.playwright-mcp/console-2026-06-02T08-37-09-619Z.log deleted file mode 100644 index 46e88688..00000000 --- a/.playwright-mcp/console-2026-06-02T08-37-09-619Z.log +++ /dev/null @@ -1,17 +0,0 @@ -[ 805ms] [VERBOSE] [DOM] Password field is not contained in a form: (More info: https://goo.gl/9p2vKq) %o @ https://accounts.google.com/v3/signin/identifier?opparams=%253Fenable_granular_consent%253Dtrue&dsh=S-1726020052%3A1780389429869879&client_id=946018238758-bi6ni53dfoddlgn97pk3b8i7nphige40.apps.googleusercontent.com&code_challenge=G6JE72YRM8qZHQ9iDSktI7om6XdRp0gUqAGmKUKK2iE&code_challenge_method=S256&include_granted_scopes=true&o2v=2&prompt=consent&redirect_uri=com.apple.Internet-Accounts-Settings.extension%3A%2F&response_type=code&scope=profile+email+https%3A%2F%2Fmail.google.com%2F+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcarddav&service=lso&flowName=GeneralOAuthFlow&continue=https%3A%2F%2Faccounts.google.com%2Fsignin%2Foauth%2Fconsent%3Fauthuser%3Dunknown%26part%3DAJi8hAOxodTm8A9zxcr3Ph4pQOU6sg9BP20PF_l74FwN-IxlKtxN0zN1i-O1JvDr9SfK9Wfa8pBDyPpxGvMVfFVb9izNCgcPCENiLBpWEM-C5Xba0OK1gHDWZUxo-cOlDK30YEmD5m6iIA18XsBJpF4BRKEGMwMJoh9kculTlyaZNl7sTD2Rvo_px2jzmgsWN1SnNu1T2koB8PoBryeshucWffDkgUPKWLx6k0LuLyIk9I4Y2Gqt6PZzL75q8Y-VXS0ucsS1ZsQKwfZE6NwnPyAU70D5Pio3G6jHsOpDhD7_MIilzX2US5NXH_Fst-vzHxZuqnUpBQD1mxqK49TS5yVxLP4VTChp8xBgv5U-9HMXmveOqtB2cvJR-j1CLRfsy_yq6hS775s_t4mGLc4PMeB4ZjspHzlrpVUJWpsOnyFI8w6GqnLgRCPTyDRkorTSxm4W8Iik_2badVewZIjISaTGXMJWEAq1ZA%26flowName%3DGeneralOAuthFlow%26as%3DS-1726020052%253A1780389429869879%26client_id%3D946018238758-bi6ni53dfoddlgn97pk3b8i7nphige40.apps.googleusercontent.com%26requestPath%3D%252Fsignin%252Foauth%252Fconsent%23&rart=ANgoxcc8ysfsndY0dS7dYHRMUrZYu5QNPpyRfZfNWuY7aWvY2O6YGQJNTQRnugzWhVtEI6H2UHsTBqfHcCkBBCsWkiTpn7jiat59X0vSEoU8OQ9ztu0P4wj4WICTjHgELYSfGjbj3PN5:0 -[ 809ms] [LOG] %c%s color: red; background: yellow; font-size: 24px; WARNING! @ https://www.gstatic.com/_/mss/boq-identity/_/js/k=boq-identity.AccountsSignInUi.en_US.5E-Cp8lktf4.es5.O/am=Ed8AAACAUfwjZADw__ffAAACCKIDvmcBmgAhAwAAAAAAAAAAFgAAQCM/d=1/excm=_b,_tp,identifierview/ed=1/dg=0/wt=2/ujg=1/rs=AOaEmlFsQIJA_AF0WIXptlgYzPiBHWxSqA/dti=1/m=_b,_tp:545 -[ 809ms] [LOG] %c%s font-size: 18px; Using this console may allow attackers to impersonate you and steal your information using an attack called Self-XSS. -Do not enter or paste code that you do not understand. @ https://www.gstatic.com/_/mss/boq-identity/_/js/k=boq-identity.AccountsSignInUi.en_US.5E-Cp8lktf4.es5.O/am=Ed8AAACAUfwjZADw__ffAAACCKIDvmcBmgAhAwAAAAAAAAAAFgAAQCM/d=1/excm=_b,_tp,identifierview/ed=1/dg=0/wt=2/ujg=1/rs=AOaEmlFsQIJA_AF0WIXptlgYzPiBHWxSqA/dti=1/m=_b,_tp:545 -[ 9084ms] [WARNING] Blocked aria-hidden on an element because its descendant retained focus. The focus must not be hidden from assistive technology users. Avoid using aria-hidden on a focused element or its ancestor. Consider using the inert attribute instead, which will also prevent focus. For more details, see the aria-hidden section of the WAI-ARIA specification at https://w3c.github.io/aria/#aria-hidden. -Element with focus: -Ancestor with aria-hidden: @ https://accounts.google.com/v3/signin/identifier?opparams=%253Fenable_granular_consent%253Dtrue&dsh=S-1726020052%3A1780389429869879&client_id=946018238758-bi6ni53dfoddlgn97pk3b8i7nphige40.apps.googleusercontent.com&code_challenge=G6JE72YRM8qZHQ9iDSktI7om6XdRp0gUqAGmKUKK2iE&code_challenge_method=S256&include_granted_scopes=true&o2v=2&prompt=consent&redirect_uri=com.apple.Internet-Accounts-Settings.extension%3A%2F&response_type=code&scope=profile+email+https%3A%2F%2Fmail.google.com%2F+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcarddav&service=lso&flowName=GeneralOAuthFlow&continue=https%3A%2F%2Faccounts.google.com%2Fsignin%2Foauth%2Fconsent%3Fauthuser%3Dunknown%26part%3DAJi8hAOxodTm8A9zxcr3Ph4pQOU6sg9BP20PF_l74FwN-IxlKtxN0zN1i-O1JvDr9SfK9Wfa8pBDyPpxGvMVfFVb9izNCgcPCENiLBpWEM-C5Xba0OK1gHDWZUxo-cOlDK30YEmD5m6iIA18XsBJpF4BRKEGMwMJoh9kculTlyaZNl7sTD2Rvo_px2jzmgsWN1SnNu1T2koB8PoBryeshucWffDkgUPKWLx6k0LuLyIk9I4Y2Gqt6PZzL75q8Y-VXS0ucsS1ZsQKwfZE6NwnPyAU70D5Pio3G6jHsOpDhD7_MIilzX2US5NXH_Fst-vzHxZuqnUpBQD1mxqK49TS5yVxLP4VTChp8xBgv5U-9HMXmveOqtB2cvJR-j1CLRfsy_yq6hS775s_t4mGLc4PMeB4ZjspHzlrpVUJWpsOnyFI8w6GqnLgRCPTyDRkorTSxm4W8Iik_2badVewZIjISaTGXMJWEAq1ZA%26flowName%3DGeneralOAuthFlow%26as%3DS-1726020052%253A1780389429869879%26client_id%3D946018238758-bi6ni53dfoddlgn97pk3b8i7nphige40.apps.googleusercontent.com%26requestPath%3D%252Fsignin%252Foauth%252Fconsent%23&rart=ANgoxcc8ysfsndY0dS7dYHRMUrZYu5QNPpyRfZfNWuY7aWvY2O6YGQJNTQRnugzWhVtEI6H2UHsTBqfHcCkBBCsWkiTpn7jiat59X0vSEoU8OQ9ztu0P4wj4WICTjHgELYSfGjbj3PN5:0 -[ 805ms] [VERBOSE] [DOM] Password field is not contained in a form: (More info: https://goo.gl/9p2vKq) %o @ https://accounts.google.com/v3/signin/identifier?opparams=%253Fenable_granular_consent%253Dtrue&dsh=S-1726020052%3A1780389429869879&client_id=946018238758-bi6ni53dfoddlgn97pk3b8i7nphige40.apps.googleusercontent.com&code_challenge=G6JE72YRM8qZHQ9iDSktI7om6XdRp0gUqAGmKUKK2iE&code_challenge_method=S256&include_granted_scopes=true&o2v=2&prompt=consent&redirect_uri=com.apple.Internet-Accounts-Settings.extension%3A%2F&response_type=code&scope=profile+email+https%3A%2F%2Fmail.google.com%2F+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcarddav&service=lso&flowName=GeneralOAuthFlow&continue=https%3A%2F%2Faccounts.google.com%2Fsignin%2Foauth%2Fconsent%3Fauthuser%3Dunknown%26part%3DAJi8hAOxodTm8A9zxcr3Ph4pQOU6sg9BP20PF_l74FwN-IxlKtxN0zN1i-O1JvDr9SfK9Wfa8pBDyPpxGvMVfFVb9izNCgcPCENiLBpWEM-C5Xba0OK1gHDWZUxo-cOlDK30YEmD5m6iIA18XsBJpF4BRKEGMwMJoh9kculTlyaZNl7sTD2Rvo_px2jzmgsWN1SnNu1T2koB8PoBryeshucWffDkgUPKWLx6k0LuLyIk9I4Y2Gqt6PZzL75q8Y-VXS0ucsS1ZsQKwfZE6NwnPyAU70D5Pio3G6jHsOpDhD7_MIilzX2US5NXH_Fst-vzHxZuqnUpBQD1mxqK49TS5yVxLP4VTChp8xBgv5U-9HMXmveOqtB2cvJR-j1CLRfsy_yq6hS775s_t4mGLc4PMeB4ZjspHzlrpVUJWpsOnyFI8w6GqnLgRCPTyDRkorTSxm4W8Iik_2badVewZIjISaTGXMJWEAq1ZA%26flowName%3DGeneralOAuthFlow%26as%3DS-1726020052%253A1780389429869879%26client_id%3D946018238758-bi6ni53dfoddlgn97pk3b8i7nphige40.apps.googleusercontent.com%26requestPath%3D%252Fsignin%252Foauth%252Fconsent%23&rart=ANgoxcc8ysfsndY0dS7dYHRMUrZYu5QNPpyRfZfNWuY7aWvY2O6YGQJNTQRnugzWhVtEI6H2UHsTBqfHcCkBBCsWkiTpn7jiat59X0vSEoU8OQ9ztu0P4wj4WICTjHgELYSfGjbj3PN5:0 -[ 9084ms] [WARNING] Blocked aria-hidden on an element because its descendant retained focus. The focus must not be hidden from assistive technology users. Avoid using aria-hidden on a focused element or its ancestor. Consider using the inert attribute instead, which will also prevent focus. For more details, see the aria-hidden section of the WAI-ARIA specification at https://w3c.github.io/aria/#aria-hidden. -Element with focus: -Ancestor with aria-hidden: @ https://accounts.google.com/v3/signin/identifier?opparams=%253Fenable_granular_consent%253Dtrue&dsh=S-1726020052%3A1780389429869879&client_id=946018238758-bi6ni53dfoddlgn97pk3b8i7nphige40.apps.googleusercontent.com&code_challenge=G6JE72YRM8qZHQ9iDSktI7om6XdRp0gUqAGmKUKK2iE&code_challenge_method=S256&include_granted_scopes=true&o2v=2&prompt=consent&redirect_uri=com.apple.Internet-Accounts-Settings.extension%3A%2F&response_type=code&scope=profile+email+https%3A%2F%2Fmail.google.com%2F+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcarddav&service=lso&flowName=GeneralOAuthFlow&continue=https%3A%2F%2Faccounts.google.com%2Fsignin%2Foauth%2Fconsent%3Fauthuser%3Dunknown%26part%3DAJi8hAOxodTm8A9zxcr3Ph4pQOU6sg9BP20PF_l74FwN-IxlKtxN0zN1i-O1JvDr9SfK9Wfa8pBDyPpxGvMVfFVb9izNCgcPCENiLBpWEM-C5Xba0OK1gHDWZUxo-cOlDK30YEmD5m6iIA18XsBJpF4BRKEGMwMJoh9kculTlyaZNl7sTD2Rvo_px2jzmgsWN1SnNu1T2koB8PoBryeshucWffDkgUPKWLx6k0LuLyIk9I4Y2Gqt6PZzL75q8Y-VXS0ucsS1ZsQKwfZE6NwnPyAU70D5Pio3G6jHsOpDhD7_MIilzX2US5NXH_Fst-vzHxZuqnUpBQD1mxqK49TS5yVxLP4VTChp8xBgv5U-9HMXmveOqtB2cvJR-j1CLRfsy_yq6hS775s_t4mGLc4PMeB4ZjspHzlrpVUJWpsOnyFI8w6GqnLgRCPTyDRkorTSxm4W8Iik_2badVewZIjISaTGXMJWEAq1ZA%26flowName%3DGeneralOAuthFlow%26as%3DS-1726020052%253A1780389429869879%26client_id%3D946018238758-bi6ni53dfoddlgn97pk3b8i7nphige40.apps.googleusercontent.com%26requestPath%3D%252Fsignin%252Foauth%252Fconsent%23&rart=ANgoxcc8ysfsndY0dS7dYHRMUrZYu5QNPpyRfZfNWuY7aWvY2O6YGQJNTQRnugzWhVtEI6H2UHsTBqfHcCkBBCsWkiTpn7jiat59X0vSEoU8OQ9ztu0P4wj4WICTjHgELYSfGjbj3PN5:0 -[ 12416ms] [LOG] %c%s color: red; background: yellow; font-size: 24px; WARNING! @ https://www.gstatic.com/_/mss/boq-identity/_/js/k=boq-identity.OAuthUi.en_GB.-QjSzSQ9aM8.es5.O/am=8gMAAAAA_wAGAP-__wYQRAfeM0ABAgAAAAAAAACAAQAgAg/d=1/excm=_b,_tp,attributesview/ed=1/dg=0/wt=2/ujg=1/rs=AOaEmlF7dHSRzi7i6avN91i_S56SHTkyWQ/dti=1/m=_b,_tp:532 -[ 12416ms] [LOG] %c%s font-size: 18px; Using this console may allow attackers to impersonate you and steal your information using an attack called Self-XSS. -Do not enter or paste code that you don't understand. @ https://www.gstatic.com/_/mss/boq-identity/_/js/k=boq-identity.OAuthUi.en_GB.-QjSzSQ9aM8.es5.O/am=8gMAAAAA_wAGAP-__wYQRAfeM0ABAgAAAAAAAACAAQAgAg/d=1/excm=_b,_tp,attributesview/ed=1/dg=0/wt=2/ujg=1/rs=AOaEmlF7dHSRzi7i6avN91i_S56SHTkyWQ/dti=1/m=_b,_tp:532 -[ 13068ms] [WARNING] Blocked aria-hidden on an element because its descendant retained focus. The focus must not be hidden from assistive technology users. Avoid using aria-hidden on a focused element or its ancestor. Consider using the inert attribute instead, which will also prevent focus. For more details, see the aria-hidden section of the WAI-ARIA specification at https://w3c.github.io/aria/#aria-hidden. -Element with focus: -Ancestor with aria-hidden: @ https://accounts.google.com/signin/oauth/id?authuser=0&part=AJi8hAP2QL-uwioYXqGvF-eZZ8hjgEpnjKqembnXcoZa77oSPHPvvoIRHMECGFegZCEeKGt7SCRSklJi5YZafTQiLuStybSb69z9-KnbF7DuxmEi2atTdHz-67rK8PPmz8ydtkqi-4rm2IJMz79xnc7DLVTa7VKK16mdwPwiy-8ydnPOX8r9lsD8Bit7Z96gk8ED1it24apX9HrNYJpCMW2ZyrWfzAM8b0JlRBs5hYoX0SOOLfekPduPVtQtR7L3wWKG9V-jy3s7JCCLPnucYHSEaardBECPcx2r9dAICqia2gfOxcUKcFyIWUjPxL7WdRiVOAM8ni8m1zFpqZvztalE7fr_z8mOClAslwSoTQbTsQe2HM2NMdHOxqBh2YIpBzYfiB2sm_fZSn8JIrk5XXefaRdbdN-aBfkS27DJqsn8iJUpfk2FIlp02y3HHdTCzmKyttt-akvmOOVAI_eYb3ZEiHBmw0GBqwUtld-ZThM9GqLlJwJMTVWtb9-VWVPg3_DzbN1oB1T3QbpvPHVK7fcLOpcbDo-qUUXy52eR4nJ4TWJ-ER9DMQC1e2DBjKntmcmWgRaqyzqxxdxDjz1IvDUonXMzxs3j9QDGYmPG9T6AeYpirCJU3pg-dz3-AjiXmQafTv77OP6WyXwnhu-NcVi4yAveH2klHDP7X4Fm80ljBdIJ_06fyH_ArQM2YpmfPN74XYIUyiZautADOIYQMe-jt1VHl7m4zlWuJX9ybDveUjUFw_xB2CEuNVzA0nWuyK4kDzcQuAUP7lzkJs26cpd6xPZfQLV6TWBkoSRZJPOeUkuu1YKnEOPvV3Zvo4sZKv6PC26HK0DQcihbBFfbuxs34_ryMopaqldczWJqC1_hzfoALTaMzyUCcexHq77E3tANqs9vdheAnD7jX_OG54qTCxLjcKDtajfp2KUnc3EyKEJJi6Vzg5iXVLp37qE3NV28Mn4Rab8YtqRQ_tXWd58HE_yF6pNdwgBxUWFnLLsJkbbtDPP6lws&flowName=GeneralOAuthFlow&as=S-1726020052%3A1780389429869879&client_id=946018238758-bi6ni53dfoddlgn97pk3b8i7nphige40.apps.googleusercontent.com&rapt=AEjHL4MOcReAm2eyhkicwX74YBhw02GzMMj8n8SbH3YQqnCVn1HTBoQubAtd43DBA6GQtFhgLKHT8Khsnd2PY8NdhN4tRPty1FTHehGBmLESwYLeTnF3q3U#:0 diff --git a/.playwright-mcp/page-2026-06-02T05-35-47-870Z.yml b/.playwright-mcp/page-2026-06-02T05-35-47-870Z.yml deleted file mode 100644 index f9885ecf..00000000 --- a/.playwright-mcp/page-2026-06-02T05-35-47-870Z.yml +++ /dev/null @@ -1,348 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "Primary navigation" [ref=e3]: - - link "CellScript home" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link "Model" [ref=e8] [cursor=pointer]: - - /url: "#core-model" - - link "Assurance" [ref=e9] [cursor=pointer]: - - /url: "#assurance" - - link "Commands" [ref=e10] [cursor=pointer]: - - /url: "#tooling" - - link "Examples" [ref=e11] [cursor=pointer]: - - /url: "#examples" - - link "Source" [ref=e12] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - button "Switch to dark mode" [pressed] [ref=e13] [cursor=pointer]: - - generic [ref=e16]: Dark - - main [ref=e17]: - - region "CellScript" [ref=e18]: - - generic [ref=e19]: - - heading "CellScript" [level=1] [ref=e20] - - paragraph [ref=e21]: Write Cell contracts as typed transitions, not raw wire format. - - generic [ref=e22]: - - link "Get started" [ref=e23] [cursor=pointer]: - - /url: "#getting-started" - - link "Model fit" [ref=e24] [cursor=pointer]: - - /url: "#identity" - - generic "CellScript contract surface" [ref=e25]: - - generic [ref=e26]: - - term [ref=e27]: target - - definition [ref=e28]: ckb-vm RISC-V - - generic [ref=e29]: - - term [ref=e30]: model - - definition [ref=e31]: schema-backed Cells - - generic [ref=e32]: - - term [ref=e33]: output - - definition [ref=e34]: metadata + ProofPlan - - generic [ref=e35]: - - generic [ref=e37]: token.cell - - tablist "CellScript examples" [ref=e39]: - - tab "Fungible Token" [selected] [ref=e40] [cursor=pointer] - - tab "NFT" [ref=e41] [cursor=pointer] - - tab "AMM Pool" [ref=e42] [cursor=pointer] - - tab "Vesting" [ref=e43] [cursor=pointer] - - tabpanel "Fungible Token" [ref=e45]: - - generic [ref=e46]: - - generic [ref=e47]: "1" - - generic [ref=e48]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e49]: - - generic [ref=e50]: "2" - - generic [ref=e51]: "amount: u64," - - generic [ref=e52]: - - generic [ref=e53]: "3" - - generic [ref=e54]: "symbol: [u8; 8]," - - generic [ref=e55]: - - generic [ref=e56]: "4" - - generic [ref=e57]: "}" - - generic [ref=e59]: "5" - - generic [ref=e60]: - - generic [ref=e61]: "6" - - generic [ref=e62]: "action transfer_token(token: Token, to: Address)" - - generic [ref=e63]: - - generic [ref=e64]: "7" - - generic [ref=e65]: "-> next_token: Token" - - generic [ref=e66]: - - generic [ref=e67]: "8" - - generic [ref=e68]: where - - generic [ref=e69]: - - generic [ref=e70]: "9" - - generic [ref=e71]: consume token - - generic [ref=e72]: - - generic [ref=e73]: "10" - - generic [ref=e74]: "create next_token = Token {" - - generic [ref=e75]: - - generic [ref=e76]: "11" - - generic [ref=e77]: "amount: token.amount," - - generic [ref=e78]: - - generic [ref=e79]: "12" - - generic [ref=e80]: "symbol: token.symbol" - - generic [ref=e81]: - - generic [ref=e82]: "13" - - generic [ref=e83]: "} with_lock(to)" - - generic [ref=e85]: "14" - - generic [ref=e86]: - - generic [ref=e87]: "15" - - generic [ref=e88]: "action burn(token: Token)" - - generic [ref=e89]: - - generic [ref=e90]: $ - - generic [ref=e91]: cellc examples/token.cell --target-profile ckb - - region "Getting Started" [ref=e92]: - - heading "Getting Started" [level=2] [ref=e93] - - generic [ref=e94]: - - article [ref=e95]: - - generic [ref=e96]: "1" - - heading "Install" [level=3] [ref=e97] - - paragraph [ref=e98]: - - code [ref=e99]: cargo install --path . - - article [ref=e100]: - - generic [ref=e101]: "2" - - heading "Compile" [level=3] [ref=e102] - - paragraph [ref=e103]: - - code [ref=e104]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e105]: - - generic [ref=e106]: "3" - - heading "Check" [level=3] [ref=e107] - - paragraph [ref=e108]: - - code [ref=e109]: cellc check --target-profile ckb - - region "Compiler Workflow" [ref=e110]: - - heading "Compiler Workflow" [level=2] [ref=e111] - - generic [ref=e112]: - - generic "Compiler workflow from .cell source to CKB artefact" [ref=e113]: - - article [ref=e114]: - - img [ref=e116]: - - generic [ref=e119]: .cell - - heading "CellScript source" [level=3] [ref=e120] - - img [ref=e122] - - article [ref=e124]: - - img [ref=e126] - - heading "Parse & check" [level=3] [ref=e130] - - paragraph [ref=e131]: Syntax, types, effects - - img [ref=e133] - - article [ref=e135]: - - img [ref=e137] - - heading "IR + Metadata" [level=3] [ref=e143] - - paragraph [ref=e144]: Typed model & assurance info - - img [ref=e146] - - article [ref=e148]: - - img [ref=e150] - - heading "Lower to RISC-V" [level=3] [ref=e154] - - paragraph [ref=e155]: ckb-vm codegen & optimisations - - img [ref=e157] - - article [ref=e159]: - - img [ref=e161]: - - generic [ref=e164]: .elf - - heading "ELF / Assembly" [level=3] [ref=e165] - - paragraph [ref=e166]: RISC-V artefacts for ckb-vm - - complementary "Build for CKB" [ref=e167]: - - heading "Build for CKB" [level=3] [ref=e168] - - list [ref=e169]: - - listitem [ref=e170]: - - img [ref=e171] - - generic [ref=e173]: ckb-vm compatible - - listitem [ref=e174]: - - img [ref=e175] - - generic [ref=e177]: Deterministic execution - - listitem [ref=e178]: - - img [ref=e179] - - generic [ref=e181]: Minimal syscalls - - listitem [ref=e182]: - - img [ref=e183] - - generic [ref=e185]: Scheduler-aware - - region "Core Model" [ref=e186]: - - heading "Core Model" [level=2] [ref=e187] - - paragraph [ref=e188]: "CellScript keeps the contract model visible: Cell shapes, effects, locks, flows, and review metadata stay in one typed surface." - - paragraph [ref=e189]: "Narrow by design: not a general-purpose runtime, not a new VM, not account storage in disguise." - - generic [ref=e190]: - - tablist "CellScript core primitives" [ref=e191]: - - tab "resource" [selected] [ref=e192] [cursor=pointer]: - - generic [ref=e193]: resource - - tab "shared" [ref=e194] [cursor=pointer]: - - generic [ref=e195]: shared - - tab "receipt" [ref=e196] [cursor=pointer]: - - generic [ref=e197]: receipt - - tab "action" [ref=e198] [cursor=pointer]: - - generic [ref=e199]: action - - tab "lock" [ref=e200] [cursor=pointer]: - - generic [ref=e201]: lock - - tab "flow" [ref=e202] [cursor=pointer]: - - generic [ref=e203]: flow - - tab "invariant" [ref=e204] [cursor=pointer]: - - generic [ref=e205]: invariant - - tab "struct / enum" [ref=e206] [cursor=pointer]: - - generic [ref=e207]: struct / enum - - tab "identity" [ref=e208] [cursor=pointer]: - - generic [ref=e209]: identity - - tabpanel "resource" [ref=e211]: - - paragraph [ref=e213]: Owned Cell state with explicit lifecycle effects. - - generic [ref=e214]: - - generic [ref=e215]: - - generic [ref=e216]: Example excerpt - - generic [ref=e217]: examples/token.cell - - generic [ref=e218]: "resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance Output" [ref=e219]: - - generic [ref=e220]: - - heading "Assurance Output" [level=2] [ref=e221] - - paragraph [ref=e222]: A local build emits review metadata. Treat the sidecar as useful evidence, not an authenticated proof outside the build boundary. - - article [ref=e224]: - - generic "Assurance output summary" [ref=e225]: - - article [ref=e226]: - - text: Schema - - strong [ref=e227]: v42 - - paragraph [ref=e228]: current compiler metadata schema - - article [ref=e229]: - - text: Source - - strong [ref=e230]: vesting.cell - - paragraph [ref=e231]: shared + receipt + flow - - article [ref=e232]: - - text: Boundary - - strong [ref=e233]: local sidecar - - paragraph [ref=e234]: validated; provenance required when shared - - group [ref=e235]: - - generic "- Metadata excerpt" [ref=e236] [cursor=pointer] - - generic [ref=e237]: "# Representative sidecar excerpt from a local build; keep provenance checks separate." - - generic [ref=e238]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": [ { \"name\": \"VestingConfig\", \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, { \"name\": \"VestingGrant\", \"kind\": \"receipt\", \"capabilities\": [\"store\", \"create\", \"consume\"], \"flow_state_field\": \"state\", \"flow_transitions\": [ { \"from\": \"Granted\", \"to\": \"Claimable\" }, { \"from\": \"Claimable\", \"to\": \"FullyClaimed\" } ] } ], \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"limits\": [ \"sidecar is not authenticated\", \"aggregate invariants may be metadata-only\" ] } }" - - region "Model Fit" [ref=e239]: - - generic [ref=e240]: - - heading "Model Fit" [level=2] [ref=e241] - - paragraph [ref=e242]: "This is a positioning map, not a league table. CellScript is narrow on purpose: it speaks CKB Cells directly and leaves account-storage assumptions at the door." - - table [ref=e244]: - - rowgroup [ref=e245]: - - row "Axis CellScript Account contracts Move-family languages UTXO contract languages" [ref=e246]: - - columnheader "Axis" [ref=e247] - - columnheader "CellScript" [ref=e248] - - columnheader "Account contracts" [ref=e249] - - columnheader "Move-family languages" [ref=e250] - - columnheader "UTXO contract languages" [ref=e251] - - rowgroup [ref=e252]: - - row "Where state lives Named Cells with schema-backed data and explicit locks Contract-owned account storage VM-managed objects or resource values UTXO state plus contract logic" [ref=e253]: - - cell "Where state lives" [ref=e254] - - cell "Named Cells with schema-backed data and explicit locks" [ref=e255] - - cell "Contract-owned account storage" [ref=e256] - - cell "VM-managed objects or resource values" [ref=e257] - - cell "UTXO state plus contract logic" [ref=e258] - - row "What is checked Linear lifecycle, declared effects, typed field access, and transition shape ABI conventions, storage layout, external linters VM-enforced abilities and module rules Spend predicates and transaction validity" [ref=e259]: - - cell "What is checked" [ref=e260] - - cell "Linear lifecycle, declared effects, typed field access, and transition shape" [ref=e261] - - cell "ABI conventions, storage layout, external linters" [ref=e262] - - cell "VM-enforced abilities and module rules" [ref=e263] - - cell "Spend predicates and transaction validity" [ref=e264] - - row "What reviewers see metadata, constraints, ProofPlan, source hashes, access summary ABI, events, storage diff, runtime traces module bytecode plus VM safety properties Contract ABI plus transaction semantics" [ref=e265]: - - cell "What reviewers see" [ref=e266] - - cell "metadata, constraints, ProofPlan, source hashes, access summary" [ref=e267] - - cell "ABI, events, storage diff, runtime traces" [ref=e268] - - cell "module bytecode plus VM safety properties" [ref=e269] - - cell "Contract ABI plus transaction semantics" [ref=e270] - - row "What it refuses general-purpose runtime, new VM, or account-storage shim Cell-native state by default ckb-vm RISC-V target by default CellScript semantic metadata by default" [ref=e271]: - - cell "What it refuses" [ref=e272] - - cell "general-purpose runtime, new VM, or account-storage shim" [ref=e273] - - cell "Cell-native state by default" [ref=e274] - - cell "ckb-vm RISC-V target by default" [ref=e275] - - cell "CellScript semantic metadata by default" [ref=e276] - - row "Best fit CKB Cell transitions that need explicit effects and audit evidence Applications built around mutable account state Resource-centric ecosystems already running a Move VM UTXO apps whose compiler model is not Cell-specific" [ref=e277]: - - cell "Best fit" [ref=e278] - - cell "CKB Cell transitions that need explicit effects and audit evidence" [ref=e279] - - cell "Applications built around mutable account state" [ref=e280] - - cell "Resource-centric ecosystems already running a Move VM" [ref=e281] - - cell "UTXO apps whose compiler model is not Cell-specific" [ref=e282] - - region "Tooling Surface" [ref=e283]: - - heading "Tooling Surface" [level=2] [ref=e284] - - generic [ref=e285]: - - tablist "CellScript tooling commands" [ref=e286]: - - tab "cellc metadata Read/write surface" [selected] [ref=e287] [cursor=pointer]: - - code [ref=e288]: cellc metadata - - generic [ref=e289]: Read/write surface - - tab "cellc constraints Transaction shape" [ref=e290] [cursor=pointer]: - - code [ref=e291]: cellc constraints - - generic [ref=e292]: Transaction shape - - tab "cellc audit-bundle Reviewer packet" [ref=e293] [cursor=pointer]: - - code [ref=e294]: cellc audit-bundle - - generic [ref=e295]: Reviewer packet - - tab "cellc lsp Editor feedback" [ref=e296] [cursor=pointer]: - - code [ref=e297]: cellc lsp - - generic [ref=e298]: Editor feedback - - tabpanel "cellc metadata Read/write surface" [ref=e300]: - - generic [ref=e301]: - - generic [ref=e302]: When - - paragraph [ref=e303]: Use before review or integration. - - generic [ref=e304]: Output - - paragraph [ref=e305]: Schema, effects, source hashes, target profile. - - generic [ref=e306]: - - generic [ref=e307]: Run - - generic [ref=e308]: "# Emit review metadata for a real example." - - code [ref=e309]: cellc metadata examples/vesting.cell --target-profile ckb --json - - region "Examples" [ref=e310]: - - heading "Examples" [level=2] [ref=e311] - - generic [ref=e312]: - - link "token.cell Mint, transfer, burn, and typed metadata. resource consume/create burn" [ref=e313] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - heading "token.cell" [level=3] [ref=e314]: - - code [ref=e315]: token.cell - - paragraph [ref=e316]: Mint, transfer, burn, and typed metadata. - - generic [ref=e317]: - - generic [ref=e318]: resource - - generic [ref=e319]: consume/create - - generic [ref=e320]: burn - - link "nft.cell Ownership transfer with preserve and relock. resource preserve relock" [ref=e321] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - heading "nft.cell" [level=3] [ref=e322]: - - code [ref=e323]: nft.cell - - paragraph [ref=e324]: Ownership transfer with preserve and relock. - - generic [ref=e325]: - - generic [ref=e326]: resource - - generic [ref=e327]: preserve - - generic [ref=e328]: relock - - link "amm_pool.cell Shared reserves with slippage checks. shared replace slippage" [ref=e329] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - heading "amm_pool.cell" [level=3] [ref=e330]: - - code [ref=e331]: amm_pool.cell - - paragraph [ref=e332]: Shared reserves with slippage checks. - - generic [ref=e333]: - - generic [ref=e334]: shared - - generic [ref=e335]: replace - - generic [ref=e336]: slippage - - link "vesting.cell Grant state flow into claimed output. flow transition receipt" [ref=e337] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - heading "vesting.cell" [level=3] [ref=e338]: - - code [ref=e339]: vesting.cell - - paragraph [ref=e340]: Grant state flow into claimed output. - - generic [ref=e341]: - - generic [ref=e342]: flow - - generic [ref=e343]: transition - - generic [ref=e344]: receipt - - link "multisig.cell Witness checks for threshold-style locks. lock witness threshold" [ref=e345] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/multisig.cell - - heading "multisig.cell" [level=3] [ref=e346]: - - code [ref=e347]: multisig.cell - - paragraph [ref=e348]: Witness checks for threshold-style locks. - - generic [ref=e349]: - - generic [ref=e350]: lock - - generic [ref=e351]: witness - - generic [ref=e352]: threshold - - link "timelock.cell Time-bound spending from Cell state. lock env timepoint" [ref=e353] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/timelock.cell - - heading "timelock.cell" [level=3] [ref=e354]: - - code [ref=e355]: timelock.cell - - paragraph [ref=e356]: Time-bound spending from Cell state. - - generic [ref=e357]: - - generic [ref=e358]: lock - - generic [ref=e359]: env - - generic [ref=e360]: timepoint - - contentinfo [ref=e361]: - - generic [ref=e362]: - - generic [ref=e363]: - - link "CellScript" [ref=e364] [cursor=pointer]: - - /url: "#top" - - generic [ref=e366]: CellScript - - paragraph [ref=e367]: A semantic DSL for CKB Cell-based smart contracts, with typed metadata and assurance by design. Docs, spec, examples, and source live with the repository. - - generic [ref=e368]: - - link "Docs" [ref=e369] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "Spec" [ref=e370] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "Examples" [ref=e371] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "Source" [ref=e372] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-02T05-37-28-163Z.yml b/.playwright-mcp/page-2026-06-02T05-37-28-163Z.yml deleted file mode 100644 index d662ab1a..00000000 --- a/.playwright-mcp/page-2026-06-02T05-37-28-163Z.yml +++ /dev/null @@ -1,348 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "Primary navigation" [ref=e3]: - - link "CellScript home" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link "Model" [ref=e8] [cursor=pointer]: - - /url: "#core-model" - - link "Assurance" [ref=e9] [cursor=pointer]: - - /url: "#assurance" - - link "Commands" [ref=e10] [cursor=pointer]: - - /url: "#tooling" - - link "Examples" [ref=e11] [cursor=pointer]: - - /url: "#examples" - - link "Source" [ref=e12] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - button "Switch to dark mode" [pressed] [ref=e13] [cursor=pointer]: - - generic [ref=e16]: Dark - - main [ref=e17]: - - region "CellScript" [ref=e18]: - - generic [ref=e19]: - - heading "CellScript" [level=1] [ref=e20] - - paragraph [ref=e21]: Write Cell contracts as typed transitions, not raw wire format. - - generic [ref=e22]: - - link "Get started" [ref=e23] [cursor=pointer]: - - /url: "#getting-started" - - link "Model fit" [ref=e24] [cursor=pointer]: - - /url: "#identity" - - generic "CellScript contract surface" [ref=e25]: - - generic [ref=e26]: - - term [ref=e27]: target - - definition [ref=e28]: ckb-vm RISC-V - - generic [ref=e29]: - - term [ref=e30]: model - - definition [ref=e31]: schema-backed Cells - - generic [ref=e32]: - - term [ref=e33]: output - - definition [ref=e34]: metadata + ProofPlan - - generic [ref=e35]: - - generic [ref=e37]: token.cell - - tablist "CellScript examples" [ref=e39]: - - tab "Fungible Token" [selected] [ref=e40] [cursor=pointer] - - tab "NFT" [ref=e41] [cursor=pointer] - - tab "AMM Pool" [ref=e42] [cursor=pointer] - - tab "Vesting" [ref=e43] [cursor=pointer] - - tabpanel "Fungible Token" [ref=e45]: - - generic [ref=e46]: - - generic [ref=e47]: "1" - - generic [ref=e48]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e49]: - - generic [ref=e50]: "2" - - generic [ref=e51]: "amount: u64," - - generic [ref=e52]: - - generic [ref=e53]: "3" - - generic [ref=e54]: "symbol: [u8; 8]," - - generic [ref=e55]: - - generic [ref=e56]: "4" - - generic [ref=e57]: "}" - - generic [ref=e59]: "5" - - generic [ref=e60]: - - generic [ref=e61]: "6" - - generic [ref=e62]: "action transfer_token(token: Token, to: Address)" - - generic [ref=e63]: - - generic [ref=e64]: "7" - - generic [ref=e65]: "-> next_token: Token" - - generic [ref=e66]: - - generic [ref=e67]: "8" - - generic [ref=e68]: where - - generic [ref=e69]: - - generic [ref=e70]: "9" - - generic [ref=e71]: consume token - - generic [ref=e72]: - - generic [ref=e73]: "10" - - generic [ref=e74]: "create next_token = Token {" - - generic [ref=e75]: - - generic [ref=e76]: "11" - - generic [ref=e77]: "amount: token.amount," - - generic [ref=e78]: - - generic [ref=e79]: "12" - - generic [ref=e80]: "symbol: token.symbol" - - generic [ref=e81]: - - generic [ref=e82]: "13" - - generic [ref=e83]: "} with_lock(to)" - - generic [ref=e85]: "14" - - generic [ref=e86]: - - generic [ref=e87]: "15" - - generic [ref=e88]: "action burn(token: Token)" - - generic [ref=e89]: - - generic [ref=e90]: $ - - generic [ref=e91]: cellc examples/token.cell --target-profile ckb - - region "Getting Started" [ref=e92]: - - heading "Getting Started" [level=2] [ref=e93] - - generic [ref=e94]: - - article [ref=e95]: - - generic [ref=e96]: "1" - - heading "Install" [level=3] [ref=e97] - - paragraph [ref=e98]: - - code [ref=e99]: cargo install --path . - - article [ref=e100]: - - generic [ref=e101]: "2" - - heading "Compile" [level=3] [ref=e102] - - paragraph [ref=e103]: - - code [ref=e104]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e105]: - - generic [ref=e106]: "3" - - heading "Check" [level=3] [ref=e107] - - paragraph [ref=e108]: - - code [ref=e109]: cellc check --target-profile ckb - - region "Compiler Workflow" [ref=e110]: - - heading "Compiler Workflow" [level=2] [ref=e111] - - generic [ref=e112]: - - generic "Compiler workflow from .cell source to CKB artefact" [ref=e113]: - - article [ref=e114]: - - img [ref=e116]: - - generic [ref=e119]: .cell - - heading "CellScript source" [level=3] [ref=e120] - - img [ref=e122] - - article [ref=e124]: - - img [ref=e126] - - heading "Parse & check" [level=3] [ref=e130] - - paragraph [ref=e131]: Syntax, types, effects - - img [ref=e133] - - article [ref=e135]: - - img [ref=e137] - - heading "IR + Metadata" [level=3] [ref=e143] - - paragraph [ref=e144]: Typed model & assurance info - - img [ref=e146] - - article [ref=e148]: - - img [ref=e150] - - heading "Lower to RISC-V" [level=3] [ref=e154] - - paragraph [ref=e155]: ckb-vm codegen & optimisations - - img [ref=e157] - - article [ref=e159]: - - img [ref=e161]: - - generic [ref=e164]: .elf - - heading "ELF / Assembly" [level=3] [ref=e165] - - paragraph [ref=e166]: RISC-V artefacts for ckb-vm - - complementary "Build for CKB" [ref=e167]: - - heading "Build for CKB" [level=3] [ref=e168] - - list [ref=e169]: - - listitem [ref=e170]: - - img [ref=e171] - - generic [ref=e173]: ckb-vm compatible - - listitem [ref=e174]: - - img [ref=e175] - - generic [ref=e177]: Deterministic execution - - listitem [ref=e178]: - - img [ref=e179] - - generic [ref=e181]: Minimal syscalls - - listitem [ref=e182]: - - img [ref=e183] - - generic [ref=e185]: Scheduler-aware - - region "Core Model" [ref=e186]: - - heading "Core Model" [level=2] [ref=e187] - - paragraph [ref=e188]: "CellScript keeps the contract model visible: Cell shapes, effects, locks, flows, and review metadata stay in one typed surface." - - paragraph [ref=e189]: "Narrow by design: not a general-purpose runtime, not a new VM, not account storage in disguise." - - generic [ref=e190]: - - tablist "CellScript core primitives" [ref=e191]: - - tab "resource" [selected] [ref=e192] [cursor=pointer]: - - generic [ref=e193]: resource - - tab "shared" [ref=e194] [cursor=pointer]: - - generic [ref=e195]: shared - - tab "receipt" [ref=e196] [cursor=pointer]: - - generic [ref=e197]: receipt - - tab "action" [ref=e198] [cursor=pointer]: - - generic [ref=e199]: action - - tab "lock" [ref=e200] [cursor=pointer]: - - generic [ref=e201]: lock - - tab "flow" [ref=e202] [cursor=pointer]: - - generic [ref=e203]: flow - - tab "invariant" [ref=e204] [cursor=pointer]: - - generic [ref=e205]: invariant - - tab "struct / enum" [ref=e206] [cursor=pointer]: - - generic [ref=e207]: struct / enum - - tab "identity" [ref=e208] [cursor=pointer]: - - generic [ref=e209]: identity - - tabpanel "resource" [ref=e211]: - - paragraph [ref=e213]: Owned Cell state with explicit lifecycle effects. - - generic [ref=e214]: - - generic [ref=e215]: - - generic [ref=e216]: Example excerpt - - generic [ref=e217]: examples/token.cell - - generic [ref=e218]: "resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance Output" [ref=e219]: - - generic [ref=e220]: - - heading "Assurance Output" [level=2] [ref=e221] - - paragraph [ref=e222]: A local build emits review metadata. Treat the sidecar as useful evidence, not an authenticated proof outside the build boundary. - - article [ref=e224]: - - generic "Assurance output summary" [ref=e225]: - - article [ref=e226]: - - text: Schema - - strong [ref=e227]: v42 - - paragraph [ref=e228]: current compiler metadata schema - - article [ref=e229]: - - text: Source - - strong [ref=e230]: vesting.cell - - paragraph [ref=e231]: shared + receipt + flow - - article [ref=e232]: - - text: Boundary - - strong [ref=e233]: local sidecar - - paragraph [ref=e234]: validated; provenance required when shared - - group [ref=e235]: - - generic "- Metadata excerpt" [ref=e236] [cursor=pointer] - - generic [ref=e237]: "# Representative sidecar excerpt from a local build; keep provenance checks separate." - - generic [ref=e238]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": { \"VestingConfig\": { \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, \"VestingGrant\": { \"kind\": \"receipt\", \"flow\": \"Granted -> Claimable -> FullyClaimed\" } }, \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"status\": \"review evidence\", \"limit\": \"sidecar provenance must be checked outside the compiler\" } }" - - region "Model Fit" [ref=e239]: - - generic [ref=e240]: - - heading "Model Fit" [level=2] [ref=e241] - - paragraph [ref=e242]: "This is a positioning map, not a league table. CellScript is narrow on purpose: it speaks CKB Cells directly and leaves account-storage assumptions at the door." - - table [ref=e244]: - - rowgroup [ref=e245]: - - row "Axis CellScript Account contracts Move-family languages UTXO contract languages" [ref=e246]: - - columnheader "Axis" [ref=e247] - - columnheader "CellScript" [ref=e248] - - columnheader "Account contracts" [ref=e249] - - columnheader "Move-family languages" [ref=e250] - - columnheader "UTXO contract languages" [ref=e251] - - rowgroup [ref=e252]: - - row "Where state lives Named Cells with schema-backed data and explicit locks Contract-owned account storage VM-managed objects or resource values UTXO state plus contract logic" [ref=e253]: - - cell "Where state lives" [ref=e254] - - cell "Named Cells with schema-backed data and explicit locks" [ref=e255] - - cell "Contract-owned account storage" [ref=e256] - - cell "VM-managed objects or resource values" [ref=e257] - - cell "UTXO state plus contract logic" [ref=e258] - - row "What is checked Linear lifecycle, declared effects, typed field access, and transition shape ABI conventions, storage layout, external linters VM-enforced abilities and module rules Spend predicates and transaction validity" [ref=e259]: - - cell "What is checked" [ref=e260] - - cell "Linear lifecycle, declared effects, typed field access, and transition shape" [ref=e261] - - cell "ABI conventions, storage layout, external linters" [ref=e262] - - cell "VM-enforced abilities and module rules" [ref=e263] - - cell "Spend predicates and transaction validity" [ref=e264] - - row "What reviewers see metadata, constraints, ProofPlan, source hashes, access summary ABI, events, storage diff, runtime traces module bytecode plus VM safety properties Contract ABI plus transaction semantics" [ref=e265]: - - cell "What reviewers see" [ref=e266] - - cell "metadata, constraints, ProofPlan, source hashes, access summary" [ref=e267] - - cell "ABI, events, storage diff, runtime traces" [ref=e268] - - cell "module bytecode plus VM safety properties" [ref=e269] - - cell "Contract ABI plus transaction semantics" [ref=e270] - - row "What it refuses general-purpose runtime, new VM, or account-storage shim Cell-native state by default ckb-vm RISC-V target by default CellScript semantic metadata by default" [ref=e271]: - - cell "What it refuses" [ref=e272] - - cell "general-purpose runtime, new VM, or account-storage shim" [ref=e273] - - cell "Cell-native state by default" [ref=e274] - - cell "ckb-vm RISC-V target by default" [ref=e275] - - cell "CellScript semantic metadata by default" [ref=e276] - - row "Best fit CKB Cell transitions that need explicit effects and audit evidence Applications built around mutable account state Resource-centric ecosystems already running a Move VM UTXO apps whose compiler model is not Cell-specific" [ref=e277]: - - cell "Best fit" [ref=e278] - - cell "CKB Cell transitions that need explicit effects and audit evidence" [ref=e279] - - cell "Applications built around mutable account state" [ref=e280] - - cell "Resource-centric ecosystems already running a Move VM" [ref=e281] - - cell "UTXO apps whose compiler model is not Cell-specific" [ref=e282] - - region "Tooling Surface" [ref=e283]: - - heading "Tooling Surface" [level=2] [ref=e284] - - generic [ref=e285]: - - tablist "CellScript tooling commands" [ref=e286]: - - tab "cellc metadata Read/write surface" [selected] [ref=e287] [cursor=pointer]: - - code [ref=e288]: cellc metadata - - generic [ref=e289]: Read/write surface - - tab "cellc constraints Transaction shape" [ref=e290] [cursor=pointer]: - - code [ref=e291]: cellc constraints - - generic [ref=e292]: Transaction shape - - tab "cellc audit-bundle Reviewer packet" [ref=e293] [cursor=pointer]: - - code [ref=e294]: cellc audit-bundle - - generic [ref=e295]: Reviewer packet - - tab "cellc lsp Editor feedback" [ref=e296] [cursor=pointer]: - - code [ref=e297]: cellc lsp - - generic [ref=e298]: Editor feedback - - tabpanel "cellc metadata Read/write surface" [ref=e300]: - - generic [ref=e301]: - - generic [ref=e302]: When - - paragraph [ref=e303]: Use before review or integration. - - generic [ref=e304]: Output - - paragraph [ref=e305]: Schema, effects, source hashes, target profile. - - generic [ref=e306]: - - generic [ref=e307]: Run - - generic [ref=e308]: "# Emit review metadata for a real example." - - code [ref=e309]: cellc metadata examples/vesting.cell --target-profile ckb --json - - region "Examples" [ref=e310]: - - heading "Examples" [level=2] [ref=e311] - - generic [ref=e312]: - - link "token.cell Mint, transfer, burn, and typed metadata. resource consume/create burn" [ref=e313] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - heading "token.cell" [level=3] [ref=e314]: - - code [ref=e315]: token.cell - - paragraph [ref=e316]: Mint, transfer, burn, and typed metadata. - - generic [ref=e317]: - - generic [ref=e318]: resource - - generic [ref=e319]: consume/create - - generic [ref=e320]: burn - - link "nft.cell Ownership transfer with preserve and relock. resource preserve relock" [ref=e321] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - heading "nft.cell" [level=3] [ref=e322]: - - code [ref=e323]: nft.cell - - paragraph [ref=e324]: Ownership transfer with preserve and relock. - - generic [ref=e325]: - - generic [ref=e326]: resource - - generic [ref=e327]: preserve - - generic [ref=e328]: relock - - link "amm_pool.cell Shared reserves with slippage checks. shared replace slippage" [ref=e329] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - heading "amm_pool.cell" [level=3] [ref=e330]: - - code [ref=e331]: amm_pool.cell - - paragraph [ref=e332]: Shared reserves with slippage checks. - - generic [ref=e333]: - - generic [ref=e334]: shared - - generic [ref=e335]: replace - - generic [ref=e336]: slippage - - link "vesting.cell Grant state flow into claimed output. flow transition receipt" [ref=e337] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - heading "vesting.cell" [level=3] [ref=e338]: - - code [ref=e339]: vesting.cell - - paragraph [ref=e340]: Grant state flow into claimed output. - - generic [ref=e341]: - - generic [ref=e342]: flow - - generic [ref=e343]: transition - - generic [ref=e344]: receipt - - link "multisig.cell Witness checks for threshold-style locks. lock witness threshold" [ref=e345] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/multisig.cell - - heading "multisig.cell" [level=3] [ref=e346]: - - code [ref=e347]: multisig.cell - - paragraph [ref=e348]: Witness checks for threshold-style locks. - - generic [ref=e349]: - - generic [ref=e350]: lock - - generic [ref=e351]: witness - - generic [ref=e352]: threshold - - link "timelock.cell Time-bound spending from Cell state. lock env timepoint" [ref=e353] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/timelock.cell - - heading "timelock.cell" [level=3] [ref=e354]: - - code [ref=e355]: timelock.cell - - paragraph [ref=e356]: Time-bound spending from Cell state. - - generic [ref=e357]: - - generic [ref=e358]: lock - - generic [ref=e359]: env - - generic [ref=e360]: timepoint - - contentinfo [ref=e361]: - - generic [ref=e362]: - - generic [ref=e363]: - - link "CellScript" [ref=e364] [cursor=pointer]: - - /url: "#top" - - generic [ref=e366]: CellScript - - paragraph [ref=e367]: A semantic DSL for CKB Cell-based smart contracts, with typed metadata and assurance by design. Docs, spec, examples, and source live with the repository. - - generic [ref=e368]: - - link "Docs" [ref=e369] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "Spec" [ref=e370] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "Examples" [ref=e371] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "Source" [ref=e372] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-02T05-38-32-816Z.yml b/.playwright-mcp/page-2026-06-02T05-38-32-816Z.yml deleted file mode 100644 index 52c14c48..00000000 --- a/.playwright-mcp/page-2026-06-02T05-38-32-816Z.yml +++ /dev/null @@ -1,339 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "Primary navigation" [ref=e3]: - - link "CellScript home" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link "Source" [ref=e8] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - button "Switch to dark mode" [pressed] [ref=e9] [cursor=pointer] - - main [ref=e12]: - - region "CellScript" [ref=e13]: - - generic [ref=e14]: - - heading "CellScript" [level=1] [ref=e15] - - paragraph [ref=e16]: Write Cell contracts as typed transitions, not raw wire format. - - generic [ref=e17]: - - link "Get started" [ref=e18] [cursor=pointer]: - - /url: "#getting-started" - - link "Model fit" [ref=e19] [cursor=pointer]: - - /url: "#identity" - - generic "CellScript contract surface" [ref=e20]: - - generic [ref=e21]: - - term [ref=e22]: target - - definition [ref=e23]: ckb-vm RISC-V - - generic [ref=e24]: - - term [ref=e25]: model - - definition [ref=e26]: schema-backed Cells - - generic [ref=e27]: - - term [ref=e28]: output - - definition [ref=e29]: metadata + ProofPlan - - generic [ref=e30]: - - generic [ref=e32]: token.cell - - combobox "Choose CellScript example" [ref=e34]: - - option "Fungible Token" [selected] - - option "NFT" - - option "AMM Pool" - - option "Vesting" - - tabpanel "Fungible Token" [ref=e36]: - - generic [ref=e37]: - - generic [ref=e38]: "1" - - generic [ref=e39]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e40]: - - generic [ref=e41]: "2" - - generic [ref=e42]: "amount: u64," - - generic [ref=e43]: - - generic [ref=e44]: "3" - - generic [ref=e45]: "symbol: [u8; 8]," - - generic [ref=e46]: - - generic [ref=e47]: "4" - - generic [ref=e48]: "}" - - generic [ref=e50]: "5" - - generic [ref=e51]: - - generic [ref=e52]: "6" - - generic [ref=e53]: "action transfer_token(token: Token, to: Address)" - - generic [ref=e54]: - - generic [ref=e55]: "7" - - generic [ref=e56]: "-> next_token: Token" - - generic [ref=e57]: - - generic [ref=e58]: "8" - - generic [ref=e59]: where - - generic [ref=e60]: - - generic [ref=e61]: "9" - - generic [ref=e62]: consume token - - generic [ref=e63]: - - generic [ref=e64]: "10" - - generic [ref=e65]: "create next_token = Token {" - - generic [ref=e66]: - - generic [ref=e67]: "11" - - generic [ref=e68]: "amount: token.amount," - - generic [ref=e69]: - - generic [ref=e70]: "12" - - generic [ref=e71]: "symbol: token.symbol" - - generic [ref=e72]: - - generic [ref=e73]: "13" - - generic [ref=e74]: "} with_lock(to)" - - generic [ref=e76]: "14" - - generic [ref=e77]: - - generic [ref=e78]: "15" - - generic [ref=e79]: "action burn(token: Token)" - - generic [ref=e80]: - - generic [ref=e81]: $ - - generic [ref=e82]: cellc examples/token.cell --target-profile ckb - - region "Getting Started" [ref=e83]: - - heading "Getting Started" [level=2] [ref=e84] - - generic [ref=e85]: - - article [ref=e86]: - - generic [ref=e87]: "1" - - heading "Install" [level=3] [ref=e88] - - paragraph [ref=e89]: - - code [ref=e90]: cargo install --path . - - article [ref=e91]: - - generic [ref=e92]: "2" - - heading "Compile" [level=3] [ref=e93] - - paragraph [ref=e94]: - - code [ref=e95]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e96]: - - generic [ref=e97]: "3" - - heading "Check" [level=3] [ref=e98] - - paragraph [ref=e99]: - - code [ref=e100]: cellc check --target-profile ckb - - region "Compiler Workflow" [ref=e101]: - - heading "Compiler Workflow" [level=2] [ref=e102] - - generic [ref=e103]: - - generic "Compiler workflow from .cell source to CKB artefact" [ref=e104]: - - article [ref=e105]: - - img [ref=e107]: - - generic [ref=e110]: .cell - - heading "CellScript source" [level=3] [ref=e111] - - img [ref=e113] - - article [ref=e115]: - - img [ref=e117] - - heading "Parse & check" [level=3] [ref=e121] - - paragraph [ref=e122]: Syntax, types, effects - - img [ref=e124] - - article [ref=e126]: - - img [ref=e128] - - heading "IR + Metadata" [level=3] [ref=e134] - - paragraph [ref=e135]: Typed model & assurance info - - img [ref=e137] - - article [ref=e139]: - - img [ref=e141] - - heading "Lower to RISC-V" [level=3] [ref=e145] - - paragraph [ref=e146]: ckb-vm codegen & optimisations - - img [ref=e148] - - article [ref=e150]: - - img [ref=e152]: - - generic [ref=e155]: .elf - - heading "ELF / Assembly" [level=3] [ref=e156] - - paragraph [ref=e157]: RISC-V artefacts for ckb-vm - - complementary "Build for CKB" [ref=e158]: - - heading "Build for CKB" [level=3] [ref=e159] - - list [ref=e160]: - - listitem [ref=e161]: - - img [ref=e162] - - generic [ref=e164]: ckb-vm compatible - - listitem [ref=e165]: - - img [ref=e166] - - generic [ref=e168]: Deterministic execution - - listitem [ref=e169]: - - img [ref=e170] - - generic [ref=e172]: Minimal syscalls - - listitem [ref=e173]: - - img [ref=e174] - - generic [ref=e176]: Scheduler-aware - - region "Core Model" [ref=e177]: - - heading "Core Model" [level=2] [ref=e178] - - paragraph [ref=e179]: "CellScript keeps the contract model visible: Cell shapes, effects, locks, flows, and review metadata stay in one typed surface." - - paragraph [ref=e180]: "Narrow by design: not a general-purpose runtime, not a new VM, not account storage in disguise." - - generic [ref=e181]: - - tablist "CellScript core primitives" [ref=e182]: - - tab "resource" [selected] [ref=e183] [cursor=pointer]: - - generic [ref=e184]: resource - - tab "shared" [ref=e185] [cursor=pointer]: - - generic [ref=e186]: shared - - tab "receipt" [ref=e187] [cursor=pointer]: - - generic [ref=e188]: receipt - - tab "action" [ref=e189] [cursor=pointer]: - - generic [ref=e190]: action - - tab "lock" [ref=e191] [cursor=pointer]: - - generic [ref=e192]: lock - - tab "flow" [ref=e193] [cursor=pointer]: - - generic [ref=e194]: flow - - tab "invariant" [ref=e195] [cursor=pointer]: - - generic [ref=e196]: invariant - - tab "struct / enum" [ref=e197] [cursor=pointer]: - - generic [ref=e198]: struct / enum - - tab "identity" [ref=e199] [cursor=pointer]: - - generic [ref=e200]: identity - - tabpanel "resource" [ref=e202]: - - paragraph [ref=e204]: Owned Cell state with explicit lifecycle effects. - - generic [ref=e205]: - - generic [ref=e206]: - - generic [ref=e207]: Example excerpt - - generic [ref=e208]: examples/token.cell - - generic [ref=e209]: "resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance Output" [ref=e210]: - - generic [ref=e211]: - - heading "Assurance Output" [level=2] [ref=e212] - - paragraph [ref=e213]: A local build emits review metadata. Treat the sidecar as useful evidence, not an authenticated proof outside the build boundary. - - article [ref=e215]: - - generic "Assurance output summary" [ref=e216]: - - article [ref=e217]: - - text: Schema - - strong [ref=e218]: v42 - - paragraph [ref=e219]: current compiler metadata schema - - article [ref=e220]: - - text: Source - - strong [ref=e221]: vesting.cell - - paragraph [ref=e222]: shared + receipt + flow - - article [ref=e223]: - - text: Boundary - - strong [ref=e224]: local sidecar - - paragraph [ref=e225]: validated; provenance required when shared - - group [ref=e226]: - - generic "- Metadata excerpt" [ref=e227] [cursor=pointer] - - generic [ref=e228]: "# Representative sidecar excerpt from a local build; keep provenance checks separate." - - generic [ref=e229]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": { \"VestingConfig\": { \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, \"VestingGrant\": { \"kind\": \"receipt\", \"flow\": \"Granted -> Claimable -> FullyClaimed\" } }, \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"status\": \"review evidence\", \"limit\": \"sidecar provenance must be checked outside the compiler\" } }" - - region "Model Fit" [ref=e230]: - - generic [ref=e231]: - - heading "Model Fit" [level=2] [ref=e232] - - paragraph [ref=e233]: "This is a positioning map, not a league table. CellScript is narrow on purpose: it speaks CKB Cells directly and leaves account-storage assumptions at the door." - - table [ref=e235]: - - rowgroup [ref=e236]: - - row "Axis CellScript Account contracts Move-family languages UTXO contract languages" [ref=e237]: - - columnheader "Axis" [ref=e238] - - columnheader "CellScript" [ref=e239] - - columnheader "Account contracts" [ref=e240] - - columnheader "Move-family languages" [ref=e241] - - columnheader "UTXO contract languages" [ref=e242] - - rowgroup [ref=e243]: - - row "Where state lives Named Cells with schema-backed data and explicit locks Contract-owned account storage VM-managed objects or resource values UTXO state plus contract logic" [ref=e244]: - - cell "Where state lives" [ref=e245] - - cell "Named Cells with schema-backed data and explicit locks" [ref=e246] - - cell "Contract-owned account storage" [ref=e247] - - cell "VM-managed objects or resource values" [ref=e248] - - cell "UTXO state plus contract logic" [ref=e249] - - row "What is checked Linear lifecycle, declared effects, typed field access, and transition shape ABI conventions, storage layout, external linters VM-enforced abilities and module rules Spend predicates and transaction validity" [ref=e250]: - - cell "What is checked" [ref=e251] - - cell "Linear lifecycle, declared effects, typed field access, and transition shape" [ref=e252] - - cell "ABI conventions, storage layout, external linters" [ref=e253] - - cell "VM-enforced abilities and module rules" [ref=e254] - - cell "Spend predicates and transaction validity" [ref=e255] - - row "What reviewers see metadata, constraints, ProofPlan, source hashes, access summary ABI, events, storage diff, runtime traces module bytecode plus VM safety properties Contract ABI plus transaction semantics" [ref=e256]: - - cell "What reviewers see" [ref=e257] - - cell "metadata, constraints, ProofPlan, source hashes, access summary" [ref=e258] - - cell "ABI, events, storage diff, runtime traces" [ref=e259] - - cell "module bytecode plus VM safety properties" [ref=e260] - - cell "Contract ABI plus transaction semantics" [ref=e261] - - row "What it refuses general-purpose runtime, new VM, or account-storage shim Cell-native state by default ckb-vm RISC-V target by default CellScript semantic metadata by default" [ref=e262]: - - cell "What it refuses" [ref=e263] - - cell "general-purpose runtime, new VM, or account-storage shim" [ref=e264] - - cell "Cell-native state by default" [ref=e265] - - cell "ckb-vm RISC-V target by default" [ref=e266] - - cell "CellScript semantic metadata by default" [ref=e267] - - row "Best fit CKB Cell transitions that need explicit effects and audit evidence Applications built around mutable account state Resource-centric ecosystems already running a Move VM UTXO apps whose compiler model is not Cell-specific" [ref=e268]: - - cell "Best fit" [ref=e269] - - cell "CKB Cell transitions that need explicit effects and audit evidence" [ref=e270] - - cell "Applications built around mutable account state" [ref=e271] - - cell "Resource-centric ecosystems already running a Move VM" [ref=e272] - - cell "UTXO apps whose compiler model is not Cell-specific" [ref=e273] - - region "Tooling Surface" [ref=e274]: - - heading "Tooling Surface" [level=2] [ref=e275] - - generic [ref=e276]: - - tablist "CellScript tooling commands" [ref=e277]: - - tab "cellc metadata Read/write surface" [selected] [ref=e278] [cursor=pointer]: - - code [ref=e279]: cellc metadata - - generic [ref=e280]: Read/write surface - - tab "cellc constraints Transaction shape" [ref=e281] [cursor=pointer]: - - code [ref=e282]: cellc constraints - - generic [ref=e283]: Transaction shape - - tab "cellc audit-bundle Reviewer packet" [ref=e284] [cursor=pointer]: - - code [ref=e285]: cellc audit-bundle - - generic [ref=e286]: Reviewer packet - - tab "cellc lsp Editor feedback" [ref=e287] [cursor=pointer]: - - code [ref=e288]: cellc lsp - - generic [ref=e289]: Editor feedback - - tabpanel "cellc metadata Read/write surface" [ref=e291]: - - generic [ref=e292]: - - generic [ref=e293]: When - - paragraph [ref=e294]: Use before review or integration. - - generic [ref=e295]: Output - - paragraph [ref=e296]: Schema, effects, source hashes, target profile. - - generic [ref=e297]: - - generic [ref=e298]: Run - - generic [ref=e299]: "# Emit review metadata for a real example." - - code [ref=e300]: cellc metadata examples/vesting.cell --target-profile ckb --json - - region "Examples" [ref=e301]: - - heading "Examples" [level=2] [ref=e302] - - generic [ref=e303]: - - link "token.cell Mint, transfer, burn, and typed metadata. resource consume/create burn" [ref=e304] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - heading "token.cell" [level=3] [ref=e305]: - - code [ref=e306]: token.cell - - paragraph [ref=e307]: Mint, transfer, burn, and typed metadata. - - generic [ref=e308]: - - generic [ref=e309]: resource - - generic [ref=e310]: consume/create - - generic [ref=e311]: burn - - link "nft.cell Ownership transfer with preserve and relock. resource preserve relock" [ref=e312] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - heading "nft.cell" [level=3] [ref=e313]: - - code [ref=e314]: nft.cell - - paragraph [ref=e315]: Ownership transfer with preserve and relock. - - generic [ref=e316]: - - generic [ref=e317]: resource - - generic [ref=e318]: preserve - - generic [ref=e319]: relock - - link "amm_pool.cell Shared reserves with slippage checks. shared replace slippage" [ref=e320] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - heading "amm_pool.cell" [level=3] [ref=e321]: - - code [ref=e322]: amm_pool.cell - - paragraph [ref=e323]: Shared reserves with slippage checks. - - generic [ref=e324]: - - generic [ref=e325]: shared - - generic [ref=e326]: replace - - generic [ref=e327]: slippage - - link "vesting.cell Grant state flow into claimed output. flow transition receipt" [ref=e328] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - heading "vesting.cell" [level=3] [ref=e329]: - - code [ref=e330]: vesting.cell - - paragraph [ref=e331]: Grant state flow into claimed output. - - generic [ref=e332]: - - generic [ref=e333]: flow - - generic [ref=e334]: transition - - generic [ref=e335]: receipt - - link "multisig.cell Witness checks for threshold-style locks. lock witness threshold" [ref=e336] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/multisig.cell - - heading "multisig.cell" [level=3] [ref=e337]: - - code [ref=e338]: multisig.cell - - paragraph [ref=e339]: Witness checks for threshold-style locks. - - generic [ref=e340]: - - generic [ref=e341]: lock - - generic [ref=e342]: witness - - generic [ref=e343]: threshold - - link "timelock.cell Time-bound spending from Cell state. lock env timepoint" [ref=e344] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/timelock.cell - - heading "timelock.cell" [level=3] [ref=e345]: - - code [ref=e346]: timelock.cell - - paragraph [ref=e347]: Time-bound spending from Cell state. - - generic [ref=e348]: - - generic [ref=e349]: lock - - generic [ref=e350]: env - - generic [ref=e351]: timepoint - - contentinfo [ref=e352]: - - generic [ref=e353]: - - generic [ref=e354]: - - link "CellScript" [ref=e355] [cursor=pointer]: - - /url: "#top" - - generic [ref=e357]: CellScript - - paragraph [ref=e358]: A semantic DSL for CKB Cell-based smart contracts, with typed metadata and assurance by design. Docs, spec, examples, and source live with the repository. - - generic [ref=e359]: - - link "Docs" [ref=e360] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "Spec" [ref=e361] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "Examples" [ref=e362] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "Source" [ref=e363] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-02T05-41-08-297Z.yml b/.playwright-mcp/page-2026-06-02T05-41-08-297Z.yml deleted file mode 100644 index d662ab1a..00000000 --- a/.playwright-mcp/page-2026-06-02T05-41-08-297Z.yml +++ /dev/null @@ -1,348 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "Primary navigation" [ref=e3]: - - link "CellScript home" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link "Model" [ref=e8] [cursor=pointer]: - - /url: "#core-model" - - link "Assurance" [ref=e9] [cursor=pointer]: - - /url: "#assurance" - - link "Commands" [ref=e10] [cursor=pointer]: - - /url: "#tooling" - - link "Examples" [ref=e11] [cursor=pointer]: - - /url: "#examples" - - link "Source" [ref=e12] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - button "Switch to dark mode" [pressed] [ref=e13] [cursor=pointer]: - - generic [ref=e16]: Dark - - main [ref=e17]: - - region "CellScript" [ref=e18]: - - generic [ref=e19]: - - heading "CellScript" [level=1] [ref=e20] - - paragraph [ref=e21]: Write Cell contracts as typed transitions, not raw wire format. - - generic [ref=e22]: - - link "Get started" [ref=e23] [cursor=pointer]: - - /url: "#getting-started" - - link "Model fit" [ref=e24] [cursor=pointer]: - - /url: "#identity" - - generic "CellScript contract surface" [ref=e25]: - - generic [ref=e26]: - - term [ref=e27]: target - - definition [ref=e28]: ckb-vm RISC-V - - generic [ref=e29]: - - term [ref=e30]: model - - definition [ref=e31]: schema-backed Cells - - generic [ref=e32]: - - term [ref=e33]: output - - definition [ref=e34]: metadata + ProofPlan - - generic [ref=e35]: - - generic [ref=e37]: token.cell - - tablist "CellScript examples" [ref=e39]: - - tab "Fungible Token" [selected] [ref=e40] [cursor=pointer] - - tab "NFT" [ref=e41] [cursor=pointer] - - tab "AMM Pool" [ref=e42] [cursor=pointer] - - tab "Vesting" [ref=e43] [cursor=pointer] - - tabpanel "Fungible Token" [ref=e45]: - - generic [ref=e46]: - - generic [ref=e47]: "1" - - generic [ref=e48]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e49]: - - generic [ref=e50]: "2" - - generic [ref=e51]: "amount: u64," - - generic [ref=e52]: - - generic [ref=e53]: "3" - - generic [ref=e54]: "symbol: [u8; 8]," - - generic [ref=e55]: - - generic [ref=e56]: "4" - - generic [ref=e57]: "}" - - generic [ref=e59]: "5" - - generic [ref=e60]: - - generic [ref=e61]: "6" - - generic [ref=e62]: "action transfer_token(token: Token, to: Address)" - - generic [ref=e63]: - - generic [ref=e64]: "7" - - generic [ref=e65]: "-> next_token: Token" - - generic [ref=e66]: - - generic [ref=e67]: "8" - - generic [ref=e68]: where - - generic [ref=e69]: - - generic [ref=e70]: "9" - - generic [ref=e71]: consume token - - generic [ref=e72]: - - generic [ref=e73]: "10" - - generic [ref=e74]: "create next_token = Token {" - - generic [ref=e75]: - - generic [ref=e76]: "11" - - generic [ref=e77]: "amount: token.amount," - - generic [ref=e78]: - - generic [ref=e79]: "12" - - generic [ref=e80]: "symbol: token.symbol" - - generic [ref=e81]: - - generic [ref=e82]: "13" - - generic [ref=e83]: "} with_lock(to)" - - generic [ref=e85]: "14" - - generic [ref=e86]: - - generic [ref=e87]: "15" - - generic [ref=e88]: "action burn(token: Token)" - - generic [ref=e89]: - - generic [ref=e90]: $ - - generic [ref=e91]: cellc examples/token.cell --target-profile ckb - - region "Getting Started" [ref=e92]: - - heading "Getting Started" [level=2] [ref=e93] - - generic [ref=e94]: - - article [ref=e95]: - - generic [ref=e96]: "1" - - heading "Install" [level=3] [ref=e97] - - paragraph [ref=e98]: - - code [ref=e99]: cargo install --path . - - article [ref=e100]: - - generic [ref=e101]: "2" - - heading "Compile" [level=3] [ref=e102] - - paragraph [ref=e103]: - - code [ref=e104]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e105]: - - generic [ref=e106]: "3" - - heading "Check" [level=3] [ref=e107] - - paragraph [ref=e108]: - - code [ref=e109]: cellc check --target-profile ckb - - region "Compiler Workflow" [ref=e110]: - - heading "Compiler Workflow" [level=2] [ref=e111] - - generic [ref=e112]: - - generic "Compiler workflow from .cell source to CKB artefact" [ref=e113]: - - article [ref=e114]: - - img [ref=e116]: - - generic [ref=e119]: .cell - - heading "CellScript source" [level=3] [ref=e120] - - img [ref=e122] - - article [ref=e124]: - - img [ref=e126] - - heading "Parse & check" [level=3] [ref=e130] - - paragraph [ref=e131]: Syntax, types, effects - - img [ref=e133] - - article [ref=e135]: - - img [ref=e137] - - heading "IR + Metadata" [level=3] [ref=e143] - - paragraph [ref=e144]: Typed model & assurance info - - img [ref=e146] - - article [ref=e148]: - - img [ref=e150] - - heading "Lower to RISC-V" [level=3] [ref=e154] - - paragraph [ref=e155]: ckb-vm codegen & optimisations - - img [ref=e157] - - article [ref=e159]: - - img [ref=e161]: - - generic [ref=e164]: .elf - - heading "ELF / Assembly" [level=3] [ref=e165] - - paragraph [ref=e166]: RISC-V artefacts for ckb-vm - - complementary "Build for CKB" [ref=e167]: - - heading "Build for CKB" [level=3] [ref=e168] - - list [ref=e169]: - - listitem [ref=e170]: - - img [ref=e171] - - generic [ref=e173]: ckb-vm compatible - - listitem [ref=e174]: - - img [ref=e175] - - generic [ref=e177]: Deterministic execution - - listitem [ref=e178]: - - img [ref=e179] - - generic [ref=e181]: Minimal syscalls - - listitem [ref=e182]: - - img [ref=e183] - - generic [ref=e185]: Scheduler-aware - - region "Core Model" [ref=e186]: - - heading "Core Model" [level=2] [ref=e187] - - paragraph [ref=e188]: "CellScript keeps the contract model visible: Cell shapes, effects, locks, flows, and review metadata stay in one typed surface." - - paragraph [ref=e189]: "Narrow by design: not a general-purpose runtime, not a new VM, not account storage in disguise." - - generic [ref=e190]: - - tablist "CellScript core primitives" [ref=e191]: - - tab "resource" [selected] [ref=e192] [cursor=pointer]: - - generic [ref=e193]: resource - - tab "shared" [ref=e194] [cursor=pointer]: - - generic [ref=e195]: shared - - tab "receipt" [ref=e196] [cursor=pointer]: - - generic [ref=e197]: receipt - - tab "action" [ref=e198] [cursor=pointer]: - - generic [ref=e199]: action - - tab "lock" [ref=e200] [cursor=pointer]: - - generic [ref=e201]: lock - - tab "flow" [ref=e202] [cursor=pointer]: - - generic [ref=e203]: flow - - tab "invariant" [ref=e204] [cursor=pointer]: - - generic [ref=e205]: invariant - - tab "struct / enum" [ref=e206] [cursor=pointer]: - - generic [ref=e207]: struct / enum - - tab "identity" [ref=e208] [cursor=pointer]: - - generic [ref=e209]: identity - - tabpanel "resource" [ref=e211]: - - paragraph [ref=e213]: Owned Cell state with explicit lifecycle effects. - - generic [ref=e214]: - - generic [ref=e215]: - - generic [ref=e216]: Example excerpt - - generic [ref=e217]: examples/token.cell - - generic [ref=e218]: "resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance Output" [ref=e219]: - - generic [ref=e220]: - - heading "Assurance Output" [level=2] [ref=e221] - - paragraph [ref=e222]: A local build emits review metadata. Treat the sidecar as useful evidence, not an authenticated proof outside the build boundary. - - article [ref=e224]: - - generic "Assurance output summary" [ref=e225]: - - article [ref=e226]: - - text: Schema - - strong [ref=e227]: v42 - - paragraph [ref=e228]: current compiler metadata schema - - article [ref=e229]: - - text: Source - - strong [ref=e230]: vesting.cell - - paragraph [ref=e231]: shared + receipt + flow - - article [ref=e232]: - - text: Boundary - - strong [ref=e233]: local sidecar - - paragraph [ref=e234]: validated; provenance required when shared - - group [ref=e235]: - - generic "- Metadata excerpt" [ref=e236] [cursor=pointer] - - generic [ref=e237]: "# Representative sidecar excerpt from a local build; keep provenance checks separate." - - generic [ref=e238]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": { \"VestingConfig\": { \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, \"VestingGrant\": { \"kind\": \"receipt\", \"flow\": \"Granted -> Claimable -> FullyClaimed\" } }, \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"status\": \"review evidence\", \"limit\": \"sidecar provenance must be checked outside the compiler\" } }" - - region "Model Fit" [ref=e239]: - - generic [ref=e240]: - - heading "Model Fit" [level=2] [ref=e241] - - paragraph [ref=e242]: "This is a positioning map, not a league table. CellScript is narrow on purpose: it speaks CKB Cells directly and leaves account-storage assumptions at the door." - - table [ref=e244]: - - rowgroup [ref=e245]: - - row "Axis CellScript Account contracts Move-family languages UTXO contract languages" [ref=e246]: - - columnheader "Axis" [ref=e247] - - columnheader "CellScript" [ref=e248] - - columnheader "Account contracts" [ref=e249] - - columnheader "Move-family languages" [ref=e250] - - columnheader "UTXO contract languages" [ref=e251] - - rowgroup [ref=e252]: - - row "Where state lives Named Cells with schema-backed data and explicit locks Contract-owned account storage VM-managed objects or resource values UTXO state plus contract logic" [ref=e253]: - - cell "Where state lives" [ref=e254] - - cell "Named Cells with schema-backed data and explicit locks" [ref=e255] - - cell "Contract-owned account storage" [ref=e256] - - cell "VM-managed objects or resource values" [ref=e257] - - cell "UTXO state plus contract logic" [ref=e258] - - row "What is checked Linear lifecycle, declared effects, typed field access, and transition shape ABI conventions, storage layout, external linters VM-enforced abilities and module rules Spend predicates and transaction validity" [ref=e259]: - - cell "What is checked" [ref=e260] - - cell "Linear lifecycle, declared effects, typed field access, and transition shape" [ref=e261] - - cell "ABI conventions, storage layout, external linters" [ref=e262] - - cell "VM-enforced abilities and module rules" [ref=e263] - - cell "Spend predicates and transaction validity" [ref=e264] - - row "What reviewers see metadata, constraints, ProofPlan, source hashes, access summary ABI, events, storage diff, runtime traces module bytecode plus VM safety properties Contract ABI plus transaction semantics" [ref=e265]: - - cell "What reviewers see" [ref=e266] - - cell "metadata, constraints, ProofPlan, source hashes, access summary" [ref=e267] - - cell "ABI, events, storage diff, runtime traces" [ref=e268] - - cell "module bytecode plus VM safety properties" [ref=e269] - - cell "Contract ABI plus transaction semantics" [ref=e270] - - row "What it refuses general-purpose runtime, new VM, or account-storage shim Cell-native state by default ckb-vm RISC-V target by default CellScript semantic metadata by default" [ref=e271]: - - cell "What it refuses" [ref=e272] - - cell "general-purpose runtime, new VM, or account-storage shim" [ref=e273] - - cell "Cell-native state by default" [ref=e274] - - cell "ckb-vm RISC-V target by default" [ref=e275] - - cell "CellScript semantic metadata by default" [ref=e276] - - row "Best fit CKB Cell transitions that need explicit effects and audit evidence Applications built around mutable account state Resource-centric ecosystems already running a Move VM UTXO apps whose compiler model is not Cell-specific" [ref=e277]: - - cell "Best fit" [ref=e278] - - cell "CKB Cell transitions that need explicit effects and audit evidence" [ref=e279] - - cell "Applications built around mutable account state" [ref=e280] - - cell "Resource-centric ecosystems already running a Move VM" [ref=e281] - - cell "UTXO apps whose compiler model is not Cell-specific" [ref=e282] - - region "Tooling Surface" [ref=e283]: - - heading "Tooling Surface" [level=2] [ref=e284] - - generic [ref=e285]: - - tablist "CellScript tooling commands" [ref=e286]: - - tab "cellc metadata Read/write surface" [selected] [ref=e287] [cursor=pointer]: - - code [ref=e288]: cellc metadata - - generic [ref=e289]: Read/write surface - - tab "cellc constraints Transaction shape" [ref=e290] [cursor=pointer]: - - code [ref=e291]: cellc constraints - - generic [ref=e292]: Transaction shape - - tab "cellc audit-bundle Reviewer packet" [ref=e293] [cursor=pointer]: - - code [ref=e294]: cellc audit-bundle - - generic [ref=e295]: Reviewer packet - - tab "cellc lsp Editor feedback" [ref=e296] [cursor=pointer]: - - code [ref=e297]: cellc lsp - - generic [ref=e298]: Editor feedback - - tabpanel "cellc metadata Read/write surface" [ref=e300]: - - generic [ref=e301]: - - generic [ref=e302]: When - - paragraph [ref=e303]: Use before review or integration. - - generic [ref=e304]: Output - - paragraph [ref=e305]: Schema, effects, source hashes, target profile. - - generic [ref=e306]: - - generic [ref=e307]: Run - - generic [ref=e308]: "# Emit review metadata for a real example." - - code [ref=e309]: cellc metadata examples/vesting.cell --target-profile ckb --json - - region "Examples" [ref=e310]: - - heading "Examples" [level=2] [ref=e311] - - generic [ref=e312]: - - link "token.cell Mint, transfer, burn, and typed metadata. resource consume/create burn" [ref=e313] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - heading "token.cell" [level=3] [ref=e314]: - - code [ref=e315]: token.cell - - paragraph [ref=e316]: Mint, transfer, burn, and typed metadata. - - generic [ref=e317]: - - generic [ref=e318]: resource - - generic [ref=e319]: consume/create - - generic [ref=e320]: burn - - link "nft.cell Ownership transfer with preserve and relock. resource preserve relock" [ref=e321] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - heading "nft.cell" [level=3] [ref=e322]: - - code [ref=e323]: nft.cell - - paragraph [ref=e324]: Ownership transfer with preserve and relock. - - generic [ref=e325]: - - generic [ref=e326]: resource - - generic [ref=e327]: preserve - - generic [ref=e328]: relock - - link "amm_pool.cell Shared reserves with slippage checks. shared replace slippage" [ref=e329] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - heading "amm_pool.cell" [level=3] [ref=e330]: - - code [ref=e331]: amm_pool.cell - - paragraph [ref=e332]: Shared reserves with slippage checks. - - generic [ref=e333]: - - generic [ref=e334]: shared - - generic [ref=e335]: replace - - generic [ref=e336]: slippage - - link "vesting.cell Grant state flow into claimed output. flow transition receipt" [ref=e337] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - heading "vesting.cell" [level=3] [ref=e338]: - - code [ref=e339]: vesting.cell - - paragraph [ref=e340]: Grant state flow into claimed output. - - generic [ref=e341]: - - generic [ref=e342]: flow - - generic [ref=e343]: transition - - generic [ref=e344]: receipt - - link "multisig.cell Witness checks for threshold-style locks. lock witness threshold" [ref=e345] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/multisig.cell - - heading "multisig.cell" [level=3] [ref=e346]: - - code [ref=e347]: multisig.cell - - paragraph [ref=e348]: Witness checks for threshold-style locks. - - generic [ref=e349]: - - generic [ref=e350]: lock - - generic [ref=e351]: witness - - generic [ref=e352]: threshold - - link "timelock.cell Time-bound spending from Cell state. lock env timepoint" [ref=e353] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/timelock.cell - - heading "timelock.cell" [level=3] [ref=e354]: - - code [ref=e355]: timelock.cell - - paragraph [ref=e356]: Time-bound spending from Cell state. - - generic [ref=e357]: - - generic [ref=e358]: lock - - generic [ref=e359]: env - - generic [ref=e360]: timepoint - - contentinfo [ref=e361]: - - generic [ref=e362]: - - generic [ref=e363]: - - link "CellScript" [ref=e364] [cursor=pointer]: - - /url: "#top" - - generic [ref=e366]: CellScript - - paragraph [ref=e367]: A semantic DSL for CKB Cell-based smart contracts, with typed metadata and assurance by design. Docs, spec, examples, and source live with the repository. - - generic [ref=e368]: - - link "Docs" [ref=e369] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "Spec" [ref=e370] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "Examples" [ref=e371] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "Source" [ref=e372] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-02T05-43-09-119Z.yml b/.playwright-mcp/page-2026-06-02T05-43-09-119Z.yml deleted file mode 100644 index d662ab1a..00000000 --- a/.playwright-mcp/page-2026-06-02T05-43-09-119Z.yml +++ /dev/null @@ -1,348 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "Primary navigation" [ref=e3]: - - link "CellScript home" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link "Model" [ref=e8] [cursor=pointer]: - - /url: "#core-model" - - link "Assurance" [ref=e9] [cursor=pointer]: - - /url: "#assurance" - - link "Commands" [ref=e10] [cursor=pointer]: - - /url: "#tooling" - - link "Examples" [ref=e11] [cursor=pointer]: - - /url: "#examples" - - link "Source" [ref=e12] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - button "Switch to dark mode" [pressed] [ref=e13] [cursor=pointer]: - - generic [ref=e16]: Dark - - main [ref=e17]: - - region "CellScript" [ref=e18]: - - generic [ref=e19]: - - heading "CellScript" [level=1] [ref=e20] - - paragraph [ref=e21]: Write Cell contracts as typed transitions, not raw wire format. - - generic [ref=e22]: - - link "Get started" [ref=e23] [cursor=pointer]: - - /url: "#getting-started" - - link "Model fit" [ref=e24] [cursor=pointer]: - - /url: "#identity" - - generic "CellScript contract surface" [ref=e25]: - - generic [ref=e26]: - - term [ref=e27]: target - - definition [ref=e28]: ckb-vm RISC-V - - generic [ref=e29]: - - term [ref=e30]: model - - definition [ref=e31]: schema-backed Cells - - generic [ref=e32]: - - term [ref=e33]: output - - definition [ref=e34]: metadata + ProofPlan - - generic [ref=e35]: - - generic [ref=e37]: token.cell - - tablist "CellScript examples" [ref=e39]: - - tab "Fungible Token" [selected] [ref=e40] [cursor=pointer] - - tab "NFT" [ref=e41] [cursor=pointer] - - tab "AMM Pool" [ref=e42] [cursor=pointer] - - tab "Vesting" [ref=e43] [cursor=pointer] - - tabpanel "Fungible Token" [ref=e45]: - - generic [ref=e46]: - - generic [ref=e47]: "1" - - generic [ref=e48]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e49]: - - generic [ref=e50]: "2" - - generic [ref=e51]: "amount: u64," - - generic [ref=e52]: - - generic [ref=e53]: "3" - - generic [ref=e54]: "symbol: [u8; 8]," - - generic [ref=e55]: - - generic [ref=e56]: "4" - - generic [ref=e57]: "}" - - generic [ref=e59]: "5" - - generic [ref=e60]: - - generic [ref=e61]: "6" - - generic [ref=e62]: "action transfer_token(token: Token, to: Address)" - - generic [ref=e63]: - - generic [ref=e64]: "7" - - generic [ref=e65]: "-> next_token: Token" - - generic [ref=e66]: - - generic [ref=e67]: "8" - - generic [ref=e68]: where - - generic [ref=e69]: - - generic [ref=e70]: "9" - - generic [ref=e71]: consume token - - generic [ref=e72]: - - generic [ref=e73]: "10" - - generic [ref=e74]: "create next_token = Token {" - - generic [ref=e75]: - - generic [ref=e76]: "11" - - generic [ref=e77]: "amount: token.amount," - - generic [ref=e78]: - - generic [ref=e79]: "12" - - generic [ref=e80]: "symbol: token.symbol" - - generic [ref=e81]: - - generic [ref=e82]: "13" - - generic [ref=e83]: "} with_lock(to)" - - generic [ref=e85]: "14" - - generic [ref=e86]: - - generic [ref=e87]: "15" - - generic [ref=e88]: "action burn(token: Token)" - - generic [ref=e89]: - - generic [ref=e90]: $ - - generic [ref=e91]: cellc examples/token.cell --target-profile ckb - - region "Getting Started" [ref=e92]: - - heading "Getting Started" [level=2] [ref=e93] - - generic [ref=e94]: - - article [ref=e95]: - - generic [ref=e96]: "1" - - heading "Install" [level=3] [ref=e97] - - paragraph [ref=e98]: - - code [ref=e99]: cargo install --path . - - article [ref=e100]: - - generic [ref=e101]: "2" - - heading "Compile" [level=3] [ref=e102] - - paragraph [ref=e103]: - - code [ref=e104]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e105]: - - generic [ref=e106]: "3" - - heading "Check" [level=3] [ref=e107] - - paragraph [ref=e108]: - - code [ref=e109]: cellc check --target-profile ckb - - region "Compiler Workflow" [ref=e110]: - - heading "Compiler Workflow" [level=2] [ref=e111] - - generic [ref=e112]: - - generic "Compiler workflow from .cell source to CKB artefact" [ref=e113]: - - article [ref=e114]: - - img [ref=e116]: - - generic [ref=e119]: .cell - - heading "CellScript source" [level=3] [ref=e120] - - img [ref=e122] - - article [ref=e124]: - - img [ref=e126] - - heading "Parse & check" [level=3] [ref=e130] - - paragraph [ref=e131]: Syntax, types, effects - - img [ref=e133] - - article [ref=e135]: - - img [ref=e137] - - heading "IR + Metadata" [level=3] [ref=e143] - - paragraph [ref=e144]: Typed model & assurance info - - img [ref=e146] - - article [ref=e148]: - - img [ref=e150] - - heading "Lower to RISC-V" [level=3] [ref=e154] - - paragraph [ref=e155]: ckb-vm codegen & optimisations - - img [ref=e157] - - article [ref=e159]: - - img [ref=e161]: - - generic [ref=e164]: .elf - - heading "ELF / Assembly" [level=3] [ref=e165] - - paragraph [ref=e166]: RISC-V artefacts for ckb-vm - - complementary "Build for CKB" [ref=e167]: - - heading "Build for CKB" [level=3] [ref=e168] - - list [ref=e169]: - - listitem [ref=e170]: - - img [ref=e171] - - generic [ref=e173]: ckb-vm compatible - - listitem [ref=e174]: - - img [ref=e175] - - generic [ref=e177]: Deterministic execution - - listitem [ref=e178]: - - img [ref=e179] - - generic [ref=e181]: Minimal syscalls - - listitem [ref=e182]: - - img [ref=e183] - - generic [ref=e185]: Scheduler-aware - - region "Core Model" [ref=e186]: - - heading "Core Model" [level=2] [ref=e187] - - paragraph [ref=e188]: "CellScript keeps the contract model visible: Cell shapes, effects, locks, flows, and review metadata stay in one typed surface." - - paragraph [ref=e189]: "Narrow by design: not a general-purpose runtime, not a new VM, not account storage in disguise." - - generic [ref=e190]: - - tablist "CellScript core primitives" [ref=e191]: - - tab "resource" [selected] [ref=e192] [cursor=pointer]: - - generic [ref=e193]: resource - - tab "shared" [ref=e194] [cursor=pointer]: - - generic [ref=e195]: shared - - tab "receipt" [ref=e196] [cursor=pointer]: - - generic [ref=e197]: receipt - - tab "action" [ref=e198] [cursor=pointer]: - - generic [ref=e199]: action - - tab "lock" [ref=e200] [cursor=pointer]: - - generic [ref=e201]: lock - - tab "flow" [ref=e202] [cursor=pointer]: - - generic [ref=e203]: flow - - tab "invariant" [ref=e204] [cursor=pointer]: - - generic [ref=e205]: invariant - - tab "struct / enum" [ref=e206] [cursor=pointer]: - - generic [ref=e207]: struct / enum - - tab "identity" [ref=e208] [cursor=pointer]: - - generic [ref=e209]: identity - - tabpanel "resource" [ref=e211]: - - paragraph [ref=e213]: Owned Cell state with explicit lifecycle effects. - - generic [ref=e214]: - - generic [ref=e215]: - - generic [ref=e216]: Example excerpt - - generic [ref=e217]: examples/token.cell - - generic [ref=e218]: "resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance Output" [ref=e219]: - - generic [ref=e220]: - - heading "Assurance Output" [level=2] [ref=e221] - - paragraph [ref=e222]: A local build emits review metadata. Treat the sidecar as useful evidence, not an authenticated proof outside the build boundary. - - article [ref=e224]: - - generic "Assurance output summary" [ref=e225]: - - article [ref=e226]: - - text: Schema - - strong [ref=e227]: v42 - - paragraph [ref=e228]: current compiler metadata schema - - article [ref=e229]: - - text: Source - - strong [ref=e230]: vesting.cell - - paragraph [ref=e231]: shared + receipt + flow - - article [ref=e232]: - - text: Boundary - - strong [ref=e233]: local sidecar - - paragraph [ref=e234]: validated; provenance required when shared - - group [ref=e235]: - - generic "- Metadata excerpt" [ref=e236] [cursor=pointer] - - generic [ref=e237]: "# Representative sidecar excerpt from a local build; keep provenance checks separate." - - generic [ref=e238]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": { \"VestingConfig\": { \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, \"VestingGrant\": { \"kind\": \"receipt\", \"flow\": \"Granted -> Claimable -> FullyClaimed\" } }, \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"status\": \"review evidence\", \"limit\": \"sidecar provenance must be checked outside the compiler\" } }" - - region "Model Fit" [ref=e239]: - - generic [ref=e240]: - - heading "Model Fit" [level=2] [ref=e241] - - paragraph [ref=e242]: "This is a positioning map, not a league table. CellScript is narrow on purpose: it speaks CKB Cells directly and leaves account-storage assumptions at the door." - - table [ref=e244]: - - rowgroup [ref=e245]: - - row "Axis CellScript Account contracts Move-family languages UTXO contract languages" [ref=e246]: - - columnheader "Axis" [ref=e247] - - columnheader "CellScript" [ref=e248] - - columnheader "Account contracts" [ref=e249] - - columnheader "Move-family languages" [ref=e250] - - columnheader "UTXO contract languages" [ref=e251] - - rowgroup [ref=e252]: - - row "Where state lives Named Cells with schema-backed data and explicit locks Contract-owned account storage VM-managed objects or resource values UTXO state plus contract logic" [ref=e253]: - - cell "Where state lives" [ref=e254] - - cell "Named Cells with schema-backed data and explicit locks" [ref=e255] - - cell "Contract-owned account storage" [ref=e256] - - cell "VM-managed objects or resource values" [ref=e257] - - cell "UTXO state plus contract logic" [ref=e258] - - row "What is checked Linear lifecycle, declared effects, typed field access, and transition shape ABI conventions, storage layout, external linters VM-enforced abilities and module rules Spend predicates and transaction validity" [ref=e259]: - - cell "What is checked" [ref=e260] - - cell "Linear lifecycle, declared effects, typed field access, and transition shape" [ref=e261] - - cell "ABI conventions, storage layout, external linters" [ref=e262] - - cell "VM-enforced abilities and module rules" [ref=e263] - - cell "Spend predicates and transaction validity" [ref=e264] - - row "What reviewers see metadata, constraints, ProofPlan, source hashes, access summary ABI, events, storage diff, runtime traces module bytecode plus VM safety properties Contract ABI plus transaction semantics" [ref=e265]: - - cell "What reviewers see" [ref=e266] - - cell "metadata, constraints, ProofPlan, source hashes, access summary" [ref=e267] - - cell "ABI, events, storage diff, runtime traces" [ref=e268] - - cell "module bytecode plus VM safety properties" [ref=e269] - - cell "Contract ABI plus transaction semantics" [ref=e270] - - row "What it refuses general-purpose runtime, new VM, or account-storage shim Cell-native state by default ckb-vm RISC-V target by default CellScript semantic metadata by default" [ref=e271]: - - cell "What it refuses" [ref=e272] - - cell "general-purpose runtime, new VM, or account-storage shim" [ref=e273] - - cell "Cell-native state by default" [ref=e274] - - cell "ckb-vm RISC-V target by default" [ref=e275] - - cell "CellScript semantic metadata by default" [ref=e276] - - row "Best fit CKB Cell transitions that need explicit effects and audit evidence Applications built around mutable account state Resource-centric ecosystems already running a Move VM UTXO apps whose compiler model is not Cell-specific" [ref=e277]: - - cell "Best fit" [ref=e278] - - cell "CKB Cell transitions that need explicit effects and audit evidence" [ref=e279] - - cell "Applications built around mutable account state" [ref=e280] - - cell "Resource-centric ecosystems already running a Move VM" [ref=e281] - - cell "UTXO apps whose compiler model is not Cell-specific" [ref=e282] - - region "Tooling Surface" [ref=e283]: - - heading "Tooling Surface" [level=2] [ref=e284] - - generic [ref=e285]: - - tablist "CellScript tooling commands" [ref=e286]: - - tab "cellc metadata Read/write surface" [selected] [ref=e287] [cursor=pointer]: - - code [ref=e288]: cellc metadata - - generic [ref=e289]: Read/write surface - - tab "cellc constraints Transaction shape" [ref=e290] [cursor=pointer]: - - code [ref=e291]: cellc constraints - - generic [ref=e292]: Transaction shape - - tab "cellc audit-bundle Reviewer packet" [ref=e293] [cursor=pointer]: - - code [ref=e294]: cellc audit-bundle - - generic [ref=e295]: Reviewer packet - - tab "cellc lsp Editor feedback" [ref=e296] [cursor=pointer]: - - code [ref=e297]: cellc lsp - - generic [ref=e298]: Editor feedback - - tabpanel "cellc metadata Read/write surface" [ref=e300]: - - generic [ref=e301]: - - generic [ref=e302]: When - - paragraph [ref=e303]: Use before review or integration. - - generic [ref=e304]: Output - - paragraph [ref=e305]: Schema, effects, source hashes, target profile. - - generic [ref=e306]: - - generic [ref=e307]: Run - - generic [ref=e308]: "# Emit review metadata for a real example." - - code [ref=e309]: cellc metadata examples/vesting.cell --target-profile ckb --json - - region "Examples" [ref=e310]: - - heading "Examples" [level=2] [ref=e311] - - generic [ref=e312]: - - link "token.cell Mint, transfer, burn, and typed metadata. resource consume/create burn" [ref=e313] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - heading "token.cell" [level=3] [ref=e314]: - - code [ref=e315]: token.cell - - paragraph [ref=e316]: Mint, transfer, burn, and typed metadata. - - generic [ref=e317]: - - generic [ref=e318]: resource - - generic [ref=e319]: consume/create - - generic [ref=e320]: burn - - link "nft.cell Ownership transfer with preserve and relock. resource preserve relock" [ref=e321] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - heading "nft.cell" [level=3] [ref=e322]: - - code [ref=e323]: nft.cell - - paragraph [ref=e324]: Ownership transfer with preserve and relock. - - generic [ref=e325]: - - generic [ref=e326]: resource - - generic [ref=e327]: preserve - - generic [ref=e328]: relock - - link "amm_pool.cell Shared reserves with slippage checks. shared replace slippage" [ref=e329] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - heading "amm_pool.cell" [level=3] [ref=e330]: - - code [ref=e331]: amm_pool.cell - - paragraph [ref=e332]: Shared reserves with slippage checks. - - generic [ref=e333]: - - generic [ref=e334]: shared - - generic [ref=e335]: replace - - generic [ref=e336]: slippage - - link "vesting.cell Grant state flow into claimed output. flow transition receipt" [ref=e337] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - heading "vesting.cell" [level=3] [ref=e338]: - - code [ref=e339]: vesting.cell - - paragraph [ref=e340]: Grant state flow into claimed output. - - generic [ref=e341]: - - generic [ref=e342]: flow - - generic [ref=e343]: transition - - generic [ref=e344]: receipt - - link "multisig.cell Witness checks for threshold-style locks. lock witness threshold" [ref=e345] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/multisig.cell - - heading "multisig.cell" [level=3] [ref=e346]: - - code [ref=e347]: multisig.cell - - paragraph [ref=e348]: Witness checks for threshold-style locks. - - generic [ref=e349]: - - generic [ref=e350]: lock - - generic [ref=e351]: witness - - generic [ref=e352]: threshold - - link "timelock.cell Time-bound spending from Cell state. lock env timepoint" [ref=e353] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/timelock.cell - - heading "timelock.cell" [level=3] [ref=e354]: - - code [ref=e355]: timelock.cell - - paragraph [ref=e356]: Time-bound spending from Cell state. - - generic [ref=e357]: - - generic [ref=e358]: lock - - generic [ref=e359]: env - - generic [ref=e360]: timepoint - - contentinfo [ref=e361]: - - generic [ref=e362]: - - generic [ref=e363]: - - link "CellScript" [ref=e364] [cursor=pointer]: - - /url: "#top" - - generic [ref=e366]: CellScript - - paragraph [ref=e367]: A semantic DSL for CKB Cell-based smart contracts, with typed metadata and assurance by design. Docs, spec, examples, and source live with the repository. - - generic [ref=e368]: - - link "Docs" [ref=e369] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "Spec" [ref=e370] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "Examples" [ref=e371] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "Source" [ref=e372] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-02T05-47-41-638Z.yml b/.playwright-mcp/page-2026-06-02T05-47-41-638Z.yml deleted file mode 100644 index c63be6bb..00000000 --- a/.playwright-mcp/page-2026-06-02T05-47-41-638Z.yml +++ /dev/null @@ -1,307 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "Primary navigation" [ref=e3]: - - link "CellScript home" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link "Model" [ref=e8] [cursor=pointer]: - - /url: "#core-model" - - link "Assurance" [ref=e9] [cursor=pointer]: - - /url: "#assurance" - - link "Commands" [ref=e10] [cursor=pointer]: - - /url: "#tooling" - - link "Examples" [ref=e11] [cursor=pointer]: - - /url: "#examples" - - link "Source" [ref=e12] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - button "Switch to dark mode" [pressed] [ref=e13] [cursor=pointer]: - - generic [ref=e16]: Dark - - main [ref=e17]: - - region "CellScript" [ref=e18]: - - generic [ref=e19]: - - heading "CellScript" [level=1] [ref=e20] - - paragraph [ref=e21]: Write Cell contracts as typed transitions, not raw wire format. - - generic [ref=e22]: - - link "Get started" [ref=e23] [cursor=pointer]: - - /url: "#getting-started" - - link "Core model" [ref=e24] [cursor=pointer]: - - /url: "#core-model" - - generic "CellScript contract surface" [ref=e25]: - - generic [ref=e26]: - - term [ref=e27]: target - - definition [ref=e28]: ckb-vm RISC-V - - generic [ref=e29]: - - term [ref=e30]: model - - definition [ref=e31]: schema-backed Cells - - generic [ref=e32]: - - term [ref=e33]: output - - definition [ref=e34]: metadata + ProofPlan - - generic [ref=e35]: - - generic [ref=e37]: token.cell - - tablist "CellScript examples" [ref=e39]: - - tab "Fungible Token" [selected] [ref=e40] [cursor=pointer] - - tab "NFT" [ref=e41] [cursor=pointer] - - tab "AMM Pool" [ref=e42] [cursor=pointer] - - tab "Vesting" [ref=e43] [cursor=pointer] - - tabpanel "Fungible Token" [ref=e45]: - - generic [ref=e46]: - - generic [ref=e47]: "1" - - generic [ref=e48]: module cellscript::fungible_token - - generic [ref=e49]: - - generic [ref=e50]: "2" - - generic [ref=e51]: // ... invariant and MintAuthority omitted - - generic [ref=e52]: - - generic [ref=e53]: "3" - - generic [ref=e54]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e55]: - - generic [ref=e56]: "4" - - generic [ref=e57]: "amount: u64," - - generic [ref=e58]: - - generic [ref=e59]: "5" - - generic [ref=e60]: "symbol: [u8; 8]," - - generic [ref=e61]: - - generic [ref=e62]: "6" - - generic [ref=e63]: "}" - - generic [ref=e65]: "7" - - generic [ref=e66]: - - generic [ref=e67]: "8" - - generic [ref=e68]: "action transfer_token(token: Token, to: Address) -> next_token: Token" - - generic [ref=e69]: - - generic [ref=e70]: "9" - - generic [ref=e71]: where - - generic [ref=e72]: - - generic [ref=e73]: "10" - - generic [ref=e74]: consume token - - generic [ref=e75]: - - generic [ref=e76]: "11" - - generic [ref=e77]: "create next_token = Token { amount: token.amount, symbol: token.symbol } with_lock(to)" - - generic [ref=e79]: "12" - - generic [ref=e80]: - - generic [ref=e81]: "13" - - generic [ref=e82]: "action burn(token: Token)" - - generic [ref=e83]: - - generic [ref=e84]: "14" - - generic [ref=e85]: where - - generic [ref=e86]: - - generic [ref=e87]: "15" - - generic [ref=e88]: assert(token.amount > 0, "cannot burn zero") - - generic [ref=e89]: - - generic [ref=e90]: "16" - - generic [ref=e91]: destroy token - - generic [ref=e92]: - - generic [ref=e93]: $ - - generic [ref=e94]: cellc examples/token.cell --target-profile ckb - - region "Getting Started" [ref=e95]: - - heading "Getting Started" [level=2] [ref=e96] - - generic [ref=e97]: - - article [ref=e98]: - - generic [ref=e99]: "1" - - heading "Install" [level=3] [ref=e100] - - paragraph [ref=e101]: - - code [ref=e102]: cargo install --path . - - article [ref=e103]: - - generic [ref=e104]: "2" - - heading "Compile" [level=3] [ref=e105] - - paragraph [ref=e106]: - - code [ref=e107]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e108]: - - generic [ref=e109]: "3" - - heading "Check" [level=3] [ref=e110] - - paragraph [ref=e111]: - - code [ref=e112]: cellc check --target-profile ckb - - region "Compiler Workflow" [ref=e113]: - - heading "Compiler Workflow" [level=2] [ref=e114] - - generic [ref=e115]: - - generic "Compiler workflow from .cell source to CKB artefact" [ref=e116]: - - article [ref=e117]: - - img [ref=e119]: - - generic [ref=e122]: .cell - - heading "CellScript source" [level=3] [ref=e123] - - img [ref=e125] - - article [ref=e127]: - - img [ref=e129] - - heading "Parse & check" [level=3] [ref=e133] - - paragraph [ref=e134]: Syntax, types, effects - - img [ref=e136] - - article [ref=e138]: - - img [ref=e140] - - heading "IR + Metadata" [level=3] [ref=e146] - - paragraph [ref=e147]: Typed model & assurance info - - img [ref=e149] - - article [ref=e151]: - - img [ref=e153] - - heading "Lower to RISC-V" [level=3] [ref=e157] - - paragraph [ref=e158]: ckb-vm codegen & optimisations - - img [ref=e160] - - article [ref=e162]: - - img [ref=e164]: - - generic [ref=e167]: .elf - - heading "ELF / Assembly" [level=3] [ref=e168] - - paragraph [ref=e169]: RISC-V artefacts for ckb-vm - - complementary "Build for CKB" [ref=e170]: - - heading "Build for CKB" [level=3] [ref=e171] - - list [ref=e172]: - - listitem [ref=e173]: - - img [ref=e174] - - generic [ref=e176]: ckb-vm compatible - - listitem [ref=e177]: - - img [ref=e178] - - generic [ref=e180]: Deterministic execution - - listitem [ref=e181]: - - img [ref=e182] - - generic [ref=e184]: Minimal syscalls - - listitem [ref=e185]: - - img [ref=e186] - - generic [ref=e188]: Scheduler-aware - - region "Core Model" [ref=e189]: - - heading "Core Model" [level=2] [ref=e190] - - paragraph [ref=e191]: "CellScript keeps the contract model visible: Cell shapes, effects, locks, flows, and review metadata stay in one typed surface." - - paragraph [ref=e192]: "Narrow by design: not a general-purpose runtime, not a new VM, not account storage in disguise." - - generic [ref=e193]: - - tablist "CellScript core primitives" [ref=e194]: - - tab "resource" [selected] [ref=e195] [cursor=pointer]: - - generic [ref=e196]: resource - - tab "shared" [ref=e197] [cursor=pointer]: - - generic [ref=e198]: shared - - tab "receipt" [ref=e199] [cursor=pointer]: - - generic [ref=e200]: receipt - - tab "action" [ref=e201] [cursor=pointer]: - - generic [ref=e202]: action - - tab "lock" [ref=e203] [cursor=pointer]: - - generic [ref=e204]: lock - - tab "flow" [ref=e205] [cursor=pointer]: - - generic [ref=e206]: flow - - tab "invariant" [ref=e207] [cursor=pointer]: - - generic [ref=e208]: invariant - - tab "struct / enum" [ref=e209] [cursor=pointer]: - - generic [ref=e210]: struct / enum - - tab "identity" [ref=e211] [cursor=pointer]: - - generic [ref=e212]: identity - - tabpanel "resource" [ref=e214]: - - paragraph [ref=e216]: Owned Cell state with explicit lifecycle effects. - - generic [ref=e217]: - - generic [ref=e218]: - - generic [ref=e219]: Example excerpt - - generic [ref=e220]: examples/token.cell - - generic [ref=e221]: "// examples/token.cell resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance Output" [ref=e222]: - - generic [ref=e223]: - - heading "Assurance Output" [level=2] [ref=e224] - - paragraph [ref=e225]: A local build emits review metadata. Treat the sidecar as useful evidence, not an authenticated proof outside the build boundary. - - article [ref=e227]: - - generic "Assurance output summary" [ref=e228]: - - article [ref=e229]: - - text: Schema - - strong [ref=e230]: v42 - - paragraph [ref=e231]: current compiler metadata schema - - article [ref=e232]: - - text: Source - - strong [ref=e233]: vesting.cell - - paragraph [ref=e234]: shared + receipt + flow - - article [ref=e235]: - - text: Boundary - - strong [ref=e236]: local sidecar - - paragraph [ref=e237]: validated; provenance required when shared - - group [ref=e238]: - - generic "- Metadata excerpt" [ref=e239] [cursor=pointer] - - generic [ref=e240]: "# Representative sidecar excerpt from a local build; keep provenance checks separate." - - generic [ref=e241]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": { \"VestingConfig\": { \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, \"VestingGrant\": { \"kind\": \"receipt\", \"flow\": \"Granted -> Claimable -> FullyClaimed\" } }, \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"status\": \"review evidence\", \"limit\": \"sidecar provenance must be checked outside the compiler\" } }" - - region "Tooling Surface" [ref=e242]: - - heading "Tooling Surface" [level=2] [ref=e243] - - generic [ref=e244]: - - tablist "CellScript tooling commands" [ref=e245]: - - tab "cellc metadata Read/write surface" [selected] [ref=e246] [cursor=pointer]: - - code [ref=e247]: cellc metadata - - generic [ref=e248]: Read/write surface - - tab "cellc constraints Transaction shape" [ref=e249] [cursor=pointer]: - - code [ref=e250]: cellc constraints - - generic [ref=e251]: Transaction shape - - tab "cellc audit-bundle Reviewer packet" [ref=e252] [cursor=pointer]: - - code [ref=e253]: cellc audit-bundle - - generic [ref=e254]: Reviewer packet - - tab "cellc lsp Editor feedback" [ref=e255] [cursor=pointer]: - - code [ref=e256]: cellc lsp - - generic [ref=e257]: Editor feedback - - tabpanel "cellc metadata Read/write surface" [ref=e259]: - - generic [ref=e260]: - - generic [ref=e261]: When - - paragraph [ref=e262]: Use before review or integration. - - generic [ref=e263]: Output - - paragraph [ref=e264]: Schema, effects, source hashes, target profile. - - generic [ref=e265]: - - code [ref=e266]: cellc metadata examples/vesting.cell --target-profile ckb --json - - generic [ref=e267]: "# Emit review metadata for a real example." - - region "Examples" [ref=e268]: - - heading "Examples" [level=2] [ref=e269] - - generic [ref=e270]: - - link "token.cell Mint, transfer, burn, and typed metadata. resource consume/create burn" [ref=e271] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - heading "token.cell" [level=3] [ref=e272]: - - code [ref=e273]: token.cell - - paragraph [ref=e274]: Mint, transfer, burn, and typed metadata. - - generic [ref=e275]: - - generic [ref=e276]: resource - - generic [ref=e277]: consume/create - - generic [ref=e278]: burn - - link "nft.cell Ownership transfer with preserve and relock. resource preserve relock" [ref=e279] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - heading "nft.cell" [level=3] [ref=e280]: - - code [ref=e281]: nft.cell - - paragraph [ref=e282]: Ownership transfer with preserve and relock. - - generic [ref=e283]: - - generic [ref=e284]: resource - - generic [ref=e285]: preserve - - generic [ref=e286]: relock - - link "amm_pool.cell Shared reserves with slippage checks. shared replace slippage" [ref=e287] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - heading "amm_pool.cell" [level=3] [ref=e288]: - - code [ref=e289]: amm_pool.cell - - paragraph [ref=e290]: Shared reserves with slippage checks. - - generic [ref=e291]: - - generic [ref=e292]: shared - - generic [ref=e293]: replace - - generic [ref=e294]: slippage - - link "vesting.cell Grant state flow into claimed output. flow transition receipt" [ref=e295] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - heading "vesting.cell" [level=3] [ref=e296]: - - code [ref=e297]: vesting.cell - - paragraph [ref=e298]: Grant state flow into claimed output. - - generic [ref=e299]: - - generic [ref=e300]: flow - - generic [ref=e301]: transition - - generic [ref=e302]: receipt - - link "multisig.cell Witness checks for threshold-style locks. lock witness threshold" [ref=e303] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/multisig.cell - - heading "multisig.cell" [level=3] [ref=e304]: - - code [ref=e305]: multisig.cell - - paragraph [ref=e306]: Witness checks for threshold-style locks. - - generic [ref=e307]: - - generic [ref=e308]: lock - - generic [ref=e309]: witness - - generic [ref=e310]: threshold - - link "timelock.cell Time-bound spending from Cell state. lock env timepoint" [ref=e311] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/timelock.cell - - heading "timelock.cell" [level=3] [ref=e312]: - - code [ref=e313]: timelock.cell - - paragraph [ref=e314]: Time-bound spending from Cell state. - - generic [ref=e315]: - - generic [ref=e316]: lock - - generic [ref=e317]: env - - generic [ref=e318]: timepoint - - contentinfo [ref=e319]: - - generic [ref=e320]: - - generic [ref=e321]: - - link "CellScript" [ref=e322] [cursor=pointer]: - - /url: "#top" - - generic [ref=e324]: CellScript - - paragraph [ref=e325]: A semantic DSL for CKB Cell-based smart contracts, with typed metadata and assurance by design. Docs, spec, examples, and source live with the repository. - - generic [ref=e326]: - - link "Docs" [ref=e327] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "Spec" [ref=e328] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "Examples" [ref=e329] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "Source" [ref=e330] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-02T05-49-42-482Z.yml b/.playwright-mcp/page-2026-06-02T05-49-42-482Z.yml deleted file mode 100644 index 73014772..00000000 --- a/.playwright-mcp/page-2026-06-02T05-49-42-482Z.yml +++ /dev/null @@ -1,298 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "Primary navigation" [ref=e3]: - - link "CellScript home" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link "Source" [ref=e8] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - button "Switch to dark mode" [pressed] [ref=e9] [cursor=pointer] - - main [ref=e12]: - - region "CellScript" [ref=e13]: - - generic [ref=e14]: - - heading "CellScript" [level=1] [ref=e15] - - paragraph [ref=e16]: Write Cell contracts as typed transitions, not raw wire format. - - generic [ref=e17]: - - link "Get started" [ref=e18] [cursor=pointer]: - - /url: "#getting-started" - - link "Core model" [ref=e19] [cursor=pointer]: - - /url: "#core-model" - - generic "CellScript contract surface" [ref=e20]: - - generic [ref=e21]: - - term [ref=e22]: target - - definition [ref=e23]: ckb-vm RISC-V - - generic [ref=e24]: - - term [ref=e25]: model - - definition [ref=e26]: schema-backed Cells - - generic [ref=e27]: - - term [ref=e28]: output - - definition [ref=e29]: metadata + ProofPlan - - generic [ref=e30]: - - generic [ref=e32]: token.cell - - combobox "Choose CellScript example" [ref=e34]: - - option "Fungible Token" [selected] - - option "NFT" - - option "AMM Pool" - - option "Vesting" - - tabpanel "Fungible Token" [ref=e36]: - - generic [ref=e37]: - - generic [ref=e38]: "1" - - generic [ref=e39]: module cellscript::fungible_token - - generic [ref=e40]: - - generic [ref=e41]: "2" - - generic [ref=e42]: // ... invariant and MintAuthority omitted - - generic [ref=e43]: - - generic [ref=e44]: "3" - - generic [ref=e45]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e46]: - - generic [ref=e47]: "4" - - generic [ref=e48]: "amount: u64," - - generic [ref=e49]: - - generic [ref=e50]: "5" - - generic [ref=e51]: "symbol: [u8; 8]," - - generic [ref=e52]: - - generic [ref=e53]: "6" - - generic [ref=e54]: "}" - - generic [ref=e56]: "7" - - generic [ref=e57]: - - generic [ref=e58]: "8" - - generic [ref=e59]: "action transfer_token(token: Token, to: Address) -> next_token: Token" - - generic [ref=e60]: - - generic [ref=e61]: "9" - - generic [ref=e62]: where - - generic [ref=e63]: - - generic [ref=e64]: "10" - - generic [ref=e65]: consume token - - generic [ref=e66]: - - generic [ref=e67]: "11" - - generic [ref=e68]: "create next_token = Token { amount: token.amount, symbol: token.symbol } with_lock(to)" - - generic [ref=e70]: "12" - - generic [ref=e71]: - - generic [ref=e72]: "13" - - generic [ref=e73]: "action burn(token: Token)" - - generic [ref=e74]: - - generic [ref=e75]: "14" - - generic [ref=e76]: where - - generic [ref=e77]: - - generic [ref=e78]: "15" - - generic [ref=e79]: assert(token.amount > 0, "cannot burn zero") - - generic [ref=e80]: - - generic [ref=e81]: "16" - - generic [ref=e82]: destroy token - - generic [ref=e83]: - - generic [ref=e84]: $ - - generic [ref=e85]: cellc examples/token.cell --target-profile ckb - - region "Getting Started" [ref=e86]: - - heading "Getting Started" [level=2] [ref=e87] - - generic [ref=e88]: - - article [ref=e89]: - - generic [ref=e90]: "1" - - heading "Install" [level=3] [ref=e91] - - paragraph [ref=e92]: - - code [ref=e93]: cargo install --path . - - article [ref=e94]: - - generic [ref=e95]: "2" - - heading "Compile" [level=3] [ref=e96] - - paragraph [ref=e97]: - - code [ref=e98]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e99]: - - generic [ref=e100]: "3" - - heading "Check" [level=3] [ref=e101] - - paragraph [ref=e102]: - - code [ref=e103]: cellc check --target-profile ckb - - region "Compiler Workflow" [ref=e104]: - - heading "Compiler Workflow" [level=2] [ref=e105] - - generic [ref=e106]: - - generic "Compiler workflow from .cell source to CKB artefact" [ref=e107]: - - article [ref=e108]: - - img [ref=e110]: - - generic [ref=e113]: .cell - - heading "CellScript source" [level=3] [ref=e114] - - img [ref=e116] - - article [ref=e118]: - - img [ref=e120] - - heading "Parse & check" [level=3] [ref=e124] - - paragraph [ref=e125]: Syntax, types, effects - - img [ref=e127] - - article [ref=e129]: - - img [ref=e131] - - heading "IR + Metadata" [level=3] [ref=e137] - - paragraph [ref=e138]: Typed model & assurance info - - img [ref=e140] - - article [ref=e142]: - - img [ref=e144] - - heading "Lower to RISC-V" [level=3] [ref=e148] - - paragraph [ref=e149]: ckb-vm codegen & optimisations - - img [ref=e151] - - article [ref=e153]: - - img [ref=e155]: - - generic [ref=e158]: .elf - - heading "ELF / Assembly" [level=3] [ref=e159] - - paragraph [ref=e160]: RISC-V artefacts for ckb-vm - - complementary "Build for CKB" [ref=e161]: - - heading "Build for CKB" [level=3] [ref=e162] - - list [ref=e163]: - - listitem [ref=e164]: - - img [ref=e165] - - generic [ref=e167]: ckb-vm compatible - - listitem [ref=e168]: - - img [ref=e169] - - generic [ref=e171]: Deterministic execution - - listitem [ref=e172]: - - img [ref=e173] - - generic [ref=e175]: Minimal syscalls - - listitem [ref=e176]: - - img [ref=e177] - - generic [ref=e179]: Scheduler-aware - - region "Core Model" [ref=e180]: - - heading "Core Model" [level=2] [ref=e181] - - paragraph [ref=e182]: "CellScript keeps the contract model visible: Cell shapes, effects, locks, flows, and review metadata stay in one typed surface." - - paragraph [ref=e183]: "Narrow by design: not a general-purpose runtime, not a new VM, not account storage in disguise." - - generic [ref=e184]: - - tablist "CellScript core primitives" [ref=e185]: - - tab "resource" [selected] [ref=e186] [cursor=pointer]: - - generic [ref=e187]: resource - - tab "shared" [ref=e188] [cursor=pointer]: - - generic [ref=e189]: shared - - tab "receipt" [ref=e190] [cursor=pointer]: - - generic [ref=e191]: receipt - - tab "action" [ref=e192] [cursor=pointer]: - - generic [ref=e193]: action - - tab "lock" [ref=e194] [cursor=pointer]: - - generic [ref=e195]: lock - - tab "flow" [ref=e196] [cursor=pointer]: - - generic [ref=e197]: flow - - tab "invariant" [ref=e198] [cursor=pointer]: - - generic [ref=e199]: invariant - - tab "struct / enum" [ref=e200] [cursor=pointer]: - - generic [ref=e201]: struct / enum - - tab "identity" [ref=e202] [cursor=pointer]: - - generic [ref=e203]: identity - - tabpanel "resource" [ref=e205]: - - paragraph [ref=e207]: Owned Cell state with explicit lifecycle effects. - - generic [ref=e208]: - - generic [ref=e209]: - - generic [ref=e210]: Example excerpt - - generic [ref=e211]: examples/token.cell - - generic [ref=e212]: "// examples/token.cell resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance Output" [ref=e213]: - - generic [ref=e214]: - - heading "Assurance Output" [level=2] [ref=e215] - - paragraph [ref=e216]: A local build emits review metadata. Treat the sidecar as useful evidence, not an authenticated proof outside the build boundary. - - article [ref=e218]: - - generic "Assurance output summary" [ref=e219]: - - article [ref=e220]: - - text: Schema - - strong [ref=e221]: v42 - - paragraph [ref=e222]: current compiler metadata schema - - article [ref=e223]: - - text: Source - - strong [ref=e224]: vesting.cell - - paragraph [ref=e225]: shared + receipt + flow - - article [ref=e226]: - - text: Boundary - - strong [ref=e227]: local sidecar - - paragraph [ref=e228]: validated; provenance required when shared - - group [ref=e229]: - - generic "- Metadata excerpt" [ref=e230] [cursor=pointer] - - generic [ref=e231]: "# Representative sidecar excerpt from a local build; keep provenance checks separate." - - generic [ref=e232]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": { \"VestingConfig\": { \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, \"VestingGrant\": { \"kind\": \"receipt\", \"flow\": \"Granted -> Claimable -> FullyClaimed\" } }, \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"status\": \"review evidence\", \"limit\": \"sidecar provenance must be checked outside the compiler\" } }" - - region "Tooling Surface" [ref=e233]: - - heading "Tooling Surface" [level=2] [ref=e234] - - generic [ref=e235]: - - tablist "CellScript tooling commands" [ref=e236]: - - tab "cellc metadata Read/write surface" [selected] [ref=e237] [cursor=pointer]: - - code [ref=e238]: cellc metadata - - generic [ref=e239]: Read/write surface - - tab "cellc constraints Transaction shape" [ref=e240] [cursor=pointer]: - - code [ref=e241]: cellc constraints - - generic [ref=e242]: Transaction shape - - tab "cellc audit-bundle Reviewer packet" [ref=e243] [cursor=pointer]: - - code [ref=e244]: cellc audit-bundle - - generic [ref=e245]: Reviewer packet - - tab "cellc lsp Editor feedback" [ref=e246] [cursor=pointer]: - - code [ref=e247]: cellc lsp - - generic [ref=e248]: Editor feedback - - tabpanel "cellc metadata Read/write surface" [ref=e250]: - - generic [ref=e251]: - - generic [ref=e252]: When - - paragraph [ref=e253]: Use before review or integration. - - generic [ref=e254]: Output - - paragraph [ref=e255]: Schema, effects, source hashes, target profile. - - generic [ref=e256]: - - code [ref=e257]: cellc metadata examples/vesting.cell --target-profile ckb --json - - generic [ref=e258]: "# Emit review metadata for a real example." - - region "Examples" [ref=e259]: - - heading "Examples" [level=2] [ref=e260] - - generic [ref=e261]: - - link "token.cell Mint, transfer, burn, and typed metadata. resource consume/create burn" [ref=e262] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - heading "token.cell" [level=3] [ref=e263]: - - code [ref=e264]: token.cell - - paragraph [ref=e265]: Mint, transfer, burn, and typed metadata. - - generic [ref=e266]: - - generic [ref=e267]: resource - - generic [ref=e268]: consume/create - - generic [ref=e269]: burn - - link "nft.cell Ownership transfer with preserve and relock. resource preserve relock" [ref=e270] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - heading "nft.cell" [level=3] [ref=e271]: - - code [ref=e272]: nft.cell - - paragraph [ref=e273]: Ownership transfer with preserve and relock. - - generic [ref=e274]: - - generic [ref=e275]: resource - - generic [ref=e276]: preserve - - generic [ref=e277]: relock - - link "amm_pool.cell Shared reserves with slippage checks. shared replace slippage" [ref=e278] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - heading "amm_pool.cell" [level=3] [ref=e279]: - - code [ref=e280]: amm_pool.cell - - paragraph [ref=e281]: Shared reserves with slippage checks. - - generic [ref=e282]: - - generic [ref=e283]: shared - - generic [ref=e284]: replace - - generic [ref=e285]: slippage - - link "vesting.cell Grant state flow into claimed output. flow transition receipt" [ref=e286] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - heading "vesting.cell" [level=3] [ref=e287]: - - code [ref=e288]: vesting.cell - - paragraph [ref=e289]: Grant state flow into claimed output. - - generic [ref=e290]: - - generic [ref=e291]: flow - - generic [ref=e292]: transition - - generic [ref=e293]: receipt - - link "multisig.cell Witness checks for threshold-style locks. lock witness threshold" [ref=e294] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/multisig.cell - - heading "multisig.cell" [level=3] [ref=e295]: - - code [ref=e296]: multisig.cell - - paragraph [ref=e297]: Witness checks for threshold-style locks. - - generic [ref=e298]: - - generic [ref=e299]: lock - - generic [ref=e300]: witness - - generic [ref=e301]: threshold - - link "timelock.cell Time-bound spending from Cell state. lock env timepoint" [ref=e302] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/timelock.cell - - heading "timelock.cell" [level=3] [ref=e303]: - - code [ref=e304]: timelock.cell - - paragraph [ref=e305]: Time-bound spending from Cell state. - - generic [ref=e306]: - - generic [ref=e307]: lock - - generic [ref=e308]: env - - generic [ref=e309]: timepoint - - contentinfo [ref=e310]: - - generic [ref=e311]: - - generic [ref=e312]: - - link "CellScript" [ref=e313] [cursor=pointer]: - - /url: "#top" - - generic [ref=e315]: CellScript - - paragraph [ref=e316]: A semantic DSL for CKB Cell-based smart contracts, with typed metadata and assurance by design. Docs, spec, examples, and source live with the repository. - - generic [ref=e317]: - - link "Docs" [ref=e318] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "Spec" [ref=e319] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "Examples" [ref=e320] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "Source" [ref=e321] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-02T05-51-04-536Z.yml b/.playwright-mcp/page-2026-06-02T05-51-04-536Z.yml deleted file mode 100644 index c63be6bb..00000000 --- a/.playwright-mcp/page-2026-06-02T05-51-04-536Z.yml +++ /dev/null @@ -1,307 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "Primary navigation" [ref=e3]: - - link "CellScript home" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link "Model" [ref=e8] [cursor=pointer]: - - /url: "#core-model" - - link "Assurance" [ref=e9] [cursor=pointer]: - - /url: "#assurance" - - link "Commands" [ref=e10] [cursor=pointer]: - - /url: "#tooling" - - link "Examples" [ref=e11] [cursor=pointer]: - - /url: "#examples" - - link "Source" [ref=e12] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - button "Switch to dark mode" [pressed] [ref=e13] [cursor=pointer]: - - generic [ref=e16]: Dark - - main [ref=e17]: - - region "CellScript" [ref=e18]: - - generic [ref=e19]: - - heading "CellScript" [level=1] [ref=e20] - - paragraph [ref=e21]: Write Cell contracts as typed transitions, not raw wire format. - - generic [ref=e22]: - - link "Get started" [ref=e23] [cursor=pointer]: - - /url: "#getting-started" - - link "Core model" [ref=e24] [cursor=pointer]: - - /url: "#core-model" - - generic "CellScript contract surface" [ref=e25]: - - generic [ref=e26]: - - term [ref=e27]: target - - definition [ref=e28]: ckb-vm RISC-V - - generic [ref=e29]: - - term [ref=e30]: model - - definition [ref=e31]: schema-backed Cells - - generic [ref=e32]: - - term [ref=e33]: output - - definition [ref=e34]: metadata + ProofPlan - - generic [ref=e35]: - - generic [ref=e37]: token.cell - - tablist "CellScript examples" [ref=e39]: - - tab "Fungible Token" [selected] [ref=e40] [cursor=pointer] - - tab "NFT" [ref=e41] [cursor=pointer] - - tab "AMM Pool" [ref=e42] [cursor=pointer] - - tab "Vesting" [ref=e43] [cursor=pointer] - - tabpanel "Fungible Token" [ref=e45]: - - generic [ref=e46]: - - generic [ref=e47]: "1" - - generic [ref=e48]: module cellscript::fungible_token - - generic [ref=e49]: - - generic [ref=e50]: "2" - - generic [ref=e51]: // ... invariant and MintAuthority omitted - - generic [ref=e52]: - - generic [ref=e53]: "3" - - generic [ref=e54]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e55]: - - generic [ref=e56]: "4" - - generic [ref=e57]: "amount: u64," - - generic [ref=e58]: - - generic [ref=e59]: "5" - - generic [ref=e60]: "symbol: [u8; 8]," - - generic [ref=e61]: - - generic [ref=e62]: "6" - - generic [ref=e63]: "}" - - generic [ref=e65]: "7" - - generic [ref=e66]: - - generic [ref=e67]: "8" - - generic [ref=e68]: "action transfer_token(token: Token, to: Address) -> next_token: Token" - - generic [ref=e69]: - - generic [ref=e70]: "9" - - generic [ref=e71]: where - - generic [ref=e72]: - - generic [ref=e73]: "10" - - generic [ref=e74]: consume token - - generic [ref=e75]: - - generic [ref=e76]: "11" - - generic [ref=e77]: "create next_token = Token { amount: token.amount, symbol: token.symbol } with_lock(to)" - - generic [ref=e79]: "12" - - generic [ref=e80]: - - generic [ref=e81]: "13" - - generic [ref=e82]: "action burn(token: Token)" - - generic [ref=e83]: - - generic [ref=e84]: "14" - - generic [ref=e85]: where - - generic [ref=e86]: - - generic [ref=e87]: "15" - - generic [ref=e88]: assert(token.amount > 0, "cannot burn zero") - - generic [ref=e89]: - - generic [ref=e90]: "16" - - generic [ref=e91]: destroy token - - generic [ref=e92]: - - generic [ref=e93]: $ - - generic [ref=e94]: cellc examples/token.cell --target-profile ckb - - region "Getting Started" [ref=e95]: - - heading "Getting Started" [level=2] [ref=e96] - - generic [ref=e97]: - - article [ref=e98]: - - generic [ref=e99]: "1" - - heading "Install" [level=3] [ref=e100] - - paragraph [ref=e101]: - - code [ref=e102]: cargo install --path . - - article [ref=e103]: - - generic [ref=e104]: "2" - - heading "Compile" [level=3] [ref=e105] - - paragraph [ref=e106]: - - code [ref=e107]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e108]: - - generic [ref=e109]: "3" - - heading "Check" [level=3] [ref=e110] - - paragraph [ref=e111]: - - code [ref=e112]: cellc check --target-profile ckb - - region "Compiler Workflow" [ref=e113]: - - heading "Compiler Workflow" [level=2] [ref=e114] - - generic [ref=e115]: - - generic "Compiler workflow from .cell source to CKB artefact" [ref=e116]: - - article [ref=e117]: - - img [ref=e119]: - - generic [ref=e122]: .cell - - heading "CellScript source" [level=3] [ref=e123] - - img [ref=e125] - - article [ref=e127]: - - img [ref=e129] - - heading "Parse & check" [level=3] [ref=e133] - - paragraph [ref=e134]: Syntax, types, effects - - img [ref=e136] - - article [ref=e138]: - - img [ref=e140] - - heading "IR + Metadata" [level=3] [ref=e146] - - paragraph [ref=e147]: Typed model & assurance info - - img [ref=e149] - - article [ref=e151]: - - img [ref=e153] - - heading "Lower to RISC-V" [level=3] [ref=e157] - - paragraph [ref=e158]: ckb-vm codegen & optimisations - - img [ref=e160] - - article [ref=e162]: - - img [ref=e164]: - - generic [ref=e167]: .elf - - heading "ELF / Assembly" [level=3] [ref=e168] - - paragraph [ref=e169]: RISC-V artefacts for ckb-vm - - complementary "Build for CKB" [ref=e170]: - - heading "Build for CKB" [level=3] [ref=e171] - - list [ref=e172]: - - listitem [ref=e173]: - - img [ref=e174] - - generic [ref=e176]: ckb-vm compatible - - listitem [ref=e177]: - - img [ref=e178] - - generic [ref=e180]: Deterministic execution - - listitem [ref=e181]: - - img [ref=e182] - - generic [ref=e184]: Minimal syscalls - - listitem [ref=e185]: - - img [ref=e186] - - generic [ref=e188]: Scheduler-aware - - region "Core Model" [ref=e189]: - - heading "Core Model" [level=2] [ref=e190] - - paragraph [ref=e191]: "CellScript keeps the contract model visible: Cell shapes, effects, locks, flows, and review metadata stay in one typed surface." - - paragraph [ref=e192]: "Narrow by design: not a general-purpose runtime, not a new VM, not account storage in disguise." - - generic [ref=e193]: - - tablist "CellScript core primitives" [ref=e194]: - - tab "resource" [selected] [ref=e195] [cursor=pointer]: - - generic [ref=e196]: resource - - tab "shared" [ref=e197] [cursor=pointer]: - - generic [ref=e198]: shared - - tab "receipt" [ref=e199] [cursor=pointer]: - - generic [ref=e200]: receipt - - tab "action" [ref=e201] [cursor=pointer]: - - generic [ref=e202]: action - - tab "lock" [ref=e203] [cursor=pointer]: - - generic [ref=e204]: lock - - tab "flow" [ref=e205] [cursor=pointer]: - - generic [ref=e206]: flow - - tab "invariant" [ref=e207] [cursor=pointer]: - - generic [ref=e208]: invariant - - tab "struct / enum" [ref=e209] [cursor=pointer]: - - generic [ref=e210]: struct / enum - - tab "identity" [ref=e211] [cursor=pointer]: - - generic [ref=e212]: identity - - tabpanel "resource" [ref=e214]: - - paragraph [ref=e216]: Owned Cell state with explicit lifecycle effects. - - generic [ref=e217]: - - generic [ref=e218]: - - generic [ref=e219]: Example excerpt - - generic [ref=e220]: examples/token.cell - - generic [ref=e221]: "// examples/token.cell resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance Output" [ref=e222]: - - generic [ref=e223]: - - heading "Assurance Output" [level=2] [ref=e224] - - paragraph [ref=e225]: A local build emits review metadata. Treat the sidecar as useful evidence, not an authenticated proof outside the build boundary. - - article [ref=e227]: - - generic "Assurance output summary" [ref=e228]: - - article [ref=e229]: - - text: Schema - - strong [ref=e230]: v42 - - paragraph [ref=e231]: current compiler metadata schema - - article [ref=e232]: - - text: Source - - strong [ref=e233]: vesting.cell - - paragraph [ref=e234]: shared + receipt + flow - - article [ref=e235]: - - text: Boundary - - strong [ref=e236]: local sidecar - - paragraph [ref=e237]: validated; provenance required when shared - - group [ref=e238]: - - generic "- Metadata excerpt" [ref=e239] [cursor=pointer] - - generic [ref=e240]: "# Representative sidecar excerpt from a local build; keep provenance checks separate." - - generic [ref=e241]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": { \"VestingConfig\": { \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, \"VestingGrant\": { \"kind\": \"receipt\", \"flow\": \"Granted -> Claimable -> FullyClaimed\" } }, \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"status\": \"review evidence\", \"limit\": \"sidecar provenance must be checked outside the compiler\" } }" - - region "Tooling Surface" [ref=e242]: - - heading "Tooling Surface" [level=2] [ref=e243] - - generic [ref=e244]: - - tablist "CellScript tooling commands" [ref=e245]: - - tab "cellc metadata Read/write surface" [selected] [ref=e246] [cursor=pointer]: - - code [ref=e247]: cellc metadata - - generic [ref=e248]: Read/write surface - - tab "cellc constraints Transaction shape" [ref=e249] [cursor=pointer]: - - code [ref=e250]: cellc constraints - - generic [ref=e251]: Transaction shape - - tab "cellc audit-bundle Reviewer packet" [ref=e252] [cursor=pointer]: - - code [ref=e253]: cellc audit-bundle - - generic [ref=e254]: Reviewer packet - - tab "cellc lsp Editor feedback" [ref=e255] [cursor=pointer]: - - code [ref=e256]: cellc lsp - - generic [ref=e257]: Editor feedback - - tabpanel "cellc metadata Read/write surface" [ref=e259]: - - generic [ref=e260]: - - generic [ref=e261]: When - - paragraph [ref=e262]: Use before review or integration. - - generic [ref=e263]: Output - - paragraph [ref=e264]: Schema, effects, source hashes, target profile. - - generic [ref=e265]: - - code [ref=e266]: cellc metadata examples/vesting.cell --target-profile ckb --json - - generic [ref=e267]: "# Emit review metadata for a real example." - - region "Examples" [ref=e268]: - - heading "Examples" [level=2] [ref=e269] - - generic [ref=e270]: - - link "token.cell Mint, transfer, burn, and typed metadata. resource consume/create burn" [ref=e271] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - heading "token.cell" [level=3] [ref=e272]: - - code [ref=e273]: token.cell - - paragraph [ref=e274]: Mint, transfer, burn, and typed metadata. - - generic [ref=e275]: - - generic [ref=e276]: resource - - generic [ref=e277]: consume/create - - generic [ref=e278]: burn - - link "nft.cell Ownership transfer with preserve and relock. resource preserve relock" [ref=e279] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - heading "nft.cell" [level=3] [ref=e280]: - - code [ref=e281]: nft.cell - - paragraph [ref=e282]: Ownership transfer with preserve and relock. - - generic [ref=e283]: - - generic [ref=e284]: resource - - generic [ref=e285]: preserve - - generic [ref=e286]: relock - - link "amm_pool.cell Shared reserves with slippage checks. shared replace slippage" [ref=e287] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - heading "amm_pool.cell" [level=3] [ref=e288]: - - code [ref=e289]: amm_pool.cell - - paragraph [ref=e290]: Shared reserves with slippage checks. - - generic [ref=e291]: - - generic [ref=e292]: shared - - generic [ref=e293]: replace - - generic [ref=e294]: slippage - - link "vesting.cell Grant state flow into claimed output. flow transition receipt" [ref=e295] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - heading "vesting.cell" [level=3] [ref=e296]: - - code [ref=e297]: vesting.cell - - paragraph [ref=e298]: Grant state flow into claimed output. - - generic [ref=e299]: - - generic [ref=e300]: flow - - generic [ref=e301]: transition - - generic [ref=e302]: receipt - - link "multisig.cell Witness checks for threshold-style locks. lock witness threshold" [ref=e303] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/multisig.cell - - heading "multisig.cell" [level=3] [ref=e304]: - - code [ref=e305]: multisig.cell - - paragraph [ref=e306]: Witness checks for threshold-style locks. - - generic [ref=e307]: - - generic [ref=e308]: lock - - generic [ref=e309]: witness - - generic [ref=e310]: threshold - - link "timelock.cell Time-bound spending from Cell state. lock env timepoint" [ref=e311] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/timelock.cell - - heading "timelock.cell" [level=3] [ref=e312]: - - code [ref=e313]: timelock.cell - - paragraph [ref=e314]: Time-bound spending from Cell state. - - generic [ref=e315]: - - generic [ref=e316]: lock - - generic [ref=e317]: env - - generic [ref=e318]: timepoint - - contentinfo [ref=e319]: - - generic [ref=e320]: - - generic [ref=e321]: - - link "CellScript" [ref=e322] [cursor=pointer]: - - /url: "#top" - - generic [ref=e324]: CellScript - - paragraph [ref=e325]: A semantic DSL for CKB Cell-based smart contracts, with typed metadata and assurance by design. Docs, spec, examples, and source live with the repository. - - generic [ref=e326]: - - link "Docs" [ref=e327] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "Spec" [ref=e328] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "Examples" [ref=e329] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "Source" [ref=e330] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-02T05-52-35-366Z.yml b/.playwright-mcp/page-2026-06-02T05-52-35-366Z.yml deleted file mode 100644 index c63be6bb..00000000 --- a/.playwright-mcp/page-2026-06-02T05-52-35-366Z.yml +++ /dev/null @@ -1,307 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "Primary navigation" [ref=e3]: - - link "CellScript home" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link "Model" [ref=e8] [cursor=pointer]: - - /url: "#core-model" - - link "Assurance" [ref=e9] [cursor=pointer]: - - /url: "#assurance" - - link "Commands" [ref=e10] [cursor=pointer]: - - /url: "#tooling" - - link "Examples" [ref=e11] [cursor=pointer]: - - /url: "#examples" - - link "Source" [ref=e12] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - button "Switch to dark mode" [pressed] [ref=e13] [cursor=pointer]: - - generic [ref=e16]: Dark - - main [ref=e17]: - - region "CellScript" [ref=e18]: - - generic [ref=e19]: - - heading "CellScript" [level=1] [ref=e20] - - paragraph [ref=e21]: Write Cell contracts as typed transitions, not raw wire format. - - generic [ref=e22]: - - link "Get started" [ref=e23] [cursor=pointer]: - - /url: "#getting-started" - - link "Core model" [ref=e24] [cursor=pointer]: - - /url: "#core-model" - - generic "CellScript contract surface" [ref=e25]: - - generic [ref=e26]: - - term [ref=e27]: target - - definition [ref=e28]: ckb-vm RISC-V - - generic [ref=e29]: - - term [ref=e30]: model - - definition [ref=e31]: schema-backed Cells - - generic [ref=e32]: - - term [ref=e33]: output - - definition [ref=e34]: metadata + ProofPlan - - generic [ref=e35]: - - generic [ref=e37]: token.cell - - tablist "CellScript examples" [ref=e39]: - - tab "Fungible Token" [selected] [ref=e40] [cursor=pointer] - - tab "NFT" [ref=e41] [cursor=pointer] - - tab "AMM Pool" [ref=e42] [cursor=pointer] - - tab "Vesting" [ref=e43] [cursor=pointer] - - tabpanel "Fungible Token" [ref=e45]: - - generic [ref=e46]: - - generic [ref=e47]: "1" - - generic [ref=e48]: module cellscript::fungible_token - - generic [ref=e49]: - - generic [ref=e50]: "2" - - generic [ref=e51]: // ... invariant and MintAuthority omitted - - generic [ref=e52]: - - generic [ref=e53]: "3" - - generic [ref=e54]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e55]: - - generic [ref=e56]: "4" - - generic [ref=e57]: "amount: u64," - - generic [ref=e58]: - - generic [ref=e59]: "5" - - generic [ref=e60]: "symbol: [u8; 8]," - - generic [ref=e61]: - - generic [ref=e62]: "6" - - generic [ref=e63]: "}" - - generic [ref=e65]: "7" - - generic [ref=e66]: - - generic [ref=e67]: "8" - - generic [ref=e68]: "action transfer_token(token: Token, to: Address) -> next_token: Token" - - generic [ref=e69]: - - generic [ref=e70]: "9" - - generic [ref=e71]: where - - generic [ref=e72]: - - generic [ref=e73]: "10" - - generic [ref=e74]: consume token - - generic [ref=e75]: - - generic [ref=e76]: "11" - - generic [ref=e77]: "create next_token = Token { amount: token.amount, symbol: token.symbol } with_lock(to)" - - generic [ref=e79]: "12" - - generic [ref=e80]: - - generic [ref=e81]: "13" - - generic [ref=e82]: "action burn(token: Token)" - - generic [ref=e83]: - - generic [ref=e84]: "14" - - generic [ref=e85]: where - - generic [ref=e86]: - - generic [ref=e87]: "15" - - generic [ref=e88]: assert(token.amount > 0, "cannot burn zero") - - generic [ref=e89]: - - generic [ref=e90]: "16" - - generic [ref=e91]: destroy token - - generic [ref=e92]: - - generic [ref=e93]: $ - - generic [ref=e94]: cellc examples/token.cell --target-profile ckb - - region "Getting Started" [ref=e95]: - - heading "Getting Started" [level=2] [ref=e96] - - generic [ref=e97]: - - article [ref=e98]: - - generic [ref=e99]: "1" - - heading "Install" [level=3] [ref=e100] - - paragraph [ref=e101]: - - code [ref=e102]: cargo install --path . - - article [ref=e103]: - - generic [ref=e104]: "2" - - heading "Compile" [level=3] [ref=e105] - - paragraph [ref=e106]: - - code [ref=e107]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e108]: - - generic [ref=e109]: "3" - - heading "Check" [level=3] [ref=e110] - - paragraph [ref=e111]: - - code [ref=e112]: cellc check --target-profile ckb - - region "Compiler Workflow" [ref=e113]: - - heading "Compiler Workflow" [level=2] [ref=e114] - - generic [ref=e115]: - - generic "Compiler workflow from .cell source to CKB artefact" [ref=e116]: - - article [ref=e117]: - - img [ref=e119]: - - generic [ref=e122]: .cell - - heading "CellScript source" [level=3] [ref=e123] - - img [ref=e125] - - article [ref=e127]: - - img [ref=e129] - - heading "Parse & check" [level=3] [ref=e133] - - paragraph [ref=e134]: Syntax, types, effects - - img [ref=e136] - - article [ref=e138]: - - img [ref=e140] - - heading "IR + Metadata" [level=3] [ref=e146] - - paragraph [ref=e147]: Typed model & assurance info - - img [ref=e149] - - article [ref=e151]: - - img [ref=e153] - - heading "Lower to RISC-V" [level=3] [ref=e157] - - paragraph [ref=e158]: ckb-vm codegen & optimisations - - img [ref=e160] - - article [ref=e162]: - - img [ref=e164]: - - generic [ref=e167]: .elf - - heading "ELF / Assembly" [level=3] [ref=e168] - - paragraph [ref=e169]: RISC-V artefacts for ckb-vm - - complementary "Build for CKB" [ref=e170]: - - heading "Build for CKB" [level=3] [ref=e171] - - list [ref=e172]: - - listitem [ref=e173]: - - img [ref=e174] - - generic [ref=e176]: ckb-vm compatible - - listitem [ref=e177]: - - img [ref=e178] - - generic [ref=e180]: Deterministic execution - - listitem [ref=e181]: - - img [ref=e182] - - generic [ref=e184]: Minimal syscalls - - listitem [ref=e185]: - - img [ref=e186] - - generic [ref=e188]: Scheduler-aware - - region "Core Model" [ref=e189]: - - heading "Core Model" [level=2] [ref=e190] - - paragraph [ref=e191]: "CellScript keeps the contract model visible: Cell shapes, effects, locks, flows, and review metadata stay in one typed surface." - - paragraph [ref=e192]: "Narrow by design: not a general-purpose runtime, not a new VM, not account storage in disguise." - - generic [ref=e193]: - - tablist "CellScript core primitives" [ref=e194]: - - tab "resource" [selected] [ref=e195] [cursor=pointer]: - - generic [ref=e196]: resource - - tab "shared" [ref=e197] [cursor=pointer]: - - generic [ref=e198]: shared - - tab "receipt" [ref=e199] [cursor=pointer]: - - generic [ref=e200]: receipt - - tab "action" [ref=e201] [cursor=pointer]: - - generic [ref=e202]: action - - tab "lock" [ref=e203] [cursor=pointer]: - - generic [ref=e204]: lock - - tab "flow" [ref=e205] [cursor=pointer]: - - generic [ref=e206]: flow - - tab "invariant" [ref=e207] [cursor=pointer]: - - generic [ref=e208]: invariant - - tab "struct / enum" [ref=e209] [cursor=pointer]: - - generic [ref=e210]: struct / enum - - tab "identity" [ref=e211] [cursor=pointer]: - - generic [ref=e212]: identity - - tabpanel "resource" [ref=e214]: - - paragraph [ref=e216]: Owned Cell state with explicit lifecycle effects. - - generic [ref=e217]: - - generic [ref=e218]: - - generic [ref=e219]: Example excerpt - - generic [ref=e220]: examples/token.cell - - generic [ref=e221]: "// examples/token.cell resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance Output" [ref=e222]: - - generic [ref=e223]: - - heading "Assurance Output" [level=2] [ref=e224] - - paragraph [ref=e225]: A local build emits review metadata. Treat the sidecar as useful evidence, not an authenticated proof outside the build boundary. - - article [ref=e227]: - - generic "Assurance output summary" [ref=e228]: - - article [ref=e229]: - - text: Schema - - strong [ref=e230]: v42 - - paragraph [ref=e231]: current compiler metadata schema - - article [ref=e232]: - - text: Source - - strong [ref=e233]: vesting.cell - - paragraph [ref=e234]: shared + receipt + flow - - article [ref=e235]: - - text: Boundary - - strong [ref=e236]: local sidecar - - paragraph [ref=e237]: validated; provenance required when shared - - group [ref=e238]: - - generic "- Metadata excerpt" [ref=e239] [cursor=pointer] - - generic [ref=e240]: "# Representative sidecar excerpt from a local build; keep provenance checks separate." - - generic [ref=e241]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": { \"VestingConfig\": { \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, \"VestingGrant\": { \"kind\": \"receipt\", \"flow\": \"Granted -> Claimable -> FullyClaimed\" } }, \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"status\": \"review evidence\", \"limit\": \"sidecar provenance must be checked outside the compiler\" } }" - - region "Tooling Surface" [ref=e242]: - - heading "Tooling Surface" [level=2] [ref=e243] - - generic [ref=e244]: - - tablist "CellScript tooling commands" [ref=e245]: - - tab "cellc metadata Read/write surface" [selected] [ref=e246] [cursor=pointer]: - - code [ref=e247]: cellc metadata - - generic [ref=e248]: Read/write surface - - tab "cellc constraints Transaction shape" [ref=e249] [cursor=pointer]: - - code [ref=e250]: cellc constraints - - generic [ref=e251]: Transaction shape - - tab "cellc audit-bundle Reviewer packet" [ref=e252] [cursor=pointer]: - - code [ref=e253]: cellc audit-bundle - - generic [ref=e254]: Reviewer packet - - tab "cellc lsp Editor feedback" [ref=e255] [cursor=pointer]: - - code [ref=e256]: cellc lsp - - generic [ref=e257]: Editor feedback - - tabpanel "cellc metadata Read/write surface" [ref=e259]: - - generic [ref=e260]: - - generic [ref=e261]: When - - paragraph [ref=e262]: Use before review or integration. - - generic [ref=e263]: Output - - paragraph [ref=e264]: Schema, effects, source hashes, target profile. - - generic [ref=e265]: - - code [ref=e266]: cellc metadata examples/vesting.cell --target-profile ckb --json - - generic [ref=e267]: "# Emit review metadata for a real example." - - region "Examples" [ref=e268]: - - heading "Examples" [level=2] [ref=e269] - - generic [ref=e270]: - - link "token.cell Mint, transfer, burn, and typed metadata. resource consume/create burn" [ref=e271] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - heading "token.cell" [level=3] [ref=e272]: - - code [ref=e273]: token.cell - - paragraph [ref=e274]: Mint, transfer, burn, and typed metadata. - - generic [ref=e275]: - - generic [ref=e276]: resource - - generic [ref=e277]: consume/create - - generic [ref=e278]: burn - - link "nft.cell Ownership transfer with preserve and relock. resource preserve relock" [ref=e279] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - heading "nft.cell" [level=3] [ref=e280]: - - code [ref=e281]: nft.cell - - paragraph [ref=e282]: Ownership transfer with preserve and relock. - - generic [ref=e283]: - - generic [ref=e284]: resource - - generic [ref=e285]: preserve - - generic [ref=e286]: relock - - link "amm_pool.cell Shared reserves with slippage checks. shared replace slippage" [ref=e287] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - heading "amm_pool.cell" [level=3] [ref=e288]: - - code [ref=e289]: amm_pool.cell - - paragraph [ref=e290]: Shared reserves with slippage checks. - - generic [ref=e291]: - - generic [ref=e292]: shared - - generic [ref=e293]: replace - - generic [ref=e294]: slippage - - link "vesting.cell Grant state flow into claimed output. flow transition receipt" [ref=e295] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - heading "vesting.cell" [level=3] [ref=e296]: - - code [ref=e297]: vesting.cell - - paragraph [ref=e298]: Grant state flow into claimed output. - - generic [ref=e299]: - - generic [ref=e300]: flow - - generic [ref=e301]: transition - - generic [ref=e302]: receipt - - link "multisig.cell Witness checks for threshold-style locks. lock witness threshold" [ref=e303] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/multisig.cell - - heading "multisig.cell" [level=3] [ref=e304]: - - code [ref=e305]: multisig.cell - - paragraph [ref=e306]: Witness checks for threshold-style locks. - - generic [ref=e307]: - - generic [ref=e308]: lock - - generic [ref=e309]: witness - - generic [ref=e310]: threshold - - link "timelock.cell Time-bound spending from Cell state. lock env timepoint" [ref=e311] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/timelock.cell - - heading "timelock.cell" [level=3] [ref=e312]: - - code [ref=e313]: timelock.cell - - paragraph [ref=e314]: Time-bound spending from Cell state. - - generic [ref=e315]: - - generic [ref=e316]: lock - - generic [ref=e317]: env - - generic [ref=e318]: timepoint - - contentinfo [ref=e319]: - - generic [ref=e320]: - - generic [ref=e321]: - - link "CellScript" [ref=e322] [cursor=pointer]: - - /url: "#top" - - generic [ref=e324]: CellScript - - paragraph [ref=e325]: A semantic DSL for CKB Cell-based smart contracts, with typed metadata and assurance by design. Docs, spec, examples, and source live with the repository. - - generic [ref=e326]: - - link "Docs" [ref=e327] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "Spec" [ref=e328] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "Examples" [ref=e329] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "Source" [ref=e330] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-02T05-54-02-720Z.yml b/.playwright-mcp/page-2026-06-02T05-54-02-720Z.yml deleted file mode 100644 index c63be6bb..00000000 --- a/.playwright-mcp/page-2026-06-02T05-54-02-720Z.yml +++ /dev/null @@ -1,307 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "Primary navigation" [ref=e3]: - - link "CellScript home" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link "Model" [ref=e8] [cursor=pointer]: - - /url: "#core-model" - - link "Assurance" [ref=e9] [cursor=pointer]: - - /url: "#assurance" - - link "Commands" [ref=e10] [cursor=pointer]: - - /url: "#tooling" - - link "Examples" [ref=e11] [cursor=pointer]: - - /url: "#examples" - - link "Source" [ref=e12] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - button "Switch to dark mode" [pressed] [ref=e13] [cursor=pointer]: - - generic [ref=e16]: Dark - - main [ref=e17]: - - region "CellScript" [ref=e18]: - - generic [ref=e19]: - - heading "CellScript" [level=1] [ref=e20] - - paragraph [ref=e21]: Write Cell contracts as typed transitions, not raw wire format. - - generic [ref=e22]: - - link "Get started" [ref=e23] [cursor=pointer]: - - /url: "#getting-started" - - link "Core model" [ref=e24] [cursor=pointer]: - - /url: "#core-model" - - generic "CellScript contract surface" [ref=e25]: - - generic [ref=e26]: - - term [ref=e27]: target - - definition [ref=e28]: ckb-vm RISC-V - - generic [ref=e29]: - - term [ref=e30]: model - - definition [ref=e31]: schema-backed Cells - - generic [ref=e32]: - - term [ref=e33]: output - - definition [ref=e34]: metadata + ProofPlan - - generic [ref=e35]: - - generic [ref=e37]: token.cell - - tablist "CellScript examples" [ref=e39]: - - tab "Fungible Token" [selected] [ref=e40] [cursor=pointer] - - tab "NFT" [ref=e41] [cursor=pointer] - - tab "AMM Pool" [ref=e42] [cursor=pointer] - - tab "Vesting" [ref=e43] [cursor=pointer] - - tabpanel "Fungible Token" [ref=e45]: - - generic [ref=e46]: - - generic [ref=e47]: "1" - - generic [ref=e48]: module cellscript::fungible_token - - generic [ref=e49]: - - generic [ref=e50]: "2" - - generic [ref=e51]: // ... invariant and MintAuthority omitted - - generic [ref=e52]: - - generic [ref=e53]: "3" - - generic [ref=e54]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e55]: - - generic [ref=e56]: "4" - - generic [ref=e57]: "amount: u64," - - generic [ref=e58]: - - generic [ref=e59]: "5" - - generic [ref=e60]: "symbol: [u8; 8]," - - generic [ref=e61]: - - generic [ref=e62]: "6" - - generic [ref=e63]: "}" - - generic [ref=e65]: "7" - - generic [ref=e66]: - - generic [ref=e67]: "8" - - generic [ref=e68]: "action transfer_token(token: Token, to: Address) -> next_token: Token" - - generic [ref=e69]: - - generic [ref=e70]: "9" - - generic [ref=e71]: where - - generic [ref=e72]: - - generic [ref=e73]: "10" - - generic [ref=e74]: consume token - - generic [ref=e75]: - - generic [ref=e76]: "11" - - generic [ref=e77]: "create next_token = Token { amount: token.amount, symbol: token.symbol } with_lock(to)" - - generic [ref=e79]: "12" - - generic [ref=e80]: - - generic [ref=e81]: "13" - - generic [ref=e82]: "action burn(token: Token)" - - generic [ref=e83]: - - generic [ref=e84]: "14" - - generic [ref=e85]: where - - generic [ref=e86]: - - generic [ref=e87]: "15" - - generic [ref=e88]: assert(token.amount > 0, "cannot burn zero") - - generic [ref=e89]: - - generic [ref=e90]: "16" - - generic [ref=e91]: destroy token - - generic [ref=e92]: - - generic [ref=e93]: $ - - generic [ref=e94]: cellc examples/token.cell --target-profile ckb - - region "Getting Started" [ref=e95]: - - heading "Getting Started" [level=2] [ref=e96] - - generic [ref=e97]: - - article [ref=e98]: - - generic [ref=e99]: "1" - - heading "Install" [level=3] [ref=e100] - - paragraph [ref=e101]: - - code [ref=e102]: cargo install --path . - - article [ref=e103]: - - generic [ref=e104]: "2" - - heading "Compile" [level=3] [ref=e105] - - paragraph [ref=e106]: - - code [ref=e107]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e108]: - - generic [ref=e109]: "3" - - heading "Check" [level=3] [ref=e110] - - paragraph [ref=e111]: - - code [ref=e112]: cellc check --target-profile ckb - - region "Compiler Workflow" [ref=e113]: - - heading "Compiler Workflow" [level=2] [ref=e114] - - generic [ref=e115]: - - generic "Compiler workflow from .cell source to CKB artefact" [ref=e116]: - - article [ref=e117]: - - img [ref=e119]: - - generic [ref=e122]: .cell - - heading "CellScript source" [level=3] [ref=e123] - - img [ref=e125] - - article [ref=e127]: - - img [ref=e129] - - heading "Parse & check" [level=3] [ref=e133] - - paragraph [ref=e134]: Syntax, types, effects - - img [ref=e136] - - article [ref=e138]: - - img [ref=e140] - - heading "IR + Metadata" [level=3] [ref=e146] - - paragraph [ref=e147]: Typed model & assurance info - - img [ref=e149] - - article [ref=e151]: - - img [ref=e153] - - heading "Lower to RISC-V" [level=3] [ref=e157] - - paragraph [ref=e158]: ckb-vm codegen & optimisations - - img [ref=e160] - - article [ref=e162]: - - img [ref=e164]: - - generic [ref=e167]: .elf - - heading "ELF / Assembly" [level=3] [ref=e168] - - paragraph [ref=e169]: RISC-V artefacts for ckb-vm - - complementary "Build for CKB" [ref=e170]: - - heading "Build for CKB" [level=3] [ref=e171] - - list [ref=e172]: - - listitem [ref=e173]: - - img [ref=e174] - - generic [ref=e176]: ckb-vm compatible - - listitem [ref=e177]: - - img [ref=e178] - - generic [ref=e180]: Deterministic execution - - listitem [ref=e181]: - - img [ref=e182] - - generic [ref=e184]: Minimal syscalls - - listitem [ref=e185]: - - img [ref=e186] - - generic [ref=e188]: Scheduler-aware - - region "Core Model" [ref=e189]: - - heading "Core Model" [level=2] [ref=e190] - - paragraph [ref=e191]: "CellScript keeps the contract model visible: Cell shapes, effects, locks, flows, and review metadata stay in one typed surface." - - paragraph [ref=e192]: "Narrow by design: not a general-purpose runtime, not a new VM, not account storage in disguise." - - generic [ref=e193]: - - tablist "CellScript core primitives" [ref=e194]: - - tab "resource" [selected] [ref=e195] [cursor=pointer]: - - generic [ref=e196]: resource - - tab "shared" [ref=e197] [cursor=pointer]: - - generic [ref=e198]: shared - - tab "receipt" [ref=e199] [cursor=pointer]: - - generic [ref=e200]: receipt - - tab "action" [ref=e201] [cursor=pointer]: - - generic [ref=e202]: action - - tab "lock" [ref=e203] [cursor=pointer]: - - generic [ref=e204]: lock - - tab "flow" [ref=e205] [cursor=pointer]: - - generic [ref=e206]: flow - - tab "invariant" [ref=e207] [cursor=pointer]: - - generic [ref=e208]: invariant - - tab "struct / enum" [ref=e209] [cursor=pointer]: - - generic [ref=e210]: struct / enum - - tab "identity" [ref=e211] [cursor=pointer]: - - generic [ref=e212]: identity - - tabpanel "resource" [ref=e214]: - - paragraph [ref=e216]: Owned Cell state with explicit lifecycle effects. - - generic [ref=e217]: - - generic [ref=e218]: - - generic [ref=e219]: Example excerpt - - generic [ref=e220]: examples/token.cell - - generic [ref=e221]: "// examples/token.cell resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance Output" [ref=e222]: - - generic [ref=e223]: - - heading "Assurance Output" [level=2] [ref=e224] - - paragraph [ref=e225]: A local build emits review metadata. Treat the sidecar as useful evidence, not an authenticated proof outside the build boundary. - - article [ref=e227]: - - generic "Assurance output summary" [ref=e228]: - - article [ref=e229]: - - text: Schema - - strong [ref=e230]: v42 - - paragraph [ref=e231]: current compiler metadata schema - - article [ref=e232]: - - text: Source - - strong [ref=e233]: vesting.cell - - paragraph [ref=e234]: shared + receipt + flow - - article [ref=e235]: - - text: Boundary - - strong [ref=e236]: local sidecar - - paragraph [ref=e237]: validated; provenance required when shared - - group [ref=e238]: - - generic "- Metadata excerpt" [ref=e239] [cursor=pointer] - - generic [ref=e240]: "# Representative sidecar excerpt from a local build; keep provenance checks separate." - - generic [ref=e241]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": { \"VestingConfig\": { \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, \"VestingGrant\": { \"kind\": \"receipt\", \"flow\": \"Granted -> Claimable -> FullyClaimed\" } }, \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"status\": \"review evidence\", \"limit\": \"sidecar provenance must be checked outside the compiler\" } }" - - region "Tooling Surface" [ref=e242]: - - heading "Tooling Surface" [level=2] [ref=e243] - - generic [ref=e244]: - - tablist "CellScript tooling commands" [ref=e245]: - - tab "cellc metadata Read/write surface" [selected] [ref=e246] [cursor=pointer]: - - code [ref=e247]: cellc metadata - - generic [ref=e248]: Read/write surface - - tab "cellc constraints Transaction shape" [ref=e249] [cursor=pointer]: - - code [ref=e250]: cellc constraints - - generic [ref=e251]: Transaction shape - - tab "cellc audit-bundle Reviewer packet" [ref=e252] [cursor=pointer]: - - code [ref=e253]: cellc audit-bundle - - generic [ref=e254]: Reviewer packet - - tab "cellc lsp Editor feedback" [ref=e255] [cursor=pointer]: - - code [ref=e256]: cellc lsp - - generic [ref=e257]: Editor feedback - - tabpanel "cellc metadata Read/write surface" [ref=e259]: - - generic [ref=e260]: - - generic [ref=e261]: When - - paragraph [ref=e262]: Use before review or integration. - - generic [ref=e263]: Output - - paragraph [ref=e264]: Schema, effects, source hashes, target profile. - - generic [ref=e265]: - - code [ref=e266]: cellc metadata examples/vesting.cell --target-profile ckb --json - - generic [ref=e267]: "# Emit review metadata for a real example." - - region "Examples" [ref=e268]: - - heading "Examples" [level=2] [ref=e269] - - generic [ref=e270]: - - link "token.cell Mint, transfer, burn, and typed metadata. resource consume/create burn" [ref=e271] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - heading "token.cell" [level=3] [ref=e272]: - - code [ref=e273]: token.cell - - paragraph [ref=e274]: Mint, transfer, burn, and typed metadata. - - generic [ref=e275]: - - generic [ref=e276]: resource - - generic [ref=e277]: consume/create - - generic [ref=e278]: burn - - link "nft.cell Ownership transfer with preserve and relock. resource preserve relock" [ref=e279] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - heading "nft.cell" [level=3] [ref=e280]: - - code [ref=e281]: nft.cell - - paragraph [ref=e282]: Ownership transfer with preserve and relock. - - generic [ref=e283]: - - generic [ref=e284]: resource - - generic [ref=e285]: preserve - - generic [ref=e286]: relock - - link "amm_pool.cell Shared reserves with slippage checks. shared replace slippage" [ref=e287] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - heading "amm_pool.cell" [level=3] [ref=e288]: - - code [ref=e289]: amm_pool.cell - - paragraph [ref=e290]: Shared reserves with slippage checks. - - generic [ref=e291]: - - generic [ref=e292]: shared - - generic [ref=e293]: replace - - generic [ref=e294]: slippage - - link "vesting.cell Grant state flow into claimed output. flow transition receipt" [ref=e295] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - heading "vesting.cell" [level=3] [ref=e296]: - - code [ref=e297]: vesting.cell - - paragraph [ref=e298]: Grant state flow into claimed output. - - generic [ref=e299]: - - generic [ref=e300]: flow - - generic [ref=e301]: transition - - generic [ref=e302]: receipt - - link "multisig.cell Witness checks for threshold-style locks. lock witness threshold" [ref=e303] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/multisig.cell - - heading "multisig.cell" [level=3] [ref=e304]: - - code [ref=e305]: multisig.cell - - paragraph [ref=e306]: Witness checks for threshold-style locks. - - generic [ref=e307]: - - generic [ref=e308]: lock - - generic [ref=e309]: witness - - generic [ref=e310]: threshold - - link "timelock.cell Time-bound spending from Cell state. lock env timepoint" [ref=e311] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/timelock.cell - - heading "timelock.cell" [level=3] [ref=e312]: - - code [ref=e313]: timelock.cell - - paragraph [ref=e314]: Time-bound spending from Cell state. - - generic [ref=e315]: - - generic [ref=e316]: lock - - generic [ref=e317]: env - - generic [ref=e318]: timepoint - - contentinfo [ref=e319]: - - generic [ref=e320]: - - generic [ref=e321]: - - link "CellScript" [ref=e322] [cursor=pointer]: - - /url: "#top" - - generic [ref=e324]: CellScript - - paragraph [ref=e325]: A semantic DSL for CKB Cell-based smart contracts, with typed metadata and assurance by design. Docs, spec, examples, and source live with the repository. - - generic [ref=e326]: - - link "Docs" [ref=e327] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "Spec" [ref=e328] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "Examples" [ref=e329] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "Source" [ref=e330] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-02T06-00-24-008Z.yml b/.playwright-mcp/page-2026-06-02T06-00-24-008Z.yml deleted file mode 100644 index c63be6bb..00000000 --- a/.playwright-mcp/page-2026-06-02T06-00-24-008Z.yml +++ /dev/null @@ -1,307 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "Primary navigation" [ref=e3]: - - link "CellScript home" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link "Model" [ref=e8] [cursor=pointer]: - - /url: "#core-model" - - link "Assurance" [ref=e9] [cursor=pointer]: - - /url: "#assurance" - - link "Commands" [ref=e10] [cursor=pointer]: - - /url: "#tooling" - - link "Examples" [ref=e11] [cursor=pointer]: - - /url: "#examples" - - link "Source" [ref=e12] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - button "Switch to dark mode" [pressed] [ref=e13] [cursor=pointer]: - - generic [ref=e16]: Dark - - main [ref=e17]: - - region "CellScript" [ref=e18]: - - generic [ref=e19]: - - heading "CellScript" [level=1] [ref=e20] - - paragraph [ref=e21]: Write Cell contracts as typed transitions, not raw wire format. - - generic [ref=e22]: - - link "Get started" [ref=e23] [cursor=pointer]: - - /url: "#getting-started" - - link "Core model" [ref=e24] [cursor=pointer]: - - /url: "#core-model" - - generic "CellScript contract surface" [ref=e25]: - - generic [ref=e26]: - - term [ref=e27]: target - - definition [ref=e28]: ckb-vm RISC-V - - generic [ref=e29]: - - term [ref=e30]: model - - definition [ref=e31]: schema-backed Cells - - generic [ref=e32]: - - term [ref=e33]: output - - definition [ref=e34]: metadata + ProofPlan - - generic [ref=e35]: - - generic [ref=e37]: token.cell - - tablist "CellScript examples" [ref=e39]: - - tab "Fungible Token" [selected] [ref=e40] [cursor=pointer] - - tab "NFT" [ref=e41] [cursor=pointer] - - tab "AMM Pool" [ref=e42] [cursor=pointer] - - tab "Vesting" [ref=e43] [cursor=pointer] - - tabpanel "Fungible Token" [ref=e45]: - - generic [ref=e46]: - - generic [ref=e47]: "1" - - generic [ref=e48]: module cellscript::fungible_token - - generic [ref=e49]: - - generic [ref=e50]: "2" - - generic [ref=e51]: // ... invariant and MintAuthority omitted - - generic [ref=e52]: - - generic [ref=e53]: "3" - - generic [ref=e54]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e55]: - - generic [ref=e56]: "4" - - generic [ref=e57]: "amount: u64," - - generic [ref=e58]: - - generic [ref=e59]: "5" - - generic [ref=e60]: "symbol: [u8; 8]," - - generic [ref=e61]: - - generic [ref=e62]: "6" - - generic [ref=e63]: "}" - - generic [ref=e65]: "7" - - generic [ref=e66]: - - generic [ref=e67]: "8" - - generic [ref=e68]: "action transfer_token(token: Token, to: Address) -> next_token: Token" - - generic [ref=e69]: - - generic [ref=e70]: "9" - - generic [ref=e71]: where - - generic [ref=e72]: - - generic [ref=e73]: "10" - - generic [ref=e74]: consume token - - generic [ref=e75]: - - generic [ref=e76]: "11" - - generic [ref=e77]: "create next_token = Token { amount: token.amount, symbol: token.symbol } with_lock(to)" - - generic [ref=e79]: "12" - - generic [ref=e80]: - - generic [ref=e81]: "13" - - generic [ref=e82]: "action burn(token: Token)" - - generic [ref=e83]: - - generic [ref=e84]: "14" - - generic [ref=e85]: where - - generic [ref=e86]: - - generic [ref=e87]: "15" - - generic [ref=e88]: assert(token.amount > 0, "cannot burn zero") - - generic [ref=e89]: - - generic [ref=e90]: "16" - - generic [ref=e91]: destroy token - - generic [ref=e92]: - - generic [ref=e93]: $ - - generic [ref=e94]: cellc examples/token.cell --target-profile ckb - - region "Getting Started" [ref=e95]: - - heading "Getting Started" [level=2] [ref=e96] - - generic [ref=e97]: - - article [ref=e98]: - - generic [ref=e99]: "1" - - heading "Install" [level=3] [ref=e100] - - paragraph [ref=e101]: - - code [ref=e102]: cargo install --path . - - article [ref=e103]: - - generic [ref=e104]: "2" - - heading "Compile" [level=3] [ref=e105] - - paragraph [ref=e106]: - - code [ref=e107]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e108]: - - generic [ref=e109]: "3" - - heading "Check" [level=3] [ref=e110] - - paragraph [ref=e111]: - - code [ref=e112]: cellc check --target-profile ckb - - region "Compiler Workflow" [ref=e113]: - - heading "Compiler Workflow" [level=2] [ref=e114] - - generic [ref=e115]: - - generic "Compiler workflow from .cell source to CKB artefact" [ref=e116]: - - article [ref=e117]: - - img [ref=e119]: - - generic [ref=e122]: .cell - - heading "CellScript source" [level=3] [ref=e123] - - img [ref=e125] - - article [ref=e127]: - - img [ref=e129] - - heading "Parse & check" [level=3] [ref=e133] - - paragraph [ref=e134]: Syntax, types, effects - - img [ref=e136] - - article [ref=e138]: - - img [ref=e140] - - heading "IR + Metadata" [level=3] [ref=e146] - - paragraph [ref=e147]: Typed model & assurance info - - img [ref=e149] - - article [ref=e151]: - - img [ref=e153] - - heading "Lower to RISC-V" [level=3] [ref=e157] - - paragraph [ref=e158]: ckb-vm codegen & optimisations - - img [ref=e160] - - article [ref=e162]: - - img [ref=e164]: - - generic [ref=e167]: .elf - - heading "ELF / Assembly" [level=3] [ref=e168] - - paragraph [ref=e169]: RISC-V artefacts for ckb-vm - - complementary "Build for CKB" [ref=e170]: - - heading "Build for CKB" [level=3] [ref=e171] - - list [ref=e172]: - - listitem [ref=e173]: - - img [ref=e174] - - generic [ref=e176]: ckb-vm compatible - - listitem [ref=e177]: - - img [ref=e178] - - generic [ref=e180]: Deterministic execution - - listitem [ref=e181]: - - img [ref=e182] - - generic [ref=e184]: Minimal syscalls - - listitem [ref=e185]: - - img [ref=e186] - - generic [ref=e188]: Scheduler-aware - - region "Core Model" [ref=e189]: - - heading "Core Model" [level=2] [ref=e190] - - paragraph [ref=e191]: "CellScript keeps the contract model visible: Cell shapes, effects, locks, flows, and review metadata stay in one typed surface." - - paragraph [ref=e192]: "Narrow by design: not a general-purpose runtime, not a new VM, not account storage in disguise." - - generic [ref=e193]: - - tablist "CellScript core primitives" [ref=e194]: - - tab "resource" [selected] [ref=e195] [cursor=pointer]: - - generic [ref=e196]: resource - - tab "shared" [ref=e197] [cursor=pointer]: - - generic [ref=e198]: shared - - tab "receipt" [ref=e199] [cursor=pointer]: - - generic [ref=e200]: receipt - - tab "action" [ref=e201] [cursor=pointer]: - - generic [ref=e202]: action - - tab "lock" [ref=e203] [cursor=pointer]: - - generic [ref=e204]: lock - - tab "flow" [ref=e205] [cursor=pointer]: - - generic [ref=e206]: flow - - tab "invariant" [ref=e207] [cursor=pointer]: - - generic [ref=e208]: invariant - - tab "struct / enum" [ref=e209] [cursor=pointer]: - - generic [ref=e210]: struct / enum - - tab "identity" [ref=e211] [cursor=pointer]: - - generic [ref=e212]: identity - - tabpanel "resource" [ref=e214]: - - paragraph [ref=e216]: Owned Cell state with explicit lifecycle effects. - - generic [ref=e217]: - - generic [ref=e218]: - - generic [ref=e219]: Example excerpt - - generic [ref=e220]: examples/token.cell - - generic [ref=e221]: "// examples/token.cell resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance Output" [ref=e222]: - - generic [ref=e223]: - - heading "Assurance Output" [level=2] [ref=e224] - - paragraph [ref=e225]: A local build emits review metadata. Treat the sidecar as useful evidence, not an authenticated proof outside the build boundary. - - article [ref=e227]: - - generic "Assurance output summary" [ref=e228]: - - article [ref=e229]: - - text: Schema - - strong [ref=e230]: v42 - - paragraph [ref=e231]: current compiler metadata schema - - article [ref=e232]: - - text: Source - - strong [ref=e233]: vesting.cell - - paragraph [ref=e234]: shared + receipt + flow - - article [ref=e235]: - - text: Boundary - - strong [ref=e236]: local sidecar - - paragraph [ref=e237]: validated; provenance required when shared - - group [ref=e238]: - - generic "- Metadata excerpt" [ref=e239] [cursor=pointer] - - generic [ref=e240]: "# Representative sidecar excerpt from a local build; keep provenance checks separate." - - generic [ref=e241]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": { \"VestingConfig\": { \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, \"VestingGrant\": { \"kind\": \"receipt\", \"flow\": \"Granted -> Claimable -> FullyClaimed\" } }, \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"status\": \"review evidence\", \"limit\": \"sidecar provenance must be checked outside the compiler\" } }" - - region "Tooling Surface" [ref=e242]: - - heading "Tooling Surface" [level=2] [ref=e243] - - generic [ref=e244]: - - tablist "CellScript tooling commands" [ref=e245]: - - tab "cellc metadata Read/write surface" [selected] [ref=e246] [cursor=pointer]: - - code [ref=e247]: cellc metadata - - generic [ref=e248]: Read/write surface - - tab "cellc constraints Transaction shape" [ref=e249] [cursor=pointer]: - - code [ref=e250]: cellc constraints - - generic [ref=e251]: Transaction shape - - tab "cellc audit-bundle Reviewer packet" [ref=e252] [cursor=pointer]: - - code [ref=e253]: cellc audit-bundle - - generic [ref=e254]: Reviewer packet - - tab "cellc lsp Editor feedback" [ref=e255] [cursor=pointer]: - - code [ref=e256]: cellc lsp - - generic [ref=e257]: Editor feedback - - tabpanel "cellc metadata Read/write surface" [ref=e259]: - - generic [ref=e260]: - - generic [ref=e261]: When - - paragraph [ref=e262]: Use before review or integration. - - generic [ref=e263]: Output - - paragraph [ref=e264]: Schema, effects, source hashes, target profile. - - generic [ref=e265]: - - code [ref=e266]: cellc metadata examples/vesting.cell --target-profile ckb --json - - generic [ref=e267]: "# Emit review metadata for a real example." - - region "Examples" [ref=e268]: - - heading "Examples" [level=2] [ref=e269] - - generic [ref=e270]: - - link "token.cell Mint, transfer, burn, and typed metadata. resource consume/create burn" [ref=e271] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - heading "token.cell" [level=3] [ref=e272]: - - code [ref=e273]: token.cell - - paragraph [ref=e274]: Mint, transfer, burn, and typed metadata. - - generic [ref=e275]: - - generic [ref=e276]: resource - - generic [ref=e277]: consume/create - - generic [ref=e278]: burn - - link "nft.cell Ownership transfer with preserve and relock. resource preserve relock" [ref=e279] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - heading "nft.cell" [level=3] [ref=e280]: - - code [ref=e281]: nft.cell - - paragraph [ref=e282]: Ownership transfer with preserve and relock. - - generic [ref=e283]: - - generic [ref=e284]: resource - - generic [ref=e285]: preserve - - generic [ref=e286]: relock - - link "amm_pool.cell Shared reserves with slippage checks. shared replace slippage" [ref=e287] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - heading "amm_pool.cell" [level=3] [ref=e288]: - - code [ref=e289]: amm_pool.cell - - paragraph [ref=e290]: Shared reserves with slippage checks. - - generic [ref=e291]: - - generic [ref=e292]: shared - - generic [ref=e293]: replace - - generic [ref=e294]: slippage - - link "vesting.cell Grant state flow into claimed output. flow transition receipt" [ref=e295] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - heading "vesting.cell" [level=3] [ref=e296]: - - code [ref=e297]: vesting.cell - - paragraph [ref=e298]: Grant state flow into claimed output. - - generic [ref=e299]: - - generic [ref=e300]: flow - - generic [ref=e301]: transition - - generic [ref=e302]: receipt - - link "multisig.cell Witness checks for threshold-style locks. lock witness threshold" [ref=e303] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/multisig.cell - - heading "multisig.cell" [level=3] [ref=e304]: - - code [ref=e305]: multisig.cell - - paragraph [ref=e306]: Witness checks for threshold-style locks. - - generic [ref=e307]: - - generic [ref=e308]: lock - - generic [ref=e309]: witness - - generic [ref=e310]: threshold - - link "timelock.cell Time-bound spending from Cell state. lock env timepoint" [ref=e311] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/timelock.cell - - heading "timelock.cell" [level=3] [ref=e312]: - - code [ref=e313]: timelock.cell - - paragraph [ref=e314]: Time-bound spending from Cell state. - - generic [ref=e315]: - - generic [ref=e316]: lock - - generic [ref=e317]: env - - generic [ref=e318]: timepoint - - contentinfo [ref=e319]: - - generic [ref=e320]: - - generic [ref=e321]: - - link "CellScript" [ref=e322] [cursor=pointer]: - - /url: "#top" - - generic [ref=e324]: CellScript - - paragraph [ref=e325]: A semantic DSL for CKB Cell-based smart contracts, with typed metadata and assurance by design. Docs, spec, examples, and source live with the repository. - - generic [ref=e326]: - - link "Docs" [ref=e327] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "Spec" [ref=e328] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "Examples" [ref=e329] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "Source" [ref=e330] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-02T06-04-13-837Z.yml b/.playwright-mcp/page-2026-06-02T06-04-13-837Z.yml deleted file mode 100644 index ea61c7b2..00000000 --- a/.playwright-mcp/page-2026-06-02T06-04-13-837Z.yml +++ /dev/null @@ -1,323 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "Primary navigation" [ref=e3]: - - link "CellScript home" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link "Model" [ref=e8] [cursor=pointer]: - - /url: "#core-model" - - link "Assurance" [ref=e9] [cursor=pointer]: - - /url: "#assurance" - - link "Commands" [ref=e10] [cursor=pointer]: - - /url: "#tooling" - - link "Examples" [ref=e11] [cursor=pointer]: - - /url: "#examples" - - link "Source" [ref=e12] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - button "Switch to dark mode" [pressed] [ref=e13] [cursor=pointer]: - - generic [ref=e16]: Dark - - main [ref=e17]: - - region "CellScript" [ref=e18]: - - generic [ref=e19]: - - heading "CellScript" [level=1] [ref=e20] - - paragraph [ref=e21]: Write Cell contracts as typed transitions, not raw wire format. - - generic [ref=e22]: - - link "Get started" [ref=e23] [cursor=pointer]: - - /url: "#getting-started" - - link "Core model" [ref=e24] [cursor=pointer]: - - /url: "#core-model" - - generic "CellScript contract surface" [ref=e25]: - - generic [ref=e26]: - - term [ref=e27]: target - - definition [ref=e28]: ckb-vm RISC-V - - generic [ref=e29]: - - term [ref=e30]: model - - definition [ref=e31]: schema-backed Cells - - generic [ref=e32]: - - term [ref=e33]: output - - definition [ref=e34]: metadata + ProofPlan - - generic [ref=e35]: - - generic [ref=e37]: token.cell - - tablist "CellScript examples" [ref=e39]: - - tab "Fungible Token" [selected] [ref=e40] [cursor=pointer] - - tab "NFT" [ref=e41] [cursor=pointer] - - tab "AMM Pool" [ref=e42] [cursor=pointer] - - tab "Vesting" [ref=e43] [cursor=pointer] - - tabpanel "Fungible Token" [ref=e45]: - - generic [ref=e46]: - - generic [ref=e47]: "1" - - generic [ref=e48]: module cellscript::fungible_token - - generic [ref=e49]: - - generic [ref=e50]: "2" - - generic [ref=e51]: // ... invariant and MintAuthority omitted - - generic [ref=e52]: - - generic [ref=e53]: "3" - - generic [ref=e54]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e55]: - - generic [ref=e56]: "4" - - generic [ref=e57]: "amount: u64," - - generic [ref=e58]: - - generic [ref=e59]: "5" - - generic [ref=e60]: "symbol: [u8; 8]," - - generic [ref=e61]: - - generic [ref=e62]: "6" - - generic [ref=e63]: "}" - - generic [ref=e65]: "7" - - generic [ref=e66]: - - generic [ref=e67]: "8" - - generic [ref=e68]: "action transfer_token(token: Token, to: Address) -> next_token: Token" - - generic [ref=e69]: - - generic [ref=e70]: "9" - - generic [ref=e71]: where - - generic [ref=e72]: - - generic [ref=e73]: "10" - - generic [ref=e74]: consume token - - generic [ref=e75]: - - generic [ref=e76]: "11" - - generic [ref=e77]: "create next_token = Token { amount: token.amount, symbol: token.symbol } with_lock(to)" - - generic [ref=e79]: "12" - - generic [ref=e80]: - - generic [ref=e81]: "13" - - generic [ref=e82]: "action burn(token: Token)" - - generic [ref=e83]: - - generic [ref=e84]: "14" - - generic [ref=e85]: where - - generic [ref=e86]: - - generic [ref=e87]: "15" - - generic [ref=e88]: assert(token.amount > 0, "cannot burn zero") - - generic [ref=e89]: - - generic [ref=e90]: "16" - - generic [ref=e91]: destroy token - - generic [ref=e92]: - - generic [ref=e93]: $ - - generic [ref=e94]: cellc examples/token.cell --target-profile ckb - - region "Getting Started" [ref=e95]: - - heading "Getting Started" [level=2] [ref=e96] - - generic [ref=e97]: - - article [ref=e98]: - - generic [ref=e99]: "1" - - heading "Install" [level=3] [ref=e100] - - paragraph [ref=e101]: - - code [ref=e102]: cargo install --path . - - article [ref=e103]: - - generic [ref=e104]: "2" - - heading "Compile" [level=3] [ref=e105] - - paragraph [ref=e106]: - - code [ref=e107]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e108]: - - generic [ref=e109]: "3" - - heading "Check" [level=3] [ref=e110] - - paragraph [ref=e111]: - - code [ref=e112]: cellc check --target-profile ckb - - region "Compiler Workflow" [ref=e113]: - - heading "Compiler Workflow" [level=2] [ref=e114] - - generic [ref=e115]: - - generic "Compiler workflow from .cell source to CKB artefact" [ref=e116]: - - article [ref=e117]: - - img [ref=e119]: - - generic [ref=e122]: .cell - - heading "CellScript source" [level=3] [ref=e123] - - img [ref=e125] - - article [ref=e127]: - - img [ref=e129] - - heading "Parse & check" [level=3] [ref=e133] - - paragraph [ref=e134]: Syntax, types, effects - - img [ref=e136] - - article [ref=e138]: - - img [ref=e140] - - heading "IR + Metadata" [level=3] [ref=e146] - - paragraph [ref=e147]: Typed model & assurance info - - img [ref=e149] - - article [ref=e151]: - - img [ref=e153] - - heading "Lower to RISC-V" [level=3] [ref=e157] - - paragraph [ref=e158]: ckb-vm codegen & optimisations - - img [ref=e160] - - article [ref=e162]: - - img [ref=e164]: - - generic [ref=e167]: .elf - - heading "ELF / Assembly" [level=3] [ref=e168] - - paragraph [ref=e169]: RISC-V artefacts for ckb-vm - - complementary "Build for CKB" [ref=e170]: - - heading "Build for CKB" [level=3] [ref=e171] - - list [ref=e172]: - - listitem [ref=e173]: - - img [ref=e174] - - generic [ref=e176]: ckb-vm compatible - - listitem [ref=e177]: - - img [ref=e178] - - generic [ref=e180]: Deterministic execution - - listitem [ref=e181]: - - img [ref=e182] - - generic [ref=e184]: Minimal syscalls - - listitem [ref=e185]: - - img [ref=e186] - - generic [ref=e188]: Scheduler-aware - - region "Core Model" [ref=e189]: - - heading "Core Model" [level=2] [ref=e190] - - paragraph [ref=e191]: "CellScript keeps the contract model visible: Cell shapes, effects, locks, flows, and review metadata stay in one typed surface." - - paragraph [ref=e192]: "Narrow by design: not a general-purpose runtime, not a new VM, not account storage in disguise." - - generic [ref=e193]: - - tablist "CellScript core primitives" [ref=e194]: - - tab "resource" [selected] [ref=e195] [cursor=pointer]: - - generic [ref=e196]: resource - - tab "shared" [ref=e197] [cursor=pointer]: - - generic [ref=e198]: shared - - tab "receipt" [ref=e199] [cursor=pointer]: - - generic [ref=e200]: receipt - - tab "action" [ref=e201] [cursor=pointer]: - - generic [ref=e202]: action - - tab "lock" [ref=e203] [cursor=pointer]: - - generic [ref=e204]: lock - - tab "flow" [ref=e205] [cursor=pointer]: - - generic [ref=e206]: flow - - tab "invariant" [ref=e207] [cursor=pointer]: - - generic [ref=e208]: invariant - - tab "struct / enum" [ref=e209] [cursor=pointer]: - - generic [ref=e210]: struct / enum - - tab "identity" [ref=e211] [cursor=pointer]: - - generic [ref=e212]: identity - - tabpanel "resource" [ref=e214]: - - paragraph [ref=e216]: Owned Cell state with explicit lifecycle effects. - - generic [ref=e217]: - - generic [ref=e218]: - - generic [ref=e219]: Example excerpt - - generic [ref=e220]: examples/token.cell - - generic [ref=e221]: "// examples/token.cell resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance Output" [ref=e222]: - - generic [ref=e223]: - - heading "Assurance Output" [level=2] [ref=e224] - - paragraph [ref=e225]: A local build emits review metadata. Treat the sidecar as useful evidence, not an authenticated proof outside the build boundary. - - article [ref=e227]: - - generic "Assurance output summary" [ref=e228]: - - article [ref=e229]: - - text: Schema - - strong [ref=e230]: v42 - - paragraph [ref=e231]: current compiler metadata schema - - article [ref=e232]: - - text: Source - - strong [ref=e233]: vesting.cell - - paragraph [ref=e234]: shared + receipt + flow - - article [ref=e235]: - - text: Boundary - - strong [ref=e236]: local sidecar - - paragraph [ref=e237]: validated; provenance required when shared - - group [ref=e238]: - - generic "- Metadata excerpt" [ref=e239] [cursor=pointer] - - generic [ref=e240]: "# Representative sidecar excerpt from a local build; keep provenance checks separate." - - generic [ref=e241]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": { \"VestingConfig\": { \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, \"VestingGrant\": { \"kind\": \"receipt\", \"flow\": \"Granted -> Claimable -> FullyClaimed\" } }, \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"status\": \"review evidence\", \"limit\": \"sidecar provenance must be checked outside the compiler\" } }" - - region "Tooling Surface" [ref=e242]: - - heading "Tooling Surface" [level=2] [ref=e243] - - generic [ref=e244]: - - tablist "CellScript tooling commands" [ref=e245]: - - tab "cellc metadata Read/write surface" [selected] [ref=e246] [cursor=pointer]: - - code [ref=e247]: cellc metadata - - generic [ref=e248]: Read/write surface - - tab "cellc constraints Transaction shape" [ref=e249] [cursor=pointer]: - - code [ref=e250]: cellc constraints - - generic [ref=e251]: Transaction shape - - tab "cellc audit-bundle Reviewer packet" [ref=e252] [cursor=pointer]: - - code [ref=e253]: cellc audit-bundle - - generic [ref=e254]: Reviewer packet - - tab "cellc lsp Editor feedback" [ref=e255] [cursor=pointer]: - - code [ref=e256]: cellc lsp - - generic [ref=e257]: Editor feedback - - tabpanel "cellc metadata Read/write surface" [ref=e259]: - - generic [ref=e260]: - - generic [ref=e261]: When - - paragraph [ref=e262]: Use before review or integration. - - generic [ref=e263]: Output - - paragraph [ref=e264]: Schema, effects, source hashes, target profile. - - generic [ref=e265]: - - code [ref=e266]: cellc metadata examples/vesting.cell --target-profile ckb --json - - generic [ref=e267]: "# Emit review metadata for a real example." - - region "Examples" [ref=e268]: - - heading "Examples" [level=2] [ref=e269] - - generic [ref=e270]: - - tablist "Example groups" [ref=e271]: - - tab "Protocols 6 files" [selected] [ref=e272] [cursor=pointer]: - - generic [ref=e273]: Protocols - - generic [ref=e274]: 6 files - - tab "Primitives 6 files" [ref=e275] [cursor=pointer]: - - generic [ref=e276]: Primitives - - generic [ref=e277]: 6 files - - tab "Language 6 files" [ref=e278] [cursor=pointer]: - - generic [ref=e279]: Language - - generic [ref=e280]: 6 files - - tabpanel "Protocols 6 files" [ref=e282]: - - generic [ref=e283]: - - paragraph [ref=e284]: End-to-end contract examples with Cells, actions, and constraints. - - link "Open examples directory" [ref=e285] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - generic [ref=e286]: - - link "examples/token.cell Fungible token Mint, transfer, burn, merge, and amount invariant. resource invariant burn" [ref=e287] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - generic [ref=e288]: examples/token.cell - - heading "Fungible token" [level=3] [ref=e289] - - paragraph [ref=e290]: Mint, transfer, burn, merge, and amount invariant. - - generic [ref=e291]: - - generic [ref=e292]: resource - - generic [ref=e293]: invariant - - generic [ref=e294]: burn - - link "examples/nft.cell NFT marketplace Collection state, listing receipts, transfer, royalty payment. resource receipt preserve" [ref=e295] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - generic [ref=e296]: examples/nft.cell - - heading "NFT marketplace" [level=3] [ref=e297] - - paragraph [ref=e298]: Collection state, listing receipts, transfer, royalty payment. - - generic [ref=e299]: - - generic [ref=e300]: resource - - generic [ref=e301]: receipt - - generic [ref=e302]: preserve - - link "examples/amm_pool.cell AMM pool Shared reserves, LP receipts, swap and liquidity actions. shared receipt slippage" [ref=e303] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - generic [ref=e304]: examples/amm_pool.cell - - heading "AMM pool" [level=3] [ref=e305] - - paragraph [ref=e306]: Shared reserves, LP receipts, swap and liquidity actions. - - generic [ref=e307]: - - generic [ref=e308]: shared - - generic [ref=e309]: receipt - - generic [ref=e310]: slippage - - link "examples/vesting.cell Vesting Grant flow, timepoint checks, claim and revoke paths. flow receipt env" [ref=e311] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - generic [ref=e312]: examples/vesting.cell - - heading "Vesting" [level=3] [ref=e313] - - paragraph [ref=e314]: Grant flow, timepoint checks, claim and revoke paths. - - generic [ref=e315]: - - generic [ref=e316]: flow - - generic [ref=e317]: receipt - - generic [ref=e318]: env - - link "examples/launch.cell Launch flow Launch state, settlement, and sale lifecycle. flow settle claim" [ref=e319] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/launch.cell - - generic [ref=e320]: examples/launch.cell - - heading "Launch flow" [level=3] [ref=e321] - - paragraph [ref=e322]: Launch state, settlement, and sale lifecycle. - - generic [ref=e323]: - - generic [ref=e324]: flow - - generic [ref=e325]: settle - - generic [ref=e326]: claim - - link "examples/registry.cell Registry Name ownership and registry-style state transitions. resource identity replace" [ref=e327] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/registry.cell - - generic [ref=e328]: examples/registry.cell - - heading "Registry" [level=3] [ref=e329] - - paragraph [ref=e330]: Name ownership and registry-style state transitions. - - generic [ref=e331]: - - generic [ref=e332]: resource - - generic [ref=e333]: identity - - generic [ref=e334]: replace - - contentinfo [ref=e335]: - - generic [ref=e336]: - - generic [ref=e337]: - - link "CellScript" [ref=e338] [cursor=pointer]: - - /url: "#top" - - generic [ref=e340]: CellScript - - paragraph [ref=e341]: A semantic DSL for CKB Cell-based smart contracts, with typed metadata and assurance by design. Docs, spec, examples, and source live with the repository. - - generic [ref=e342]: - - link "Docs" [ref=e343] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "Spec" [ref=e344] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "Examples" [ref=e345] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "Source" [ref=e346] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-02T06-05-49-895Z.yml b/.playwright-mcp/page-2026-06-02T06-05-49-895Z.yml deleted file mode 100644 index ea61c7b2..00000000 --- a/.playwright-mcp/page-2026-06-02T06-05-49-895Z.yml +++ /dev/null @@ -1,323 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "Primary navigation" [ref=e3]: - - link "CellScript home" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link "Model" [ref=e8] [cursor=pointer]: - - /url: "#core-model" - - link "Assurance" [ref=e9] [cursor=pointer]: - - /url: "#assurance" - - link "Commands" [ref=e10] [cursor=pointer]: - - /url: "#tooling" - - link "Examples" [ref=e11] [cursor=pointer]: - - /url: "#examples" - - link "Source" [ref=e12] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - button "Switch to dark mode" [pressed] [ref=e13] [cursor=pointer]: - - generic [ref=e16]: Dark - - main [ref=e17]: - - region "CellScript" [ref=e18]: - - generic [ref=e19]: - - heading "CellScript" [level=1] [ref=e20] - - paragraph [ref=e21]: Write Cell contracts as typed transitions, not raw wire format. - - generic [ref=e22]: - - link "Get started" [ref=e23] [cursor=pointer]: - - /url: "#getting-started" - - link "Core model" [ref=e24] [cursor=pointer]: - - /url: "#core-model" - - generic "CellScript contract surface" [ref=e25]: - - generic [ref=e26]: - - term [ref=e27]: target - - definition [ref=e28]: ckb-vm RISC-V - - generic [ref=e29]: - - term [ref=e30]: model - - definition [ref=e31]: schema-backed Cells - - generic [ref=e32]: - - term [ref=e33]: output - - definition [ref=e34]: metadata + ProofPlan - - generic [ref=e35]: - - generic [ref=e37]: token.cell - - tablist "CellScript examples" [ref=e39]: - - tab "Fungible Token" [selected] [ref=e40] [cursor=pointer] - - tab "NFT" [ref=e41] [cursor=pointer] - - tab "AMM Pool" [ref=e42] [cursor=pointer] - - tab "Vesting" [ref=e43] [cursor=pointer] - - tabpanel "Fungible Token" [ref=e45]: - - generic [ref=e46]: - - generic [ref=e47]: "1" - - generic [ref=e48]: module cellscript::fungible_token - - generic [ref=e49]: - - generic [ref=e50]: "2" - - generic [ref=e51]: // ... invariant and MintAuthority omitted - - generic [ref=e52]: - - generic [ref=e53]: "3" - - generic [ref=e54]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e55]: - - generic [ref=e56]: "4" - - generic [ref=e57]: "amount: u64," - - generic [ref=e58]: - - generic [ref=e59]: "5" - - generic [ref=e60]: "symbol: [u8; 8]," - - generic [ref=e61]: - - generic [ref=e62]: "6" - - generic [ref=e63]: "}" - - generic [ref=e65]: "7" - - generic [ref=e66]: - - generic [ref=e67]: "8" - - generic [ref=e68]: "action transfer_token(token: Token, to: Address) -> next_token: Token" - - generic [ref=e69]: - - generic [ref=e70]: "9" - - generic [ref=e71]: where - - generic [ref=e72]: - - generic [ref=e73]: "10" - - generic [ref=e74]: consume token - - generic [ref=e75]: - - generic [ref=e76]: "11" - - generic [ref=e77]: "create next_token = Token { amount: token.amount, symbol: token.symbol } with_lock(to)" - - generic [ref=e79]: "12" - - generic [ref=e80]: - - generic [ref=e81]: "13" - - generic [ref=e82]: "action burn(token: Token)" - - generic [ref=e83]: - - generic [ref=e84]: "14" - - generic [ref=e85]: where - - generic [ref=e86]: - - generic [ref=e87]: "15" - - generic [ref=e88]: assert(token.amount > 0, "cannot burn zero") - - generic [ref=e89]: - - generic [ref=e90]: "16" - - generic [ref=e91]: destroy token - - generic [ref=e92]: - - generic [ref=e93]: $ - - generic [ref=e94]: cellc examples/token.cell --target-profile ckb - - region "Getting Started" [ref=e95]: - - heading "Getting Started" [level=2] [ref=e96] - - generic [ref=e97]: - - article [ref=e98]: - - generic [ref=e99]: "1" - - heading "Install" [level=3] [ref=e100] - - paragraph [ref=e101]: - - code [ref=e102]: cargo install --path . - - article [ref=e103]: - - generic [ref=e104]: "2" - - heading "Compile" [level=3] [ref=e105] - - paragraph [ref=e106]: - - code [ref=e107]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e108]: - - generic [ref=e109]: "3" - - heading "Check" [level=3] [ref=e110] - - paragraph [ref=e111]: - - code [ref=e112]: cellc check --target-profile ckb - - region "Compiler Workflow" [ref=e113]: - - heading "Compiler Workflow" [level=2] [ref=e114] - - generic [ref=e115]: - - generic "Compiler workflow from .cell source to CKB artefact" [ref=e116]: - - article [ref=e117]: - - img [ref=e119]: - - generic [ref=e122]: .cell - - heading "CellScript source" [level=3] [ref=e123] - - img [ref=e125] - - article [ref=e127]: - - img [ref=e129] - - heading "Parse & check" [level=3] [ref=e133] - - paragraph [ref=e134]: Syntax, types, effects - - img [ref=e136] - - article [ref=e138]: - - img [ref=e140] - - heading "IR + Metadata" [level=3] [ref=e146] - - paragraph [ref=e147]: Typed model & assurance info - - img [ref=e149] - - article [ref=e151]: - - img [ref=e153] - - heading "Lower to RISC-V" [level=3] [ref=e157] - - paragraph [ref=e158]: ckb-vm codegen & optimisations - - img [ref=e160] - - article [ref=e162]: - - img [ref=e164]: - - generic [ref=e167]: .elf - - heading "ELF / Assembly" [level=3] [ref=e168] - - paragraph [ref=e169]: RISC-V artefacts for ckb-vm - - complementary "Build for CKB" [ref=e170]: - - heading "Build for CKB" [level=3] [ref=e171] - - list [ref=e172]: - - listitem [ref=e173]: - - img [ref=e174] - - generic [ref=e176]: ckb-vm compatible - - listitem [ref=e177]: - - img [ref=e178] - - generic [ref=e180]: Deterministic execution - - listitem [ref=e181]: - - img [ref=e182] - - generic [ref=e184]: Minimal syscalls - - listitem [ref=e185]: - - img [ref=e186] - - generic [ref=e188]: Scheduler-aware - - region "Core Model" [ref=e189]: - - heading "Core Model" [level=2] [ref=e190] - - paragraph [ref=e191]: "CellScript keeps the contract model visible: Cell shapes, effects, locks, flows, and review metadata stay in one typed surface." - - paragraph [ref=e192]: "Narrow by design: not a general-purpose runtime, not a new VM, not account storage in disguise." - - generic [ref=e193]: - - tablist "CellScript core primitives" [ref=e194]: - - tab "resource" [selected] [ref=e195] [cursor=pointer]: - - generic [ref=e196]: resource - - tab "shared" [ref=e197] [cursor=pointer]: - - generic [ref=e198]: shared - - tab "receipt" [ref=e199] [cursor=pointer]: - - generic [ref=e200]: receipt - - tab "action" [ref=e201] [cursor=pointer]: - - generic [ref=e202]: action - - tab "lock" [ref=e203] [cursor=pointer]: - - generic [ref=e204]: lock - - tab "flow" [ref=e205] [cursor=pointer]: - - generic [ref=e206]: flow - - tab "invariant" [ref=e207] [cursor=pointer]: - - generic [ref=e208]: invariant - - tab "struct / enum" [ref=e209] [cursor=pointer]: - - generic [ref=e210]: struct / enum - - tab "identity" [ref=e211] [cursor=pointer]: - - generic [ref=e212]: identity - - tabpanel "resource" [ref=e214]: - - paragraph [ref=e216]: Owned Cell state with explicit lifecycle effects. - - generic [ref=e217]: - - generic [ref=e218]: - - generic [ref=e219]: Example excerpt - - generic [ref=e220]: examples/token.cell - - generic [ref=e221]: "// examples/token.cell resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance Output" [ref=e222]: - - generic [ref=e223]: - - heading "Assurance Output" [level=2] [ref=e224] - - paragraph [ref=e225]: A local build emits review metadata. Treat the sidecar as useful evidence, not an authenticated proof outside the build boundary. - - article [ref=e227]: - - generic "Assurance output summary" [ref=e228]: - - article [ref=e229]: - - text: Schema - - strong [ref=e230]: v42 - - paragraph [ref=e231]: current compiler metadata schema - - article [ref=e232]: - - text: Source - - strong [ref=e233]: vesting.cell - - paragraph [ref=e234]: shared + receipt + flow - - article [ref=e235]: - - text: Boundary - - strong [ref=e236]: local sidecar - - paragraph [ref=e237]: validated; provenance required when shared - - group [ref=e238]: - - generic "- Metadata excerpt" [ref=e239] [cursor=pointer] - - generic [ref=e240]: "# Representative sidecar excerpt from a local build; keep provenance checks separate." - - generic [ref=e241]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": { \"VestingConfig\": { \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, \"VestingGrant\": { \"kind\": \"receipt\", \"flow\": \"Granted -> Claimable -> FullyClaimed\" } }, \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"status\": \"review evidence\", \"limit\": \"sidecar provenance must be checked outside the compiler\" } }" - - region "Tooling Surface" [ref=e242]: - - heading "Tooling Surface" [level=2] [ref=e243] - - generic [ref=e244]: - - tablist "CellScript tooling commands" [ref=e245]: - - tab "cellc metadata Read/write surface" [selected] [ref=e246] [cursor=pointer]: - - code [ref=e247]: cellc metadata - - generic [ref=e248]: Read/write surface - - tab "cellc constraints Transaction shape" [ref=e249] [cursor=pointer]: - - code [ref=e250]: cellc constraints - - generic [ref=e251]: Transaction shape - - tab "cellc audit-bundle Reviewer packet" [ref=e252] [cursor=pointer]: - - code [ref=e253]: cellc audit-bundle - - generic [ref=e254]: Reviewer packet - - tab "cellc lsp Editor feedback" [ref=e255] [cursor=pointer]: - - code [ref=e256]: cellc lsp - - generic [ref=e257]: Editor feedback - - tabpanel "cellc metadata Read/write surface" [ref=e259]: - - generic [ref=e260]: - - generic [ref=e261]: When - - paragraph [ref=e262]: Use before review or integration. - - generic [ref=e263]: Output - - paragraph [ref=e264]: Schema, effects, source hashes, target profile. - - generic [ref=e265]: - - code [ref=e266]: cellc metadata examples/vesting.cell --target-profile ckb --json - - generic [ref=e267]: "# Emit review metadata for a real example." - - region "Examples" [ref=e268]: - - heading "Examples" [level=2] [ref=e269] - - generic [ref=e270]: - - tablist "Example groups" [ref=e271]: - - tab "Protocols 6 files" [selected] [ref=e272] [cursor=pointer]: - - generic [ref=e273]: Protocols - - generic [ref=e274]: 6 files - - tab "Primitives 6 files" [ref=e275] [cursor=pointer]: - - generic [ref=e276]: Primitives - - generic [ref=e277]: 6 files - - tab "Language 6 files" [ref=e278] [cursor=pointer]: - - generic [ref=e279]: Language - - generic [ref=e280]: 6 files - - tabpanel "Protocols 6 files" [ref=e282]: - - generic [ref=e283]: - - paragraph [ref=e284]: End-to-end contract examples with Cells, actions, and constraints. - - link "Open examples directory" [ref=e285] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - generic [ref=e286]: - - link "examples/token.cell Fungible token Mint, transfer, burn, merge, and amount invariant. resource invariant burn" [ref=e287] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - generic [ref=e288]: examples/token.cell - - heading "Fungible token" [level=3] [ref=e289] - - paragraph [ref=e290]: Mint, transfer, burn, merge, and amount invariant. - - generic [ref=e291]: - - generic [ref=e292]: resource - - generic [ref=e293]: invariant - - generic [ref=e294]: burn - - link "examples/nft.cell NFT marketplace Collection state, listing receipts, transfer, royalty payment. resource receipt preserve" [ref=e295] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - generic [ref=e296]: examples/nft.cell - - heading "NFT marketplace" [level=3] [ref=e297] - - paragraph [ref=e298]: Collection state, listing receipts, transfer, royalty payment. - - generic [ref=e299]: - - generic [ref=e300]: resource - - generic [ref=e301]: receipt - - generic [ref=e302]: preserve - - link "examples/amm_pool.cell AMM pool Shared reserves, LP receipts, swap and liquidity actions. shared receipt slippage" [ref=e303] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - generic [ref=e304]: examples/amm_pool.cell - - heading "AMM pool" [level=3] [ref=e305] - - paragraph [ref=e306]: Shared reserves, LP receipts, swap and liquidity actions. - - generic [ref=e307]: - - generic [ref=e308]: shared - - generic [ref=e309]: receipt - - generic [ref=e310]: slippage - - link "examples/vesting.cell Vesting Grant flow, timepoint checks, claim and revoke paths. flow receipt env" [ref=e311] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - generic [ref=e312]: examples/vesting.cell - - heading "Vesting" [level=3] [ref=e313] - - paragraph [ref=e314]: Grant flow, timepoint checks, claim and revoke paths. - - generic [ref=e315]: - - generic [ref=e316]: flow - - generic [ref=e317]: receipt - - generic [ref=e318]: env - - link "examples/launch.cell Launch flow Launch state, settlement, and sale lifecycle. flow settle claim" [ref=e319] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/launch.cell - - generic [ref=e320]: examples/launch.cell - - heading "Launch flow" [level=3] [ref=e321] - - paragraph [ref=e322]: Launch state, settlement, and sale lifecycle. - - generic [ref=e323]: - - generic [ref=e324]: flow - - generic [ref=e325]: settle - - generic [ref=e326]: claim - - link "examples/registry.cell Registry Name ownership and registry-style state transitions. resource identity replace" [ref=e327] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/registry.cell - - generic [ref=e328]: examples/registry.cell - - heading "Registry" [level=3] [ref=e329] - - paragraph [ref=e330]: Name ownership and registry-style state transitions. - - generic [ref=e331]: - - generic [ref=e332]: resource - - generic [ref=e333]: identity - - generic [ref=e334]: replace - - contentinfo [ref=e335]: - - generic [ref=e336]: - - generic [ref=e337]: - - link "CellScript" [ref=e338] [cursor=pointer]: - - /url: "#top" - - generic [ref=e340]: CellScript - - paragraph [ref=e341]: A semantic DSL for CKB Cell-based smart contracts, with typed metadata and assurance by design. Docs, spec, examples, and source live with the repository. - - generic [ref=e342]: - - link "Docs" [ref=e343] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "Spec" [ref=e344] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "Examples" [ref=e345] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "Source" [ref=e346] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-02T06-17-07-504Z.yml b/.playwright-mcp/page-2026-06-02T06-17-07-504Z.yml deleted file mode 100644 index 79cd93ef..00000000 --- a/.playwright-mcp/page-2026-06-02T06-17-07-504Z.yml +++ /dev/null @@ -1,320 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "Primary navigation" [ref=e3]: - - link "CellScript home" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link "Docs" [ref=e8] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - img [ref=e9] - - generic [ref=e12]: Docs - - link "Source" [ref=e13] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - img [ref=e14] - - generic [ref=e16]: Source - - button "Switch to dark mode" [pressed] [ref=e17] [cursor=pointer] - - main [ref=e20]: - - region "CellScript" [ref=e21]: - - generic [ref=e22]: - - heading "CellScript" [level=1] [ref=e23] - - paragraph [ref=e24]: Write Cell contracts as typed transitions, not raw wire format. - - generic [ref=e25]: - - link "Get started" [ref=e26] [cursor=pointer]: - - /url: "#getting-started" - - link "Core model" [ref=e27] [cursor=pointer]: - - /url: "#core-model" - - generic "CellScript contract surface" [ref=e28]: - - generic [ref=e29]: - - term [ref=e30]: target - - definition [ref=e31]: ckb-vm RISC-V - - generic [ref=e32]: - - term [ref=e33]: model - - definition [ref=e34]: schema-backed Cells - - generic [ref=e35]: - - term [ref=e36]: output - - definition [ref=e37]: metadata + ProofPlan - - generic [ref=e38]: - - generic [ref=e40]: token.cell - - combobox "Choose CellScript example" [ref=e42]: - - option "Fungible Token" [selected] - - option "NFT" - - option "AMM Pool" - - option "Vesting" - - tabpanel "Fungible Token" [ref=e44]: - - generic [ref=e45]: - - generic [ref=e46]: "1" - - generic [ref=e47]: module cellscript::fungible_token - - generic [ref=e48]: - - generic [ref=e49]: "2" - - generic [ref=e50]: // ... invariant and MintAuthority omitted - - generic [ref=e51]: - - generic [ref=e52]: "3" - - generic [ref=e53]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e54]: - - generic [ref=e55]: "4" - - generic [ref=e56]: "amount: u64," - - generic [ref=e57]: - - generic [ref=e58]: "5" - - generic [ref=e59]: "symbol: [u8; 8]," - - generic [ref=e60]: - - generic [ref=e61]: "6" - - generic [ref=e62]: "}" - - generic [ref=e64]: "7" - - generic [ref=e65]: - - generic [ref=e66]: "8" - - generic [ref=e67]: "action transfer_token(token: Token, to: Address) -> next_token: Token" - - generic [ref=e68]: - - generic [ref=e69]: "9" - - generic [ref=e70]: where - - generic [ref=e71]: - - generic [ref=e72]: "10" - - generic [ref=e73]: consume token - - generic [ref=e74]: - - generic [ref=e75]: "11" - - generic [ref=e76]: "create next_token = Token { amount: token.amount, symbol: token.symbol } with_lock(to)" - - generic [ref=e78]: "12" - - generic [ref=e79]: - - generic [ref=e80]: "13" - - generic [ref=e81]: "action burn(token: Token)" - - generic [ref=e82]: - - generic [ref=e83]: "14" - - generic [ref=e84]: where - - generic [ref=e85]: - - generic [ref=e86]: "15" - - generic [ref=e87]: assert(token.amount > 0, "cannot burn zero") - - generic [ref=e88]: - - generic [ref=e89]: "16" - - generic [ref=e90]: destroy token - - generic [ref=e91]: - - generic [ref=e92]: $ - - generic [ref=e93]: cellc examples/token.cell --target-profile ckb - - region "Getting Started" [ref=e94]: - - heading "Getting Started" [level=2] [ref=e95] - - generic [ref=e96]: - - article [ref=e97]: - - generic [ref=e98]: "1" - - heading "Install" [level=3] [ref=e99] - - paragraph [ref=e100]: - - code [ref=e101]: cargo install --path . - - article [ref=e102]: - - generic [ref=e103]: "2" - - heading "Compile" [level=3] [ref=e104] - - paragraph [ref=e105]: - - code [ref=e106]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e107]: - - generic [ref=e108]: "3" - - heading "Check" [level=3] [ref=e109] - - paragraph [ref=e110]: - - code [ref=e111]: cellc check --target-profile ckb - - region "Compiler Workflow" [ref=e112]: - - heading "Compiler Workflow" [level=2] [ref=e113] - - generic [ref=e114]: - - generic "Compiler workflow from .cell source to CKB artefact" [ref=e115]: - - article [ref=e116]: - - img [ref=e118]: - - generic [ref=e121]: .cell - - heading "CellScript source" [level=3] [ref=e122] - - img [ref=e124] - - article [ref=e126]: - - img [ref=e128] - - heading "Parse & check" [level=3] [ref=e132] - - paragraph [ref=e133]: Syntax, types, effects - - img [ref=e135] - - article [ref=e137]: - - img [ref=e139] - - heading "IR + Metadata" [level=3] [ref=e145] - - paragraph [ref=e146]: Typed model & assurance info - - img [ref=e148] - - article [ref=e150]: - - img [ref=e152] - - heading "Lower to RISC-V" [level=3] [ref=e156] - - paragraph [ref=e157]: ckb-vm codegen & optimisations - - img [ref=e159] - - article [ref=e161]: - - img [ref=e163]: - - generic [ref=e166]: .elf - - heading "ELF / Assembly" [level=3] [ref=e167] - - paragraph [ref=e168]: RISC-V artefacts for ckb-vm - - complementary "Build for CKB" [ref=e169]: - - heading "Build for CKB" [level=3] [ref=e170] - - list [ref=e171]: - - listitem [ref=e172]: - - img [ref=e173] - - generic [ref=e175]: ckb-vm compatible - - listitem [ref=e176]: - - img [ref=e177] - - generic [ref=e179]: Deterministic execution - - listitem [ref=e180]: - - img [ref=e181] - - generic [ref=e183]: Minimal syscalls - - listitem [ref=e184]: - - img [ref=e185] - - generic [ref=e187]: Scheduler-aware - - region "Core Model" [ref=e188]: - - heading "Core Model" [level=2] [ref=e189] - - paragraph [ref=e190]: "CellScript keeps the contract model visible: Cell shapes, effects, locks, flows, and review metadata stay in one typed surface." - - paragraph [ref=e191]: "Narrow by design: not a general-purpose runtime, not a new VM, not account storage in disguise." - - generic [ref=e192]: - - tablist "CellScript core primitives" [ref=e193]: - - tab "resource" [selected] [ref=e194] [cursor=pointer]: - - generic [ref=e195]: resource - - tab "shared" [ref=e196] [cursor=pointer]: - - generic [ref=e197]: shared - - tab "receipt" [ref=e198] [cursor=pointer]: - - generic [ref=e199]: receipt - - tab "action" [ref=e200] [cursor=pointer]: - - generic [ref=e201]: action - - tab "lock" [ref=e202] [cursor=pointer]: - - generic [ref=e203]: lock - - tab "flow" [ref=e204] [cursor=pointer]: - - generic [ref=e205]: flow - - tab "invariant" [ref=e206] [cursor=pointer]: - - generic [ref=e207]: invariant - - tab "struct / enum" [ref=e208] [cursor=pointer]: - - generic [ref=e209]: struct / enum - - tab "identity" [ref=e210] [cursor=pointer]: - - generic [ref=e211]: identity - - tabpanel "resource" [ref=e213]: - - paragraph [ref=e215]: Owned Cell state with explicit lifecycle effects. - - generic [ref=e216]: - - generic [ref=e217]: - - generic [ref=e218]: Example excerpt - - generic [ref=e219]: examples/token.cell - - generic [ref=e220]: "// examples/token.cell resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance Output" [ref=e221]: - - generic [ref=e222]: - - heading "Assurance Output" [level=2] [ref=e223] - - paragraph [ref=e224]: A local build emits review metadata. Treat the sidecar as useful evidence, not an authenticated proof outside the build boundary. - - article [ref=e226]: - - generic "Assurance output summary" [ref=e227]: - - article [ref=e228]: - - text: Schema - - strong [ref=e229]: v42 - - paragraph [ref=e230]: current compiler metadata schema - - article [ref=e231]: - - text: Source - - strong [ref=e232]: vesting.cell - - paragraph [ref=e233]: shared + receipt + flow - - article [ref=e234]: - - text: Boundary - - strong [ref=e235]: local sidecar - - paragraph [ref=e236]: validated; provenance required when shared - - group [ref=e237]: - - generic "- Metadata excerpt" [ref=e238] [cursor=pointer] - - generic [ref=e239]: "# Representative sidecar excerpt from a local build; keep provenance checks separate." - - generic [ref=e240]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": { \"VestingConfig\": { \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, \"VestingGrant\": { \"kind\": \"receipt\", \"flow\": \"Granted -> Claimable -> FullyClaimed\" } }, \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"status\": \"review evidence\", \"limit\": \"sidecar provenance must be checked outside the compiler\" } }" - - region "Tooling Surface" [ref=e241]: - - heading "Tooling Surface" [level=2] [ref=e242] - - generic [ref=e243]: - - tablist "CellScript tooling commands" [ref=e244]: - - tab "cellc metadata Read/write surface" [selected] [ref=e245] [cursor=pointer]: - - code [ref=e246]: cellc metadata - - generic [ref=e247]: Read/write surface - - tab "cellc constraints Transaction shape" [ref=e248] [cursor=pointer]: - - code [ref=e249]: cellc constraints - - generic [ref=e250]: Transaction shape - - tab "cellc audit-bundle Reviewer packet" [ref=e251] [cursor=pointer]: - - code [ref=e252]: cellc audit-bundle - - generic [ref=e253]: Reviewer packet - - tab "cellc lsp Editor feedback" [ref=e254] [cursor=pointer]: - - code [ref=e255]: cellc lsp - - generic [ref=e256]: Editor feedback - - tabpanel "cellc metadata Read/write surface" [ref=e258]: - - generic [ref=e259]: - - generic [ref=e260]: When - - paragraph [ref=e261]: Use before review or integration. - - generic [ref=e262]: Output - - paragraph [ref=e263]: Schema, effects, source hashes, target profile. - - generic [ref=e264]: - - code [ref=e265]: cellc metadata examples/vesting.cell --target-profile ckb --json - - generic [ref=e266]: "# Emit review metadata for a real example." - - region "Examples" [ref=e267]: - - heading "Examples" [level=2] [ref=e268] - - generic [ref=e269]: - - tablist "Example groups" [ref=e270]: - - tab "Protocols 6 files" [selected] [ref=e271] [cursor=pointer]: - - generic [ref=e272]: Protocols - - generic [ref=e273]: 6 files - - tab "Primitives 6 files" [ref=e274] [cursor=pointer]: - - generic [ref=e275]: Primitives - - generic [ref=e276]: 6 files - - tab "Language 6 files" [ref=e277] [cursor=pointer]: - - generic [ref=e278]: Language - - generic [ref=e279]: 6 files - - tabpanel "Protocols 6 files" [ref=e281]: - - generic [ref=e282]: - - paragraph [ref=e283]: End-to-end contract examples with Cells, actions, and constraints. - - link "Open examples directory" [ref=e284] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - generic [ref=e285]: - - link "examples/token.cell Fungible token Mint, transfer, burn, merge, and amount invariant. resource invariant burn" [ref=e286] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - generic [ref=e287]: examples/token.cell - - heading "Fungible token" [level=3] [ref=e288] - - paragraph [ref=e289]: Mint, transfer, burn, merge, and amount invariant. - - generic [ref=e290]: - - generic [ref=e291]: resource - - generic [ref=e292]: invariant - - generic [ref=e293]: burn - - link "examples/nft.cell NFT marketplace Collection state, listing receipts, transfer, royalty payment. resource receipt preserve" [ref=e294] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - generic [ref=e295]: examples/nft.cell - - heading "NFT marketplace" [level=3] [ref=e296] - - paragraph [ref=e297]: Collection state, listing receipts, transfer, royalty payment. - - generic [ref=e298]: - - generic [ref=e299]: resource - - generic [ref=e300]: receipt - - generic [ref=e301]: preserve - - link "examples/amm_pool.cell AMM pool Shared reserves, LP receipts, swap and liquidity actions. shared receipt slippage" [ref=e302] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - generic [ref=e303]: examples/amm_pool.cell - - heading "AMM pool" [level=3] [ref=e304] - - paragraph [ref=e305]: Shared reserves, LP receipts, swap and liquidity actions. - - generic [ref=e306]: - - generic [ref=e307]: shared - - generic [ref=e308]: receipt - - generic [ref=e309]: slippage - - link "examples/vesting.cell Vesting Grant flow, timepoint checks, claim and revoke paths. flow receipt env" [ref=e310] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - generic [ref=e311]: examples/vesting.cell - - heading "Vesting" [level=3] [ref=e312] - - paragraph [ref=e313]: Grant flow, timepoint checks, claim and revoke paths. - - generic [ref=e314]: - - generic [ref=e315]: flow - - generic [ref=e316]: receipt - - generic [ref=e317]: env - - link "examples/launch.cell Launch flow Launch state, settlement, and sale lifecycle. flow settle claim" [ref=e318] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/launch.cell - - generic [ref=e319]: examples/launch.cell - - heading "Launch flow" [level=3] [ref=e320] - - paragraph [ref=e321]: Launch state, settlement, and sale lifecycle. - - generic [ref=e322]: - - generic [ref=e323]: flow - - generic [ref=e324]: settle - - generic [ref=e325]: claim - - link "examples/registry.cell Registry Name ownership and registry-style state transitions. resource identity replace" [ref=e326] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/registry.cell - - generic [ref=e327]: examples/registry.cell - - heading "Registry" [level=3] [ref=e328] - - paragraph [ref=e329]: Name ownership and registry-style state transitions. - - generic [ref=e330]: - - generic [ref=e331]: resource - - generic [ref=e332]: identity - - generic [ref=e333]: replace - - contentinfo [ref=e334]: - - generic [ref=e335]: - - generic [ref=e336]: - - link "CellScript" [ref=e337] [cursor=pointer]: - - /url: "#top" - - generic [ref=e339]: CellScript - - paragraph [ref=e340]: A semantic DSL for CKB Cell-based smart contracts, with typed metadata and assurance by design. Docs, spec, examples, and source live with the repository. - - generic [ref=e341]: - - link "Docs" [ref=e342] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "Spec" [ref=e343] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "Examples" [ref=e344] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "Source" [ref=e345] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-02T06-22-57-012Z.yml b/.playwright-mcp/page-2026-06-02T06-22-57-012Z.yml deleted file mode 100644 index 8dc7b9da..00000000 --- a/.playwright-mcp/page-2026-06-02T06-22-57-012Z.yml +++ /dev/null @@ -1,318 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "Primary navigation" [ref=e3]: - - link "CellScript home" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link [ref=e8] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - img [ref=e9] - - link [ref=e12] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - img [ref=e13] - - button "Switch to dark mode" [pressed] [ref=e15] [cursor=pointer] - - main [ref=e18]: - - region "CellScript" [ref=e19]: - - generic [ref=e20]: - - heading "CellScript" [level=1] [ref=e21] - - paragraph [ref=e22]: Write Cell contracts as typed transitions, not raw wire format. - - generic [ref=e23]: - - link "Get started" [ref=e24] [cursor=pointer]: - - /url: "#getting-started" - - link "Core model" [ref=e25] [cursor=pointer]: - - /url: "#core-model" - - generic "CellScript contract surface" [ref=e26]: - - generic [ref=e27]: - - term [ref=e28]: target - - definition [ref=e29]: ckb-vm RISC-V - - generic [ref=e30]: - - term [ref=e31]: model - - definition [ref=e32]: schema-backed Cells - - generic [ref=e33]: - - term [ref=e34]: output - - definition [ref=e35]: metadata + ProofPlan - - generic [ref=e36]: - - generic [ref=e38]: token.cell - - combobox "Choose CellScript example" [ref=e40]: - - option "Fungible Token" [selected] - - option "NFT" - - option "AMM Pool" - - option "Vesting" - - tabpanel "Fungible Token" [ref=e42]: - - generic [ref=e43]: - - generic [ref=e44]: "1" - - generic [ref=e45]: module cellscript::fungible_token - - generic [ref=e46]: - - generic [ref=e47]: "2" - - generic [ref=e48]: // ... invariant and MintAuthority omitted - - generic [ref=e49]: - - generic [ref=e50]: "3" - - generic [ref=e51]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e52]: - - generic [ref=e53]: "4" - - generic [ref=e54]: "amount: u64," - - generic [ref=e55]: - - generic [ref=e56]: "5" - - generic [ref=e57]: "symbol: [u8; 8]," - - generic [ref=e58]: - - generic [ref=e59]: "6" - - generic [ref=e60]: "}" - - generic [ref=e62]: "7" - - generic [ref=e63]: - - generic [ref=e64]: "8" - - generic [ref=e65]: "action transfer_token(token: Token, to: Address) -> next_token: Token" - - generic [ref=e66]: - - generic [ref=e67]: "9" - - generic [ref=e68]: where - - generic [ref=e69]: - - generic [ref=e70]: "10" - - generic [ref=e71]: consume token - - generic [ref=e72]: - - generic [ref=e73]: "11" - - generic [ref=e74]: "create next_token = Token { amount: token.amount, symbol: token.symbol } with_lock(to)" - - generic [ref=e76]: "12" - - generic [ref=e77]: - - generic [ref=e78]: "13" - - generic [ref=e79]: "action burn(token: Token)" - - generic [ref=e80]: - - generic [ref=e81]: "14" - - generic [ref=e82]: where - - generic [ref=e83]: - - generic [ref=e84]: "15" - - generic [ref=e85]: assert(token.amount > 0, "cannot burn zero") - - generic [ref=e86]: - - generic [ref=e87]: "16" - - generic [ref=e88]: destroy token - - generic [ref=e89]: - - generic [ref=e90]: $ - - generic [ref=e91]: cellc examples/token.cell --target-profile ckb - - region "Getting Started" [ref=e92]: - - heading "Getting Started" [level=2] [ref=e93] - - generic [ref=e94]: - - article [ref=e95]: - - generic [ref=e96]: "1" - - heading "Install" [level=3] [ref=e97] - - paragraph [ref=e98]: - - code [ref=e99]: cargo install --path . - - article [ref=e100]: - - generic [ref=e101]: "2" - - heading "Compile" [level=3] [ref=e102] - - paragraph [ref=e103]: - - code [ref=e104]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e105]: - - generic [ref=e106]: "3" - - heading "Check" [level=3] [ref=e107] - - paragraph [ref=e108]: - - code [ref=e109]: cellc check --target-profile ckb - - region "Compiler Workflow" [ref=e110]: - - heading "Compiler Workflow" [level=2] [ref=e111] - - generic [ref=e112]: - - generic "Compiler workflow from .cell source to CKB artefact" [ref=e113]: - - article [ref=e114]: - - img [ref=e116]: - - generic [ref=e119]: .cell - - heading "CellScript source" [level=3] [ref=e120] - - img [ref=e122] - - article [ref=e124]: - - img [ref=e126] - - heading "Parse & check" [level=3] [ref=e130] - - paragraph [ref=e131]: Syntax, types, effects - - img [ref=e133] - - article [ref=e135]: - - img [ref=e137] - - heading "IR + Metadata" [level=3] [ref=e143] - - paragraph [ref=e144]: Typed model & assurance info - - img [ref=e146] - - article [ref=e148]: - - img [ref=e150] - - heading "Lower to RISC-V" [level=3] [ref=e154] - - paragraph [ref=e155]: ckb-vm codegen & optimisations - - img [ref=e157] - - article [ref=e159]: - - img [ref=e161]: - - generic [ref=e164]: .elf - - heading "ELF / Assembly" [level=3] [ref=e165] - - paragraph [ref=e166]: RISC-V artefacts for ckb-vm - - complementary "Build for CKB" [ref=e167]: - - heading "Build for CKB" [level=3] [ref=e168] - - list [ref=e169]: - - listitem [ref=e170]: - - img [ref=e171] - - generic [ref=e173]: ckb-vm compatible - - listitem [ref=e174]: - - img [ref=e175] - - generic [ref=e177]: Deterministic execution - - listitem [ref=e178]: - - img [ref=e179] - - generic [ref=e181]: Minimal syscalls - - listitem [ref=e182]: - - img [ref=e183] - - generic [ref=e185]: Scheduler-aware - - region "Core Model" [ref=e186]: - - heading "Core Model" [level=2] [ref=e187] - - paragraph [ref=e188]: "CellScript keeps the contract model visible: Cell shapes, effects, locks, flows, and review metadata stay in one typed surface." - - paragraph [ref=e189]: "Narrow by design: not a general-purpose runtime, not a new VM, not account storage in disguise." - - generic [ref=e190]: - - tablist "CellScript core primitives" [ref=e191]: - - tab "resource" [selected] [ref=e192] [cursor=pointer]: - - generic [ref=e193]: resource - - tab "shared" [ref=e194] [cursor=pointer]: - - generic [ref=e195]: shared - - tab "receipt" [ref=e196] [cursor=pointer]: - - generic [ref=e197]: receipt - - tab "action" [ref=e198] [cursor=pointer]: - - generic [ref=e199]: action - - tab "lock" [ref=e200] [cursor=pointer]: - - generic [ref=e201]: lock - - tab "flow" [ref=e202] [cursor=pointer]: - - generic [ref=e203]: flow - - tab "invariant" [ref=e204] [cursor=pointer]: - - generic [ref=e205]: invariant - - tab "struct / enum" [ref=e206] [cursor=pointer]: - - generic [ref=e207]: struct / enum - - tab "identity" [ref=e208] [cursor=pointer]: - - generic [ref=e209]: identity - - tabpanel "resource" [ref=e211]: - - paragraph [ref=e213]: Owned Cell state with explicit lifecycle effects. - - generic [ref=e214]: - - generic [ref=e215]: - - generic [ref=e216]: Example excerpt - - generic [ref=e217]: examples/token.cell - - generic [ref=e218]: "// examples/token.cell resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance Output" [ref=e219]: - - generic [ref=e220]: - - heading "Assurance Output" [level=2] [ref=e221] - - paragraph [ref=e222]: A local build emits review metadata. Treat the sidecar as useful evidence, not an authenticated proof outside the build boundary. - - article [ref=e224]: - - generic "Assurance output summary" [ref=e225]: - - article [ref=e226]: - - text: Schema - - strong [ref=e227]: v42 - - paragraph [ref=e228]: current compiler metadata schema - - article [ref=e229]: - - text: Source - - strong [ref=e230]: vesting.cell - - paragraph [ref=e231]: shared + receipt + flow - - article [ref=e232]: - - text: Boundary - - strong [ref=e233]: local sidecar - - paragraph [ref=e234]: validated; provenance required when shared - - group [ref=e235]: - - generic "- Metadata excerpt" [ref=e236] [cursor=pointer] - - generic [ref=e237]: "# Representative sidecar excerpt from a local build; keep provenance checks separate." - - generic [ref=e238]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": { \"VestingConfig\": { \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, \"VestingGrant\": { \"kind\": \"receipt\", \"flow\": \"Granted -> Claimable -> FullyClaimed\" } }, \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"status\": \"review evidence\", \"limit\": \"sidecar provenance must be checked outside the compiler\" } }" - - region "Tooling Surface" [ref=e239]: - - heading "Tooling Surface" [level=2] [ref=e240] - - generic [ref=e241]: - - tablist "CellScript tooling commands" [ref=e242]: - - tab "cellc metadata Read/write surface" [selected] [ref=e243] [cursor=pointer]: - - code [ref=e244]: cellc metadata - - generic [ref=e245]: Read/write surface - - tab "cellc constraints Transaction shape" [ref=e246] [cursor=pointer]: - - code [ref=e247]: cellc constraints - - generic [ref=e248]: Transaction shape - - tab "cellc audit-bundle Reviewer packet" [ref=e249] [cursor=pointer]: - - code [ref=e250]: cellc audit-bundle - - generic [ref=e251]: Reviewer packet - - tab "cellc lsp Editor feedback" [ref=e252] [cursor=pointer]: - - code [ref=e253]: cellc lsp - - generic [ref=e254]: Editor feedback - - tabpanel "cellc metadata Read/write surface" [ref=e256]: - - generic [ref=e257]: - - generic [ref=e258]: When - - paragraph [ref=e259]: Use before review or integration. - - generic [ref=e260]: Output - - paragraph [ref=e261]: Schema, effects, source hashes, target profile. - - generic [ref=e262]: - - code [ref=e263]: cellc metadata examples/vesting.cell --target-profile ckb --json - - generic [ref=e264]: "# Emit review metadata for a real example." - - region "Examples" [ref=e265]: - - heading "Examples" [level=2] [ref=e266] - - generic [ref=e267]: - - tablist "Example groups" [ref=e268]: - - tab "Protocols 6 files" [selected] [ref=e269] [cursor=pointer]: - - generic [ref=e270]: Protocols - - generic [ref=e271]: 6 files - - tab "Primitives 6 files" [ref=e272] [cursor=pointer]: - - generic [ref=e273]: Primitives - - generic [ref=e274]: 6 files - - tab "Language 6 files" [ref=e275] [cursor=pointer]: - - generic [ref=e276]: Language - - generic [ref=e277]: 6 files - - tabpanel "Protocols 6 files" [ref=e279]: - - generic [ref=e280]: - - paragraph [ref=e281]: End-to-end contract examples with Cells, actions, and constraints. - - link "Open examples directory" [ref=e282] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - generic [ref=e283]: - - link "examples/token.cell Fungible token Mint, transfer, burn, merge, and amount invariant. resource invariant burn" [ref=e284] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - generic [ref=e285]: examples/token.cell - - heading "Fungible token" [level=3] [ref=e286] - - paragraph [ref=e287]: Mint, transfer, burn, merge, and amount invariant. - - generic [ref=e288]: - - generic [ref=e289]: resource - - generic [ref=e290]: invariant - - generic [ref=e291]: burn - - link "examples/nft.cell NFT marketplace Collection state, listing receipts, transfer, royalty payment. resource receipt preserve" [ref=e292] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - generic [ref=e293]: examples/nft.cell - - heading "NFT marketplace" [level=3] [ref=e294] - - paragraph [ref=e295]: Collection state, listing receipts, transfer, royalty payment. - - generic [ref=e296]: - - generic [ref=e297]: resource - - generic [ref=e298]: receipt - - generic [ref=e299]: preserve - - link "examples/amm_pool.cell AMM pool Shared reserves, LP receipts, swap and liquidity actions. shared receipt slippage" [ref=e300] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - generic [ref=e301]: examples/amm_pool.cell - - heading "AMM pool" [level=3] [ref=e302] - - paragraph [ref=e303]: Shared reserves, LP receipts, swap and liquidity actions. - - generic [ref=e304]: - - generic [ref=e305]: shared - - generic [ref=e306]: receipt - - generic [ref=e307]: slippage - - link "examples/vesting.cell Vesting Grant flow, timepoint checks, claim and revoke paths. flow receipt env" [ref=e308] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - generic [ref=e309]: examples/vesting.cell - - heading "Vesting" [level=3] [ref=e310] - - paragraph [ref=e311]: Grant flow, timepoint checks, claim and revoke paths. - - generic [ref=e312]: - - generic [ref=e313]: flow - - generic [ref=e314]: receipt - - generic [ref=e315]: env - - link "examples/launch.cell Launch flow Launch state, settlement, and sale lifecycle. flow settle claim" [ref=e316] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/launch.cell - - generic [ref=e317]: examples/launch.cell - - heading "Launch flow" [level=3] [ref=e318] - - paragraph [ref=e319]: Launch state, settlement, and sale lifecycle. - - generic [ref=e320]: - - generic [ref=e321]: flow - - generic [ref=e322]: settle - - generic [ref=e323]: claim - - link "examples/registry.cell Registry Name ownership and registry-style state transitions. resource identity replace" [ref=e324] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/registry.cell - - generic [ref=e325]: examples/registry.cell - - heading "Registry" [level=3] [ref=e326] - - paragraph [ref=e327]: Name ownership and registry-style state transitions. - - generic [ref=e328]: - - generic [ref=e329]: resource - - generic [ref=e330]: identity - - generic [ref=e331]: replace - - contentinfo [ref=e332]: - - generic [ref=e333]: - - generic [ref=e334]: - - link "CellScript" [ref=e335] [cursor=pointer]: - - /url: "#top" - - generic [ref=e337]: CellScript - - paragraph [ref=e338]: A semantic DSL for CKB Cell-based smart contracts, with typed metadata and assurance by design. Docs, spec, examples, and source live with the repository. - - generic [ref=e339]: - - link "Docs" [ref=e340] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "Spec" [ref=e341] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "Examples" [ref=e342] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "Source" [ref=e343] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-02T06-24-37-533Z.yml b/.playwright-mcp/page-2026-06-02T06-24-37-533Z.yml deleted file mode 100644 index 8dc7b9da..00000000 --- a/.playwright-mcp/page-2026-06-02T06-24-37-533Z.yml +++ /dev/null @@ -1,318 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "Primary navigation" [ref=e3]: - - link "CellScript home" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link [ref=e8] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - img [ref=e9] - - link [ref=e12] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - img [ref=e13] - - button "Switch to dark mode" [pressed] [ref=e15] [cursor=pointer] - - main [ref=e18]: - - region "CellScript" [ref=e19]: - - generic [ref=e20]: - - heading "CellScript" [level=1] [ref=e21] - - paragraph [ref=e22]: Write Cell contracts as typed transitions, not raw wire format. - - generic [ref=e23]: - - link "Get started" [ref=e24] [cursor=pointer]: - - /url: "#getting-started" - - link "Core model" [ref=e25] [cursor=pointer]: - - /url: "#core-model" - - generic "CellScript contract surface" [ref=e26]: - - generic [ref=e27]: - - term [ref=e28]: target - - definition [ref=e29]: ckb-vm RISC-V - - generic [ref=e30]: - - term [ref=e31]: model - - definition [ref=e32]: schema-backed Cells - - generic [ref=e33]: - - term [ref=e34]: output - - definition [ref=e35]: metadata + ProofPlan - - generic [ref=e36]: - - generic [ref=e38]: token.cell - - combobox "Choose CellScript example" [ref=e40]: - - option "Fungible Token" [selected] - - option "NFT" - - option "AMM Pool" - - option "Vesting" - - tabpanel "Fungible Token" [ref=e42]: - - generic [ref=e43]: - - generic [ref=e44]: "1" - - generic [ref=e45]: module cellscript::fungible_token - - generic [ref=e46]: - - generic [ref=e47]: "2" - - generic [ref=e48]: // ... invariant and MintAuthority omitted - - generic [ref=e49]: - - generic [ref=e50]: "3" - - generic [ref=e51]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e52]: - - generic [ref=e53]: "4" - - generic [ref=e54]: "amount: u64," - - generic [ref=e55]: - - generic [ref=e56]: "5" - - generic [ref=e57]: "symbol: [u8; 8]," - - generic [ref=e58]: - - generic [ref=e59]: "6" - - generic [ref=e60]: "}" - - generic [ref=e62]: "7" - - generic [ref=e63]: - - generic [ref=e64]: "8" - - generic [ref=e65]: "action transfer_token(token: Token, to: Address) -> next_token: Token" - - generic [ref=e66]: - - generic [ref=e67]: "9" - - generic [ref=e68]: where - - generic [ref=e69]: - - generic [ref=e70]: "10" - - generic [ref=e71]: consume token - - generic [ref=e72]: - - generic [ref=e73]: "11" - - generic [ref=e74]: "create next_token = Token { amount: token.amount, symbol: token.symbol } with_lock(to)" - - generic [ref=e76]: "12" - - generic [ref=e77]: - - generic [ref=e78]: "13" - - generic [ref=e79]: "action burn(token: Token)" - - generic [ref=e80]: - - generic [ref=e81]: "14" - - generic [ref=e82]: where - - generic [ref=e83]: - - generic [ref=e84]: "15" - - generic [ref=e85]: assert(token.amount > 0, "cannot burn zero") - - generic [ref=e86]: - - generic [ref=e87]: "16" - - generic [ref=e88]: destroy token - - generic [ref=e89]: - - generic [ref=e90]: $ - - generic [ref=e91]: cellc examples/token.cell --target-profile ckb - - region "Getting Started" [ref=e92]: - - heading "Getting Started" [level=2] [ref=e93] - - generic [ref=e94]: - - article [ref=e95]: - - generic [ref=e96]: "1" - - heading "Install" [level=3] [ref=e97] - - paragraph [ref=e98]: - - code [ref=e99]: cargo install --path . - - article [ref=e100]: - - generic [ref=e101]: "2" - - heading "Compile" [level=3] [ref=e102] - - paragraph [ref=e103]: - - code [ref=e104]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e105]: - - generic [ref=e106]: "3" - - heading "Check" [level=3] [ref=e107] - - paragraph [ref=e108]: - - code [ref=e109]: cellc check --target-profile ckb - - region "Compiler Workflow" [ref=e110]: - - heading "Compiler Workflow" [level=2] [ref=e111] - - generic [ref=e112]: - - generic "Compiler workflow from .cell source to CKB artefact" [ref=e113]: - - article [ref=e114]: - - img [ref=e116]: - - generic [ref=e119]: .cell - - heading "CellScript source" [level=3] [ref=e120] - - img [ref=e122] - - article [ref=e124]: - - img [ref=e126] - - heading "Parse & check" [level=3] [ref=e130] - - paragraph [ref=e131]: Syntax, types, effects - - img [ref=e133] - - article [ref=e135]: - - img [ref=e137] - - heading "IR + Metadata" [level=3] [ref=e143] - - paragraph [ref=e144]: Typed model & assurance info - - img [ref=e146] - - article [ref=e148]: - - img [ref=e150] - - heading "Lower to RISC-V" [level=3] [ref=e154] - - paragraph [ref=e155]: ckb-vm codegen & optimisations - - img [ref=e157] - - article [ref=e159]: - - img [ref=e161]: - - generic [ref=e164]: .elf - - heading "ELF / Assembly" [level=3] [ref=e165] - - paragraph [ref=e166]: RISC-V artefacts for ckb-vm - - complementary "Build for CKB" [ref=e167]: - - heading "Build for CKB" [level=3] [ref=e168] - - list [ref=e169]: - - listitem [ref=e170]: - - img [ref=e171] - - generic [ref=e173]: ckb-vm compatible - - listitem [ref=e174]: - - img [ref=e175] - - generic [ref=e177]: Deterministic execution - - listitem [ref=e178]: - - img [ref=e179] - - generic [ref=e181]: Minimal syscalls - - listitem [ref=e182]: - - img [ref=e183] - - generic [ref=e185]: Scheduler-aware - - region "Core Model" [ref=e186]: - - heading "Core Model" [level=2] [ref=e187] - - paragraph [ref=e188]: "CellScript keeps the contract model visible: Cell shapes, effects, locks, flows, and review metadata stay in one typed surface." - - paragraph [ref=e189]: "Narrow by design: not a general-purpose runtime, not a new VM, not account storage in disguise." - - generic [ref=e190]: - - tablist "CellScript core primitives" [ref=e191]: - - tab "resource" [selected] [ref=e192] [cursor=pointer]: - - generic [ref=e193]: resource - - tab "shared" [ref=e194] [cursor=pointer]: - - generic [ref=e195]: shared - - tab "receipt" [ref=e196] [cursor=pointer]: - - generic [ref=e197]: receipt - - tab "action" [ref=e198] [cursor=pointer]: - - generic [ref=e199]: action - - tab "lock" [ref=e200] [cursor=pointer]: - - generic [ref=e201]: lock - - tab "flow" [ref=e202] [cursor=pointer]: - - generic [ref=e203]: flow - - tab "invariant" [ref=e204] [cursor=pointer]: - - generic [ref=e205]: invariant - - tab "struct / enum" [ref=e206] [cursor=pointer]: - - generic [ref=e207]: struct / enum - - tab "identity" [ref=e208] [cursor=pointer]: - - generic [ref=e209]: identity - - tabpanel "resource" [ref=e211]: - - paragraph [ref=e213]: Owned Cell state with explicit lifecycle effects. - - generic [ref=e214]: - - generic [ref=e215]: - - generic [ref=e216]: Example excerpt - - generic [ref=e217]: examples/token.cell - - generic [ref=e218]: "// examples/token.cell resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance Output" [ref=e219]: - - generic [ref=e220]: - - heading "Assurance Output" [level=2] [ref=e221] - - paragraph [ref=e222]: A local build emits review metadata. Treat the sidecar as useful evidence, not an authenticated proof outside the build boundary. - - article [ref=e224]: - - generic "Assurance output summary" [ref=e225]: - - article [ref=e226]: - - text: Schema - - strong [ref=e227]: v42 - - paragraph [ref=e228]: current compiler metadata schema - - article [ref=e229]: - - text: Source - - strong [ref=e230]: vesting.cell - - paragraph [ref=e231]: shared + receipt + flow - - article [ref=e232]: - - text: Boundary - - strong [ref=e233]: local sidecar - - paragraph [ref=e234]: validated; provenance required when shared - - group [ref=e235]: - - generic "- Metadata excerpt" [ref=e236] [cursor=pointer] - - generic [ref=e237]: "# Representative sidecar excerpt from a local build; keep provenance checks separate." - - generic [ref=e238]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": { \"VestingConfig\": { \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, \"VestingGrant\": { \"kind\": \"receipt\", \"flow\": \"Granted -> Claimable -> FullyClaimed\" } }, \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"status\": \"review evidence\", \"limit\": \"sidecar provenance must be checked outside the compiler\" } }" - - region "Tooling Surface" [ref=e239]: - - heading "Tooling Surface" [level=2] [ref=e240] - - generic [ref=e241]: - - tablist "CellScript tooling commands" [ref=e242]: - - tab "cellc metadata Read/write surface" [selected] [ref=e243] [cursor=pointer]: - - code [ref=e244]: cellc metadata - - generic [ref=e245]: Read/write surface - - tab "cellc constraints Transaction shape" [ref=e246] [cursor=pointer]: - - code [ref=e247]: cellc constraints - - generic [ref=e248]: Transaction shape - - tab "cellc audit-bundle Reviewer packet" [ref=e249] [cursor=pointer]: - - code [ref=e250]: cellc audit-bundle - - generic [ref=e251]: Reviewer packet - - tab "cellc lsp Editor feedback" [ref=e252] [cursor=pointer]: - - code [ref=e253]: cellc lsp - - generic [ref=e254]: Editor feedback - - tabpanel "cellc metadata Read/write surface" [ref=e256]: - - generic [ref=e257]: - - generic [ref=e258]: When - - paragraph [ref=e259]: Use before review or integration. - - generic [ref=e260]: Output - - paragraph [ref=e261]: Schema, effects, source hashes, target profile. - - generic [ref=e262]: - - code [ref=e263]: cellc metadata examples/vesting.cell --target-profile ckb --json - - generic [ref=e264]: "# Emit review metadata for a real example." - - region "Examples" [ref=e265]: - - heading "Examples" [level=2] [ref=e266] - - generic [ref=e267]: - - tablist "Example groups" [ref=e268]: - - tab "Protocols 6 files" [selected] [ref=e269] [cursor=pointer]: - - generic [ref=e270]: Protocols - - generic [ref=e271]: 6 files - - tab "Primitives 6 files" [ref=e272] [cursor=pointer]: - - generic [ref=e273]: Primitives - - generic [ref=e274]: 6 files - - tab "Language 6 files" [ref=e275] [cursor=pointer]: - - generic [ref=e276]: Language - - generic [ref=e277]: 6 files - - tabpanel "Protocols 6 files" [ref=e279]: - - generic [ref=e280]: - - paragraph [ref=e281]: End-to-end contract examples with Cells, actions, and constraints. - - link "Open examples directory" [ref=e282] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - generic [ref=e283]: - - link "examples/token.cell Fungible token Mint, transfer, burn, merge, and amount invariant. resource invariant burn" [ref=e284] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - generic [ref=e285]: examples/token.cell - - heading "Fungible token" [level=3] [ref=e286] - - paragraph [ref=e287]: Mint, transfer, burn, merge, and amount invariant. - - generic [ref=e288]: - - generic [ref=e289]: resource - - generic [ref=e290]: invariant - - generic [ref=e291]: burn - - link "examples/nft.cell NFT marketplace Collection state, listing receipts, transfer, royalty payment. resource receipt preserve" [ref=e292] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - generic [ref=e293]: examples/nft.cell - - heading "NFT marketplace" [level=3] [ref=e294] - - paragraph [ref=e295]: Collection state, listing receipts, transfer, royalty payment. - - generic [ref=e296]: - - generic [ref=e297]: resource - - generic [ref=e298]: receipt - - generic [ref=e299]: preserve - - link "examples/amm_pool.cell AMM pool Shared reserves, LP receipts, swap and liquidity actions. shared receipt slippage" [ref=e300] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - generic [ref=e301]: examples/amm_pool.cell - - heading "AMM pool" [level=3] [ref=e302] - - paragraph [ref=e303]: Shared reserves, LP receipts, swap and liquidity actions. - - generic [ref=e304]: - - generic [ref=e305]: shared - - generic [ref=e306]: receipt - - generic [ref=e307]: slippage - - link "examples/vesting.cell Vesting Grant flow, timepoint checks, claim and revoke paths. flow receipt env" [ref=e308] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - generic [ref=e309]: examples/vesting.cell - - heading "Vesting" [level=3] [ref=e310] - - paragraph [ref=e311]: Grant flow, timepoint checks, claim and revoke paths. - - generic [ref=e312]: - - generic [ref=e313]: flow - - generic [ref=e314]: receipt - - generic [ref=e315]: env - - link "examples/launch.cell Launch flow Launch state, settlement, and sale lifecycle. flow settle claim" [ref=e316] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/launch.cell - - generic [ref=e317]: examples/launch.cell - - heading "Launch flow" [level=3] [ref=e318] - - paragraph [ref=e319]: Launch state, settlement, and sale lifecycle. - - generic [ref=e320]: - - generic [ref=e321]: flow - - generic [ref=e322]: settle - - generic [ref=e323]: claim - - link "examples/registry.cell Registry Name ownership and registry-style state transitions. resource identity replace" [ref=e324] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/registry.cell - - generic [ref=e325]: examples/registry.cell - - heading "Registry" [level=3] [ref=e326] - - paragraph [ref=e327]: Name ownership and registry-style state transitions. - - generic [ref=e328]: - - generic [ref=e329]: resource - - generic [ref=e330]: identity - - generic [ref=e331]: replace - - contentinfo [ref=e332]: - - generic [ref=e333]: - - generic [ref=e334]: - - link "CellScript" [ref=e335] [cursor=pointer]: - - /url: "#top" - - generic [ref=e337]: CellScript - - paragraph [ref=e338]: A semantic DSL for CKB Cell-based smart contracts, with typed metadata and assurance by design. Docs, spec, examples, and source live with the repository. - - generic [ref=e339]: - - link "Docs" [ref=e340] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "Spec" [ref=e341] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "Examples" [ref=e342] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "Source" [ref=e343] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-02T06-42-26-450Z.yml b/.playwright-mcp/page-2026-06-02T06-42-26-450Z.yml deleted file mode 100644 index 27e33fdc..00000000 --- a/.playwright-mcp/page-2026-06-02T06-42-26-450Z.yml +++ /dev/null @@ -1,320 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "Primary navigation" [ref=e3]: - - link "CellScript home" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link [ref=e8] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - img [ref=e9] - - link [ref=e12] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - img [ref=e13] - - button "Switch language" [ref=e15] [cursor=pointer]: - - generic [ref=e16]: 中文 - - button "Switch to dark mode" [pressed] [ref=e17] [cursor=pointer] - - main [ref=e20]: - - region "CellScript" [ref=e21]: - - generic [ref=e22]: - - heading "CellScript" [level=1] [ref=e23] - - paragraph [ref=e24]: Write Cell contracts as typed transitions, not raw wire format. - - generic [ref=e25]: - - link "Get started" [ref=e26] [cursor=pointer]: - - /url: "#getting-started" - - link "Core model" [ref=e27] [cursor=pointer]: - - /url: "#core-model" - - generic "CellScript contract surface" [ref=e28]: - - generic [ref=e29]: - - term [ref=e30]: target - - definition [ref=e31]: ckb-vm RISC-V - - generic [ref=e32]: - - term [ref=e33]: model - - definition [ref=e34]: schema-backed Cells - - generic [ref=e35]: - - term [ref=e36]: output - - definition [ref=e37]: metadata + ProofPlan - - generic [ref=e38]: - - generic [ref=e40]: token.cell - - combobox "Choose CellScript example" [ref=e42]: - - option "Fungible Token" [selected] - - option "NFT" - - option "AMM Pool" - - option "Vesting" - - tabpanel "Fungible Token" [ref=e44]: - - generic [ref=e45]: - - generic [ref=e46]: "1" - - generic [ref=e47]: module cellscript::fungible_token - - generic [ref=e48]: - - generic [ref=e49]: "2" - - generic [ref=e50]: // ... invariant and MintAuthority omitted - - generic [ref=e51]: - - generic [ref=e52]: "3" - - generic [ref=e53]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e54]: - - generic [ref=e55]: "4" - - generic [ref=e56]: "amount: u64," - - generic [ref=e57]: - - generic [ref=e58]: "5" - - generic [ref=e59]: "symbol: [u8; 8]," - - generic [ref=e60]: - - generic [ref=e61]: "6" - - generic [ref=e62]: "}" - - generic [ref=e64]: "7" - - generic [ref=e65]: - - generic [ref=e66]: "8" - - generic [ref=e67]: "action transfer_token(token: Token, to: Address) -> next_token: Token" - - generic [ref=e68]: - - generic [ref=e69]: "9" - - generic [ref=e70]: where - - generic [ref=e71]: - - generic [ref=e72]: "10" - - generic [ref=e73]: consume token - - generic [ref=e74]: - - generic [ref=e75]: "11" - - generic [ref=e76]: "create next_token = Token { amount: token.amount, symbol: token.symbol } with_lock(to)" - - generic [ref=e78]: "12" - - generic [ref=e79]: - - generic [ref=e80]: "13" - - generic [ref=e81]: "action burn(token: Token)" - - generic [ref=e82]: - - generic [ref=e83]: "14" - - generic [ref=e84]: where - - generic [ref=e85]: - - generic [ref=e86]: "15" - - generic [ref=e87]: assert(token.amount > 0, "cannot burn zero") - - generic [ref=e88]: - - generic [ref=e89]: "16" - - generic [ref=e90]: destroy token - - generic [ref=e91]: - - generic [ref=e92]: $ - - generic [ref=e93]: cellc examples/token.cell --target-profile ckb - - region "Getting Started" [ref=e94]: - - heading "Getting Started" [level=2] [ref=e95] - - generic [ref=e96]: - - article [ref=e97]: - - generic [ref=e98]: "1" - - heading "Install" [level=3] [ref=e99] - - paragraph [ref=e100]: - - code [ref=e101]: cargo install --path . - - article [ref=e102]: - - generic [ref=e103]: "2" - - heading "Compile" [level=3] [ref=e104] - - paragraph [ref=e105]: - - code [ref=e106]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e107]: - - generic [ref=e108]: "3" - - heading "Check" [level=3] [ref=e109] - - paragraph [ref=e110]: - - code [ref=e111]: cellc check --target-profile ckb - - region "Compiler Workflow" [ref=e112]: - - heading "Compiler Workflow" [level=2] [ref=e113] - - generic [ref=e114]: - - generic "Compiler workflow from .cell source to CKB artefact" [ref=e115]: - - article [ref=e116]: - - img [ref=e118]: - - generic [ref=e121]: .cell - - heading "CellScript source" [level=3] [ref=e122] - - img [ref=e124] - - article [ref=e126]: - - img [ref=e128] - - heading "Parse & check" [level=3] [ref=e132] - - paragraph [ref=e133]: Syntax, types, effects - - img [ref=e135] - - article [ref=e137]: - - img [ref=e139] - - heading "IR + Metadata" [level=3] [ref=e145] - - paragraph [ref=e146]: Typed model & assurance info - - img [ref=e148] - - article [ref=e150]: - - img [ref=e152] - - heading "Lower to RISC-V" [level=3] [ref=e156] - - paragraph [ref=e157]: ckb-vm codegen & optimisations - - img [ref=e159] - - article [ref=e161]: - - img [ref=e163]: - - generic [ref=e166]: .elf - - heading "ELF / Assembly" [level=3] [ref=e167] - - paragraph [ref=e168]: RISC-V artefacts for ckb-vm - - complementary "Build for CKB" [ref=e169]: - - heading "Build for CKB" [level=3] [ref=e170] - - list [ref=e171]: - - listitem [ref=e172]: - - img [ref=e173] - - generic [ref=e175]: ckb-vm compatible - - listitem [ref=e176]: - - img [ref=e177] - - generic [ref=e179]: Deterministic execution - - listitem [ref=e180]: - - img [ref=e181] - - generic [ref=e183]: Minimal syscalls - - listitem [ref=e184]: - - img [ref=e185] - - generic [ref=e187]: Scheduler-aware - - region "Core Model" [ref=e188]: - - heading "Core Model" [level=2] [ref=e189] - - paragraph [ref=e190]: "CellScript keeps the contract model visible: Cell shapes, effects, locks, flows, and review metadata stay in one typed surface." - - paragraph [ref=e191]: "Narrow by design: not a general-purpose runtime, not a new VM, not account storage in disguise." - - generic [ref=e192]: - - tablist "CellScript core primitives" [ref=e193]: - - tab "resource" [selected] [ref=e194] [cursor=pointer]: - - generic [ref=e195]: resource - - tab "shared" [ref=e196] [cursor=pointer]: - - generic [ref=e197]: shared - - tab "receipt" [ref=e198] [cursor=pointer]: - - generic [ref=e199]: receipt - - tab "action" [ref=e200] [cursor=pointer]: - - generic [ref=e201]: action - - tab "lock" [ref=e202] [cursor=pointer]: - - generic [ref=e203]: lock - - tab "flow" [ref=e204] [cursor=pointer]: - - generic [ref=e205]: flow - - tab "invariant" [ref=e206] [cursor=pointer]: - - generic [ref=e207]: invariant - - tab "struct / enum" [ref=e208] [cursor=pointer]: - - generic [ref=e209]: struct / enum - - tab "identity" [ref=e210] [cursor=pointer]: - - generic [ref=e211]: identity - - tabpanel "resource" [ref=e213]: - - paragraph [ref=e215]: Owned Cell state with explicit lifecycle effects. - - generic [ref=e216]: - - generic [ref=e217]: - - generic [ref=e218]: Example excerpt - - generic [ref=e219]: examples/token.cell - - generic [ref=e220]: "// examples/token.cell resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance Output" [ref=e221]: - - generic [ref=e222]: - - heading "Assurance Output" [level=2] [ref=e223] - - paragraph [ref=e224]: A local build emits review metadata. Treat the sidecar as useful evidence, not an authenticated proof outside the build boundary. - - article [ref=e226]: - - generic "Assurance output summary" [ref=e227]: - - article [ref=e228]: - - text: Schema - - strong [ref=e229]: v42 - - paragraph [ref=e230]: current compiler metadata schema - - article [ref=e231]: - - text: Source - - strong [ref=e232]: vesting.cell - - paragraph [ref=e233]: shared + receipt + flow - - article [ref=e234]: - - text: Boundary - - strong [ref=e235]: local sidecar - - paragraph [ref=e236]: validated; provenance required when shared - - group [ref=e237]: - - generic "- Metadata excerpt" [ref=e238] [cursor=pointer] - - generic [ref=e239]: "# Representative sidecar excerpt from a local build; keep provenance checks separate." - - generic [ref=e240]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": { \"VestingConfig\": { \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, \"VestingGrant\": { \"kind\": \"receipt\", \"flow\": \"Granted -> Claimable -> FullyClaimed\" } }, \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"status\": \"review evidence\", \"limit\": \"sidecar provenance must be checked outside the compiler\" } }" - - region "Tooling Surface" [ref=e241]: - - heading "Tooling Surface" [level=2] [ref=e242] - - generic [ref=e243]: - - tablist "CellScript tooling commands" [ref=e244]: - - tab "cellc metadata Read/write surface" [selected] [ref=e245] [cursor=pointer]: - - code [ref=e246]: cellc metadata - - generic [ref=e247]: Read/write surface - - tab "cellc constraints Transaction shape" [ref=e248] [cursor=pointer]: - - code [ref=e249]: cellc constraints - - generic [ref=e250]: Transaction shape - - tab "cellc audit-bundle Reviewer packet" [ref=e251] [cursor=pointer]: - - code [ref=e252]: cellc audit-bundle - - generic [ref=e253]: Reviewer packet - - tab "cellc lsp Editor feedback" [ref=e254] [cursor=pointer]: - - code [ref=e255]: cellc lsp - - generic [ref=e256]: Editor feedback - - tabpanel "cellc metadata Read/write surface" [ref=e258]: - - generic [ref=e259]: - - generic [ref=e260]: When - - paragraph [ref=e261]: Use before review or integration. - - generic [ref=e262]: Output - - paragraph [ref=e263]: Schema, effects, source hashes, target profile. - - generic [ref=e264]: - - code [ref=e265]: cellc metadata examples/vesting.cell --target-profile ckb --json - - generic [ref=e266]: "# Emit review metadata for a real example." - - region "Examples" [ref=e267]: - - heading "Examples" [level=2] [ref=e268] - - generic [ref=e269]: - - tablist "Example groups" [ref=e270]: - - tab "Protocols 6 files" [selected] [ref=e271] [cursor=pointer]: - - generic [ref=e272]: Protocols - - generic [ref=e273]: 6 files - - tab "Primitives 6 files" [ref=e274] [cursor=pointer]: - - generic [ref=e275]: Primitives - - generic [ref=e276]: 6 files - - tab "Language 6 files" [ref=e277] [cursor=pointer]: - - generic [ref=e278]: Language - - generic [ref=e279]: 6 files - - tabpanel "Protocols 6 files" [ref=e281]: - - generic [ref=e282]: - - paragraph [ref=e283]: End-to-end contract examples with Cells, actions, and constraints. - - link "Open examples directory" [ref=e284] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - generic [ref=e285]: - - link "examples/token.cell Fungible token Mint, transfer, burn, merge, and amount invariant. resource invariant burn" [ref=e286] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - generic [ref=e287]: examples/token.cell - - heading "Fungible token" [level=3] [ref=e288] - - paragraph [ref=e289]: Mint, transfer, burn, merge, and amount invariant. - - generic [ref=e290]: - - generic [ref=e291]: resource - - generic [ref=e292]: invariant - - generic [ref=e293]: burn - - link "examples/nft.cell NFT marketplace Collection state, listing receipts, transfer, royalty payment. resource receipt preserve" [ref=e294] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - generic [ref=e295]: examples/nft.cell - - heading "NFT marketplace" [level=3] [ref=e296] - - paragraph [ref=e297]: Collection state, listing receipts, transfer, royalty payment. - - generic [ref=e298]: - - generic [ref=e299]: resource - - generic [ref=e300]: receipt - - generic [ref=e301]: preserve - - link "examples/amm_pool.cell AMM pool Shared reserves, LP receipts, swap and liquidity actions. shared receipt slippage" [ref=e302] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - generic [ref=e303]: examples/amm_pool.cell - - heading "AMM pool" [level=3] [ref=e304] - - paragraph [ref=e305]: Shared reserves, LP receipts, swap and liquidity actions. - - generic [ref=e306]: - - generic [ref=e307]: shared - - generic [ref=e308]: receipt - - generic [ref=e309]: slippage - - link "examples/vesting.cell Vesting Grant flow, timepoint checks, claim and revoke paths. flow receipt env" [ref=e310] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - generic [ref=e311]: examples/vesting.cell - - heading "Vesting" [level=3] [ref=e312] - - paragraph [ref=e313]: Grant flow, timepoint checks, claim and revoke paths. - - generic [ref=e314]: - - generic [ref=e315]: flow - - generic [ref=e316]: receipt - - generic [ref=e317]: env - - link "examples/launch.cell Launch flow Launch state, settlement, and sale lifecycle. flow settle claim" [ref=e318] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/launch.cell - - generic [ref=e319]: examples/launch.cell - - heading "Launch flow" [level=3] [ref=e320] - - paragraph [ref=e321]: Launch state, settlement, and sale lifecycle. - - generic [ref=e322]: - - generic [ref=e323]: flow - - generic [ref=e324]: settle - - generic [ref=e325]: claim - - link "examples/registry.cell Registry Name ownership and registry-style state transitions. resource identity replace" [ref=e326] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/registry.cell - - generic [ref=e327]: examples/registry.cell - - heading "Registry" [level=3] [ref=e328] - - paragraph [ref=e329]: Name ownership and registry-style state transitions. - - generic [ref=e330]: - - generic [ref=e331]: resource - - generic [ref=e332]: identity - - generic [ref=e333]: replace - - contentinfo [ref=e334]: - - generic [ref=e335]: - - generic [ref=e336]: - - link "CellScript" [ref=e337] [cursor=pointer]: - - /url: "#top" - - generic [ref=e339]: CellScript - - paragraph [ref=e340]: A semantic DSL for CKB Cell-based smart contracts, with typed metadata and assurance by design. Docs, spec, examples, and source live with the repository. - - generic [ref=e341]: - - link "Docs" [ref=e342] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "Spec" [ref=e343] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "Examples" [ref=e344] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "Source" [ref=e345] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-02T06-43-57-736Z.yml b/.playwright-mcp/page-2026-06-02T06-43-57-736Z.yml deleted file mode 100644 index 77cb1b3e..00000000 --- a/.playwright-mcp/page-2026-06-02T06-43-57-736Z.yml +++ /dev/null @@ -1,323 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "Primary navigation" [ref=e3]: - - link "CellScript home" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link "Docs" [ref=e8] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - img [ref=e9] - - generic [ref=e12]: Docs - - link "Source" [ref=e13] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - img [ref=e14] - - generic [ref=e16]: Source - - button "Switch language" [ref=e17] [cursor=pointer]: - - generic [ref=e18]: 中文 - - button "Switch to dark mode" [pressed] [ref=e19] [cursor=pointer]: - - generic [ref=e22]: Dark - - main [ref=e23]: - - region "CellScript" [ref=e24]: - - generic [ref=e25]: - - heading "CellScript" [level=1] [ref=e26] - - paragraph [ref=e27]: Write Cell contracts as typed transitions, not raw wire format. - - generic [ref=e28]: - - link "Get started" [ref=e29] [cursor=pointer]: - - /url: "#getting-started" - - link "Core model" [ref=e30] [cursor=pointer]: - - /url: "#core-model" - - generic "CellScript contract surface" [ref=e31]: - - generic [ref=e32]: - - term [ref=e33]: target - - definition [ref=e34]: ckb-vm RISC-V - - generic [ref=e35]: - - term [ref=e36]: model - - definition [ref=e37]: schema-backed Cells - - generic [ref=e38]: - - term [ref=e39]: output - - definition [ref=e40]: metadata + ProofPlan - - generic [ref=e41]: - - generic [ref=e43]: token.cell - - tablist "CellScript examples" [ref=e45]: - - tab "Fungible Token" [selected] [ref=e46] [cursor=pointer] - - tab "NFT" [ref=e47] [cursor=pointer] - - tab "AMM Pool" [ref=e48] [cursor=pointer] - - tab "Vesting" [ref=e49] [cursor=pointer] - - tabpanel "Fungible Token" [ref=e51]: - - generic [ref=e52]: - - generic [ref=e53]: "1" - - generic [ref=e54]: module cellscript::fungible_token - - generic [ref=e55]: - - generic [ref=e56]: "2" - - generic [ref=e57]: // ... invariant and MintAuthority omitted - - generic [ref=e58]: - - generic [ref=e59]: "3" - - generic [ref=e60]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e61]: - - generic [ref=e62]: "4" - - generic [ref=e63]: "amount: u64," - - generic [ref=e64]: - - generic [ref=e65]: "5" - - generic [ref=e66]: "symbol: [u8; 8]," - - generic [ref=e67]: - - generic [ref=e68]: "6" - - generic [ref=e69]: "}" - - generic [ref=e71]: "7" - - generic [ref=e72]: - - generic [ref=e73]: "8" - - generic [ref=e74]: "action transfer_token(token: Token, to: Address) -> next_token: Token" - - generic [ref=e75]: - - generic [ref=e76]: "9" - - generic [ref=e77]: where - - generic [ref=e78]: - - generic [ref=e79]: "10" - - generic [ref=e80]: consume token - - generic [ref=e81]: - - generic [ref=e82]: "11" - - generic [ref=e83]: "create next_token = Token { amount: token.amount, symbol: token.symbol } with_lock(to)" - - generic [ref=e85]: "12" - - generic [ref=e86]: - - generic [ref=e87]: "13" - - generic [ref=e88]: "action burn(token: Token)" - - generic [ref=e89]: - - generic [ref=e90]: "14" - - generic [ref=e91]: where - - generic [ref=e92]: - - generic [ref=e93]: "15" - - generic [ref=e94]: assert(token.amount > 0, "cannot burn zero") - - generic [ref=e95]: - - generic [ref=e96]: "16" - - generic [ref=e97]: destroy token - - generic [ref=e98]: - - generic [ref=e99]: $ - - generic [ref=e100]: cellc examples/token.cell --target-profile ckb - - region "Getting Started" [ref=e101]: - - heading "Getting Started" [level=2] [ref=e102] - - generic [ref=e103]: - - article [ref=e104]: - - generic [ref=e105]: "1" - - heading "Install" [level=3] [ref=e106] - - paragraph [ref=e107]: - - code [ref=e108]: cargo install --path . - - article [ref=e109]: - - generic [ref=e110]: "2" - - heading "Compile" [level=3] [ref=e111] - - paragraph [ref=e112]: - - code [ref=e113]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e114]: - - generic [ref=e115]: "3" - - heading "Check" [level=3] [ref=e116] - - paragraph [ref=e117]: - - code [ref=e118]: cellc check --target-profile ckb - - region "Compiler Workflow" [ref=e119]: - - heading "Compiler Workflow" [level=2] [ref=e120] - - generic [ref=e121]: - - generic "Compiler workflow from .cell source to CKB artefact" [ref=e122]: - - article [ref=e123]: - - img [ref=e125]: - - generic [ref=e128]: .cell - - heading "CellScript source" [level=3] [ref=e129] - - img [ref=e131] - - article [ref=e133]: - - img [ref=e135] - - heading "Parse & check" [level=3] [ref=e139] - - paragraph [ref=e140]: Syntax, types, effects - - img [ref=e142] - - article [ref=e144]: - - img [ref=e146] - - heading "IR + Metadata" [level=3] [ref=e152] - - paragraph [ref=e153]: Typed model & assurance info - - img [ref=e155] - - article [ref=e157]: - - img [ref=e159] - - heading "Lower to RISC-V" [level=3] [ref=e163] - - paragraph [ref=e164]: ckb-vm codegen & optimisations - - img [ref=e166] - - article [ref=e168]: - - img [ref=e170]: - - generic [ref=e173]: .elf - - heading "ELF / Assembly" [level=3] [ref=e174] - - paragraph [ref=e175]: RISC-V artefacts for ckb-vm - - complementary "Build for CKB" [ref=e176]: - - heading "Build for CKB" [level=3] [ref=e177] - - list [ref=e178]: - - listitem [ref=e179]: - - img [ref=e180] - - generic [ref=e182]: ckb-vm compatible - - listitem [ref=e183]: - - img [ref=e184] - - generic [ref=e186]: Deterministic execution - - listitem [ref=e187]: - - img [ref=e188] - - generic [ref=e190]: Minimal syscalls - - listitem [ref=e191]: - - img [ref=e192] - - generic [ref=e194]: Scheduler-aware - - region "Core Model" [ref=e195]: - - heading "Core Model" [level=2] [ref=e196] - - paragraph [ref=e197]: "CellScript keeps the contract model visible: Cell shapes, effects, locks, flows, and review metadata stay in one typed surface." - - paragraph [ref=e198]: "Narrow by design: not a general-purpose runtime, not a new VM, not account storage in disguise." - - generic [ref=e199]: - - tablist "CellScript core primitives" [ref=e200]: - - tab "resource" [selected] [ref=e201] [cursor=pointer]: - - generic [ref=e202]: resource - - tab "shared" [ref=e203] [cursor=pointer]: - - generic [ref=e204]: shared - - tab "receipt" [ref=e205] [cursor=pointer]: - - generic [ref=e206]: receipt - - tab "action" [ref=e207] [cursor=pointer]: - - generic [ref=e208]: action - - tab "lock" [ref=e209] [cursor=pointer]: - - generic [ref=e210]: lock - - tab "flow" [ref=e211] [cursor=pointer]: - - generic [ref=e212]: flow - - tab "invariant" [ref=e213] [cursor=pointer]: - - generic [ref=e214]: invariant - - tab "struct / enum" [ref=e215] [cursor=pointer]: - - generic [ref=e216]: struct / enum - - tab "identity" [ref=e217] [cursor=pointer]: - - generic [ref=e218]: identity - - tabpanel "resource" [ref=e220]: - - paragraph [ref=e222]: Owned Cell state with explicit lifecycle effects. - - generic [ref=e223]: - - generic [ref=e224]: - - generic [ref=e225]: Example excerpt - - generic [ref=e226]: examples/token.cell - - generic [ref=e227]: "// examples/token.cell resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance Output" [ref=e228]: - - generic [ref=e229]: - - heading "Assurance Output" [level=2] [ref=e230] - - paragraph [ref=e231]: A local build emits review metadata. Treat the sidecar as useful evidence, not an authenticated proof outside the build boundary. - - article [ref=e233]: - - generic "Assurance output summary" [ref=e234]: - - article [ref=e235]: - - text: Schema - - strong [ref=e236]: v42 - - paragraph [ref=e237]: current compiler metadata schema - - article [ref=e238]: - - text: Source - - strong [ref=e239]: vesting.cell - - paragraph [ref=e240]: shared + receipt + flow - - article [ref=e241]: - - text: Boundary - - strong [ref=e242]: local sidecar - - paragraph [ref=e243]: validated; provenance required when shared - - group [ref=e244]: - - generic "- Metadata excerpt" [ref=e245] [cursor=pointer] - - generic [ref=e246]: "# Representative sidecar excerpt from a local build; keep provenance checks separate." - - generic [ref=e247]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": { \"VestingConfig\": { \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, \"VestingGrant\": { \"kind\": \"receipt\", \"flow\": \"Granted -> Claimable -> FullyClaimed\" } }, \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"status\": \"review evidence\", \"limit\": \"sidecar provenance must be checked outside the compiler\" } }" - - region "Tooling Surface" [ref=e248]: - - heading "Tooling Surface" [level=2] [ref=e249] - - generic [ref=e250]: - - tablist "CellScript tooling commands" [ref=e251]: - - tab "cellc metadata Read/write surface" [selected] [ref=e252] [cursor=pointer]: - - code [ref=e253]: cellc metadata - - generic [ref=e254]: Read/write surface - - tab "cellc constraints Transaction shape" [ref=e255] [cursor=pointer]: - - code [ref=e256]: cellc constraints - - generic [ref=e257]: Transaction shape - - tab "cellc audit-bundle Reviewer packet" [ref=e258] [cursor=pointer]: - - code [ref=e259]: cellc audit-bundle - - generic [ref=e260]: Reviewer packet - - tab "cellc lsp Editor feedback" [ref=e261] [cursor=pointer]: - - code [ref=e262]: cellc lsp - - generic [ref=e263]: Editor feedback - - tabpanel "cellc metadata Read/write surface" [ref=e265]: - - generic [ref=e266]: - - generic [ref=e267]: When - - paragraph [ref=e268]: Use before review or integration. - - generic [ref=e269]: Output - - paragraph [ref=e270]: Schema, effects, source hashes, target profile. - - generic [ref=e271]: - - code [ref=e272]: cellc metadata examples/vesting.cell --target-profile ckb --json - - generic [ref=e273]: "# Emit review metadata for a real example." - - region "Examples" [ref=e274]: - - heading "Examples" [level=2] [ref=e275] - - generic [ref=e276]: - - tablist "Example groups" [ref=e277]: - - tab "Protocols 6 files" [selected] [ref=e278] [cursor=pointer]: - - generic [ref=e279]: Protocols - - generic [ref=e280]: 6 files - - tab "Primitives 6 files" [ref=e281] [cursor=pointer]: - - generic [ref=e282]: Primitives - - generic [ref=e283]: 6 files - - tab "Language 6 files" [ref=e284] [cursor=pointer]: - - generic [ref=e285]: Language - - generic [ref=e286]: 6 files - - tabpanel "Protocols 6 files" [ref=e288]: - - generic [ref=e289]: - - paragraph [ref=e290]: End-to-end contract examples with Cells, actions, and constraints. - - link "Open examples directory" [ref=e291] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - generic [ref=e292]: - - link "examples/token.cell Fungible token Mint, transfer, burn, merge, and amount invariant. resource invariant burn" [ref=e293] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - generic [ref=e294]: examples/token.cell - - heading "Fungible token" [level=3] [ref=e295] - - paragraph [ref=e296]: Mint, transfer, burn, merge, and amount invariant. - - generic [ref=e297]: - - generic [ref=e298]: resource - - generic [ref=e299]: invariant - - generic [ref=e300]: burn - - link "examples/nft.cell NFT marketplace Collection state, listing receipts, transfer, royalty payment. resource receipt preserve" [ref=e301] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - generic [ref=e302]: examples/nft.cell - - heading "NFT marketplace" [level=3] [ref=e303] - - paragraph [ref=e304]: Collection state, listing receipts, transfer, royalty payment. - - generic [ref=e305]: - - generic [ref=e306]: resource - - generic [ref=e307]: receipt - - generic [ref=e308]: preserve - - link "examples/amm_pool.cell AMM pool Shared reserves, LP receipts, swap and liquidity actions. shared receipt slippage" [ref=e309] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - generic [ref=e310]: examples/amm_pool.cell - - heading "AMM pool" [level=3] [ref=e311] - - paragraph [ref=e312]: Shared reserves, LP receipts, swap and liquidity actions. - - generic [ref=e313]: - - generic [ref=e314]: shared - - generic [ref=e315]: receipt - - generic [ref=e316]: slippage - - link "examples/vesting.cell Vesting Grant flow, timepoint checks, claim and revoke paths. flow receipt env" [ref=e317] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - generic [ref=e318]: examples/vesting.cell - - heading "Vesting" [level=3] [ref=e319] - - paragraph [ref=e320]: Grant flow, timepoint checks, claim and revoke paths. - - generic [ref=e321]: - - generic [ref=e322]: flow - - generic [ref=e323]: receipt - - generic [ref=e324]: env - - link "examples/launch.cell Launch flow Launch state, settlement, and sale lifecycle. flow settle claim" [ref=e325] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/launch.cell - - generic [ref=e326]: examples/launch.cell - - heading "Launch flow" [level=3] [ref=e327] - - paragraph [ref=e328]: Launch state, settlement, and sale lifecycle. - - generic [ref=e329]: - - generic [ref=e330]: flow - - generic [ref=e331]: settle - - generic [ref=e332]: claim - - link "examples/registry.cell Registry Name ownership and registry-style state transitions. resource identity replace" [ref=e333] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/registry.cell - - generic [ref=e334]: examples/registry.cell - - heading "Registry" [level=3] [ref=e335] - - paragraph [ref=e336]: Name ownership and registry-style state transitions. - - generic [ref=e337]: - - generic [ref=e338]: resource - - generic [ref=e339]: identity - - generic [ref=e340]: replace - - contentinfo [ref=e341]: - - generic [ref=e342]: - - generic [ref=e343]: - - link "CellScript" [ref=e344] [cursor=pointer]: - - /url: "#top" - - generic [ref=e346]: CellScript - - paragraph [ref=e347]: A semantic DSL for CKB Cell-based smart contracts, with typed metadata and assurance by design. Docs, spec, examples, and source live with the repository. - - generic [ref=e348]: - - link "Docs" [ref=e349] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "Spec" [ref=e350] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "Examples" [ref=e351] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "Source" [ref=e352] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-02T07-47-12-690Z.yml b/.playwright-mcp/page-2026-06-02T07-47-12-690Z.yml deleted file mode 100644 index 48129a75..00000000 --- a/.playwright-mcp/page-2026-06-02T07-47-12-690Z.yml +++ /dev/null @@ -1,323 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "主导航" [ref=e3]: - - link "CellScript 首页" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link "文档" [ref=e8] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - img [ref=e9] - - generic [ref=e12]: 文档 - - link "源码" [ref=e13] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - img [ref=e14] - - generic [ref=e16]: 源码 - - button "切换语言" [ref=e17] [cursor=pointer]: - - generic [ref=e18]: English - - button "切换到深色模式" [pressed] [ref=e19] [cursor=pointer]: - - generic [ref=e22]: 深色 - - main [ref=e23]: - - region "CellScript" [ref=e24]: - - generic [ref=e25]: - - heading "CellScript" [level=1] [ref=e26] - - paragraph [ref=e27]: 用 typed transitions 编写 Cell contracts,而不是手写 raw wire format。 - - generic [ref=e28]: - - link "开始使用" [ref=e29] [cursor=pointer]: - - /url: "#getting-started" - - link "核心模型" [ref=e30] [cursor=pointer]: - - /url: "#core-model" - - generic "CellScript 合约表面" [ref=e31]: - - generic [ref=e32]: - - term [ref=e33]: 目标 - - definition [ref=e34]: ckb-vm RISC-V - - generic [ref=e35]: - - term [ref=e36]: 模型 - - definition [ref=e37]: schema-backed Cells - - generic [ref=e38]: - - term [ref=e39]: 输出 - - definition [ref=e40]: metadata + ProofPlan - - generic [ref=e41]: - - generic [ref=e43]: token.cell - - tablist "CellScript 示例" [ref=e45]: - - tab "Fungible Token" [selected] [ref=e46] [cursor=pointer] - - tab "NFT" [ref=e47] [cursor=pointer] - - tab "AMM Pool" [ref=e48] [cursor=pointer] - - tab "Vesting" [ref=e49] [cursor=pointer] - - tabpanel "Fungible Token" [ref=e51]: - - generic [ref=e52]: - - generic [ref=e53]: "1" - - generic [ref=e54]: module cellscript::fungible_token - - generic [ref=e55]: - - generic [ref=e56]: "2" - - generic [ref=e57]: // ... invariant and MintAuthority omitted - - generic [ref=e58]: - - generic [ref=e59]: "3" - - generic [ref=e60]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e61]: - - generic [ref=e62]: "4" - - generic [ref=e63]: "amount: u64," - - generic [ref=e64]: - - generic [ref=e65]: "5" - - generic [ref=e66]: "symbol: [u8; 8]," - - generic [ref=e67]: - - generic [ref=e68]: "6" - - generic [ref=e69]: "}" - - generic [ref=e71]: "7" - - generic [ref=e72]: - - generic [ref=e73]: "8" - - generic [ref=e74]: "action transfer_token(token: Token, to: Address) -> next_token: Token" - - generic [ref=e75]: - - generic [ref=e76]: "9" - - generic [ref=e77]: where - - generic [ref=e78]: - - generic [ref=e79]: "10" - - generic [ref=e80]: consume token - - generic [ref=e81]: - - generic [ref=e82]: "11" - - generic [ref=e83]: "create next_token = Token { amount: token.amount, symbol: token.symbol } with_lock(to)" - - generic [ref=e85]: "12" - - generic [ref=e86]: - - generic [ref=e87]: "13" - - generic [ref=e88]: "action burn(token: Token)" - - generic [ref=e89]: - - generic [ref=e90]: "14" - - generic [ref=e91]: where - - generic [ref=e92]: - - generic [ref=e93]: "15" - - generic [ref=e94]: assert(token.amount > 0, "cannot burn zero") - - generic [ref=e95]: - - generic [ref=e96]: "16" - - generic [ref=e97]: destroy token - - generic [ref=e98]: - - generic [ref=e99]: $ - - generic [ref=e100]: cellc examples/token.cell --target-profile ckb - - region "开始使用" [ref=e101]: - - heading "开始使用" [level=2] [ref=e102] - - generic [ref=e103]: - - article [ref=e104]: - - generic [ref=e105]: "1" - - heading "安装" [level=3] [ref=e106] - - paragraph [ref=e107]: - - code [ref=e108]: cargo install --path . - - article [ref=e109]: - - generic [ref=e110]: "2" - - heading "编译" [level=3] [ref=e111] - - paragraph [ref=e112]: - - code [ref=e113]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e114]: - - generic [ref=e115]: "3" - - heading "检查" [level=3] [ref=e116] - - paragraph [ref=e117]: - - code [ref=e118]: cellc check --target-profile ckb - - region "编译流程" [ref=e119]: - - heading "编译流程" [level=2] [ref=e120] - - generic [ref=e121]: - - generic "从 .cell source 到 CKB artefact 的 compiler workflow" [ref=e122]: - - article [ref=e123]: - - img [ref=e125]: - - generic [ref=e128]: .cell - - heading "CellScript source" [level=3] [ref=e129] - - img [ref=e131] - - article [ref=e133]: - - img [ref=e135] - - heading "解析与检查" [level=3] [ref=e139] - - paragraph [ref=e140]: 语法、类型、effects - - img [ref=e142] - - article [ref=e144]: - - img [ref=e146] - - heading "IR + Metadata" [level=3] [ref=e152] - - paragraph [ref=e153]: typed model 与 assurance 信息 - - img [ref=e155] - - article [ref=e157]: - - img [ref=e159] - - heading "Lower 到 RISC-V" [level=3] [ref=e163] - - paragraph [ref=e164]: ckb-vm codegen 与 optimisations - - img [ref=e166] - - article [ref=e168]: - - img [ref=e170]: - - generic [ref=e173]: .elf - - heading "ELF / Assembly" [level=3] [ref=e174] - - paragraph [ref=e175]: 面向 ckb-vm 的 RISC-V 产物 - - complementary "为 CKB 构建" [ref=e176]: - - heading "为 CKB 构建" [level=3] [ref=e177] - - list [ref=e178]: - - listitem [ref=e179]: - - img [ref=e180] - - generic [ref=e182]: ckb-vm 兼容 - - listitem [ref=e183]: - - img [ref=e184] - - generic [ref=e186]: 确定性执行 - - listitem [ref=e187]: - - img [ref=e188] - - generic [ref=e190]: 最小 syscalls - - listitem [ref=e191]: - - img [ref=e192] - - generic [ref=e194]: 感知 scheduler - - region "核心模型" [ref=e195]: - - heading "核心模型" [level=2] [ref=e196] - - paragraph [ref=e197]: CellScript 让 contract model 保持可见:Cell shapes、effects、locks、flows 与 review metadata 都留在同一个 typed surface 中。 - - paragraph [ref=e198]: 刻意保持窄边界:不是 general-purpose runtime,不是 new VM,也不是 account storage 的伪装。 - - generic [ref=e199]: - - tablist "CellScript core primitives" [ref=e200]: - - tab "resource" [selected] [ref=e201] [cursor=pointer]: - - generic [ref=e202]: resource - - tab "shared" [ref=e203] [cursor=pointer]: - - generic [ref=e204]: shared - - tab "receipt" [ref=e205] [cursor=pointer]: - - generic [ref=e206]: receipt - - tab "action" [ref=e207] [cursor=pointer]: - - generic [ref=e208]: action - - tab "lock" [ref=e209] [cursor=pointer]: - - generic [ref=e210]: lock - - tab "flow" [ref=e211] [cursor=pointer]: - - generic [ref=e212]: flow - - tab "invariant" [ref=e213] [cursor=pointer]: - - generic [ref=e214]: invariant - - tab "struct / enum" [ref=e215] [cursor=pointer]: - - generic [ref=e216]: struct / enum - - tab "identity" [ref=e217] [cursor=pointer]: - - generic [ref=e218]: identity - - tabpanel "resource" [ref=e220]: - - paragraph [ref=e222]: 带有显式 lifecycle effects 的 owned Cell state。 - - generic [ref=e223]: - - generic [ref=e224]: - - generic [ref=e225]: 示例摘录 - - generic [ref=e226]: examples/token.cell - - generic [ref=e227]: "// examples/token.cell resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance 输出" [ref=e228]: - - generic [ref=e229]: - - heading "Assurance 输出" [level=2] [ref=e230] - - paragraph [ref=e231]: 本地 build 会输出 review metadata。sidecar 是有用 evidence,但离开 build boundary 后不是 authenticated proof。 - - article [ref=e233]: - - generic "Assurance 输出摘要" [ref=e234]: - - article [ref=e235]: - - text: Schema - - strong [ref=e236]: v42 - - paragraph [ref=e237]: 当前 compiler metadata schema - - article [ref=e238]: - - text: Source - - strong [ref=e239]: vesting.cell - - paragraph [ref=e240]: shared + receipt + flow - - article [ref=e241]: - - text: Boundary - - strong [ref=e242]: local sidecar - - paragraph [ref=e243]: validated;shared 时仍需 provenance - - group [ref=e244]: - - generic "- Metadata 摘录" [ref=e245] [cursor=pointer] - - generic [ref=e246]: "# 来自本地 build 的代表性 sidecar excerpt;provenance checks 需要单独处理。" - - generic [ref=e247]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": { \"VestingConfig\": { \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, \"VestingGrant\": { \"kind\": \"receipt\", \"flow\": \"Granted -> Claimable -> FullyClaimed\" } }, \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"status\": \"review evidence\", \"limit\": \"sidecar provenance must be checked outside the compiler\" } }" - - region "工具接口" [ref=e248]: - - heading "工具接口" [level=2] [ref=e249] - - generic [ref=e250]: - - tablist "CellScript tooling commands" [ref=e251]: - - tab "cellc metadata 读写表面" [selected] [ref=e252] [cursor=pointer]: - - code [ref=e253]: cellc metadata - - generic [ref=e254]: 读写表面 - - tab "cellc constraints Transaction 形状" [ref=e255] [cursor=pointer]: - - code [ref=e256]: cellc constraints - - generic [ref=e257]: Transaction 形状 - - tab "cellc audit-bundle 审阅包" [ref=e258] [cursor=pointer]: - - code [ref=e259]: cellc audit-bundle - - generic [ref=e260]: 审阅包 - - tab "cellc lsp 编辑器反馈" [ref=e261] [cursor=pointer]: - - code [ref=e262]: cellc lsp - - generic [ref=e263]: 编辑器反馈 - - tabpanel "cellc metadata 读写表面" [ref=e265]: - - generic [ref=e266]: - - generic [ref=e267]: 使用时机 - - paragraph [ref=e268]: review 或 integration 前使用。 - - generic [ref=e269]: 输出 - - paragraph [ref=e270]: Schema、effects、source hashes、target profile。 - - generic [ref=e271]: - - code [ref=e272]: cellc metadata examples/vesting.cell --target-profile ckb --json - - generic [ref=e273]: "# 为真实 example 输出 review metadata。" - - region "示例" [ref=e274]: - - heading "示例" [level=2] [ref=e275] - - generic [ref=e276]: - - tablist "示例分组" [ref=e277]: - - tab "协议 6 个文件" [selected] [ref=e278] [cursor=pointer]: - - generic [ref=e279]: 协议 - - generic [ref=e280]: 6 个文件 - - tab "原语 6 个文件" [ref=e281] [cursor=pointer]: - - generic [ref=e282]: 原语 - - generic [ref=e283]: 6 个文件 - - tab "语言 6 个文件" [ref=e284] [cursor=pointer]: - - generic [ref=e285]: 语言 - - generic [ref=e286]: 6 个文件 - - tabpanel "协议 6 个文件" [ref=e288]: - - generic [ref=e289]: - - paragraph [ref=e290]: 包含 Cells、actions 与 constraints 的端到端 contract examples。 - - link "打开 examples 目录" [ref=e291] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - generic [ref=e292]: - - link "examples/token.cell Fungible Token Mint、transfer、burn、merge 与 amount invariant。 resource invariant burn" [ref=e293] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - generic [ref=e294]: examples/token.cell - - heading "Fungible Token" [level=3] [ref=e295] - - paragraph [ref=e296]: Mint、transfer、burn、merge 与 amount invariant。 - - generic [ref=e297]: - - generic [ref=e298]: resource - - generic [ref=e299]: invariant - - generic [ref=e300]: burn - - link "examples/nft.cell NFT 市场 Collection state、listing receipts、transfer 与 royalty payment。 resource receipt preserve" [ref=e301] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - generic [ref=e302]: examples/nft.cell - - heading "NFT 市场" [level=3] [ref=e303] - - paragraph [ref=e304]: Collection state、listing receipts、transfer 与 royalty payment。 - - generic [ref=e305]: - - generic [ref=e306]: resource - - generic [ref=e307]: receipt - - generic [ref=e308]: preserve - - link "examples/amm_pool.cell AMM Pool Shared reserves、LP receipts、swap 与 liquidity actions。 shared receipt slippage" [ref=e309] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - generic [ref=e310]: examples/amm_pool.cell - - heading "AMM Pool" [level=3] [ref=e311] - - paragraph [ref=e312]: Shared reserves、LP receipts、swap 与 liquidity actions。 - - generic [ref=e313]: - - generic [ref=e314]: shared - - generic [ref=e315]: receipt - - generic [ref=e316]: slippage - - link "examples/vesting.cell Vesting Grant flow、timepoint checks、claim 与 revoke paths。 flow receipt env" [ref=e317] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - generic [ref=e318]: examples/vesting.cell - - heading "Vesting" [level=3] [ref=e319] - - paragraph [ref=e320]: Grant flow、timepoint checks、claim 与 revoke paths。 - - generic [ref=e321]: - - generic [ref=e322]: flow - - generic [ref=e323]: receipt - - generic [ref=e324]: env - - link "examples/launch.cell Launch 流程 Launch state、settlement 与 sale lifecycle。 flow settle claim" [ref=e325] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/launch.cell - - generic [ref=e326]: examples/launch.cell - - heading "Launch 流程" [level=3] [ref=e327] - - paragraph [ref=e328]: Launch state、settlement 与 sale lifecycle。 - - generic [ref=e329]: - - generic [ref=e330]: flow - - generic [ref=e331]: settle - - generic [ref=e332]: claim - - link "examples/registry.cell Registry Name ownership 与 registry-style state transitions。 resource identity replace" [ref=e333] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/registry.cell - - generic [ref=e334]: examples/registry.cell - - heading "Registry" [level=3] [ref=e335] - - paragraph [ref=e336]: Name ownership 与 registry-style state transitions。 - - generic [ref=e337]: - - generic [ref=e338]: resource - - generic [ref=e339]: identity - - generic [ref=e340]: replace - - contentinfo [ref=e341]: - - generic [ref=e342]: - - generic [ref=e343]: - - link "CellScript" [ref=e344] [cursor=pointer]: - - /url: "#top" - - generic [ref=e346]: CellScript - - paragraph [ref=e347]: CellScript 是面向 CKB Cell-based smart contracts 的语义 DSL,内置 typed metadata 与 assurance by design。Docs、spec、examples 与 source 都随 repository 管理。 - - generic [ref=e348]: - - link "文档" [ref=e349] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "规范" [ref=e350] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "示例" [ref=e351] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "源码" [ref=e352] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-02T07-51-19-998Z.yml b/.playwright-mcp/page-2026-06-02T07-51-19-998Z.yml deleted file mode 100644 index d0c1d3d3..00000000 --- a/.playwright-mcp/page-2026-06-02T07-51-19-998Z.yml +++ /dev/null @@ -1,320 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "主导航" [ref=e3]: - - link "CellScript 首页" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link [ref=e8] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - img [ref=e9] - - link [ref=e12] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - img [ref=e13] - - button "切换语言" [ref=e15] [cursor=pointer]: - - generic [ref=e16]: English - - button "切换到深色模式" [pressed] [ref=e17] [cursor=pointer] - - main [ref=e20]: - - region "CellScript" [ref=e21]: - - generic [ref=e22]: - - heading "CellScript" [level=1] [ref=e23] - - paragraph [ref=e24]: 用 typed transitions 编写 Cell contracts,而不是手写 raw wire format。 - - generic [ref=e25]: - - link "开始使用" [ref=e26] [cursor=pointer]: - - /url: "#getting-started" - - link "核心模型" [ref=e27] [cursor=pointer]: - - /url: "#core-model" - - generic "CellScript 合约表面" [ref=e28]: - - generic [ref=e29]: - - term [ref=e30]: 目标 - - definition [ref=e31]: ckb-vm RISC-V - - generic [ref=e32]: - - term [ref=e33]: 模型 - - definition [ref=e34]: schema-backed Cells - - generic [ref=e35]: - - term [ref=e36]: 输出 - - definition [ref=e37]: metadata + ProofPlan - - generic [ref=e38]: - - generic [ref=e40]: token.cell - - combobox "选择 CellScript 示例" [ref=e42]: - - option "Fungible Token" [selected] - - option "NFT" - - option "AMM Pool" - - option "Vesting" - - tabpanel "Fungible Token" [ref=e44]: - - generic [ref=e45]: - - generic [ref=e46]: "1" - - generic [ref=e47]: module cellscript::fungible_token - - generic [ref=e48]: - - generic [ref=e49]: "2" - - generic [ref=e50]: // ... invariant and MintAuthority omitted - - generic [ref=e51]: - - generic [ref=e52]: "3" - - generic [ref=e53]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e54]: - - generic [ref=e55]: "4" - - generic [ref=e56]: "amount: u64," - - generic [ref=e57]: - - generic [ref=e58]: "5" - - generic [ref=e59]: "symbol: [u8; 8]," - - generic [ref=e60]: - - generic [ref=e61]: "6" - - generic [ref=e62]: "}" - - generic [ref=e64]: "7" - - generic [ref=e65]: - - generic [ref=e66]: "8" - - generic [ref=e67]: "action transfer_token(token: Token, to: Address) -> next_token: Token" - - generic [ref=e68]: - - generic [ref=e69]: "9" - - generic [ref=e70]: where - - generic [ref=e71]: - - generic [ref=e72]: "10" - - generic [ref=e73]: consume token - - generic [ref=e74]: - - generic [ref=e75]: "11" - - generic [ref=e76]: "create next_token = Token { amount: token.amount, symbol: token.symbol } with_lock(to)" - - generic [ref=e78]: "12" - - generic [ref=e79]: - - generic [ref=e80]: "13" - - generic [ref=e81]: "action burn(token: Token)" - - generic [ref=e82]: - - generic [ref=e83]: "14" - - generic [ref=e84]: where - - generic [ref=e85]: - - generic [ref=e86]: "15" - - generic [ref=e87]: assert(token.amount > 0, "cannot burn zero") - - generic [ref=e88]: - - generic [ref=e89]: "16" - - generic [ref=e90]: destroy token - - generic [ref=e91]: - - generic [ref=e92]: $ - - generic [ref=e93]: cellc examples/token.cell --target-profile ckb - - region "开始使用" [ref=e94]: - - heading "开始使用" [level=2] [ref=e95] - - generic [ref=e96]: - - article [ref=e97]: - - generic [ref=e98]: "1" - - heading "安装" [level=3] [ref=e99] - - paragraph [ref=e100]: - - code [ref=e101]: cargo install --path . - - article [ref=e102]: - - generic [ref=e103]: "2" - - heading "编译" [level=3] [ref=e104] - - paragraph [ref=e105]: - - code [ref=e106]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e107]: - - generic [ref=e108]: "3" - - heading "检查" [level=3] [ref=e109] - - paragraph [ref=e110]: - - code [ref=e111]: cellc check --target-profile ckb - - region "编译流程" [ref=e112]: - - heading "编译流程" [level=2] [ref=e113] - - generic [ref=e114]: - - generic "从 .cell source 到 CKB artefact 的 compiler workflow" [ref=e115]: - - article [ref=e116]: - - img [ref=e118]: - - generic [ref=e121]: .cell - - heading "CellScript source" [level=3] [ref=e122] - - img [ref=e124] - - article [ref=e126]: - - img [ref=e128] - - heading "解析与检查" [level=3] [ref=e132] - - paragraph [ref=e133]: 语法、类型、effects - - img [ref=e135] - - article [ref=e137]: - - img [ref=e139] - - heading "IR + Metadata" [level=3] [ref=e145] - - paragraph [ref=e146]: typed model 与 assurance 信息 - - img [ref=e148] - - article [ref=e150]: - - img [ref=e152] - - heading "Lower 到 RISC-V" [level=3] [ref=e156] - - paragraph [ref=e157]: ckb-vm codegen 与 optimisations - - img [ref=e159] - - article [ref=e161]: - - img [ref=e163]: - - generic [ref=e166]: .elf - - heading "ELF / Assembly" [level=3] [ref=e167] - - paragraph [ref=e168]: 面向 ckb-vm 的 RISC-V 产物 - - complementary "为 CKB 构建" [ref=e169]: - - heading "为 CKB 构建" [level=3] [ref=e170] - - list [ref=e171]: - - listitem [ref=e172]: - - img [ref=e173] - - generic [ref=e175]: ckb-vm 兼容 - - listitem [ref=e176]: - - img [ref=e177] - - generic [ref=e179]: 确定性执行 - - listitem [ref=e180]: - - img [ref=e181] - - generic [ref=e183]: 最小 syscalls - - listitem [ref=e184]: - - img [ref=e185] - - generic [ref=e187]: 感知 scheduler - - region "核心模型" [ref=e188]: - - heading "核心模型" [level=2] [ref=e189] - - paragraph [ref=e190]: CellScript 让 contract model 保持可见:Cell shapes、effects、locks、flows 与 review metadata 都留在同一个 typed surface 中。 - - paragraph [ref=e191]: 刻意保持窄边界:不是 general-purpose runtime,不是 new VM,也不是 account storage 的伪装。 - - generic [ref=e192]: - - tablist "CellScript core primitives" [ref=e193]: - - tab "resource" [selected] [ref=e194] [cursor=pointer]: - - generic [ref=e195]: resource - - tab "shared" [ref=e196] [cursor=pointer]: - - generic [ref=e197]: shared - - tab "receipt" [ref=e198] [cursor=pointer]: - - generic [ref=e199]: receipt - - tab "action" [ref=e200] [cursor=pointer]: - - generic [ref=e201]: action - - tab "lock" [ref=e202] [cursor=pointer]: - - generic [ref=e203]: lock - - tab "flow" [ref=e204] [cursor=pointer]: - - generic [ref=e205]: flow - - tab "invariant" [ref=e206] [cursor=pointer]: - - generic [ref=e207]: invariant - - tab "struct / enum" [ref=e208] [cursor=pointer]: - - generic [ref=e209]: struct / enum - - tab "identity" [ref=e210] [cursor=pointer]: - - generic [ref=e211]: identity - - tabpanel "resource" [ref=e213]: - - paragraph [ref=e215]: 带有显式 lifecycle effects 的 owned Cell state。 - - generic [ref=e216]: - - generic [ref=e217]: - - generic [ref=e218]: 示例摘录 - - generic [ref=e219]: examples/token.cell - - generic [ref=e220]: "// examples/token.cell resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance 输出" [ref=e221]: - - generic [ref=e222]: - - heading "Assurance 输出" [level=2] [ref=e223] - - paragraph [ref=e224]: 本地 build 会输出 review metadata。sidecar 是有用 evidence,但离开 build boundary 后不是 authenticated proof。 - - article [ref=e226]: - - generic "Assurance 输出摘要" [ref=e227]: - - article [ref=e228]: - - text: Schema - - strong [ref=e229]: v42 - - paragraph [ref=e230]: 当前 compiler metadata schema - - article [ref=e231]: - - text: Source - - strong [ref=e232]: vesting.cell - - paragraph [ref=e233]: shared + receipt + flow - - article [ref=e234]: - - text: Boundary - - strong [ref=e235]: local sidecar - - paragraph [ref=e236]: validated;shared 时仍需 provenance - - group [ref=e237]: - - generic "- Metadata 摘录" [ref=e238] [cursor=pointer] - - generic [ref=e239]: "# 来自本地 build 的代表性 sidecar excerpt;provenance checks 需要单独处理。" - - generic [ref=e240]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": { \"VestingConfig\": { \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, \"VestingGrant\": { \"kind\": \"receipt\", \"flow\": \"Granted -> Claimable -> FullyClaimed\" } }, \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"status\": \"review evidence\", \"limit\": \"sidecar provenance must be checked outside the compiler\" } }" - - region "工具接口" [ref=e241]: - - heading "工具接口" [level=2] [ref=e242] - - generic [ref=e243]: - - tablist "CellScript tooling commands" [ref=e244]: - - tab "cellc metadata 读写表面" [selected] [ref=e245] [cursor=pointer]: - - code [ref=e246]: cellc metadata - - generic [ref=e247]: 读写表面 - - tab "cellc constraints Transaction 形状" [ref=e248] [cursor=pointer]: - - code [ref=e249]: cellc constraints - - generic [ref=e250]: Transaction 形状 - - tab "cellc audit-bundle 审阅包" [ref=e251] [cursor=pointer]: - - code [ref=e252]: cellc audit-bundle - - generic [ref=e253]: 审阅包 - - tab "cellc lsp 编辑器反馈" [ref=e254] [cursor=pointer]: - - code [ref=e255]: cellc lsp - - generic [ref=e256]: 编辑器反馈 - - tabpanel "cellc metadata 读写表面" [ref=e258]: - - generic [ref=e259]: - - generic [ref=e260]: 使用时机 - - paragraph [ref=e261]: review 或 integration 前使用。 - - generic [ref=e262]: 输出 - - paragraph [ref=e263]: Schema、effects、source hashes、target profile。 - - generic [ref=e264]: - - code [ref=e265]: cellc metadata examples/vesting.cell --target-profile ckb --json - - generic [ref=e266]: "# 为真实 example 输出 review metadata。" - - region "示例" [ref=e267]: - - heading "示例" [level=2] [ref=e268] - - generic [ref=e269]: - - tablist "示例分组" [ref=e270]: - - tab "协议 6 个文件" [selected] [ref=e271] [cursor=pointer]: - - generic [ref=e272]: 协议 - - generic [ref=e273]: 6 个文件 - - tab "原语 6 个文件" [ref=e274] [cursor=pointer]: - - generic [ref=e275]: 原语 - - generic [ref=e276]: 6 个文件 - - tab "语言 6 个文件" [ref=e277] [cursor=pointer]: - - generic [ref=e278]: 语言 - - generic [ref=e279]: 6 个文件 - - tabpanel "协议 6 个文件" [ref=e281]: - - generic [ref=e282]: - - paragraph [ref=e283]: 包含 Cells、actions 与 constraints 的端到端 contract examples。 - - link "打开 examples 目录" [ref=e284] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - generic [ref=e285]: - - link "examples/token.cell Fungible Token Mint、transfer、burn、merge 与 amount invariant。 resource invariant burn" [ref=e286] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - generic [ref=e287]: examples/token.cell - - heading "Fungible Token" [level=3] [ref=e288] - - paragraph [ref=e289]: Mint、transfer、burn、merge 与 amount invariant。 - - generic [ref=e290]: - - generic [ref=e291]: resource - - generic [ref=e292]: invariant - - generic [ref=e293]: burn - - link "examples/nft.cell NFT 市场 Collection state、listing receipts、transfer 与 royalty payment。 resource receipt preserve" [ref=e294] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - generic [ref=e295]: examples/nft.cell - - heading "NFT 市场" [level=3] [ref=e296] - - paragraph [ref=e297]: Collection state、listing receipts、transfer 与 royalty payment。 - - generic [ref=e298]: - - generic [ref=e299]: resource - - generic [ref=e300]: receipt - - generic [ref=e301]: preserve - - link "examples/amm_pool.cell AMM Pool Shared reserves、LP receipts、swap 与 liquidity actions。 shared receipt slippage" [ref=e302] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - generic [ref=e303]: examples/amm_pool.cell - - heading "AMM Pool" [level=3] [ref=e304] - - paragraph [ref=e305]: Shared reserves、LP receipts、swap 与 liquidity actions。 - - generic [ref=e306]: - - generic [ref=e307]: shared - - generic [ref=e308]: receipt - - generic [ref=e309]: slippage - - link "examples/vesting.cell Vesting Grant flow、timepoint checks、claim 与 revoke paths。 flow receipt env" [ref=e310] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - generic [ref=e311]: examples/vesting.cell - - heading "Vesting" [level=3] [ref=e312] - - paragraph [ref=e313]: Grant flow、timepoint checks、claim 与 revoke paths。 - - generic [ref=e314]: - - generic [ref=e315]: flow - - generic [ref=e316]: receipt - - generic [ref=e317]: env - - link "examples/launch.cell Launch 流程 Launch state、settlement 与 sale lifecycle。 flow settle claim" [ref=e318] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/launch.cell - - generic [ref=e319]: examples/launch.cell - - heading "Launch 流程" [level=3] [ref=e320] - - paragraph [ref=e321]: Launch state、settlement 与 sale lifecycle。 - - generic [ref=e322]: - - generic [ref=e323]: flow - - generic [ref=e324]: settle - - generic [ref=e325]: claim - - link "examples/registry.cell Registry Name ownership 与 registry-style state transitions。 resource identity replace" [ref=e326] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/registry.cell - - generic [ref=e327]: examples/registry.cell - - heading "Registry" [level=3] [ref=e328] - - paragraph [ref=e329]: Name ownership 与 registry-style state transitions。 - - generic [ref=e330]: - - generic [ref=e331]: resource - - generic [ref=e332]: identity - - generic [ref=e333]: replace - - contentinfo [ref=e334]: - - generic [ref=e335]: - - generic [ref=e336]: - - link "CellScript" [ref=e337] [cursor=pointer]: - - /url: "#top" - - generic [ref=e339]: CellScript - - paragraph [ref=e340]: CellScript 是面向 CKB Cell-based smart contracts 的语义 DSL,内置 typed metadata 与 assurance by design。Docs、spec、examples 与 source 都随 repository 管理。 - - generic [ref=e341]: - - link "文档" [ref=e342] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "规范" [ref=e343] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "示例" [ref=e344] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "源码" [ref=e345] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.playwright-mcp/page-2026-06-02T07-52-23-878Z.yml b/.playwright-mcp/page-2026-06-02T07-52-23-878Z.yml deleted file mode 100644 index 77cb1b3e..00000000 --- a/.playwright-mcp/page-2026-06-02T07-52-23-878Z.yml +++ /dev/null @@ -1,323 +0,0 @@ -- generic [active] [ref=e1]: - - banner [ref=e2]: - - navigation "Primary navigation" [ref=e3]: - - link "CellScript home" [ref=e4] [cursor=pointer]: - - /url: "#top" - - generic [ref=e6]: CellScript - - generic [ref=e7]: - - link "Docs" [ref=e8] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - img [ref=e9] - - generic [ref=e12]: Docs - - link "Source" [ref=e13] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript - - img [ref=e14] - - generic [ref=e16]: Source - - button "Switch language" [ref=e17] [cursor=pointer]: - - generic [ref=e18]: 中文 - - button "Switch to dark mode" [pressed] [ref=e19] [cursor=pointer]: - - generic [ref=e22]: Dark - - main [ref=e23]: - - region "CellScript" [ref=e24]: - - generic [ref=e25]: - - heading "CellScript" [level=1] [ref=e26] - - paragraph [ref=e27]: Write Cell contracts as typed transitions, not raw wire format. - - generic [ref=e28]: - - link "Get started" [ref=e29] [cursor=pointer]: - - /url: "#getting-started" - - link "Core model" [ref=e30] [cursor=pointer]: - - /url: "#core-model" - - generic "CellScript contract surface" [ref=e31]: - - generic [ref=e32]: - - term [ref=e33]: target - - definition [ref=e34]: ckb-vm RISC-V - - generic [ref=e35]: - - term [ref=e36]: model - - definition [ref=e37]: schema-backed Cells - - generic [ref=e38]: - - term [ref=e39]: output - - definition [ref=e40]: metadata + ProofPlan - - generic [ref=e41]: - - generic [ref=e43]: token.cell - - tablist "CellScript examples" [ref=e45]: - - tab "Fungible Token" [selected] [ref=e46] [cursor=pointer] - - tab "NFT" [ref=e47] [cursor=pointer] - - tab "AMM Pool" [ref=e48] [cursor=pointer] - - tab "Vesting" [ref=e49] [cursor=pointer] - - tabpanel "Fungible Token" [ref=e51]: - - generic [ref=e52]: - - generic [ref=e53]: "1" - - generic [ref=e54]: module cellscript::fungible_token - - generic [ref=e55]: - - generic [ref=e56]: "2" - - generic [ref=e57]: // ... invariant and MintAuthority omitted - - generic [ref=e58]: - - generic [ref=e59]: "3" - - generic [ref=e60]: "resource Token has store, create, consume, replace, burn, relock {" - - generic [ref=e61]: - - generic [ref=e62]: "4" - - generic [ref=e63]: "amount: u64," - - generic [ref=e64]: - - generic [ref=e65]: "5" - - generic [ref=e66]: "symbol: [u8; 8]," - - generic [ref=e67]: - - generic [ref=e68]: "6" - - generic [ref=e69]: "}" - - generic [ref=e71]: "7" - - generic [ref=e72]: - - generic [ref=e73]: "8" - - generic [ref=e74]: "action transfer_token(token: Token, to: Address) -> next_token: Token" - - generic [ref=e75]: - - generic [ref=e76]: "9" - - generic [ref=e77]: where - - generic [ref=e78]: - - generic [ref=e79]: "10" - - generic [ref=e80]: consume token - - generic [ref=e81]: - - generic [ref=e82]: "11" - - generic [ref=e83]: "create next_token = Token { amount: token.amount, symbol: token.symbol } with_lock(to)" - - generic [ref=e85]: "12" - - generic [ref=e86]: - - generic [ref=e87]: "13" - - generic [ref=e88]: "action burn(token: Token)" - - generic [ref=e89]: - - generic [ref=e90]: "14" - - generic [ref=e91]: where - - generic [ref=e92]: - - generic [ref=e93]: "15" - - generic [ref=e94]: assert(token.amount > 0, "cannot burn zero") - - generic [ref=e95]: - - generic [ref=e96]: "16" - - generic [ref=e97]: destroy token - - generic [ref=e98]: - - generic [ref=e99]: $ - - generic [ref=e100]: cellc examples/token.cell --target-profile ckb - - region "Getting Started" [ref=e101]: - - heading "Getting Started" [level=2] [ref=e102] - - generic [ref=e103]: - - article [ref=e104]: - - generic [ref=e105]: "1" - - heading "Install" [level=3] [ref=e106] - - paragraph [ref=e107]: - - code [ref=e108]: cargo install --path . - - article [ref=e109]: - - generic [ref=e110]: "2" - - heading "Compile" [level=3] [ref=e111] - - paragraph [ref=e112]: - - code [ref=e113]: cellc examples/token.cell --target riscv64-elf --target-profile ckb - - article [ref=e114]: - - generic [ref=e115]: "3" - - heading "Check" [level=3] [ref=e116] - - paragraph [ref=e117]: - - code [ref=e118]: cellc check --target-profile ckb - - region "Compiler Workflow" [ref=e119]: - - heading "Compiler Workflow" [level=2] [ref=e120] - - generic [ref=e121]: - - generic "Compiler workflow from .cell source to CKB artefact" [ref=e122]: - - article [ref=e123]: - - img [ref=e125]: - - generic [ref=e128]: .cell - - heading "CellScript source" [level=3] [ref=e129] - - img [ref=e131] - - article [ref=e133]: - - img [ref=e135] - - heading "Parse & check" [level=3] [ref=e139] - - paragraph [ref=e140]: Syntax, types, effects - - img [ref=e142] - - article [ref=e144]: - - img [ref=e146] - - heading "IR + Metadata" [level=3] [ref=e152] - - paragraph [ref=e153]: Typed model & assurance info - - img [ref=e155] - - article [ref=e157]: - - img [ref=e159] - - heading "Lower to RISC-V" [level=3] [ref=e163] - - paragraph [ref=e164]: ckb-vm codegen & optimisations - - img [ref=e166] - - article [ref=e168]: - - img [ref=e170]: - - generic [ref=e173]: .elf - - heading "ELF / Assembly" [level=3] [ref=e174] - - paragraph [ref=e175]: RISC-V artefacts for ckb-vm - - complementary "Build for CKB" [ref=e176]: - - heading "Build for CKB" [level=3] [ref=e177] - - list [ref=e178]: - - listitem [ref=e179]: - - img [ref=e180] - - generic [ref=e182]: ckb-vm compatible - - listitem [ref=e183]: - - img [ref=e184] - - generic [ref=e186]: Deterministic execution - - listitem [ref=e187]: - - img [ref=e188] - - generic [ref=e190]: Minimal syscalls - - listitem [ref=e191]: - - img [ref=e192] - - generic [ref=e194]: Scheduler-aware - - region "Core Model" [ref=e195]: - - heading "Core Model" [level=2] [ref=e196] - - paragraph [ref=e197]: "CellScript keeps the contract model visible: Cell shapes, effects, locks, flows, and review metadata stay in one typed surface." - - paragraph [ref=e198]: "Narrow by design: not a general-purpose runtime, not a new VM, not account storage in disguise." - - generic [ref=e199]: - - tablist "CellScript core primitives" [ref=e200]: - - tab "resource" [selected] [ref=e201] [cursor=pointer]: - - generic [ref=e202]: resource - - tab "shared" [ref=e203] [cursor=pointer]: - - generic [ref=e204]: shared - - tab "receipt" [ref=e205] [cursor=pointer]: - - generic [ref=e206]: receipt - - tab "action" [ref=e207] [cursor=pointer]: - - generic [ref=e208]: action - - tab "lock" [ref=e209] [cursor=pointer]: - - generic [ref=e210]: lock - - tab "flow" [ref=e211] [cursor=pointer]: - - generic [ref=e212]: flow - - tab "invariant" [ref=e213] [cursor=pointer]: - - generic [ref=e214]: invariant - - tab "struct / enum" [ref=e215] [cursor=pointer]: - - generic [ref=e216]: struct / enum - - tab "identity" [ref=e217] [cursor=pointer]: - - generic [ref=e218]: identity - - tabpanel "resource" [ref=e220]: - - paragraph [ref=e222]: Owned Cell state with explicit lifecycle effects. - - generic [ref=e223]: - - generic [ref=e224]: - - generic [ref=e225]: Example excerpt - - generic [ref=e226]: examples/token.cell - - generic [ref=e227]: "// examples/token.cell resource Token has store, create, consume, replace, burn, relock { amount: u64, symbol: [u8; 8], }" - - region "Assurance Output" [ref=e228]: - - generic [ref=e229]: - - heading "Assurance Output" [level=2] [ref=e230] - - paragraph [ref=e231]: A local build emits review metadata. Treat the sidecar as useful evidence, not an authenticated proof outside the build boundary. - - article [ref=e233]: - - generic "Assurance output summary" [ref=e234]: - - article [ref=e235]: - - text: Schema - - strong [ref=e236]: v42 - - paragraph [ref=e237]: current compiler metadata schema - - article [ref=e238]: - - text: Source - - strong [ref=e239]: vesting.cell - - paragraph [ref=e240]: shared + receipt + flow - - article [ref=e241]: - - text: Boundary - - strong [ref=e242]: local sidecar - - paragraph [ref=e243]: validated; provenance required when shared - - group [ref=e244]: - - generic "- Metadata excerpt" [ref=e245] [cursor=pointer] - - generic [ref=e246]: "# Representative sidecar excerpt from a local build; keep provenance checks separate." - - generic [ref=e247]: "{ \"metadata_schema_version\": 42, \"module\": \"cellscript::vesting\", \"target_profile\": \"ckb\", \"types\": { \"VestingConfig\": { \"kind\": \"shared\", \"capabilities\": [\"store\", \"create\", \"read_ref\"] }, \"VestingGrant\": { \"kind\": \"receipt\", \"flow\": \"Granted -> Claimable -> FullyClaimed\" } }, \"actions\": [{ \"name\": \"claim_vested\", \"effect_class\": \"Mutating\", \"consume_set\": [\"grant\"], \"create_set\": [\"tokens\", \"updated_grant\"], \"ckb_runtime_features\": [\"current_timepoint\"] }], \"proof_plan\": { \"status\": \"review evidence\", \"limit\": \"sidecar provenance must be checked outside the compiler\" } }" - - region "Tooling Surface" [ref=e248]: - - heading "Tooling Surface" [level=2] [ref=e249] - - generic [ref=e250]: - - tablist "CellScript tooling commands" [ref=e251]: - - tab "cellc metadata Read/write surface" [selected] [ref=e252] [cursor=pointer]: - - code [ref=e253]: cellc metadata - - generic [ref=e254]: Read/write surface - - tab "cellc constraints Transaction shape" [ref=e255] [cursor=pointer]: - - code [ref=e256]: cellc constraints - - generic [ref=e257]: Transaction shape - - tab "cellc audit-bundle Reviewer packet" [ref=e258] [cursor=pointer]: - - code [ref=e259]: cellc audit-bundle - - generic [ref=e260]: Reviewer packet - - tab "cellc lsp Editor feedback" [ref=e261] [cursor=pointer]: - - code [ref=e262]: cellc lsp - - generic [ref=e263]: Editor feedback - - tabpanel "cellc metadata Read/write surface" [ref=e265]: - - generic [ref=e266]: - - generic [ref=e267]: When - - paragraph [ref=e268]: Use before review or integration. - - generic [ref=e269]: Output - - paragraph [ref=e270]: Schema, effects, source hashes, target profile. - - generic [ref=e271]: - - code [ref=e272]: cellc metadata examples/vesting.cell --target-profile ckb --json - - generic [ref=e273]: "# Emit review metadata for a real example." - - region "Examples" [ref=e274]: - - heading "Examples" [level=2] [ref=e275] - - generic [ref=e276]: - - tablist "Example groups" [ref=e277]: - - tab "Protocols 6 files" [selected] [ref=e278] [cursor=pointer]: - - generic [ref=e279]: Protocols - - generic [ref=e280]: 6 files - - tab "Primitives 6 files" [ref=e281] [cursor=pointer]: - - generic [ref=e282]: Primitives - - generic [ref=e283]: 6 files - - tab "Language 6 files" [ref=e284] [cursor=pointer]: - - generic [ref=e285]: Language - - generic [ref=e286]: 6 files - - tabpanel "Protocols 6 files" [ref=e288]: - - generic [ref=e289]: - - paragraph [ref=e290]: End-to-end contract examples with Cells, actions, and constraints. - - link "Open examples directory" [ref=e291] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - generic [ref=e292]: - - link "examples/token.cell Fungible token Mint, transfer, burn, merge, and amount invariant. resource invariant burn" [ref=e293] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/token.cell - - generic [ref=e294]: examples/token.cell - - heading "Fungible token" [level=3] [ref=e295] - - paragraph [ref=e296]: Mint, transfer, burn, merge, and amount invariant. - - generic [ref=e297]: - - generic [ref=e298]: resource - - generic [ref=e299]: invariant - - generic [ref=e300]: burn - - link "examples/nft.cell NFT marketplace Collection state, listing receipts, transfer, royalty payment. resource receipt preserve" [ref=e301] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/nft.cell - - generic [ref=e302]: examples/nft.cell - - heading "NFT marketplace" [level=3] [ref=e303] - - paragraph [ref=e304]: Collection state, listing receipts, transfer, royalty payment. - - generic [ref=e305]: - - generic [ref=e306]: resource - - generic [ref=e307]: receipt - - generic [ref=e308]: preserve - - link "examples/amm_pool.cell AMM pool Shared reserves, LP receipts, swap and liquidity actions. shared receipt slippage" [ref=e309] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/amm_pool.cell - - generic [ref=e310]: examples/amm_pool.cell - - heading "AMM pool" [level=3] [ref=e311] - - paragraph [ref=e312]: Shared reserves, LP receipts, swap and liquidity actions. - - generic [ref=e313]: - - generic [ref=e314]: shared - - generic [ref=e315]: receipt - - generic [ref=e316]: slippage - - link "examples/vesting.cell Vesting Grant flow, timepoint checks, claim and revoke paths. flow receipt env" [ref=e317] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/vesting.cell - - generic [ref=e318]: examples/vesting.cell - - heading "Vesting" [level=3] [ref=e319] - - paragraph [ref=e320]: Grant flow, timepoint checks, claim and revoke paths. - - generic [ref=e321]: - - generic [ref=e322]: flow - - generic [ref=e323]: receipt - - generic [ref=e324]: env - - link "examples/launch.cell Launch flow Launch state, settlement, and sale lifecycle. flow settle claim" [ref=e325] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/launch.cell - - generic [ref=e326]: examples/launch.cell - - heading "Launch flow" [level=3] [ref=e327] - - paragraph [ref=e328]: Launch state, settlement, and sale lifecycle. - - generic [ref=e329]: - - generic [ref=e330]: flow - - generic [ref=e331]: settle - - generic [ref=e332]: claim - - link "examples/registry.cell Registry Name ownership and registry-style state transitions. resource identity replace" [ref=e333] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/blob/main/examples/registry.cell - - generic [ref=e334]: examples/registry.cell - - heading "Registry" [level=3] [ref=e335] - - paragraph [ref=e336]: Name ownership and registry-style state transitions. - - generic [ref=e337]: - - generic [ref=e338]: resource - - generic [ref=e339]: identity - - generic [ref=e340]: replace - - contentinfo [ref=e341]: - - generic [ref=e342]: - - generic [ref=e343]: - - link "CellScript" [ref=e344] [cursor=pointer]: - - /url: "#top" - - generic [ref=e346]: CellScript - - paragraph [ref=e347]: A semantic DSL for CKB Cell-based smart contracts, with typed metadata and assurance by design. Docs, spec, examples, and source live with the repository. - - generic [ref=e348]: - - link "Docs" [ref=e349] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/wiki - - link "Spec" [ref=e350] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/docs - - link "Examples" [ref=e351] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript/tree/main/examples - - link "Source" [ref=e352] [cursor=pointer]: - - /url: https://github.com/a19q3/CellScript \ No newline at end of file diff --git a/.rustfmt.toml b/.rustfmt.toml index 9fb0991e..d6a99aec 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -4,6 +4,6 @@ use_try_shorthand = true use_small_heuristics = "Max" newline_style = "auto" edition = "2024" -# Keep the established layout while the language edition migrates. A future -# style-edition change can be reviewed as a dedicated mechanical diff. +# Formatting dialect only; this is not a CellScript or Rust language +# compatibility edition. style_edition = "2021" diff --git a/AGENTS.md b/AGENTS.md index c2fd06d8..bcc129f5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,10 +11,12 @@ crate at the repo root is `cellscript` (workspace member `.`); a sibling crate `crates/cellscript-wasm` exposes the metadata-only compile path to browsers via `wasm-bindgen`; `crates/cellscript-ckb-adapter` is a CKB-side adapter; and `crates/cellscript-fiber-adapter` implements the bounded no-profile Fiber -interoperability path. The website submodule under `website/` ships an Astro + -WASM playground that loads the prebuilt bundle. +interoperability path. `crates/cellscript-artifact-checker` independently +validates the versioned lowering/source-map/ELF boundary without loading the +compiler front end or code generator. The website submodule under `website/` +ships an Astro + WASM playground that loads the prebuilt bundle. -Version line: the workspace `Cargo.toml` pins `version = "0.22.0"`, Rust +Version line: the workspace `Cargo.toml` pins `version = "0.24.0"`, Rust Edition 2024, and `rust-version = "1.97.1"`. `rust-toolchain.toml` and CI pin that exact toolchain; do not bump either version without coordinating with the release gate. @@ -88,9 +90,9 @@ require extra tooling. | Mode | What it does | | --- | --- | -| `dev` | Explicit workspace-package formatting and checks for the compiler, Fiber adapter, CKB adapter, WASM crate, and CKB SDK builder example; strict backend audit (quick); syntax combo audit (quick); forbidden tracked-file check; `git diff --check`. Run before committing. | -| `ci` | `dev` coverage plus tests and clippy for every workspace package, full package contents check, website build check (requires `npm`), shell + Python syntax check, and trailing-whitespace check. Run before claiming merge-readiness. | -| `backend` | For IR / codegen / assembler / ABI / ELF / RISC-V changes: explicit workspace-package format checking, `cargo check --locked -p cellscript --all-targets`, `cargo test --locked -p cellscript`, `cargo clippy ... -D warnings`, strict backend audit (full, which itself fires the CKB stateful-scenarios harness via `cellscript_ckb_stateful_scenarios.sh`), `git diff --check`. | +| `dev` | Explicit workspace-package formatting and checks for the compiler, standalone artifact checker, Fiber adapter, CKB adapter, WASM crate, CKB SDK builder example, `cellscript-tools`, and both independent Registry verifiers; checker mutation/Myelin handoff tests; simulator package scenarios; reproducible Registry Type Script build and CKB-VM tests; native source-policy enforcement; strict backend audit (quick); syntax combo audit (quick); parity-gated skill-pack freshness; `git diff --check`. Run before committing. | +| `ci` | `dev` coverage plus tests and clippy for every workspace package, `cellscript-tools`, both Registry verifiers, and the Registry Type Script; simulator and CKB-VM package scenarios; Registry API tests plus Node API/verifier bundles; full package contents check, website build check (requires `npm`), shell syntax and native source-policy checks, parity-gated skill-pack freshness, and trailing-whitespace check. Run before claiming merge-readiness. | +| `backend` | For IR / codegen / assembler / ABI / ELF / RISC-V changes: explicit workspace-package format checking, compiler/checker tests and clippy, both package-scenario backends, standalone-checker dependency enforcement, strict backend audit (full, which itself fires the CKB stateful-scenarios harness via `cellscript_ckb_stateful_scenarios.sh`), and `git diff --check`. | | `release` / `release-quick` | Everything `ci` does plus release-auxiliary checks (CKB acceptance, NovaSeal pinning, NovaSeal Rust tooling for RISC-V, fresh WASM + VS Code packaging, CKB tx measure tool, etc.) and the CKB acceptance harness (`scripts/ckb_cellscript_acceptance.sh`). These modes need the pinned sibling CKB checkout from `scripts/ckb_acceptance_pin.json`, the NovaSeal submodule, a sibling `ckb-sdk-rust` checkout at tag `v5.1.0`, Docker for the canonical Linux/amd64 WASM build, and `riscv64imac-unknown-none-elf` for NovaSeal verifier builds. Do not run them casually. | Focused commands are still useful while debugging — `cargo check --locked -p @@ -102,6 +104,8 @@ Notes on Rust toolchain / target: - `rust-version = "1.97.1"` in every in-tree Cargo manifest; `rust-toolchain.toml` and CI select that exact toolchain. +- Registry reproducibility accepts either GNU `sha256sum` or Perl `shasum` and + fails closed if neither SHA-256 tool is available. - The NovaSeal verifier (`proposals/novaseal/v0-mvp-skeleton/verifier/novaseal_btc_verifier_riscv`) builds with `--target riscv64imac-unknown-none-elf` in release mode. `scripts/cellscript_gate.sh` will not pass without it. @@ -116,18 +120,39 @@ The root `Cargo.toml` declares a virtual workspace with these members: - `.` (the `cellscript` library + `cellc` bin at `src/main.rs`) - `crates/cellscript-ckb-adapter` - `crates/cellscript-fiber-adapter` +- `crates/cellscript-artifact-checker` +- `crates/cellscript-tools` - `crates/cellscript-wasm` - `examples/ckb-sdk-builder` Excluded from the workspace (still buildable through their own manifests): +`contracts/registry-type-script`, `services/registry-verifier`, +`services/registry-artifact-verifier`, `proposals/novaseal/v0-mvp-skeleton/{harness,verifier}` and `proposals/novaseal/agreement-profile-v0/harness/ckb_vm`. `tools/ckb-tx-measure` defines its own `[workspace]` (no parent) because it pulls `ckb-jsonrpc-types` and `ckb-types` from a sibling CKB checkout (`../ckb`). +The 0.23 tooling migration is complete. `cellscript-tools` is authoritative +for gate, evidence, fixture, and release validation; website data generation +uses the tracked Node modules under `website/scripts/`. Every gate runs the +native source-policy check, which rejects retired interpreter sources, +generated bytecode/cache artifacts, and interpreter references in active +tooling source across the repository and initialized submodules. + +The 0.24 package closure is lock-authoritative. `Cell.lock` version 3 uses +`cellscript-lock-v0.24-graph-v1` and binds the root manifest digest, canonical +dependency nodes/edges, dependency manifests and sources, feature/test roots, +and CKB environment chain identity. Use `cellc lock` or `cellc update` for an +intentional repin. Build/check/test must not perform mutable version selection; +`--frozen` also forbids network access and lockfile writes. Bounded external +resolvers run only during explicit repinning and normalize to exact Registry or +Git sources before the lock is written. + Features (root crate): -- `default = ["cli", "lsp"]` +- `default = ["cli", "lsp", "vm-runner"]` — native `cellc test` can execute + the authoritative CKB-VM scenario backend without an extra feature flag. - `cli` — pulls `clap`, `colored`, `env_logger`, `keyring`, `reqwest` (rustls), `ring`, `base64`. Native I/O, gated out of the wasm build. - `lsp` — pulls `tower-lsp` and `tokio` (full). Gated out of wasm. @@ -215,8 +240,9 @@ When extracting emitter methods from `codegen/mod.rs` into a sub-module: Fields of types shared across module boundaries also need `pub(crate)`. 4. When removing code by line number with `sed`, delete later ranges first so earlier line numbers stay stable. -5. After every deletion, brace-count with `python3 -c` to verify brace - balance before compiling. +5. After every deletion, run formatting and a focused Rust check. The parser + and compiler are authoritative for brace balance; do not rely on textual + brace-count heuristics. ## CLI surface (where to add a new command) @@ -239,6 +265,11 @@ Existing command families to be aware of: ## Testing approach +- Package runtime fixtures use `cellscript-test-scenario-v1` JSON under + `tests/scenarios/`. `cellc test` requires `--backend simulator|ckb-vm|all` + unless `--no-run` is explicitly selected. Simulator evidence is + non-consensus; CKB-VM evidence is runtime-only, and the v1 runner does not + inject its local Cell bookkeeping into transaction syscalls. - Integration tests live in `tests/*.rs`. Per-version suites exist (`tests/v0_14.rs`, `v0_16.rs`, `v0_17.rs`, `v0_18.rs`) — when adding a versioned boundary, add it to the latest suite and keep prior ones intact @@ -266,8 +297,8 @@ Existing command families to be aware of: ## CKB / NovaSeal gotchas - The CKB acceptance harness is `scripts/ckb_cellscript_acceptance.sh`. It - expects a sibling `../ckb-sdk-rust` checkout at tag `v5.1.0` and runs - `scripts/validate_ckb_cellscript_production_evidence.py` against the build + expects a sibling `../ckb-sdk-rust` checkout at tag `v5.1.0` and runs the + `cellscript-tools validate-production-evidence` command against the build reports. Its build reports, source provenance hashes, and production hardening gate (`final_production_hardening_gate`) are referenced by string from the gate script; if you rename them, update diff --git a/BRANCHES.md b/BRANCHES.md index 07316cd4..612c5658 100644 --- a/BRANCHES.md +++ b/BRANCHES.md @@ -6,32 +6,55 @@ The 0.12-era work is the formal proposal baseline for grant-style acceptance discussions. Do not use that historical baseline to describe the current `main` branch state. +## nightly-0.24 + +`nightly-0.24` is the closed maintenance line for independently verified +artifacts and executable package evidence. It builds on the closed 0.23 +Edition 2026 and native-tooling boundary. The stable release boundary is the +exact `v0.24.0` tag; later commits on the branch are not implicitly part of +that release. External Myelin, Fiber, and RGB++ claims remain separately +evidence gated as described in the 0.24 release notes. + +## nightly-0.23 + +`nightly-0.23` is the implementation-complete predecessor for Edition 2026, +resolved target/assurance/ABI/schema profiles, the deployed Registry path, and +the native release-tooling migration. It deliberately rejects older package, +lock, deployment, receipt, builder, and raw entry-witness identities rather +than migrating them. Its release notes are a development-scope record, not a +stable release certificate or production CKB evidence. + ## nightly-0.22 -`nightly-0.22` is the active implementation line for the 0.22 type-and-set -theory roadmap. It begins from the integrated 0.21.1 `main` checkpoint. Treat -features as shipped only when parser, formatter, type checking, lowering, -metadata, LSP, tests, docs, and the matching gate agree; the branch name is not -production evidence by itself. +`nightly-0.22` is the historical implementation line for the 0.22 type-and-set +theory roadmap. The stable release boundary is the `v0.22.0` tag, not the +nightly branch name. + +## main + +`main` is the integration baseline. Use an exact release tag for stable-release +comparisons and an exact nightly branch for development-scope comparisons; +do not infer release evidence from `main` alone. + +## v0.24.0 -## main / nightly-0.21 +`v0.24.0` is the current stable release for the verified-artifact checker, +executable package scenarios, lock-authoritative package graph, and LS-IDL +Registry path. Use the exact tag ref `refs/tags/v0.24.0` for stable +comparisons. The release does not promote the separately pending Myelin, +Fiber, or RGB++ external evidence boundaries. -`main` and `nightly-0.21` currently carry the 0.21 release-candidate -implementation checkpoint. This line includes the 0.21 compiler, metadata, -CLI, MCP, skill-pack, and builder-resolution work, but it is not a production -CKB release claim until the matching `ci`, backend, and release gates have -recorded passing evidence. +## v0.23.0 -Use this line for 0.21 maintenance work. Keep P2 Template Merkleisation and -new observation syntax out of this line unless their parser, metadata, -backend, docs, and gate evidence are all promoted together. +`v0.23.0` is the historical stable baseline for Edition 2026, the Registry, +and native release tooling. Use the exact tag ref `refs/tags/v0.23.0` when +reproducing that release. -## v0.20.0 +## v0.22.0 -`v0.20.0` is the latest stable release baseline before the 0.21 RC line. Use it -as the comparison point for 0.21 audits, metadata schema changes, and -compatibility notes. Be explicit when comparing against the tag ref -`refs/tags/v0.20.0`, because local branches may also be named `v0.20.0`. +`v0.22.0` is the historical stable baseline for the type-and-set-theory line. +Use the exact tag ref `refs/tags/v0.22.0` when reproducing that release rather +than treating a later nightly branch as equivalent evidence. ## 0.16 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e827e17..674d541b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,441 @@ # Changelog +## 0.24.0 - 2026-08-22 + +- Align the complete 0.24 release identity across every workspace and verifier + crate, the independent checker dependency, lockfiles, Registry Type Script, + Myelin handoff, VS Code extension, website WASM bundle, README, and release + documentation. Restore the 0.23 release hardening that propagates one pinned + CKB checkout through backend scenarios and transaction-measure tooling. + +- Remove the unreachable external RISC-V toolchain fallback and make the + audited internal assembler the sole ELF-emission path. Reassign `E2400` to + the verified lowering/source-map boundary that already uses it, so the + compiler error registry now matches live diagnostics. +- Split the code generator into its documented ABI, assembler, call, + collection, expression, frame, runtime, schema, and Cell-operation modules; + remove crate-wide Clippy exemptions; and replace long positional helper + signatures with named context records. +- Harden the final wide-integer boundary: resolve dynamic Molecule-backed + `u128` fields before loading limbs, preserve the left operand across a + second dynamic load, and make `u128 +/- u64` overflow and underflow fail + closed with runtime error 49. Add exact CKB-VM regression vectors, remove + zero-divisor paths from the NFT and vesting examples, and reject + non-canonical SemVer at Registry admission. +- Remove the CKB adapter's deprecated, permanently fail-closed automatic + deployment methods. Callers must build a verified unsigned deployment + transaction and hand signing to an external wallet. +- Replace the deprecated `serde_yaml` crate with the maintained + `serde_yaml_ng` continuation in the Fiber configuration renderer. +- Remove tracked browser-session traces and unused design captures, ignore + local Codex state, and make the native source-policy check reject future + `.playwright-mcp` artifacts. +- Keep the production and Pudge Testnet Registry websites on one UI contract. + The website gate now builds both environments from the same source, verifies + six shared Registry routes, and requires every generated CSS/JavaScript asset + to be byte-identical. Testnet now ships the LS-IDL route, defaults LS-IDL + lookups and API examples to `testnet`, and no longer preloads production + package records into Manage or artifact-detail fallbacks. Network-specific + origins, chain selection, sandbox expiry, no-index policy, and storage remain + isolated. +- Preserve the corrected website release lineage that removed stale 0.22 + metadata. Publish the homepage as `v0.24.0` while keeping the Playground on + the matching 0.24 compiler identity. The canonical WASM bundle uses asset + identity `20260819-v0.24.0-19ce8898` and SHA-256 + `19ce8898e8161f100edebf6f982d856f3e59bfac31572642b53f2e01c70a1a17`; + distribution checks bind the current stable release URL and displayed tag + separately from the compiler version, asset identity, and digest. + Remove inherited 0.25-only package-interface, typed-semantics, and future- + syntax presentation fields from the 0.24 website branch while retaining the + 0.24 LS-IDL surface. Publish the exact 0.24 and 0.25 website gitlinks on + separate release branches so both parent lines clone without hidden commits. +- Add first-class LS-IDL publication and discovery for CKB Lock Scripts. + `cellc artifact ls-idl` validates the bounded 0.1 schema, appends + `SHA-256(raw idl.json)` to an executable, generates a publish-ready bundle, + and fetches byte-exact IDL by deployed Script identity. Registry admission, + both verifier boundaries, immutable object storage, Postgres lookup, + canonical `/v1/ckb/scripts/:code_hash/interfaces/ls-idl` reads, and the + compatibility `/idl/:code_hash` route all enforce the same schema and + executable-suffix contract. Pin all 17 current upstream client vectors and + seven derive/example IDLs, and add an opt-in test that runs the actual + upstream Rust client against Registry's compatibility handler. Extend that + opt-in acceptance through the fixes merged upstream in `ckb_sudt_script` + PR #7, real RISC-V contract builds, LS-IDL-bound ELFs, and all 25 example + CKB-VM tests without a local compatibility overlay. Add a + runnable Rust example, website lookup/detail surfaces, and VS Code + validate/bind/fetch commands. Name the website tab `LS-IDL` rather than the + ambiguous `Interface`, give it the canonical `/registry/LS-IDL` route with a + permanent redirect from `/registry/interface`, and align its lookup panel + with the full-width Browse surface. + Keep implementation correctness and security review outside this + byte-identity claim. +- Ship the 0.24 package and Registry trust closure, informed by Sui Move's + package-alt separation of resolution from compilation. Replace permissive + custom version checks with standard SemVer; make `Cell.lock` v3 a + manifest-digest-bound dependency graph with exact source/content identity, + outgoing alias edges, runtime/test feature roots, and genesis-bound CKB + environments. Add explicit `cellc lock`, lock-authoritative build/check/test, + `--locked`/`--frozen`/`--offline`, package aliases, optional features, + test-only dependencies, environment overrides, immutable Git-commit and + Registry-snapshot caches, and bounded hash-pinned external resolvers that + normalize to an ordinary source pin and never execute during locked builds. + Keep build dependencies fail-closed until isolated execution exists. Replace + scattered Registry artifact-profile conditionals with the versioned, + fail-closed `cellscript-registry-profile-catalog-v1`; only CellScript source + profiles are dependency-resolving, while executable, reproducible, and copy + profiles remain explicit non-resolving artifacts. Add a portable + `examples/package_graph` fixture that executes alias, SemVer, feature, + test-only, environment, and override selection from the frozen graph. +- Implement the 0.24 trust-closure core. CKB ELF builds now emit canonical + `cellscript-verified-lowering-record-v1` and + `cellscript-source-artifact-map-v1` sidecars, bound by metadata schema 58 and + checked by the compiler-independent, budgeted + `cellscript-artifact-checker`. The checker independently parses static + ELF64/RISC-V layout, decodes the emitted instruction/call/branch surface, + checks CFG reachability, frames and stack restoration, ABI/ProofPlan/syscall + contracts, block digests, source ranges, and cross-file identities with + stable `V2400`-`V2418` rejection codes and deterministic mutations. Package + the checker independently and require checker-first crates.io publication + before the matching compiler crate. Extend + `verify-artifact` with separate binding, structural, lowering-record, + CKB-VM, chain, and semantic-equivalence states. Make `cellc test` require an + explicit simulator/CKB-VM backend for execution and add versioned, + fail-closed scenarios with exact runtime errors, local multi-step live-Cell + replacement, source-linked coverage, cycle/size/capacity limits, and exact + artifact/checker bindings. Add a least-privilege Registry artifact worker + whose production graph excludes the compiler. Freeze the CellScript side of + the Myelin handoff without a new profile or raw-witness alias; keep external + Myelin adoption and the incomplete Fiber/RGB++ matrices explicitly pending. + Add `examples/scenario_basics` as the runnable positive/exact-negative + scenario and four-file verified-artifact walkthrough. +- Freeze the 0.23 implementation scope around Edition 2026 and its resolved + profile/entry identities, the deployed Registry and publisher-session path, + native gate tooling, the recoverable website workbench, and the bounded Fiber + evidence actually obtained on this line. Keep mainnet Registry Script + activation, publisher-owned wallet adoption, and incomplete Fiber/RGB++ + matrices as explicit external checkpoints. Retire the proposed CellScript + Off-Chain Session Runtime target: current Myelin uses an attested external + compiler process, production requests stay on `ckb`, and Myelin-owned + extended semantics remain outside the compiler. Add the 0.24 trust-closure + roadmap for an independent bounded artifact checker, executable package + tests, source maps, the Myelin adapter handoff, and conditional ecosystem + evidence promotion. + +## 0.23.0 - 2026-08-11 + +- Make Registry chain confirmation compatible with the standard CKB v0.207.0 + RPC schema by resolving a live Cell's committed block through + `get_transaction.tx_status` instead of depending on a proxy-specific + `get_live_cell.block_hash` extension. Recorded evidence now names both RPC + methods while historical evidence identifiers remain readable. Make the + tooling-release gate parse website scripts structurally and enforce the + stable build steps in order, so adding intermediate regression checks no + longer breaks CI through an obsolete exact-string comparison. Let the full + backend stateful audit use an explicit isolated pinned CKB checkout through + `CELLSCRIPT_CKB_REPO`, avoiding any need to modify an unrelated sibling CKB + worktree during release validation. Propagate the release gate's existing + `--ckb-repo` selection to its independent `ckb-tx-measure` workspace as + well, so every CKB-dependent release check resolves against the same pin. +- Turn the browser Playground into a recoverable Cell-oriented workbench. + Browser-local workspace snapshots now retain source files, entry selection, + active panels, and an honest saved/dirty state across refreshes. Failed + compiles preserve the last valid output as explicitly stale evidence, and a + failed compiler Worker can be restarted without reloading the page. Add a + metadata-derived Cell Flow view, source-linked action/type selection, a + contextual Inspector, and an optional three-step guide while keeping raw + actions, types, metadata, diagnostics, and the existing no-ELF WASM boundary + available. Unify the site's interactive controls around dense, standard, and + workflow button sizes with distinct neutral, selected, and primary states. + Registry and Playground actions now share the same contrast-safe treatment, + compact copy controls, focus rings, press feedback, and Phosphor interaction + icons. The Playground compile action keeps a stable label and exposes busy + state without turning the action itself into a transient status display. +- Bound Registry discovery requests so the interface can no longer remain in + an indefinite loading state. The browser now delays skeletons to avoid + flashes on fast responses, reports slow and retrying requests, retries once + with a strict deadline, preserves stale or mirrored results when available, + and otherwise presents an explicit recovery action. Registry rows and empty + states use compact artifact identity marks and low-motion transitions instead + of generic placeholder panels. Redesign the global navigation around three + primary destinations, quieter utility controls, Phosphor SVG icons, and a + touch-safe mobile drawer with focus containment, Escape/backdrop dismissal, + scroll locking, and persistent theme and language controls. Source discovery + now has a quiet hover/focus label, while fixed full and compact language + controls prevent locale changes from shifting the desktop navigation. + The Playground now places its toolbar, compiler panels, and status bar in a + centred wide-screen Studio frame instead of switching ambiguously between + the site frame and an edge-to-edge editor. An explicit, persisted focus mode + removes site chrome and expands the same workbench to the viewport without a + first-paint flash; phones retain the existing panel switcher and site header. + Registry discovery now translates verification, deployment, availability, + and consumption mode into one consumer-facing use conclusion, supports + URL-restored intent filters, and shows the latest release date without + replacing the canonical status axes. Artifact details split the consumer + action from the maintainer's current evidence or deployment task, explain + each accepted evidence kind while keeping full hashes and raw JSON + accessible, and avoid presenting build verification as a security audit. + Maintenance keeps the selected task visible while progressively disclosing + alternate and destructive operations. +- Close the first-publish browser/CLI loop with `cellc publish --authorise`. + cellc now creates and stores the delegated P-256 publishing key locally, + opens a 15-minute exact-coordinate wallet session, and resumes publishing + automatically after Registry approval; `--no-open` supports remote and + terminal-only environments. Session reads expose neither the polling secret + nor the resulting key ID to the browser. The publishing key is written to + the OS keychain as `pending` before the browser opens, promoted to `active` + only when either successful status returns the matching key ID, and removed + only after the Registry confirms cancellation or pending-session expiry. A + local polling deadline performs one final authoritative read and otherwise + preserves the pending key. Completed sessions remain poll-readable for 24 + hours after their 15-minute approval window, closing the boundary race in + which wallet approval commits just before the CLI's next poll. This closes + the process-exit window after wallet approval without treating local state + as Registry authority. The browser + token survives same-tab refresh in `sessionStorage` and is removed on + completion or expiry, with an executable storage-lifecycle regression test. + Session mode now + lists only connectors that can actually complete the browser flow and folds + challenge creation, wallet signing, and completion into one **Approve + publishing access** action; the full external-wallet directory remains in + the explicit manual CLI path. Session completion atomically consumes the + nonce, records the publishing key, claims or reviews the namespace, updates + the session, and writes its audit trail. Concurrent or replayed completion + returns the committed result without duplicating authority. The Publish page + is now session-first: a direct visit presents one `cellc publish --authorise` + starting command, while a CLI session becomes a one-screen wallet approval + surface with one current action and end-to-end release progress. Artifact + identity is read-only in session mode because cellc and the manifest remain + authoritative. External signing, manifest scaffolding, and existing-key + checks remain available in a deliberately secondary advanced workspace. + Technical scope and session identifiers stay collapsed by default, and + loading, expiry, retry, review-pending, and terminal-continuation states keep + the same stable layout. Safe publishing-access reads retry once with bounded + deadlines, while signed writes are never retried automatically; an unchanged + failed request keeps its signature, and any coordinate or payload change + clears it with an explicit explanation. +- Add an isolated Pudge Testnet Registry Sandbox. Its API, Postgres database, + object volume, signing origin, RPC identity, website build, wallet storage, + and deployment evidence are separate from production. Sandbox releases are + hidden 72 hours after admission; version JSON is deleted at expiry and source + objects are deleted after a 24-hour grace period, while minimal audit + tombstones remain. The API rejects a wrong-network RPC and cross-environment + deployment payloads. `cellc artifact record-deployment --network testnet` + defaults to the Pudge Registry API, and `cell-dep` revalidates liveness on the + network recorded in accepted evidence. Pudge chain history remains immutable: + expiry removes Registry indexing and off-chain objects, not on-chain Cells. +- Complete the Registry's generalized artifact and chain-evidence path. Rust, + C, JavaScript, and other CKB artifacts now keep explicit source, build, + deployment, TCB, and copy-only identities instead of being presented as + CellScript dependencies. Reproducible profiles require P-256-signed reports + from two to sixteen policy-approved builders spanning the configured minimum + number of independent trust domains. Reports bind the signed environment, + source, recipe, executable, build log, builder identity, and predecessor + evidence before verification becomes `verified`; deployment is rejected + until that evidence exists. Add `cellc artifact reproduction-report` and + `cellc artifact reproduction-evidence`, wallet-ready mainnet commitment + transaction intents, and `cellc auth reproducer create` for generating a + builder-local P-256 key plus a public policy enrollment record without + exposing PKCS#8 material. Explicit CI-key output is mode 0600 on Unix and + no-overwrite. Add fixed Registry Type/commitment Lock configuration, + Type-Script-indexed `CSREGv1` scans, and scheduled lifecycle reconciliation + that demotes spent commitments or stale deployment Cells without deleting + historical evidence. Both Script code CellDeps must be live and sufficiently + confirmed before the chain path becomes ready. The chain path is implemented + but remains operationally disabled until the canonical mainnet Registry Type + Script, commitment custody Lock, and both CellDeps are deployed and configured. +- Harden the unified artifact Registry boundary: default discovery now hides + pending/rejected releases and paginates by package coordinate; deployment + records and admin recovery must match the immutable CKB `hash_type` and + `dep_type`; generated CellDep descriptors re-query mainnet and reject spent + code/DepGroup Cells; RPC calls are time- and size-bounded; and deployment + capability use commits with the chain-verified state. Positive static-mirror + publication now follows database admission, while suppressive states are + mirrored first to fail closed; deferred sync is audited rather than + advertising uncommitted positive state. Add the capability-signed + `cellc artifact set-availability` publisher path used by Manage, defensive + frontend page deduplication, and complete `Artifact.toml` plus bundle + scaffolding for non-CellScript submissions. +- Split delegated Registry authority into independent `publish`, `deployment`, + and `availability` scopes. Release admission no longer grants permission to + attach CKB deployment evidence or change a release's public availability; + exact-coordinate and namespace-wildcard grants remain supported. In a + package directory the CLI infers only the exact `publish` scope; deployment + and availability grants require explicit `--scope` flags. The API, Submit command builder, + validation, tests, and operator documentation now share this contract. +- Redesign the Registry submission and package-maintenance surfaces around + contextual, task-first workflows: remove the public `Manage` tab and + redundant form controls, link maintenance from package details, guide first + publication through explicit connect, sign, submit, and namespace-claim actions, show the publication + orientation only once per browser, replace the CCC post-connect surface with + a compact Registry-owned wallet chooser that has no unrelated `Manage` + action, reveal yank fields only for the yank task, and close write commands + over verify, dry-run, and publish. Registry route and workflow state changes + now use reduced-motion-aware transitions instead of abrupt swaps. Browse and + Submit share one DOM-persistent Registry header through navigation, avoiding + replacement flicker while retaining the active locale; wallet connection no + longer gates artifact definition or local preflight, and appears only after + the developer has chosen an artifact coordinate and the new-capability path. + Existing capability keys use a read-only server check for live status, + expiry, exact publish scope, and active namespace ownership; entering a key + ID never unlocks the UI locally. Final publish commands include the + server-confirmed `--capability-key-id`. Primary authorisation controls use + larger, shorter-reach interaction targets. Client-routed returns now + reinitialize Submit and artifact-detail behavior instead of leaving stale + event handlers behind. The advanced publisher keeps a per-environment, + same-tab draft of non-secret artifact fields and UI state while explicitly + excluding wallet signatures, challenge/browser tokens, capability payloads, + and private keys. Registry, Publish, and API also share one route-transition, + vertical-rhythm, active-tab, and localized-title contract; Browse reuses its + latest in-memory result during background refresh rather than flashing a + skeleton on every return. + Browse uses a no-flash loading state, URL-backed server search, and API + pagination; bundled data appears only as an explicitly labelled error + fallback. Static and live package details share one responsive view with + localized statuses and copyable audit values. Publisher authorisation now + accepts both JoyID + (`joyid_ckb`) and standard CKB secp256k1 (`ckb_secp256k1`) principals through + the CCC CKB-signer boundary. Production exposes only mainnet; the separately + built Pudge Sandbox constructs a testnet client without adding a network + selector to either environment. The frontend never accepts + mnemonic words; traditional recovery phrases remain inside the wallet. CLI + auth commands use `--wallet-signature`, with `--joyid-signature` retained as + a visible compatibility alias, and the API adds the corresponding typed + principal migration and signature verification. The compact chooser now + preserves the complete twelve-wallet CKB directory: compatible CCC CKB + signers connect directly, while other entries are explicitly labelled as + external links for importing a compatible `wallet-signature.json`; opening a + link is never represented as a wallet connection. The browser checks the + signature shape and principal binding before submission, while the API + remains authoritative for cryptographic verification. Every entry + now uses the corresponding official Nervos wallet-directory SVG rather than + an autogenerated letter mark or a runtime favicon. The chooser header no + longer reserves space for a hidden back control, so its title, explanatory + text, and wallet list share one left alignment edge. Submit now asks for the + artifact kind and source language independently, and Manage groups publish, + inspection, reproduction, deployment, commitment, and availability as + isolated task flows; hidden task fields can no longer leak into the selected + workflow. +- Deploy the public Registry production slice at + `api.registry.cellscript.dev` and `registry.cellscript.dev`: Postgres 17 is + the authoritative write store, the Node 22 adapter persists source snapshots + and version-addressed JSON to an isolated object volume, and a read-only + nginx service exposes `/packages/*` independently of the API/database + process. The production stack adds live dependency-aware readiness, bounded + request bodies, structured logs, health checks, log rotation, generated + secrets, HTTPS, and an 8 MiB proxy admission limit sized for the 5 MiB source + snapshot contract. Public package search, package detail, and ordered + evidence promotion APIs are live. A daily systemd job writes atomic, + checksum-protected Postgres/object-store backups with bounded retention; its + first backup passed database and archive restore inspection. Public version + responses now expose immutable snapshot descriptors, the read-only service + serves those content-addressed snapshots, and the CLI verifies object SHA-256, + safe paths, per-file BLAKE2b, and the whole-tree source hash before atomically + materialising a dependency. The CLI uses the public API's accepted status as + the default resolution authority while retaining the explicit + `CELLSCRIPT_REGISTRY_URL` Git/offline override, and the website renders the + live Registry with a clearly labelled read-only bundled mirror only when the + API is unavailable. The former Registry Coming Soon surface is removed. + First-publish admission is now user-reachable end to end: `cellc auth + namespace claim` and the submit page's **Claim namespace** action explicitly + establish namespace ownership between capability registration and publish. + Publish admission now commits package, snapshot, version, capability-use, + acceptance-audit, and completed-idempotency state in one database transaction; + pre-admission failures release the request-owned nonce and retry reservation, + while production readiness verifies both managed object-store prefixes and + volume initialization repairs their ownership and modes recursively. + Explicit unverified/quarantined install acknowledgements are persisted in + dependency tables, preventing lock refreshes and later builds from losing the + caller's risk policy. Publish admission now transactionally creates a leased, + bounded verification job. A separate least-privilege worker authenticates the + immutable snapshot, compiles it with the current CellScript compiler, checks + the signed manifest and compatibility-profile identities, atomically records + `verified_build` evidence, and then converges the static version object. + PostgreSQL `FOR UPDATE SKIP LOCKED` claims, expiring leases, three-attempt + retry/dead-letter handling, operator queue metrics/requeue endpoints, bounded + subprocess time/output/memory, and API readiness tied to the worker heartbeat + make the formerly documented asynchronous queue real. Public search/list now + excludes `source_published` and `indexed_pending` by default while preserving + explicit status queries and direct audit URLs. Package-manifest identity uses + canonical recursively sorted JSON, eliminating cross-process `HashMap` order + drift between publisher and verifier. Deploy that worker to the live + production topology and exercise external publish, queue claim, real + compilation, evidence promotion, static convergence, default visibility, and + a fresh consumer install/check/build without an unverified override. The + one-time seeded smoke identity and live objects were removed afterward, queue + counts returned to zero, and a checksum-verified backup captured the migrated + clean state. Production Compose now accepts explicit prebuilt API/verifier + image references so shared hosts can deploy with `--no-build`. Harden the API + and static Registry response boundary with HSTS, anti-framing, no-sniff, + permissions policy, cross-domain-policy denial, and a deny-all CSP for JSON + surfaces. Add a reproducible website production Compose/nginx contract with + a read-only root filesystem, bounded temporary filesystems, health checks, + log rotation, `no-new-privileges`, and matching browser security headers. A + production recovery drill restores the post-`0002` dump into an isolated + Postgres 17 container, extracts the object archive into an isolated volume, + verifies both migrations and all seven core Registry tables, and removes the + temporary restore resources afterward. +- Close the 0.23 syntax-audit consistency gaps: canonical type declarations + now use comma-terminated fields, syntax-combination gates cover canonical and + comma-free compatibility input, checked example mirrors use named `U64_MAX` + overflow expressions, and `dev` / `ci` reject regressions. Rebind the three + affected timelock transaction recipes to the deterministic scoped ELF data + hashes produced by those equivalent named expressions. CKB-VM crypto + primitive fixtures now place `CSARGv1` through the current + `WitnessArgs.input_type` adapter path instead of the retired raw-witness + alias. +- Make `edition = "2026"` the single mandatory CellScript package contract. + Edition is now explicitly a long-lived source-semantics epoch rather than an + annual release or complete ABI bundle. The resolved compatibility profile + independently composes source semantics, target, primitive assurance, entry + payload and placement ABIs, and metadata schemas under + `cellscript-resolved-compatibility-profile-v1`. Metadata schema 57 carries + those axes, and their hash remains bound across cache keys, registry records, + `Cell.lock` v2, `Deployed.toml` v2, compile receipts v2, generated builders, + native APIs, WASM, LSP, and the playground. Missing or different editions + and older persisted schemas are rejected; no migration or compatibility + reader is provided. Generated CKB entries also remove the raw-`CSARGv1` + witness fallback, so placement ABI v2 accepts the payload only inside + canonical `WitnessArgs.input_type`. The deployed public registry uses one + current contract: signed entries, the production database schema, + version-addressed static JSON, and the website require both Edition 2026 and + the separate compatibility-profile hash, with no fallback reader for + incomplete entries. Generic admin status changes cannot manufacture + `verified_build`, `deployed`, or `on_chain_committed` claims; those states + require the ordered evidence-promotion path. See the + [0.23 development release notes](docs/releases/CELLSCRIPT_0_23_RELEASE_NOTES.md). +- Complete the native-tooling cleanup: neutralize migration-era identifiers, + remove tracked legacy traceback logs and cache exclusions, rename the native + tooling integration suite, and add a repository-wide source-policy command + to every gate. The policy traverses initialized submodules and rejects + retired interpreter sources, generated bytecode/cache artifacts, capture + logs, and active tooling references before they can re-enter the release + contract. The canonical WASM container now explicitly selects its already + installed pinned Rust toolchain, avoiding an unnecessary network sync during + release builds. +- Restore the 0.23 release gate after the Python-to-Rust tooling migration by + checking the semantic `requires_all_bundled_examples_strict_original_ckb` + and emitted `source_provenance` CKB boundaries plus the Rust-backed NovaSeal + acceptance summary instead of retired temporary-directory, helper, and shell + field names, and refresh the NovaSeal external TCB review template to the + current Rust-migrated verifier source-tree hash. Refresh the RWA legal-review + template's profile source-tree hash after its manifest declares Edition + 2026. CKB transaction-recipe replay now tops up fresh devnet funding when a + fixture has no disposable change output and its replacement input cannot + fund every typed output. + Rebuild the website WASM bundle with the witness-placement-v2 compiler so the + playground and native release artifacts expose the same ABI. +- Add the explicit `cellscript-witnessargs-input-type-v2` placement ABI for + parameterized CKB entries. Generated wrappers now resolve witnesses relative + to the active script group, decode the `CSARGv1` payload from + `WitnessArgs.input_type`, preserve wallet/multisig ownership of `lock`, reject + malformed or wrongly placed payloads, and reject group-relative raw-v1 + placement. Builders place `input_type` before SDK signing because the + complete `WitnessArgs` is signed. A canonical signed multisig-v2 CKB-VM + regression covers a type group whose first input is not transaction input + zero and rejects post-signing witness mutation. The Rust-native v0.23 + transaction recipes are rebound to the resulting audited ELF data hashes so + the production stateful gate cannot silently replay stale code identities. + ## 0.22.0 - 2026-07-19 - Make GitHub publication depend on the full release gate. Release evidence now diff --git a/CODING_STYLE.md b/CODING_STYLE.md index b3e50ee5..6fcff59b 100644 --- a/CODING_STYLE.md +++ b/CODING_STYLE.md @@ -28,6 +28,31 @@ project contract. short reason; crate-wide or module-wide clippy allowances are only for documented legacy or transition boundaries. +## On-Chain Registry Script Rules + +`contracts/registry-type-script` is an independent `no_std` CKB Script crate. +Its release binary is part of the Registry trust boundary, not a host utility. + +- Build only with the pinned repository toolchain and + `build_reproducible_release.sh`; the script path-remaps sources, strips the + RISC-V ELF, and verifies both SHA-256 and CKB data hash against the tracked + release manifest. +- Keep host checksum tooling portable: reproducible scripts may use GNU + `sha256sum` or Perl `shasum`, must select one explicitly, and must fail closed + when neither exists. +- Keep Script args equal to the 32-byte custody Lock Script hash and the + accepted Cell data exactly `CSREGv1 || 32-byte commitment hash`. Every group + Cell must use that Lock and every transition must consume a Cell using it; + otherwise an unauthorised creator could impersonate an official commitment. + Format changes require a new protocol prefix and migration plan, not a + permissive parser. +- Run the `ckb-testtool` suite for every Script change. Positive creation, + replacement, and destruction plus unauthorised creation, incorrect custody + Locks, malformed input/output, and non-canonical args are mandatory evidence. +- Production deployment requires a live mainnet code Cell, the standard + custody Lock CellDep, sufficient confirmations, and a committed deployment + manifest. Local CKB-VM tests are not mainnet deployment evidence. + ## Backend And Codegen Rules `src/codegen/mod.rs` is the orchestration layer of a multi-file backend. @@ -96,6 +121,42 @@ implicit backend contracts more implicit. codegen. Business rules must be explicit in DSL source, structured IR, or metadata before the backend lowers them. +## Verified Artifact Boundary Rules + +- Treat the ELF, compile metadata, canonical lowering record, and canonical + source map as one build bundle. A change to any identity, schema, mapping, or + structural claim must update all producers, consumers, tests, docs, and gate + checks in the same change. +- Keep `cellscript-artifact-checker` independent of the parser, resolver, type + checker, IR, optimizer, assembler, and code generator. Production + dependencies may provide only bounded parsing, versioned schema, canonical + hashing, stable diagnostics, and minimal ELF utilities. +- Checker traversal must be preceded by byte/count budgets. Unknown schemas or + fields, malformed ranges, path escape, mismatched identities, and budget + exhaustion fail closed with one stable `V24xx` rejection code and bounded + diagnostics. +- Do not label structural validation semantic equivalence. Keep binding, + structural, lowering-record, CKB-VM, and chain evidence as separate fields. +- Any new checker invariant requires a deterministic negative mutation and a + valid compiler-produced fixture. ELF/codegen changes also require the + `backend` gate because mapped ranges, block digests, control flow, stack + discipline, or instruction policy may change. + +## Executable Package Scenario Rules + +- `cellc test` success must name and run `simulator`, `ckb-vm`, or `all` unless + `--no-run` is explicitly selected. Compile-only discovery is never described + as executed test evidence. +- Scenario and report schemas reject unknown fields. Source/oracle paths are + relative and confined; Cell names, replacement edges, scripts, witnesses, + runtime errors, and declared limits are validated before execution. +- Simulator results remain `development-non-consensus`. CKB-VM results remain + runtime evidence. Neither may be promoted to RPC admission, deployment, + commitment, confirmation, or complete source equivalence. +- The v1 local live-Cell model proves bookkeeping only; it does not inject + scenario Cells into CKB syscalls. Transaction-shaped cases continue to cite + the stateful CKB oracle until a syscall harness is explicitly promoted. + ## CKB Semantics - Use CKB terms precisely: input Cell, output Cell, lock script, type script, @@ -152,9 +213,10 @@ sub-module (e.g. `assembler.rs`, `runtime.rs`, `abi.rs`): 4. **Delete from back to front.** When removing code by line number with `sed`, delete later ranges first to keep earlier line numbers stable. -5. **Brace-count after every deletion.** Use `python3 -c` to verify brace - balance before attempting compilation. Off-by-one `sed` ranges can leave - orphaned lines or eat closing braces. +5. **Check delimiters after every deletion.** Run `cargo fmt --check`, then the + focused `cargo check --locked -p cellscript --all-targets` before the next + extraction. Off-by-one deletion ranges can leave orphaned lines or consume + closing braces. ### Module Boundary: Schema vs Cell Operations vs Orchestration diff --git a/Cargo.lock b/Cargo.lock index 05d1fc80..1392a0df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -154,6 +154,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.21.7" @@ -308,14 +314,17 @@ dependencies = [ [[package]] name = "cellscript" -version = "0.22.0" +version = "0.24.0" dependencies = [ "anyhow", "base64 0.22.1", "blake2b_simd", "camino", + "cellscript-artifact-checker", "cellscript-ckb-adapter", + "ckb-sdk", "ckb-std", + "ckb-system-scripts 0.6.0", "ckb-testtool", "ckb-types", "ckb-vm", @@ -330,6 +339,8 @@ dependencies = [ "regex", "reqwest", "ring", + "secp256k1", + "semver", "serde", "serde_json", "sha2", @@ -341,9 +352,20 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "cellscript-artifact-checker" +version = "0.24.0" +dependencies = [ + "blake2b_simd", + "clap", + "serde", + "serde_json", + "tempfile", +] + [[package]] name = "cellscript-ckb-adapter" -version = "0.22.0" +version = "0.24.0" dependencies = [ "anyhow", "ckb-hash", @@ -361,11 +383,12 @@ name = "cellscript-ckb-sdk-builder-example" version = "0.1.0" dependencies = [ "cellscript-ckb-adapter", + "ckb-types", ] [[package]] name = "cellscript-fiber-adapter" -version = "0.22.0" +version = "0.24.0" dependencies = [ "anyhow", "camino", @@ -378,14 +401,37 @@ dependencies = [ "reqwest", "serde", "serde_json", - "serde_yaml", + "serde_yaml_ng", "tempfile", "thiserror 1.0.69", ] +[[package]] +name = "cellscript-tools" +version = "0.24.0" +dependencies = [ + "anyhow", + "blake2b-ref", + "ckb-jsonrpc-types", + "ckb-types", + "clap", + "hex", + "hex-literal", + "k256", + "percent-encoding", + "regex", + "reqwest", + "serde", + "serde_json", + "sha2", + "time", + "toml 0.8.19", + "wait-timeout", +] + [[package]] name = "cellscript-wasm" -version = "0.22.0" +version = "0.24.0" dependencies = [ "cellscript", "serde", @@ -949,6 +995,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "constant_time_eq" version = "0.4.2" @@ -1024,6 +1076,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -1053,6 +1117,16 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -1120,6 +1194,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", ] @@ -1155,12 +1230,42 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d978bd5d343e8ab9b5c0fc8d93ff9c602fdc96616ffff9c05ac7a155419b824" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "signature", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "enum-repr-derive" version = "0.2.0" @@ -1224,6 +1329,16 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1366,6 +1481,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1437,6 +1553,17 @@ dependencies = [ "siphasher 0.3.11", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1482,6 +1609,12 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + [[package]] name = "http" version = "1.4.0" @@ -1811,6 +1944,19 @@ dependencies = [ "serde_json", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sha2", + "signature", +] + [[package]] name = "keccak" version = "0.1.6" @@ -2005,6 +2151,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "numext-constructor" version = "0.1.6" @@ -2823,6 +2978,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + [[package]] name = "secp256k1" version = "0.30.0" @@ -2965,10 +3133,10 @@ dependencies = [ ] [[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" +name = "serde_yaml_ng" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f" dependencies = [ "indexmap", "itoa", @@ -3036,6 +3204,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.9" @@ -3235,7 +3413,9 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde_core", "time-core", @@ -3625,6 +3805,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 7a83054e..c5f4e687 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,16 @@ [workspace] members = [ ".", + "crates/cellscript-artifact-checker", "crates/cellscript-ckb-adapter", "crates/cellscript-fiber-adapter", + "crates/cellscript-tools", "crates/cellscript-wasm", "examples/ckb-sdk-builder", ] exclude = [ + "contracts/registry-type-script", + "services/registry-verifier", "proposals/novaseal/agreement-profile-v0/harness/ckb_vm", "proposals/novaseal/v0-mvp-skeleton/harness/ckb_vm", "proposals/novaseal/v0-mvp-skeleton/verifier/novaseal_btc_verifier", @@ -17,7 +21,7 @@ resolver = "3" [package] name = "cellscript" -version = "0.22.0" +version = "0.24.0" edition = "2024" rust-version = "1.97.1" autobins = false @@ -29,6 +33,7 @@ exclude = [ ".audit-report*.md", ".cap/", ".codex/", + ".dockerignore", ".gitignore", ".gitmodules", ".idea/", @@ -39,14 +44,17 @@ exclude = [ "AGENTS.md", "BRANCHES.md", "README_REVIEW.md", + "audits/", "cellscript-*.png", + "clippy.toml", "docs/", "docs/wiki/", "editors/", + "integrations/", "proposals/", - "scripts/__pycache__/", "services/", "src/bin/", + "tests/myelin_handoff.rs", "tools/", "website/", ] @@ -74,6 +82,8 @@ serde_json = "1.0" blake2b_simd = "1.0" toml = "0.8" hex = "0.4" +semver = "1.0" +cellscript-artifact-checker = { version = "=0.24.0", path = "crates/cellscript-artifact-checker" } indexmap = "=2.2.6" @@ -101,7 +111,7 @@ tower-lsp = { version = "0.20", optional = true } tokio = { version = "1", features = ["full"], optional = true } [features] -default = ["cli", "lsp"] +default = ["cli", "lsp", "vm-runner"] # CLI surface: binary, REPL, incremental session. Gated out of the # wasm build because clap/colored/env_logger pull native I/O. cli = ["dep:base64", "dep:clap", "dep:colored", "dep:env_logger", "dep:keyring", "dep:reqwest", "dep:ring", "dep:unicode-width"] @@ -118,7 +128,10 @@ ckb-acceptance = [] pretty_assertions = "1.4" tempfile = "3.10" ckb-testtool = "1.1" +ckb-sdk = { path = "../ckb-sdk-rust" } +ckb-system-scripts-v0_6_0 = { package = "ckb-system-scripts", version = "=0.6.0" } ckb-std = { version = "1.1.0", default-features = false, features = ["type-id"] } +secp256k1 = { version = "=0.30.0", features = ["recovery"] } sha2 = "0.10" regex = "1" cellscript-ckb-adapter = { path = "crates/cellscript-ckb-adapter" } diff --git a/README.md b/README.md index e5d8fe2c..0c1e245d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

[![CellScript CI](https://github.com/CellScript-Labs/CellScript/actions/workflows/ci.yml/badge.svg)](https://github.com/CellScript-Labs/CellScript/actions/workflows/ci.yml) -[![Release: v0.22.0](https://img.shields.io/badge/release-v0.22.0-2f6f4e.svg)](https://github.com/CellScript-Labs/CellScript/releases/tag/v0.22.0) +[![Release: v0.24.0](https://img.shields.io/badge/release-v0.24.0-2f6f4e.svg)](https://github.com/CellScript-Labs/CellScript/releases/tag/v0.24.0) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE-MIT) [![Rust 1.97.1](https://img.shields.io/badge/rust-1.97.1-orange.svg)](Cargo.toml) [![Targets: CKB](https://img.shields.io/badge/targets-CKB-2f6f4e.svg)](#target-profiles) @@ -20,9 +20,12 @@ artifacts, together with typed metadata for auditing, policy checks, schema binding, and scheduler-aware execution. The current stable release is -[CellScript v0.22.0](https://github.com/CellScript-Labs/CellScript/releases/tag/v0.22.0). -See the [0.22 release notes](docs/releases/CELLSCRIPT_0_22_RELEASE_NOTES.md) -for its shipped surface, evidence boundaries, and migration checklist. +[CellScript v0.24.0](https://github.com/CellScript-Labs/CellScript/releases/tag/v0.24.0). +The [0.24 release notes](docs/releases/CELLSCRIPT_0_24_RELEASE_NOTES.md) +describe its verified-artifact, executable-test, and integration boundaries. +The [0.24 roadmap](roadmap/CELLSCRIPT_0_24_ROADMAP.md) records its independently +checked artifact, executable-test, and explicitly deferred ecosystem +boundaries. In this README, metadata means machine-readable semantic facts emitted by the compiler: schema layout, Cell effects, access summaries, source hashes, @@ -116,7 +119,7 @@ curl -fsSL https://raw.githubusercontent.com/CellScript-Labs/CellScript/main/scr Or pin a specific version: ```bash -CELLSCRIPT_VERSION=0.22.0 curl -fsSL https://raw.githubusercontent.com/CellScript-Labs/CellScript/main/scripts/install.sh | sh +CELLSCRIPT_VERSION=0.24.0 curl -fsSL https://raw.githubusercontent.com/CellScript-Labs/CellScript/main/scripts/install.sh | sh ``` The release page publishes `SHA256SUMS` alongside all four platform archives. @@ -124,7 +127,7 @@ The release page publishes `SHA256SUMS` alongside all four platform archives. Build the exact published source instead: ```bash -git clone --branch v0.22.0 --depth 1 https://github.com/CellScript-Labs/CellScript.git +git clone --branch v0.24.0 --depth 1 https://github.com/CellScript-Labs/CellScript.git cd CellScript cargo install --locked --path . ``` @@ -491,6 +494,7 @@ or CellFabric intent engine. - [VS Code extension](editors/vscode-cellscript) - [Runtime error codes](docs/CELLSCRIPT_RUNTIME_ERROR_CODES.md) +- [Edition policy](docs/CELLSCRIPT_EDITION_POLICY.md) - [Entry witness ABI](docs/CELLSCRIPT_ENTRY_WITNESS_ABI.md) - [BIP340 verifier CellDep ABI](docs/CELLSCRIPT_SIGNATURE_VERIFIER_ABI.md) - [Collections support matrix](docs/CELLSCRIPT_COLLECTIONS_SUPPORT_MATRIX.md) @@ -530,9 +534,12 @@ or CellFabric intent engine. - [0.20 release notes](docs/releases/CELLSCRIPT_0_20_RELEASE_NOTES.md) - [0.21 release notes](docs/releases/CELLSCRIPT_0_21_RELEASE_NOTES.md) - [0.22 release notes](docs/releases/CELLSCRIPT_0_22_RELEASE_NOTES.md) +- [0.23 release notes](docs/releases/CELLSCRIPT_0_23_RELEASE_NOTES.md) +- [0.24 release notes](docs/releases/CELLSCRIPT_0_24_RELEASE_NOTES.md) - [0.22 type and set theory roadmap](roadmap/CELLSCRIPT_0_22_TYPE_AND_SET_THEORY_ROADMAP.md) - [0.22 bounded Fiber interoperability guide](examples/fiber/README.md) - [Agentic Loops and cellscript-mcp tutorial](docs/wiki/Tutorial-13-Agentic-Loops-and-cellscript-mcp.md) +- [LS-IDL for CKB Lock Scripts tutorial](docs/wiki/Tutorial-15-LS-IDL-for-CKB-Lock-Scripts.md) --- @@ -598,11 +605,12 @@ Emits ckb-vm-compatible RISC-V assembly (`.s`) or ELF (`.elf`): buffers, and per-entrypoint trampolines. - CKB syscall ABI with proper syscall number tables and source-flag conventions. -### Metadata & Policy +### Metadata, Lowering Evidence & Policy -The compiler emits a single JSON metadata sidecar (`.elf.meta.json` / -`.s.meta.json`) that captures everything the chain scheduler, audit tools, and -policy gates need — without re-parsing source: +The compiler emits a JSON metadata sidecar (`.elf.meta.json` / `.s.meta.json`). +CKB ELF builds additionally emit canonical `.elf.lowering.json` and +`.elf.sourcemap.json` sidecars. The standalone artifact checker consumes all +four identities without calling the compiler front end or code generator: | What | Produced by | Consumed by | |---|---|---| @@ -610,6 +618,8 @@ policy gates need — without re-parsing source: | Effect classification, resource summaries | `types/` | Scheduler, audit tools | | Scheduler witness ABI & access domains | `codegen/` | CKB block builder, parallel scheduler | | Source hashes, artifact CKB Blake2b | `lib.rs` | `cellc verify-artifact`, CI gates | +| Stable lowering graph, ABI/frame/ProofPlan/syscall contracts, block digests | `verified_artifact.rs` | standalone checker, Registry artifact worker | +| Source spans to final ELF instruction ranges | `verified_artifact.rs` | checker, executable-test coverage, audit tools | | Verifier obligations, pool invariants | `ir/` | On-chain verifier, policy checker | | Covenant ProofPlan trigger/scope/read coverage, risk diagnostics, macro provenance | `proof_plan/` | `cellc explain proof`, auditors | | Target-profile policy violations | `lib.rs` | `cellc check`, CI gates | @@ -631,11 +641,11 @@ CKB cycle/capacity estimates. |---|---|---| | **CLI** | `cli/` + `main.rs` | `cellc` binary with all subcommands | | **LSP** | `lsp/` + `lsp/server.rs` | In-process `LspServer` + `tower-lsp` JSON-RPC over stdio (`cellc --lsp`) | -| **VS Code** | `editors/vscode-cellscript/` | Shells out to `cellc` for LSP startup, reports, action-builder generation, and package/registry verification | +| **VS Code** | `editors/vscode-cellscript/` | Shells out to `cellc` for LSP startup, reports, action-builder generation, package/registry verification, and LS-IDL validate/bind/fetch flows | | **MCP server** | `cellscript-mcp` (separate bin) | Read-only Model Context Protocol JSON-RPC server that exposes compiler reports and explain commands to MCP-aware agents (Claude Code, Cursor, Aider, Codex, etc.) | | **Formatter** | `fmt/` | Idempotent formatter for `cellc fmt` and LSP | | **Doc generator** | `docgen/` | HTML/Markdown/JSON docs from AST + metadata | -| **Simulator** | `simulate.rs` | Simulated evaluator — emits `TraceEvent` logs without ckb-vm | +| **Executable test runner** | `simulate.rs` + `cli/test_runner.rs` | Versioned scenarios under the non-consensus simulator and local authoritative CKB-VM backend, with exact runtime errors and conservative coverage | | **REPL** | `repl.rs` | Interactive read-eval-print loop | | **Generated builder package** | `cellc gen-builder --target typescript` | Emits a registry-bound TypeScript action-builder package with runtime adapter contracts and self-tests | @@ -643,7 +653,7 @@ CKB cycle/capacity estimates. | Module | What it does | |---|---| -| **Package workflow** (`package/`) | `Cell.toml` parsing, path/git/registry source-package dependency resolution, transitive `Cell.lock` reproducibility, `cellc init`/`add`/`remove`/`install --path`/`install namespace/pkg@version`/`update`/`info`. Registry source packages are resolved through discovery, tag-pinned Git provenance, `registry.json`, and verified `source_hash`; non-CellScript registry artifact profiles remain fail-closed. | +| **Package workflow** (`package/`) | `Cell.toml` parsing, standard SemVer, path/git/registry source resolution, manifest-bound `Cell.lock` v3 graphs, aliases, features/dev modes, genesis-bound environments, and bounded update-time resolvers; `cellc init`/`add`/`remove`/`lock`/`install`/`update`/`info`. Builds consume exact immutable Git/Registry pins and verified source hashes without mutable discovery; the Registry profile catalog keeps non-CellScript artifacts non-resolving. | | **Incremental compiler** (`incremental/`) | Dependency-graph-aware build cache — skips recompilation when inputs are unchanged. | | **Build integration** (`lib.rs`) | Resolves `Cell.toml` → `CellBuildConfig`, merges CLI + manifest options, selects entry scope, runs policy gates, writes artifacts + metadata. | @@ -671,11 +681,13 @@ flowchart TB Rules --> Policy Policy --> IR["IR lowering + optimizer\nCell effects, entry ABI,\nverifier obligations"] IR --> Metadata["metadata sidecar\nschema, ABI, runtime errors,\nconstraints, CKB policy"] + IR --> Lowering["verified lowering record + source map\ncanonical graph, final ranges, block digests"] IR --> Codegen["RISC-V codegen\nCKB syscalls, raw ELF,\nper-entry trampolines"] Codegen --> Artifact["CKB artifact\n.s / .elf"] - Artifact --> Verify["cellc verify-artifact\nprofile, source hash,\nartifact hash, policy flags"] + Artifact --> Verify["cellc verify-artifact\nbinding + structural + lowering states"] Metadata --> Verify + Lowering --> Verify Artifact --> Builder["builder workflow\ninputs, outputs, outputs_data,\nwitness, cell_deps, capacity floors"] Metadata --> Builder @@ -686,8 +698,10 @@ This separates three boundaries: - **compiler boundary** — parse, type/state checks, CKB policy rejection, IR, codegen, and metadata; -- **artifact boundary** — `cellc verify-artifact` proves the artifact, sidecar, - source hash, target profile, and selected policy flags agree; +- **artifact boundary** — `cellc verify-artifact` uses the independent checker + to prove binding, static ELF structure, lowering-record, source-map, target + profile, and selected policy agreement; it does not claim complete semantic + equivalence or VM execution; - **chain-evidence boundary** — builders and acceptance scripts prove concrete CKB transaction shape, capacity, cycles, tx size, and lock/action behavior. @@ -716,8 +730,9 @@ policy defaults: ```toml [package] +edition = "2026" name = "token" -version = "0.22.0" +version = "0.24.0" entry = "src/main.cell" source_roots = ["src"] @@ -732,42 +747,85 @@ deny_ckb_runtime = false deny_runtime_obligations = false ``` -Command-line flags can tighten policy checks for a build or CI job. +`edition = "2026"` is mandatory and is the only supported source-semantics +edition. The year is a long-lived epoch label, not an annual release cadence. +Target profile, primitive assurance, metadata schemas, and entry/witness ABIs +remain independently versioned; the resolved compatibility profile combines +those axes with the edition and is bound into lock, deployment, receipt, +registry, and builder identities. Older or missing persisted identities are +rejected rather than migrated. Command-line flags can tighten policy checks +for a build or CI job. The full contract is in the +[edition policy](docs/CELLSCRIPT_EDITION_POLICY.md). ### Package Workflow CellScript ships a local-first package workflow in `cellc`. Local packages, -source roots, path/git/registry source-package dependencies, lockfile refresh, +source roots, path/git/registry source-package dependencies, explicit lock refresh, and package build/check/doc/fmt flows are production-style. Registry resolution -is deliberately narrow: `cellc install`, `cellc build`, and `cellc update` -accept CellScript source packages with `Cell.toml`, `registry.json`, tag-pinned -Git provenance, and verified `source_hash`; non-CellScript artifact profiles -still fail closed. +is deliberately narrow: `cellc lock`, `cellc install`, and `cellc update` +query the public API for an accepted CellScript source-package version, while +`build`, `check`, and `test` consume only the pinned graph. Resolution commands +download its immutable Registry source snapshot, reject unsafe paths or opaque +archive formats, and verify snapshot SHA-256, every file's BLAKE2b, `Cell.toml` +identity, Edition/profile identity, and the whole-tree `source_hash`. +Non-CellScript artifact profiles still fail closed. **Supported today:** - `cellc init` — create an application or library package with `Cell.toml` - `cellc build` / `check` / `doc` / `fmt` — operate on the current package +- `cellc test --backend simulator|ckb-vm|all` — execute versioned + `*.scenario.json` fixtures; `--no-run` is the explicit compile-only mode - top-level `cellc ` and report commands accept `.cell` files, package directories, or `Cell.toml` manifests where the command supports an input - `cellc add --path` — records local path dependencies in `Cell.toml` +- `cellc lock` — explicitly resolve the complete runtime/test/feature and CKB + environment graph and write `Cell.lock` v3 - `cellc install --path` and `cellc update` — resolve local path dependency graphs and refresh `Cell.lock` - `cellc install cellscript/pkg@1.2.0` — resolve a registry source-package - dependency through discovery, tag checkout, `registry.json`, and - `source_hash` verification + dependency through the production public API, accepted-status selection, + immutable snapshot materialisation, Edition/profile checks, and layered hash + verification - Local path dependencies are resolved recursively and included in module loading, source hashing, and metadata -- `Cell.lock` — captures direct and transitive resolved dependency identity - for reproducible checks +- `Cell.lock` v3 — binds the root manifest digest, canonical dependency nodes, + outgoing alias edges, dependency manifests, whole-tree hashes, exact Git or + Registry pins, feature/test modes, and genesis-bound CKB environments +- Commit `Cell.lock` to version control. It is reviewed build input, not a local + cache; only `cellc lock`, `cellc update`, or dependency-editing commands may + repin its dependency graph +- `build`/`check`/`test --locked` — explicitly assert the existing dependency + graph; the graph is authoritative even without the flag +- `--frozen` — imply offline mode and suppress all lockfile writes; + `--offline` permits only materialized exact sources +- `[features]`, optional `dep:`, `[dev_dependencies]`, local + `package = "..."` aliases, and environment overrides are lock-graph inputs; + `[build.dependencies]` remains fail-closed pending isolated execution +- `[resolvers.]` — optional absolute-path/SHA-256-bound, time/output + bounded update-time resolver; its versioned response must normalize to an + exact Registry version or Git commit and is never executed by locked builds +- `examples/package_graph` — runnable frozen/offline alias, SemVer, feature, + test-only dependency, and explicit CKB-environment graph +- `examples/scenario_basics` — runnable positive and exact-negative scenarios + under both simulator and CKB-VM, plus a four-file artifact walkthrough +- `examples/registry_ls_idl` — runnable LS-IDL validation, executable binding, + Registry bundle scaffolding, exact-byte fetch, and compatibility vectors - `cellc info --json` — exposes package metadata for CI and tooling - `cellc package verify --json` — fails closed when `Cell.toml`, source hash, - dependency resolution, or build identity disagree with `Cell.lock` + dependency resolution, or build identity disagree with `Cell.lock`; run an + ordinary locked build first when a tracked example lock is graph-only - `cellc registry verify --json` — checks off-chain deployment facts against `Cell.lock` and `Deployed.toml` -- `cellc registry verify --live --rpc-url ... --json` — adds CKB RPC live-cell - checks for deployment records when RPC evidence is available -- `cellc publish` — public registry publish path; `cellc publish --offline` +- `cellc registry verify --live --rpc-url ... --json` — adds CKB RPC + `get_live_cell` liveness plus `get_transaction.tx_status` commit and + confirmation checks for deployment records when RPC evidence is available +- `cellc publish --authorise` — recommended interactive first-publish path; + opens an exact-coordinate 15-minute browser authorisation session, keeps the + pending delegated key recoverable, and resumes publishing after the Registry + returns the matching key ID (`--no-open` supports remote terminals) +- `cellc publish` — public registry publish path once a delegated publisher + credential is active; `cellc publish --offline` computes the package source hash and mirrors the version entry into `registry.json` for local fixtures, audit, and offline fallback - `cellc registry add` — write a discovery-index entry into the local/offline @@ -777,43 +835,90 @@ still fail closed. **Public registry boundary / fail-closed:** -- Public registry publishing is designed around JoyID-rooted publisher - identity: CCC is the connection layer, JoyID is the accepted publisher root, - and delegated publisher credentials are stored in the OS keychain for daily - `cellc publish`; see +For interactive first use, run `cellc publish --authorise`. The explicit +`auth capability create/submit` and `auth namespace claim` sequence below is +the manual, CI, recovery, and external-wallet path. + +- Public registry publishing uses typed wallet-rooted publisher identities: + CCC is the browser connection layer, `joyid_ckb` accepts JoyID passkeys, and + `ckb_secp256k1` accepts standard CKB wallets that expose a compressed public + key and recoverable CKB message signature. Delegated publisher credentials + are stored in the OS keychain for daily `cellc publish`; see [`docs/CELLSCRIPT_REGISTRY_PRODUCTION_BOUNDARY_ADR.md`](docs/CELLSCRIPT_REGISTRY_PRODUCTION_BOUNDARY_ADR.md) -- `cellc auth capability create --principal-id --scope - publish:/ --expires 90d --json > +- `cellc auth capability create --principal-type + --principal-id + --scope publish:/ --expires 90d --json > capability-payload.json` creates the local P-256 capability key when `--capability-pubkey` is not supplied, stores the private key in the OS - keychain, and prints the JoyID-bound authorisation payload. The - `principal_id` is the normalized JoyID/CKB identity binding derived by the - CCC-backed JoyID submit flow, not the display address. - After the same payload is signed through JoyID/CCC, `cellc auth capability - submit --payload capability-payload.json --joyid-signature - joyid-signature.json` registers the delegated key with the write API. Bare + keychain, and prints the wallet-bound authorisation payload. The + `principal_id` is the normalized binding derived from the connected signer, + not the display address. After the same payload is signed through CCC, + `cellc auth capability submit --payload capability-payload.json + --wallet-signature wallet-signature.json` registers the delegated key with + the write API. + `cellc auth namespace claim --namespace --payload + capability-payload.json --wallet-signature wallet-signature.json` then + establishes the required namespace ownership. Bare `cellc publish` then signs the concrete publish payload and submits the source snapshot to the public registry. +- These scopes are deliberately independent: `publish` admits immutable + releases, `deployment` attaches chain-checked deployment evidence, and + `availability` deprecates, yanks, or restores a release. A publish-only + capability cannot perform the other two operations. When the command runs + inside a package directory without explicit `--scope` flags, `cellc` infers + only the exact-coordinate `publish` scope. Deployment and availability access + must be granted explicitly. +- The Registry chooser includes Neuron, JoyID, imToken, CKBull, SafePal, + Ledger, imKey, OneKey, UTXO Global, Rei Wallet, Gate, and QuantumPurse. + Compatible CCC signers connect directly; the remaining directory entries use + the same verified `wallet-signature.json` handoff without exposing mnemonic + words to the site. - `cellc auth capability revoke --principal-id --capability-key-id --json > revoke-payload.json` - generates a JoyID-bound revocation challenge; after signing that challenge, + generates a wallet-bound revocation challenge; after signing that challenge, `cellc auth capability revoke --payload revoke-payload.json - --joyid-signature joyid-signature.json` revokes the delegated key without + --wallet-signature wallet-signature.json` revokes the delegated key without creating a separate registry account. - CI can avoid interactive keychain access by using `cellc publish --print-payload --json`, signing the `canonical_payload` externally, then submitting with `--payload --capability-signature `, or by setting `CELLSCRIPT_CAPABILITY_PRIVATE_KEY_PKCS8_B64`. -- The first write API implementation lives under - [`services/registry-api`](services/registry-api/README.md): Cloudflare - Workers, R2 source snapshots, Neon Postgres through Hyperdrive, JoyID - capability authorisation, namespace ACL checks, quota hooks, and audit events. -- Non-CellScript registry artifact profiles remain future-facing or fail-closed +- The production write API lives under + [`services/registry-api`](services/registry-api/README.md). The deployed slice + uses Node 22, Postgres 17, a bounded real-compiler verification worker, a + persistent filesystem object store, and a separate read-only nginx static + path behind trusted TLS. Publish transactionally queues source/build + verification; default search/list visibility begins at `verified_build`, and + direct URLs preserve admitted `source_published` history. The same typed app + retains a Cloudflare Worker/Hyperdrive/R2 deployment option. Both paths share + typed wallet capability authorisation, namespace ACLs, quota hooks, ordered evidence + promotion, and audit events. +- Public version responses bind a content-addressed source snapshot URL. The + read-only service exposes `/source-snapshots/*` independently of Postgres and + the API; the lockfile records that URL plus its `sha256:` revision so Registry + installs do not silently depend on Git availability. +- The versioned `cellscript-registry-profile-catalog-v1` keeps only + `cellscript_source` dependency-resolving; non-CellScript artifact profiles + remain discoverable through explicit artifact commands and fail closed in + package resolution +- Deployable CKB Lock Scripts may attach the versioned + `cellscript-registry-ls-idl-interface-v1` profile. Registry admission binds + `SHA-256` of the exact IDL bytes to the executable's final 32 bytes, and + public reads resolve those bytes by chain-verified Script identity. This is + an interface-identity check, not proof of implementation correctness or a + security audit. See the + [LS-IDL Registry profile](docs/CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md). - Git dependencies are explicit remote source fetches; treat them as review-required inputs, not the registry production path **Registry resolver boundary:** +- The default source-package authority is + `https://api.registry.cellscript.dev`; only publicly accepted statuses enter + ordinary version selection. `CELLSCRIPT_REGISTRY_API_URL` changes that API + origin, while `CELLSCRIPT_REGISTRY_URL` explicitly selects the legacy + Git/offline discovery authority. An unavailable production API does not + silently downgrade to Git discovery. - Registry discovery may grow to include CellScript packages, verifier artifacts, deployed artifact records, reproducible artifacts, and external CKB tooling artifacts. Dependency resolution stays narrower than discovery. @@ -854,21 +959,22 @@ still fail closed. | `cellc proof-diff` / `profile` / `tx trace` / `audit-bundle` | Emit v0.16 audit and debug reports | | `cellc opt-report` | Compare O0..O3 artifact size and constraints status | | `cellc receipt` / `sign-receipt` / `verify-receipt` | Emit, sign, and verify compile receipts over metadata/artifact hashes | -| `cellc verify-artifact` | Verify an artifact against its metadata sidecar, with optional receipt binding | -| `cellc test` | Run compiler and policy tests (no trusted runtime execution) | +| `cellc verify-artifact` | Independently check an ELF, metadata, lowering record, and source map; report VM/chain evidence separately; optionally bind a receipt | +| `cellc artifact ls-idl validate\|bind\|fetch\|bundle` | Validate byte-exact LS-IDL, bind its SHA-256 to a CKB executable, resolve it by deployed Script identity, or scaffold a publish-ready Registry bundle | +| `cellc test --backend simulator\|ckb-vm\|all` | Execute fail-closed package scenarios with exact outcomes and evidence tiers (`--no-run` is compile-only) | | `cellc doc` | Generate API and audit documentation | | `cellc fmt` | Format `.cell` sources or check formatting | | `cellc init` | Create a package skeleton | | `cellc add` / `remove` | Mutate local package dependencies | -| `cellc install --path` / `install namespace/pkg@version` / `update` | Resolve local, git, or registry CellScript source-package dependencies and refresh `Cell.lock` | +| `cellc lock` / `install --path` / `install namespace/pkg@version` / `update` | Explicitly resolve local, git, or registry CellScript source-package dependencies and refresh `Cell.lock` v3 | | `cellc info` | Print manifest and package information | | `cellc package verify` | Verify package/source/build identity against `Cell.lock` | | `cellc registry verify` | Verify deployment identity against `Cell.lock` and `Deployed.toml`; `--live` adds CKB RPC evidence | | `cellc certify --plugin novaseal-profile-v0` | Run the deterministic compiler-hosted NovaSeal profile certification (consumes `target/novaseal-*.json` and the local certifier source) | | `cellc repl` | Start the interactive REPL | -| `cellc run` | Run ELF entrypoints via VM runner or simulator; `--json` includes cycles for VM execution and `cycles: null` for simulation | +| `cellc run` | Run no-argument standalone ELF entrypoints via CKB-VM, or use explicit `--simulate`; parameter/transaction contexts fail closed instead of silently falling back | | `cellc publish` / `cellc publish --offline` / `cellc registry add` / `cellc registry edit --yank` | Public publish plus explicit local/offline registry metadata flow; public registry policy makes bare `cellc publish` an authenticated registry write, with Git/static metadata retained for audit and fallback | -| `cellc auth capability create/submit/revoke` / public registry write API / non-CellScript artifact install | JoyID-rooted publication policy and future-facing artifact profiles; fail-closed where unsupported | +| `cellc auth capability create/submit/revoke` / public registry write API / non-CellScript artifact install | Typed wallet-rooted publication policy and future-facing artifact profiles; fail-closed where unsupported | ### CLI Options diff --git a/assets/cellscript-logo.png b/assets/cellscript-logo.png deleted file mode 100644 index b10611e6..00000000 Binary files a/assets/cellscript-logo.png and /dev/null differ diff --git a/audits/0.23-deep-dive.md b/audits/0.23-deep-dive.md new file mode 100644 index 00000000..8952f422 --- /dev/null +++ b/audits/0.23-deep-dive.md @@ -0,0 +1,937 @@ +# CellScript 0.23 关键改动深度阐述 + +> 工作区: `/Users/arthur/RustroverProjects/CellScript` +> 涵盖: 闭集强制概念 + Cell.lock v2 + Deployed.toml v2 + Registry v1 的 0.23 强类型契约 + 风格治理(带 mermaid) +> 写于: 2026-07-31 + +--- + +## 1. 「闭集强制」是什么意思 + +### 1.1 字面意思 + +**闭集(closed set)** = enum 的所有变体在编译期穷举,**没有「通配」、「未识别」、「其它」分支**。 + +`src/edition.rs:11-15`: + +```rust +pub enum CellScriptEdition { + #[serde(rename = "2026")] + Edition2026, +} +``` + +这个 enum **只有 1 个变体**。不是「2025 / 2026 / 2027 / 0.22 / 0.23 / experimental」枚举,**就是 1 个值**。 + +`src/edition.rs:78-84` 的 `FromStr`: + +```rust +match value { + "2026" => Ok(Self::Edition2026), + other => Err(CompileError::without_span(format!( + "unsupported CellScript edition '{}'; expected 2026", other + ))), +} +``` + +任何不是 `"2026"` 的字符串 → **直接编译错误**。没有兼容回退、没有 deprecation warning、没有「unsupported edition, using default」兜底。 + +`src/edition.rs:106-110` 的测试明确钉死: + +```rust +fn only_edition_2026_is_accepted() { + assert_eq!("2026".parse::().unwrap(), CellScriptEdition::Edition2026); + assert!("unsupported".parse::().unwrap_err().message.contains("expected 2026")); +} +``` + +### 1.2 「强制」体现在哪里 + +「强制」不是 enum 本身的属性,是**这个 enum 在整个工具链里被多少地方堵死**: + +| 强制点 | 落地位置 | 行为 | +|---|---|---| +| `Cell.toml` 解析 | `src/package/mod.rs:32` `pub edition: CellScriptEdition` | 必填字段,缺 `edition` 直接 deserialize 失败 | +| `Cell.toml` 字符串匹配 | `FromStr` 拒绝任何非 `2026` | `"unsupported CellScript edition 'XYZ'; expected 2026"` | +| `Cell.lock v2` 校验 | `src/package/mod.rs:1108-1114` `validate_schema()` | 读 v1 lock → `unsupported Cell.lock version 1; expected 2` | +| `Deployed.toml v2` 校验 | `src/package/mod.rs:1568-1609` | build/deployment edition 必须等于 package edition | +| `Registry` 服务端 | `services/registry-api/migrations/0001_initial.sql:90-127` | DB CHECK `edition = '2026'` 硬保证 | +| `Registry` 协议 schema | `domain.ts:407-417` | `published["edition"] !== CELLSCRIPT_EDITION` → 400 reject | +| `Cargo.lock` 通过源码 import | 13 个 example Cell.toml 全部 `edition = "2026"` | gate dev / ci 读 example 时如果缺 edition → fail | +| 工具链发布 gate | `scripts/cellscript_gate.sh` | 校验 Cell.lock v2 / Deployed.toml v2 / Registry protocol v1 的 0.23 schema 约束 | +| **没有** migration 路径 | CHANGELOG 明确 "no migration or compatibility reader is provided" | 用户升 0.23 必须删旧 lock / deployed manifest | + +**8 个强制点 + 1 个「故意不提供迁移」** = **真正的 fail-closed**。 + +### 1.3 为什么要做闭集 + +| 备选设计 | 后果 | +|---|---| +| **开放 enum**(`2024`/`2025`/`2026`/`experimental`)| 0.22 现状——没有 edition 字段,自由字符串,doc 与代码漂移 | +| **字符串字段**(`edition: String`,任意值)| 0.22 现状——写 `edition = "2027"` 也通过,无校验 | +| **闭集 enum + 强校验**(0.23 实际)| 任何「不是 2026」的输入都 fail 在最早期(deserialize / FromStr / DB CHECK)| +| **半闭集 enum + default**(比如 `default = "2026"`)| 用户写错(`edition = "206"`)会静默 fallback 到 2026,掩盖 typo | + +0.23 选「闭集 + 强校验 + 故意不提供迁移」= **让"我以为我在用 2026 实际不是" 这种 silent bug 在生产链上没有存活空间**。 + +代价:升级时所有老 lock / deployed manifest 必须重新生成。 + +### 1.4 闭集强制的「副作用链」——为什么 edition 牵动那么多 schema + +`src/edition.rs:33-58` 的 `resolve_compatibility_profile()`: + +```rust +pub fn resolve_compatibility_profile( + edition: CellScriptEdition, + target_profile: &str, + primitive_assurance: Option<&str>, +) -> ResolvedCompatibilityProfile { + // ... 把 edition + target + assurance + ABI + 4 个 metadata schema version + // 拼成一个稳定字符串 ID + let id = format!( + "{}-{}-target-{}-primitive-{}-entry-{}-placement-{}-metadata-{}-{}-{}-{}", + COMPATIBILITY_PROFILE_SCHEMA, + source_semantics, + target_profile, primitive_assurance, + crate::ENTRY_WITNESS_ABI, // "cellscript-entry-witness-v1" + crate::ENTRY_WITNESS_PLACEMENT_ABI, // "cellscript-witnessargs-input-type-v2" + crate::METADATA_SCHEMA_VERSION, + crate::SOURCE_METADATA_SCHEMA_VERSION, + crate::ARTIFACT_METADATA_SCHEMA_VERSION, + crate::CONSTRAINTS_METADATA_SCHEMA_VERSION, + ); + // ... +} +``` + +闭集强制把"源语言 edition" 与 "target / assurance / ABI / metadata schema" 5 个独立版本轴**显式拼成一个 hash-able 字符串 ID**——这个 ID 是 `Cell.lock` / `Deployed.toml` / `RegistryIndex` 三件套共享的**事实身份**。 + +```mermaid +flowchart LR + E[CellScriptEdition
2026 闭集] --> P[ResolvedCompatibilityProfile] + T[target_profile
ckb / fiber] --> P + A[primitive_assurance
0.16 / 0.17] --> P + P --> ID["id (stable string)
cellscript-resolved-compatibility-profile-v1
-cellscript-source-semantics-2026
-target-ckb-primitive-0.16
-entry-cellscript-entry-witness-v1
-placement-cellscript-witnessargs-input-type-v2
-metadata-{4 个 schema version}"] + ID --> CL[Cell.lock v2
package_build.compatibility_profile_hash] + ID --> DT[Deployed.toml v2
build/deployments[].compatibility_profile_hash] + ID --> RI[RegistryIndexEntry
compatibility_profile_hash] + ID --> DB[(DB CHECK
source_hash 64-hex
manifest_hash 64-hex
compatibility_profile_hash 64-hex
edition='2026')] + + style E fill:#fee,stroke:#c00 + style P fill:#efe,stroke:#0a0 + style ID fill:#eef,stroke:#00c +``` + +**关键观察**: + +- edition 是闭集(`2026` 单值),但 `ResolvedCompatibilityProfile` 把 **5 个独立版本轴** 拼成 1 个 hash +- Cell.lock / Deployed.toml / Registry 三件套共享同一 `compatibility_profile_hash` → **任意一个变化 → 三个文件全部失效** +- DB 在 schema 层(CHECK 约束)独立硬约束 `edition='2026'` → 即使应用层 bug 写错 edition 也写不进 DB + +**闭集强制不是「锁一个字符串」,是「锁 5 个独立版本轴的联合身份」**。 + +--- + +## 2. Cell.lock v1 → v2 变化 + +### 2.1 数据结构变化 + +**位置**:`src/package/mod.rs:1031-1080` + +```rust +pub const CURRENT_VERSION: u32 = 2; // v1 → v2 + +pub struct Lockfile { + pub version: u32, // 1 → 2 + pub package: LockfilePackageInfo, // 加 edition + pub dependencies: BTreeMap, + pub package_build: Option, // 0.22 可能没有 → 0.23 必填带 edition + compat hash + pub deployment: BTreeMap, +} + +pub struct LockfilePackageInfo { + pub edition: CellScriptEdition, // ← 0.22 没有,0.23 必填 + pub name: String, + pub version: String, + pub namespace: Option, + pub source_hash: Option, + pub compiler_source_hash: Option, +} +``` + +### 2.2 校验链(v2 新增) + +`src/package/mod.rs:1108-1127` `validate_schema()`: + +```rust +pub fn validate_schema(&self) -> Result<()> { + // 校验 1: version 必须 = 2 + if self.version != Self::CURRENT_VERSION { // 1 ≠ 2 → reject + return Err(CompileError::without_span(format!( + "unsupported Cell.lock version {}; expected {}", + self.version, Self::CURRENT_VERSION + ))); + } + // 校验 2: build edition 必须 = package edition + if let Some(build) = &self.package_build { + if build.edition != self.package.edition { + return Err(...); + } + // 校验 3: build 必须带 compat profile hash + if build.compatibility_profile_hash.is_empty() { + return Err(...); + } + } + Ok(()) +} +``` + +### 2.3 真实 diff(举 atomic_swap) + +0.22 `Cell.lock`(推测形态): +```toml +version = 1 + +[package] +name = "atomic_swap" +version = "0.1.0" +source_hash = "0xabc..." +compiler_source_hash = "0xdef..." + +[dependencies.token] +version = "0.1.0" +source = "local" + +[deployment.testnet] +record = "ckb-..." +code_hash = "0x..." +``` + +0.23 `Cell.lock` 必填形态: +```toml +version = 2 + +[package] +edition = "2026" # ← 新增 +name = "atomic_swap" +version = "0.1.0" +source_hash = "0xabc..." +compiler_source_hash = "0xdef..." + +[package_build] # ← 新增(可 Option,但写了就强校验) +edition = "2026" # ← 必须 == package.edition +compatibility_profile_hash = "0x1234..." # ← 必填 64-hex + +[dependencies.token] +version = "0.1.0" +source = "local" + +[deployment.testnet] +record = "ckb-..." +code_hash = "0x..." +``` + +### 2.4 4 道校验闸 + +```mermaid +flowchart TD + R[读 Cell.lock from disk] --> P[toml::from_str] + P -->|fail| E1[fail-closed
'failed to parse lockfile'] + P -->|ok| V1[校验 1:
version == 2] + V1 -->|fail| E2[fail-closed
'unsupported Cell.lock version 1; expected 2'] + V1 -->|ok| V2[校验 2:
build.edition == package.edition] + V2 -->|fail| E3[fail-closed
'Cell.lock package/build edition mismatch'] + V2 -->|ok| V3[校验 3:
build.compatibility_profile_hash 非空] + V3 -->|fail| E4[fail-closed
'Cell.lock v2 package_build requires
compatibility_profile_hash'] + V3 -->|ok| OK[Ok Some Lockfile] + + style R fill:#eef + style E1 fill:#fee,stroke:#c00 + style E2 fill:#fee,stroke:#c00 + style E3 fill:#fee,stroke:#c00 + style E4 fill:#fee,stroke:#c00 + style OK fill:#efe,stroke:#0a0 +``` + +### 2.5 跨三件套的兼容性 + +Cell.lock v2 不是独立 schema,它跟 Deployed.toml v2 / Registry protocol v1 的 0.23 schema **共享同一 `compatibility_profile_hash`**: + +```mermaid +flowchart LR + subgraph "Cell.lock v2" + CL1[package.edition = 2026] + CL2[package_build.compatibility_profile_hash] + end + subgraph "Deployed.toml v2" + DT1[package.edition = 2026] + DT2[build.compatibility_profile_hash] + DT3["deployments[i].compatibility_profile_hash
(必须 == build)"] + end + subgraph "Registry protocol v1 / 0.23 entry shape" + RI1[edition = 2026] + RI2[compatibility_profile_hash] + end + + H["同源 hash
ResolvedCompatibilityProfile.id"] + H --> CL2 + H --> DT2 + H --> DT3 + H --> RI2 + + style H fill:#ffd,stroke:#aa0 +``` + +**含义**:如果用户改 `target_profile`(从 ckb 改 fiber),`compatibility_profile_hash` 变 → 3 个 schema 都 reject 当前文件 → **用户必须重 build / 重 deploy / 重 publish**。 + +这是「让"build 时用 2026, deploy 时用 2027" 这种 silent drift 没有存活空间」的硬约束。 + +--- + +## 3. Deployed.toml v1 → v2 变化 + +### 3.1 数据结构变化 + +**位置**:`src/package/mod.rs:1529-1612` + +```rust +pub const DEPLOYED_MANIFEST_SCHEMA: &str = "cellscript-deployed-v0.23-edition-2026"; + +pub struct DeployedManifest { + pub version: u32, // 1 → 2 + pub schema: String, // ← 0.22 Option,0.23 必填,== DEPLOYED_MANIFEST_SCHEMA + pub package: DeployedPackageInfo, // 加 edition + pub build: Option, // 加 edition + compat profile hash + pub deployments: Vec, // 每个加 edition + compat profile hash +} + +pub const CURRENT_VERSION: u32 = 2; +``` + +### 3.2 校验链(v2 新增,**比 Cell.lock v2 更严**) + +`src/package/mod.rs:1568-1609` `validate_schema()`: + +```rust +pub fn validate_schema(&self) -> Result<()> { + // 校验 1 + 2: 双 ID 冗余(version + schema string) + if self.version != Self::CURRENT_VERSION || self.schema != DEPLOYED_MANIFEST_SCHEMA { + return Err(...); + } + // 校验 3: build edition == package edition + if let Some(build) = &self.build { + if build.edition != self.package.edition { return Err(...); } + // 校验 4: build 必须带 compat profile hash + if build.compatibility_profile_hash.is_empty() { return Err(...); } + } + // 校验 5: 每个 deployment edition == package edition + for deployment in &self.deployments { + if deployment.edition != self.package.edition { return Err(...); } + // 校验 6: 每个 deployment 必须带 compat profile hash + if deployment.compatibility_profile_hash.is_empty() { return Err(...); } + // 校验 7: deployment compat profile == build compat profile + if let Some(build) = &self.build { + if deployment.compatibility_profile_hash != build.compatibility_profile_hash { + return Err(...); + } + } + } + Ok(()) +} +``` + +**注意 Cell.lock v2 没有校验 7**——Deployed.toml v2 多了「build 与 deployment 必须共享同一 compat profile」这一条。 + +### 3.3 为什么 Deployed.toml 要比 Cell.lock 更严 + +Cell.lock 描述**「这个包的源代码身份」**——单包单 build。 +Deployed.toml 描述**「这个包在 N 个链上部署的事实记录」**——多网络多 deployment。 + +「build 用的 2026 edition 配 deployment 的 2027 edition」是真实存在的 drift 风险(CI 升级但部署脚本没升级)。所以 Deployed.toml v2 把「build/deployment edition + compat profile」**显式绑定**——你不能错位部署。 + +### 3.4 双 ID 冗余设计 + +`version = 2` + `schema = "cellscript-deployed-v0.23-edition-2026"`: + +```mermaid +flowchart LR + subgraph "Deployed.toml v2 身份" + V["version = 2
(结构 generation)"] + S["schema = 'cellscript-deployed-v0.23-edition-2026'
(语义 edition)"] + end + V -.独立校验.-> OK[通过] + S -.独立校验.-> OK + + style V fill:#cff + style S fill:#cfc +``` + +为什么**两个** ID 都要?——**结构 generation 和语义 edition 是独立版本轴**。`docs/CELLSCRIPT_PACKAGE_PROVENANCE_AND_DEPLOYMENT_IDENTITY.md:1493` 解释: + +> 双 ID 冗余是有意的 fail-closed evidence——把"结构 generation"和"语义 edition"分开防错位。 + +意思是: +- 未来如果 Deployed.toml 改 JSON 序列化(v3),`version=3` 但 `schema` 仍指向 edition 2026 → 安全 +- 未来如果 edition 升 2027,`schema=...v0.27-edition-2027` 但 `version` 可以仍 = 2(如果结构没变)→ 也安全 +- **结构 generation 和语义 edition 独立演化**——这是 0.22 没想清楚的「manifest contract」设计 + +--- + +## 4. Registry v1 协议内的 0.23 契约收紧 + +### 4.1 协议版本与 schema 版本 + +`services/registry-api/src/domain.ts:9-11`: + +```typescript +export const PUBLISH_PROTOCOL = "cellscript-registry-publish-v1"; // 仍是 v1 +export const REGISTRY_SCHEMA_VERSION = 1; // 仍是 1 +export const CELLSCRIPT_EDITION = "2026"; // ← 新增 +``` + +注意:**PUBLISH_PROTOCOL 和 REGISTRY_SCHEMA_VERSION 0.22 → 0.23 都没变**——0.23 改的是 schema 内部字段,不是协议版本。 + +### 4.2 新增字段与新必填项 + +`domain.ts:50-80` 关键变化: + +```typescript +// 0.22: optional +export interface PublishPayload { + manifest_hash?: string; // ← 0.22 可选 + // ... +} + +// 0.23: required +export interface PublishPayload { + manifest_hash: string; // ← 0.23 必填 + // ... +} +``` + +`domain.ts:65-79` registry_entry schema 强类型化: + +```typescript +// 0.22: Record (任意 blob) +export interface PublishPayload { + registry_entry: Record; +} + +// 0.23: 强类型 + 6 必填字段 +export interface RegistryVersionEntry { + version: string; // ← 必填 + tag: string; // 必须 "v" + source_hash: string; + cellscript_version: string; + edition: typeof CELLSCRIPT_EDITION; // 必须 "2026" + compatibility_profile_hash: string; // 32-byte hex + dependencies: Record; // ← 必须含 namespace + status: "source_published"; // 初始唯一合法 + yanked: false; // 初始唯一合法 +} + +export interface RegistryIndexEntry { + schema_version: typeof REGISTRY_SCHEMA_VERSION; // 必须 1 + namespace: string; + name: string; + versions: [RegistryVersionEntry]; // 必须正好 1 个 +} +``` + +### 4.3 9 步校验链 + +`domain.ts:407-453` `validateRegistryEntry()`: + +```mermaid +flowchart TD + R[收到 publish request] --> C1["1. schema_version == 1"] + C1 -->|fail| E1[400 unsupported_registry_schema] + C1 --> C2["2. namespace/name == outer"] + C2 -->|fail| E2[400 registry_identity_mismatch] + C2 --> C3["3. versions.length == 1"] + C3 -->|fail| E3[400 invalid_registry_versions] + C3 --> C4["4. version/source_hash == outer"] + C4 -->|fail| E4[400 registry_identity_mismatch] + C4 --> C5["5. tag == 'v'"] + C5 -->|fail| E5[400 invalid_registry_tag] + C5 --> C6["6. edition == '2026'"] + C6 -->|fail| E6[400 unsupported_cellscript_edition] + C6 --> C7["7. compat profile hash 是 64-hex"] + C7 -->|fail| E7[400 invalid_compatibility_profile_hash] + C7 --> C8["8. status=='source_published' && yanked==false"] + C8 -->|fail| E8[400 invalid_initial_registry_status] + C8 --> C9["9. 每个 dep 必须有 namespace + 合法 version"] + C9 -->|fail| E9[400 invalid_registry_dependency] + C9 --> OK[接受 publish] + + style E1 fill:#fee + style E2 fill:#fee + style E3 fill:#fee + style E4 fill:#fee + style E5 fill:#fee + style E6 fill:#fee + style E7 fill:#fee + style E8 fill:#fee + style E9 fill:#fee + style OK fill:#efe,stroke:#0a0 +``` + +**9 步**全部 fail-closed。 + +### 4.4 admin API 状态机收紧 + +`services/registry-api/src/index.ts:378`: + +```typescript +// 0.22: 7 个 admin API 可设 +const adminAllowed = ["source_published", "indexed_pending", "verified_build", + "deployed", "deprecated", "yanked", "quarantined"]; + +// 0.23: 5 个(verified_build / deployed / on_chain_committed 移除 admin 权限) +const adminAllowed = ["source_published", "indexed_pending", + "deprecated", "yanked", "quarantined"]; +``` + +**但** TypeScript enum 仍 8 个状态,DB schema 仍 8 状态(migrations/0001_initial.sql `check`)。 + +```mermaid +stateDiagram-v2 + [*] --> source_published: 0.22 + 0.23 + source_published --> indexed_pending: 0.22 + 0.23 + source_published --> verified_build: 0.23 evidence endpoint + indexed_pending --> verified_build: 0.22 任意 / 0.23 仅 evidence endpoint + verified_build --> deployed: 0.22 任意 / 0.23 仅 evidence endpoint + deployed --> on_chain_committed: 0.22 任意 / 0.23 仅 evidence endpoint + source_published --> deprecated: 0.22 + 0.23 + indexed_pending --> deprecated: 0.22 + 0.23 + source_published --> yanked: 0.22 + 0.23 + indexed_pending --> yanked: 0.22 + 0.23 + source_published --> quarantined: 0.22 + 0.23 + indexed_pending --> quarantined: 0.22 + 0.23 +``` + +**0.23 状态机含义**: + +- ✅ admin API 仍可达:`source_published` / `indexed_pending` / `deprecated` / `yanked` / `quarantined` +- ✅ generic admin API **不可伪造**:`verified_build` / `deployed` / `on_chain_committed` +- ✅ `POST /v1/admin/packages/:namespace/:name/versions/:version/promote` + 已实现证据专用路径:`source_published|indexed_pending → verified_build → deployed → on_chain_committed` +- ✅ 每一步校验 `source_hash` / `manifest_hash` / `compatibility_profile_hash`;部署证据必须引用 verified-build evidence hash,链上证明必须引用 deployed evidence hash +- ✅ `package_version_evidence` 持久化 hash-addressed evidence,静态版本 JSON 与公共 evidence read API 同步公开链条 + +原报告把“generic admin 无权设置 assurance 状态”误判成状态机断路。该判断已删除: +无权直设是正确的安全边界,证据路径现已闭合并由 25 项 API 测试覆盖。 + +### 4.5 DB CHECK 约束 + +`migrations/0001_initial.sql:90-127` `package_versions` 表: + +```sql ++ edition text not null, ++ compatibility_profile_hash text not null, ++ manifest_hash text not null, -- 0.22: nullable; 0.23: NOT NULL ++ check (source_hash ~ '^(0x)?[0-9A-Fa-f]{64}$'), ++ check (manifest_hash ~ '^(0x)?[0-9A-Fa-f]{64}$'), ++ check (edition = '2026'), -- 数据库层硬约束 ++ check (compatibility_profile_hash ~ '^(0x)?[0-9A-Fa-f]{64}$') +``` + +**含义**:即使应用层(Rust + TypeScript)bug 写错,DB CHECK 也会拒绝。 + +这是 0.22 没做的「防御深度」——0.22 只在应用层校验,0.23 **应用层 + DB 双层校验**。 + +### 4.6 生产部署与读路径闭环 + +截至 2026-07-31,Registry 已不是“未部署设计”: + +- `api.registry.cellscript.dev`:Node 22 API + Postgres 17,依赖感知 + `/ready` 同时验证数据库、对象存储、管理配置和 runtime; +- `registry.cellscript.dev`:独立只读 nginx,仅挂载对象卷,不依赖 API + 进程或 Postgres 才能读取版本 JSON; +- `cellscript.dev/registry/`:列表和动态详情页以生产 API 为主,API 故障时 + 才显示明确标记的只读 bundled mirror,原 Coming Soon 已删除; +- `cellc install` / `update`:默认以公共 API 的 accepted status 为选择权威, + 随后下载内容寻址的 Registry 源码快照,校验对象 SHA-256、逐文件 BLAKE2b、 + 安全路径、Edition/profile 和整树 source hash;`CELLSCRIPT_REGISTRY_URL` + 只是显式 Git/`registry.json` offline override; +- 线上负向验收覆盖管理未授权、非法查询、静态写入、路径穿越和请求体边界; + API 重启后 readiness 与持久化审计记录仍可读。 + +仍不能伪称完成的边界是首个 publisher-owned JoyID 正向发布与 clean-machine +安装。测试签名或数据库 seed 不能替代这个交互式采用检查点。 + +--- + +## 5. 风格治理(field-commas canonical)详细阐述 + +### 5.1 治理对象 + +**问题**:0.22 之前 `field: Type`(无逗号)和 `field: Type,`(有逗号)parser 都接受,但 formatter 输出风格不统一,canonical example 与 formatter 输出漂移。 + +**示例**: + +```cellscript +// canonical_style.cell 0.22(与 formatter 漂移) +struct CanonicalFields { + amount: u64 + enabled: bool +} +``` + +```cellscript +// formatter 0.22 实际输出(带 trailing comma) +struct CanonicalFields { + amount: u64, + enabled: bool, +} +``` + +`cellc fmt --check` 0.22 状态:`changed = 1`(formatter 想加逗号,源文件没有)。 + +### 5.2 7 层治理架构 + +0.23 风格治理是 7 层叠加的「输入可接受,输出强制」体系: + +```mermaid +flowchart TB + subgraph L1["Layer 1: Formatter (src/fmt/mod.rs)"] + F1["format_type_def (line 209-242)
固定输出 'field: Type,'
trailing comma 强制"] + end + subgraph L2["Layer 2: Canonical example"] + E1["examples/language/canonical_style.cell
canonical = 'field: Type,' 风格"] + E2["4 个 production example mirrors
用 'U64_MAX' 命名常量替裸 u64 边界"] + end + subgraph L3["Layer 3: syntax-combo seed (正)"] + S1["tests/syntax_combo/seeds/field-commas-canonical.cell
phase=accept
'field: Type,' 风格"] + end + subgraph L4["Layer 4: syntax-combo seed (反)"] + S2["tests/syntax_combo/seeds/field-commas-compatibility.cell
phase=accept
'field: Type' 风格 (历史兼容)"] + end + subgraph L5["Layer 5: matrix.toml required_origins"] + M["3 mode (quick/ci/deep) 全部把两个 seed 加进
required_origins
双覆盖策略:防 canonical 意外被 reject
防 compatibility 意外被 reject"] + end + subgraph L6["Layer 6: cases.json 扩展 (+2028 行)"] + C1["bug_class_contracts
required_cases
required_origins
新加的 SCA-BUG-0.22-* bug class"] + end + subgraph L7["Layer 7: gate dev / ci"] + G1["cellc fmt --check 对 canonical example 返回 changed=0
其它 example 仍可写无逗号源码 (parser 接受)"] + end + + F1 -->|formatter 写| E1 + S1 -->|双向保护| M + S2 -->|双向保护| M + E1 -->|fmt --check| G1 + M -->|seed audit| G1 + C1 -->|gate 防回归| G1 + + style F1 fill:#cff + style E1 fill:#cfc + style S1 fill:#efe + style S2 fill:#efe + style M fill:#fee + style C1 fill:#fef + style G1 fill:#ffd +``` + +### 5.3 各层详细 + +#### Layer 1: Formatter(`src/fmt/mod.rs:209-242`) + +```rust +fn format_type_def(...) -> Result<()> { + // ... header 处理 ... + self.push_line(&format!("{} {{", header)); + self.indent_level += 1; + for field in fields { + // ← 关键:固定输出 "field: Type,"(带 trailing comma) + self.push_line(&format!("{}: {},", field.name, format_type(&field.ty))); + } + self.format_validity_block(validity); + self.indent_level -= 1; + self.push_line("}"); + Ok(()) +} +``` + +**关键设计**: +- formatter 是**无条件输出 trailing comma**(没有"如果是单行就不输出"的优化) +- 同样模式在 `format_receipt_def`(line 248-269)也实现 +- 0.22 期间已写,0.23 把它**正式定为 canonical**——之前是 "happens to do this",0.23 是 "by design does this" + +#### Layer 2: Canonical example(`examples/language/canonical_style.cell`) + +0.23 改写后: +```cellscript +module cellscript::canonical_style + +resource Vault has store, create, consume, replace, relock { + owner: Address, + asset_symbol: [u8; 8], + balance: u64, +} +``` + +**canonical 风格的"标兵"**——任何看代码的人立刻知道"我新写的 struct 也应该长这样"。 + +#### Layer 3: 正向 seed(`field-commas-canonical.cell`) + +```cellscript +// audit: phase=accept +module cellscript::audit::seed_field_commas_canonical + +struct CanonicalFields { + amount: u64, + enabled: bool, +} +``` + +**`// audit: phase=accept` 注释**说明这是 syntax_combo audit 期望**接受**的输入。如果哪天 parser bug 拒绝这种风格,audit fail。 + +#### Layer 4: 反向 seed(`field-commas-compatibility.cell`) + +```cellscript +// audit: phase=accept +module cellscript::audit::seed_field_commas_compatibility + +struct CompatibilityFields { + amount: u64 + enabled: bool +} +``` + +**没有 trailing comma**——历史无逗号源码。 + +**两个 seed 都标 `phase=accept` 都进 required_origins**——这是「双覆盖策略」: + +| Seed | 防什么 | +|---|---| +| canonical | 防"我们以后把 trailing comma 改为 mandatory"——保持 parser 接受 trailing comma | +| compatibility | 防"我们以后悄悄拒绝无逗号源码"——保持 parser 接受无逗号 | + +**两个 seed 互为反向测试**——一个防收紧,一个防放宽。 + +#### Layer 5: matrix.toml + +`tests/syntax_combo/matrix.toml:28-29, 77-78, 129-130`(3 个 mode 都列): + +```toml +required_origins = [ + # ... 27 个其他 seed ... + "tests/syntax_combo/seeds/field-commas-canonical.cell", + "tests/syntax_combo/seeds/field-commas-compatibility.cell", + # ... +] +``` + +3 个 mode(quick/ci/deep,budget 64/1000/5000)**全部**把两个 seed 加进 required_origins → syntax-combo 跑任意一个 mode 都会测这两个 seed。 + +#### Layer 6: cases.json (+2028 行) + +新加的 `cases.json`(2028 行)含: +- `bug_class_contracts`:每个已知 bug class 的合同契约 +- `required_cases`:必跑的 case 列表 +- `required_origins`:必包含的 seed 列表 + +这是把"seed 列表"从 toml 升级到结构化 JSON + 机器可读 contracts——**未来 audit 失败能 machine-readable 报错**。 + +#### Layer 7: gate dev / ci + +`scripts/cellscript_gate.sh` 在 dev / ci 模式跑: + +```bash +# 1. cellc fmt --check 对 canonical_style.cell 返回 changed=0 +# 2. syntax-combo audit quick/ci/deep 跑 27+ seed(包含 2 个 field-commas) +# 3. cases.json 校验所有 required_bug_classes 都有覆盖 +# 4. 任何 canonical example 与 formatter 不一致 → gate fail +# 5. 任何 field-commas seed 被 reject → gate fail +``` + +**两道防回归线**:`fmt --check` + `syntax-combo audit` + `cases.json`。 + +### 5.4 7 层治理的「防漂移」设计 + +```mermaid +sequenceDiagram + participant Dev as 开发者 + participant Fmt as cellc fmt + participant Parser as cellc build + participant Seed as syntax-combo audit + participant Gate as gate dev/ci + + Dev->>Fmt: 写新 struct 字段无逗号 + Fmt->>Fmt: 检测到与 canonical 漂移 + Fmt-->>Dev: 报告 changed=N + Dev->>Fmt: 应用 formatter + Fmt-->>Dev: 字段统一为 'field: Type,' + + Dev->>Parser: cellc build + Parser->>Parser: parse_fields_and_validity 接受 trailing comma + Parser-->>Dev: Ok + + Dev->>Seed: syntax-combo quick + Seed->>Seed: 跑 field-commas-canonical.cell (accept) + Seed->>Seed: 跑 field-commas-compatibility.cell (accept) + Seed-->>Dev: pass + + Dev->>Gate: ./scripts/cellscript_gate.sh dev + Gate->>Fmt: fmt --check canonical_style.cell + Gate->>Seed: syntax-combo quick + Gate-->>Dev: all pass → release OK + + Note over Dev,Gate: 任意一层失败 → 全链路 fail
7 层防漂移闭环 +``` + +### 5.5 风格治理的「输入宽容 + 输出严格」原则 + +```mermaid +flowchart LR + A[用户写 .cell 源码] -->|输入| P[Parser] + P -->|接受 'field: Type,'| OK1[canonical: pass] + P -->|接受 'field: Type'| OK2[compatibility: pass] + P -->|拒其他形式| E[error] + + OK1 --> F[Formatter 写回 .cell] + OK2 --> F + F -->|输出| OUT["'field: Type,' 风格
(trailing comma 强制)"] + + style A fill:#eef + style OK1 fill:#efe + style OK2 fill:#efe + style E fill:#fee + style OUT fill:#ffd +``` + +**原则**: +- **输入宽容** — parser 接受 canonical + compatibility 两种风格 +- **输出严格** — formatter 永远输出 canonical 一种风格 +- **结果** — 用户的源码不强制风格(不破坏历史),但 round-trip 一致 + +这是 Unix 哲学的「Robustness Principle(Postel's Law)」应用: +> Be conservative in what you do, be liberal in what you accept from others. + +### 5.6 风格治理对开发者的实际影响 + +| 场景 | 0.22 体验 | 0.23 体验 | +|---|---|---| +| 写新 struct 加新字段 | 不确定要不要逗号 | 看 canonical_style.cell 抄 `field: Type,` | +| 复制历史 .cell 源码(无逗号)| build 接受 | build 接受(**无破坏**)| +| `cellc fmt` 跑过一次 | 改了 N 行加逗号 | changed=0(**已对齐**)| +| 升级到 0.24 | 担心要不要补逗号 | 0.24 还是接受两种风格(**无破坏**)| +| 改 formatter 输出风格 | 没人拦 | 3 个 gate + 2 个 seed + 1 个 canonical example 全部 fail(**防回归**)| + +### 5.7 风格治理 0.22 → 0.23 的本质 + +**0.22 状态**:formatter 和 example **已经漂移**。formatter 想加逗号,example 不加。 + +**0.23 状态**:7 层治理**强制** formatter 与 example **对齐**,且 parser **继续接受两种**输入。 + +**本质**: + +- 0.22 是「隐性矛盾」(formatter vs example vs parser 三方各说各话) +- 0.23 是「显性约束」(formatter 写 / parser 接受双风格 / example 对齐 formatter / gate 防漂移) +- 0.23 没引入**新**语法,**没**改 parser,**没**改 formatter——只**显性化**了已有的事实 + +这是 0.23 所有改动里**最便宜**的(format_type_def 早就这么写,example 改写 + 2 个新 seed + matrix 加 6 行),但**信号最强**(7 层防漂移闭环)——告诉用户「这个项目在严肃维护,不是 prototype」。 + +--- + +## 6. 三件套协同 + +Cell.lock v2 + Deployed.toml v2 + Registry v1 的 0.23 entry shape **三件套共享同一 `compatibility_profile_hash`**,加上 Edition 2026 闭集强制 + DB CHECK 约束: + +```mermaid +flowchart TB + subgraph "源层 (Cell.toml + .cell source)" + CT["Cell.toml
[package]
edition = '2026' (必填)"] + end + + subgraph "锁层 (Cell.lock v2)" + CL["package.edition = 2026
package_build.compatibility_profile_hash (必填)"] + end + + subgraph "部署层 (Deployed.toml v2)" + DT["package.edition = 2026
build.compatibility_profile_hash
deployments[i].compatibility_profile_hash (== build)"] + end + + subgraph "注册层 (Registry v1 / 0.23 entry shape)" + RG["registry_entry.versions[0]
edition = 2026
compatibility_profile_hash"] + end + + subgraph "数据库层" + DB["CHECK (edition = '2026')
CHECK (source_hash ~ 64-hex)
CHECK (compat hash ~ 64-hex)"] + end + + subgraph "应用层 (resolve_compatibility_profile)" + RP["ResolvedCompatibilityProfile.id
(稳定字符串 hash)"] + end + + CT -->|解析| RP + CL -->|校验| RP + DT -->|校验| RP + RG -->|校验| RP + RP -->|统一 hash| CL + RP -->|统一 hash| DT + RP -->|统一 hash| RG + DB -->|应用层之上再加一层| RG + + style RP fill:#ffd,stroke:#aa0 + style DB fill:#fdd,stroke:#d00 +``` + +**核心 invariant**: + +> `Cell.lock[package_build].compatibility_profile_hash == Deployed.toml[build/deployments[]].compatibility_profile_hash == RegistryIndex.versions[0].compatibility_profile_hash` + +任意一处失配 → 三件套中对应文件被拒读。 + +--- + +## 7. 总结 + +### 7.1 闭集强制的本质 + +不是「锁一个字符串」,是 **「锁 5 个独立版本轴的联合身份」**。edition 闭集 + ResolvedCompatibilityProfile + Cell.lock v2 + Deployed.toml v2 + Registry v1 的 0.23 强类型 entry + DB CHECK 6 层叠加,让「silent drift 在生产链上无存活空间」。 + +代价:升级时所有老 lock / deployed manifest 必须重新生成。 + +### 7.2 三件套身份升级的核心变化 + +| 件 | 0.22 字段 | 0.23 字段 | 校验 | +|---|---|---|---| +| Cell.lock | `version=1` | `version=2` + `package.edition` + `package_build.compatibility_profile_hash` | 3 道 fail-closed | +| Deployed.toml | `version=1` | `version=2` + `schema='...v0.23-edition-2026'` + `package/build/deployments[].edition` + `compat profile hash` | 7 道 fail-closed | +| Registry | 弱类型 blob | 强类型 schema + 9 必填字段 + 9 步校验链 | 应用层 + DB CHECK 双层 | + +**3 件套共享同一 `compatibility_profile_hash`**——5 个版本轴的联合身份是跨文件的"事实身份"。 + +### 7.3 风格治理的本质 + +**7 层叠加**的「输入宽容 + 输出严格」体系: + +- Formatter(写回 canonical 风格) +- Canonical example(示范) +- 正向 seed(防收紧) +- 反向 seed(防放宽) +- matrix.toml(强制跑两个 seed) +- cases.json(machine-readable 契约) +- gate dev/ci(防回归) + +**7 层都"恰好"是已有的事实被显性化**——不是新功能,是「收敛已有事实的边界」。这是 0.23 改动里**最便宜**但**信号最强**的一项。 + +### 7.4 0.23 整体的「协议优先于语言」取舍 + +| 维度 | 0.23 投入 | 含义 | +|---|---|---| +| 协议层(edition / lock / deployed / registry / DB)| **大** | 收紧 fail-closed,并闭合 Registry evidence 状态机 | +| 语言核心(parser / ast / lexer / type)| **刻意稳定** | 没有为发布造新语法;canonical 输出、兼容输入和组合矩阵已经对齐 | +| 工具层(Python→Rust)| **大** | 工具链单语言 | +| DX 层(LSP / tutorial / example / website)| **中** | Edition/profile 在 LSP、WASM、教程和示例统一;Registry live browse/detail 与故障态落地 | + +**0.23 是「生产证据链硬化」+「工具链单语言」+「Registry 生产化」release**。 +原报告中“语言 P0 全部未修”“DX 退步”“evidence 状态机断路”的结论缺少与当前实现 +相符的证据,已删除。剩余边界应按可验证事实描述,而不是沿用旧报告的评分措辞。 + +--- + +> **写于**: 2026-07-31 +> **复核**: 当前工作树、线上 Registry 健康检查与 0.23 gate 契约 diff --git a/cellscript-ergonomics-desktop.png b/cellscript-ergonomics-desktop.png deleted file mode 100644 index 2952fc95..00000000 Binary files a/cellscript-ergonomics-desktop.png and /dev/null differ diff --git a/cellscript-no-grid-clean-desktop.png b/cellscript-no-grid-clean-desktop.png deleted file mode 100644 index e9aa4701..00000000 Binary files a/cellscript-no-grid-clean-desktop.png and /dev/null differ diff --git a/cellscript-no-grid-desktop.png b/cellscript-no-grid-desktop.png deleted file mode 100644 index dc637958..00000000 Binary files a/cellscript-no-grid-desktop.png and /dev/null differ diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..a5a554ae --- /dev/null +++ b/clippy.toml @@ -0,0 +1,2 @@ +too-many-arguments-threshold = 10 +large-error-threshold = 256 diff --git a/contracts/registry-type-script/.gitignore b/contracts/registry-type-script/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/contracts/registry-type-script/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/contracts/registry-type-script/Cargo.lock b/contracts/registry-type-script/Cargo.lock new file mode 100644 index 00000000..2218782d --- /dev/null +++ b/contracts/registry-type-script/Cargo.lock @@ -0,0 +1,2101 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitcoin-consensus-encoding" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207311705279250ba465076a1bac4b1ac982855fff73fc5f67e22158ac58cdc9" +dependencies = [ + "bitcoin-internals", + "hex-conservative 1.2.0", + "serde", +] + +[[package]] +name = "bitcoin-internals" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5" + +[[package]] +name = "bitcoin-io" +version = "0.1.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" +dependencies = [ + "bitcoin-consensus-encoding", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" +dependencies = [ + "bitcoin-io", + "hex-conservative 0.2.2", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "blake2b-ref" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "294d17c72e0ba59fad763caa112368d0672083779cdebbb97164f4bb4c1e339a" + +[[package]] +name = "blake2b-rs" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89a8565807f21b913288968e391819e7f9b2f0f46c7b89549c051cccf3a2771" +dependencies = [ + "cc", + "cty", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "buddy-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee741d62dcaf41ca303576ef890989ccb01d5dd77f8ce1a6d6c7846ab5d09efb" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cacache" +version = "13.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5063741c7b2e260bbede781cf4679632dd90e2718e99f7715e46824b65670b" +dependencies = [ + "digest", + "either", + "futures", + "hex", + "libc", + "memmap2", + "miette", + "reflink-copy", + "serde", + "serde_derive", + "serde_json", + "sha1", + "sha2", + "ssri", + "tempfile", + "thiserror", + "tokio", + "tokio-stream", + "walkdir", +] + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cellscript-registry-type-script" +version = "0.24.0" +dependencies = [ + "ckb-hash", + "ckb-std", + "ckb-testtool", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "ckb-always-success-script" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b3b72a38c9920a29990df12002c4d069a147c8782f0c211f8a01b2df8f42bfd" + +[[package]] +name = "ckb-chain-spec" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70187bb9e6d87c5d2c6eb95d24732ab79f02e1e221225f63edf0825dc1b9176a" +dependencies = [ + "cacache", + "ckb-constant", + "ckb-crypto", + "ckb-dao-utils", + "ckb-error", + "ckb-hash", + "ckb-jsonrpc-types", + "ckb-logger", + "ckb-pow", + "ckb-rational", + "ckb-resource", + "ckb-traits", + "ckb-types", + "serde", + "toml", +] + +[[package]] +name = "ckb-constant" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493dad5af3843e4c19cf01fd6cf13d2ef07a17ba29e0a6d28d24a5dbf44b2b5" +dependencies = [ + "phf 0.12.1", +] + +[[package]] +name = "ckb-crypto" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42c46d018105d08f0cdbe885014228a2a205d8b9c6a8050d9d2c271d7a7d8e31" +dependencies = [ + "ckb-fixed-hash", + "faster-hex", + "rand 0.8.7", + "secp256k1", + "thiserror", +] + +[[package]] +name = "ckb-dao" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed08e6129c2915f8d7d73014fbed79ff428f7baed72ac69768784d25d20fb78" +dependencies = [ + "byteorder", + "ckb-chain-spec", + "ckb-dao-utils", + "ckb-traits", + "ckb-types", +] + +[[package]] +name = "ckb-dao-utils" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62ed7a7bbe5d490550d54c21847e3bdeb96219071f8af29693a690e67adca12" +dependencies = [ + "byteorder", + "ckb-error", + "ckb-types", +] + +[[package]] +name = "ckb-error" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c062a4634615a2ad2a96d027ae08d76fe27cb3670b05648cdee83dfc2fd3e0c4" +dependencies = [ + "anyhow", + "ckb-occupied-capacity", + "derive_more 1.0.0", + "thiserror", +] + +[[package]] +name = "ckb-fixed-hash" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44a532d8d25a16495d8b272bc9541387221df4a010c1f2657e2d265ab7ae4e6b" +dependencies = [ + "ckb-fixed-hash-core", + "ckb-fixed-hash-macros", +] + +[[package]] +name = "ckb-fixed-hash-core" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "204fa623b4a2ee22f780598de7b0651c5da0e635a8c07e657bf0da4b37e92c97" +dependencies = [ + "faster-hex", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "ckb-fixed-hash-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd9ae4b569c2be40c7a0cc2ae2241bc6fadd20b8e89cbae49b511773aab6301" +dependencies = [ + "ckb-fixed-hash-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ckb-gen-types" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b0a3f407c30d8bb5077370ad129bfea0e045c5ea85fda152a404bac410dc7" +dependencies = [ + "cfg-if", + "ckb-error", + "ckb-fixed-hash", + "ckb-hash", + "ckb-occupied-capacity", + "molecule", + "numext-fixed-uint", + "seq-macro", + "strum", +] + +[[package]] +name = "ckb-hash" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "108c6bdc893999c92719b13b2b16f9516f62ce76b0b88055be73d3282c23399d" +dependencies = [ + "blake2b-ref", + "blake2b-rs", +] + +[[package]] +name = "ckb-jsonrpc-types" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c621e0ea2746f0b0b8bfd220bae8d73896158986b77e881f38161bfa17c989" +dependencies = [ + "ckb-types", + "faster-hex", + "schemars", + "seq-macro", + "serde", + "serde_json", +] + +[[package]] +name = "ckb-logger" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43745ff43529079c26b890872ea26cea89e734e9f1d1545f26615e297420f76d" +dependencies = [ + "log", +] + +[[package]] +name = "ckb-merkle-mountain-range" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ccb671c5921be8a84686e6212ca184cb1d7c51cadcdbfcbd1cc3f042f5dfb8" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ckb-mock-tx-types" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf88d2ce64e8c3d17d7cd3d0f0ff9038772d35cbe63d1e4f640e0e25428f390" +dependencies = [ + "ckb-jsonrpc-types", + "ckb-traits", + "ckb-types", + "serde", +] + +[[package]] +name = "ckb-occupied-capacity" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c32ab6698909e2d6e4153498830bd99d762653f47b32f849bee57610292dc24" +dependencies = [ + "ckb-occupied-capacity-core", + "ckb-occupied-capacity-macros", +] + +[[package]] +name = "ckb-occupied-capacity-core" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb6d9b99f6e093df3fb3740cc742f4a1e4df6fee074951804c6b57179ab08a3" +dependencies = [ + "serde", +] + +[[package]] +name = "ckb-occupied-capacity-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a36880cfd22cad07fd1e6c8f4b2d46eeb6dfae651de55bc1966a7a0554afa7" +dependencies = [ + "ckb-occupied-capacity-core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ckb-pow" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e355b9e25bbff2b9ff2ec789218c1ba4afa91f8ac57bd5bbc7f421a6312c296e" +dependencies = [ + "byteorder", + "ckb-hash", + "ckb-types", + "eaglesong", + "log", + "serde", +] + +[[package]] +name = "ckb-rational" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "141af1cad079c7d3c9bcf896adc77c58c8074c0aa492b079acdb8be2982e5a67" +dependencies = [ + "numext-fixed-uint", + "serde", +] + +[[package]] +name = "ckb-resource" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4bd2ca99cdfa3eadccf0e8e6a2ee01f25502157cc65934b6f3073e118bdd25" +dependencies = [ + "ckb-system-scripts", + "ckb-types", + "includedir", + "includedir_codegen", + "phf 0.8.0", + "serde", + "walkdir", +] + +[[package]] +name = "ckb-script" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f32599371d622aaaf3114b539ed839fb197711c41a38a7d7ded2008c0b02392f" +dependencies = [ + "byteorder", + "ckb-chain-spec", + "ckb-error", + "ckb-hash", + "ckb-logger", + "ckb-traits", + "ckb-types", + "ckb-vm", + "faster-hex", + "serde", + "tokio", +] + +[[package]] +name = "ckb-std" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7defadecfc39d5a25cddf11d86308130d745262f8f006bd9f602e7c968596460" +dependencies = [ + "buddy-alloc", + "cc", + "gcd", + "int-enum", +] + +[[package]] +name = "ckb-system-scripts" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa5c59063142de7a68cfad4449c6b3863563856219a2925dfb8c5f019ec2aa47" +dependencies = [ + "blake2b-rs", + "faster-hex", + "includedir", + "includedir_codegen", + "phf 0.8.0", +] + +[[package]] +name = "ckb-systemtime" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e01076978d1df514364e80c4d20b1ab4a4133caeac5484f04851d980fc45ee" +dependencies = [ + "web-time", +] + +[[package]] +name = "ckb-testtool" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47f7dc1615323d16c3170cf907948205ea9bbfeb42e4b271e825bb1b7be792f7" +dependencies = [ + "ckb-always-success-script", + "ckb-chain-spec", + "ckb-crypto", + "ckb-error", + "ckb-hash", + "ckb-jsonrpc-types", + "ckb-mock-tx-types", + "ckb-resource", + "ckb-script", + "ckb-traits", + "ckb-types", + "ckb-verification", + "faster-hex", + "lazy_static", + "rand 0.8.7", +] + +[[package]] +name = "ckb-traits" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc3054cb577e4bf7076ff2d63c48d26d8b48c4d103228354aa1c01deeb1d744" +dependencies = [ + "ckb-types", +] + +[[package]] +name = "ckb-types" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05da0ac5d3f7fbd8563b1aaae39d45f1d3f3172686f73bd2a0dffb394b37fce" +dependencies = [ + "bit-vec", + "bytes", + "ckb-constant", + "ckb-error", + "ckb-fixed-hash", + "ckb-gen-types", + "ckb-hash", + "ckb-merkle-mountain-range", + "ckb-occupied-capacity", + "ckb-rational", + "derive_more 1.0.0", + "golomb-coded-set", + "merkle-cbt", + "molecule", + "numext-fixed-uint", + "paste", +] + +[[package]] +name = "ckb-verification" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2080bedb22d7b0255678c8cff10931e5e2f587fe1a724a817a76845ec300c3c1" +dependencies = [ + "ckb-chain-spec", + "ckb-constant", + "ckb-dao", + "ckb-dao-utils", + "ckb-error", + "ckb-pow", + "ckb-script", + "ckb-systemtime", + "ckb-traits", + "ckb-types", + "ckb-verification-traits", + "derive_more 1.0.0", + "lru", + "tokio", +] + +[[package]] +name = "ckb-verification-traits" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8557e69dfca1330a1538a942c1bb468cab0a4323cdf72d4f99e4c670d9b908eb" +dependencies = [ + "bitflags", + "ckb-error", +] + +[[package]] +name = "ckb-vm" +version = "0.24.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad137e2f1c9a363ce19a883a2113b1dfcc00a936945e34b62e3726c49e7171fb" +dependencies = [ + "byteorder", + "bytes", + "cc", + "ckb-vm-definitions", + "derive_more 0.99.20", + "goblin 0.2.3", + "goblin 0.4.0", + "rand 0.7.3", + "scroll", + "serde", +] + +[[package]] +name = "ckb-vm-definitions" +version = "0.24.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b436017fd6676bea413d54e07a5a9cc1d7c4b5c02e4ab07d3527225a5de6677" +dependencies = [ + "paste", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "eaglesong" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d978bd5d343e8ab9b5c0fc8d93ff9c602fdc96616ffff9c05ac7a155419b824" + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "faster-hex" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e2ce894d53b295cf97b05685aa077950ff3e8541af83217fc720a6437169f8" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gcd" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "goblin" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d20fd25aa456527ce4f544271ae4fea65d2eda4a6561ea56f39fb3ee4f7e3884" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "goblin" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532a09cd3df2c6bbfc795fb0434bff8f22255d1d07328180e918a2e6ce122d4d" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "golomb-coded-set" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f314a99fb5b7f0f9d0a8388539578f83f3aca6a65f588b8dbeefb731e2f98" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "heapsize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1679e6ea370dee694f91f1dc469bf94cf8f52051d147aec3e1f9497c6fc22461" +dependencies = [ + "winapi", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex-conservative" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35431185f361ccf3ffc58254628af5f1f5d5f28531da2e02e5d6c82bbc282a10" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "includedir" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afd126bd778c00c43a9dc76d1609a0894bf4222088088b2217ccc0ce9e816db7" +dependencies = [ + "flate2", + "phf 0.8.0", +] + +[[package]] +name = "includedir_codegen" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ac1500c9780957c9808c4ec3b94002f35aab01483833f5a8bce7dfb243e3148" +dependencies = [ + "flate2", + "phf_codegen", + "walkdir", +] + +[[package]] +name = "int-enum" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366a1634cccc76b4cfd3e7580de9b605e4d93f1edac48d786c1f867c0def495" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + +[[package]] +name = "merkle-cbt" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171d2f700835121c3b04ccf0880882987a050fd5c7ae88148abf537d33dd3a56" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "miette" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" +dependencies = [ + "miette-derive", + "once_cell", + "thiserror", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "molecule" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "314eebe1fb025f681c1d6a62fdacbe831027177c1046503a8d73d8027fe19e16" +dependencies = [ + "bytes", + "cfg-if", + "faster-hex", +] + +[[package]] +name = "numext-constructor" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "621fe0f044729f810c6815cdd77e8f5e0cd803ce4f6a38380ebfc1322af98661" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "numext-fixed-uint" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c68c76f96d589d1009a666c5072f37f3114d682696505f2cf445f27766c7d70" +dependencies = [ + "numext-fixed-uint-core", + "numext-fixed-uint-hack", +] + +[[package]] +name = "numext-fixed-uint-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aab1d6457b97b49482f22a92f0f58a2f39bdd7f3b2f977eae67e8bc206aa980" +dependencies = [ + "heapsize", + "numext-constructor", + "rand 0.7.3", + "serde", + "thiserror", +] + +[[package]] +name = "numext-fixed-uint-hack" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200f8d55c36ec1b6a8cf810115be85d4814f045e0097dfd50033ba25adb4c9e" +dependencies = [ + "numext-fixed-uint-core", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_macros", + "phf_shared 0.12.1", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cbb1126afed61dd6368748dae63b1ee7dc480191c6262a3b4ff1e29d86a6c5b" +dependencies = [ + "fastrand", + "phf_shared 0.12.1", +] + +[[package]] +name = "phf_macros" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d713258393a82f091ead52047ca779d37e5766226d009de21696c4e667044368" +dependencies = [ + "phf_generator 0.12.1", + "phf_shared 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher 1.0.3", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "version_check", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "reflink-copy" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9dd7ab4af0363d5ccfd2838d782a28196cf32a5cc2e4fe3c5dc83f2be588b8b" +dependencies = [ + "cfg-if", + "libc", + "rustix", + "windows", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 3.0.3", +] + +[[package]] +name = "scroll" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda28d4b4830b807a8b43f7b0e6b5df875311b3e7621d84577188c175b6ec1ec" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaaae8f38bb311444cfb7f1979af0bc9240d95795f75f9ceddf6a59b79ceffa0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "secp256k1" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.7", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "seq-macro" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_derive_internals" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "ssri" +version = "9.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7a2b3c2bc9693bcb40870c4e9b5bf0d79f9cb46273321bf855ec513e919082" +dependencies = [ + "base64", + "digest", + "hex", + "miette", + "serde", + "sha-1", + "sha2", + "thiserror", + "xxhash-rust", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/contracts/registry-type-script/Cargo.toml b/contracts/registry-type-script/Cargo.toml new file mode 100644 index 00000000..5be5777b --- /dev/null +++ b/contracts/registry-type-script/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "cellscript-registry-type-script" +version = "0.24.0" +edition = "2024" +rust-version = "1.97.1" +publish = false + +[[bin]] +name = "cellscript-registry-type-script" +path = "src/main.rs" +test = false +doctest = false +bench = false +required-features = ["ckb-script"] + +[[bin]] +name = "cellscript-registry-type-script-hash" +path = "src/bin/ckb_data_hash.rs" +test = false +doctest = false +bench = false +required-features = ["hash-tool"] + +[features] +ckb-script = [] +hash-tool = ["dep:ckb-hash"] + +[dependencies] +ckb-hash = { version = "=1.1.1", optional = true } +ckb-std = { version = "=1.1.0", default-features = false, features = ["allocator"] } + +[dev-dependencies] +ckb-testtool = "=1.1.1" + +[workspace] + +[profile.dev] +panic = "abort" + +[profile.release] +codegen-units = 1 +lto = true +opt-level = "z" +overflow-checks = true +panic = "abort" +strip = false diff --git a/contracts/registry-type-script/README.md b/contracts/registry-type-script/README.md new file mode 100644 index 00000000..3a32032d --- /dev/null +++ b/contracts/registry-type-script/README.md @@ -0,0 +1,66 @@ +# CellScript Registry Type Script + +Canonical Type Script for mainnet Registry commitment Cells. It accepts only a +32-byte custody Lock Script hash in `args` and exact 39-byte Cell data: + +```text +"CSREGv1" || ckb_blake2b_256(canonical commitment JSON) +``` + +The Script validates the data and custody Lock of every input and output in its +Type Script group. It also requires every creation, replacement, or destruction +transaction to consume at least one Cell whose Lock Script hash equals `args`. +Creating an output locked to the Registry therefore cannot impersonate an +official commitment: the transaction must exercise the Registry custody Lock. +The Script deliberately does not interpret off-chain JSON; the Registry API +binds the 32-byte hash to accepted release and deployment evidence and +revalidates live Cells independently. + +The custody requirement is the sole on-chain authority boundary. With the +currently pinned standard sighash Lock, its one signer can create, replace, or +destroy commitment Cells; this Type Script adds no multisig, timelock, or +separate revocation path. A custody-key rotation changes the Lock Script hash +in Type args and therefore creates a new Registry Type Script identity. The +operator runbook and compromise procedure are documented under “Commitment +custody boundary and incident response” in `services/registry-api/README.md`. + +Production uses the standard mainnet `secp256k1_blake160_sighash_all` genesis +Script for custody. Type Script args are the CKB Script hash of that complete +custody Script, including its 20-byte signer args. The Registry Type Script is +immutable at the data-hash layer unless a reviewed deployment explicitly +chooses a Type ID code Cell. + +Build and test with the pinned repository toolchain: + +```bash +contracts/registry-type-script/build_reproducible_release.sh +cargo test --locked --manifest-path contracts/registry-type-script/Cargo.toml +``` + +Reproduce the canonical Linux artifact with the pinned container digest: + +```bash +contracts/registry-type-script/build_canonical_container.sh +``` + +The deployable artifact is tracked under `artifacts/v0.24.0` and was produced +for the `x86_64-unknown-linux-gnu` host with the builder image digest recorded +in `release-manifest.json`. Rust/LLVM may order identical RISC-V functions +differently on another build host, so the script claims a byte-for-byte +reproduction only on that canonical host. On every other host it still builds +the source, reports the host artifact hash, verifies the tracked canonical +identity, and places the canonical bytes at the normal target path for +downstream tooling. The CKB-VM suite always executes those deployable bytes. + +The build disables `ckb-std` default features and enables only its Rust +allocator. Fixed-size data, Script, and lock-hash buffers call the official +syscall layer directly; the contract does not carry the higher-level Molecule +type graph or depend on a host C compiler and bundled `libc.c`. +The small host-side hash utility in the same crate computes CKB's personalized +Blake2b-256 identity without depending on the root compiler workspace or a +sibling SDK checkout. + +The test suite executes the stripped RISC-V binary in CKB-VM through +`ckb-testtool`, covering authorized creation, replacement, destruction, +unauthorized creation, incorrect custody Locks, malformed data, and +non-canonical Script args. diff --git a/contracts/registry-type-script/artifacts/v0.22.0/cellscript-registry-type-script b/contracts/registry-type-script/artifacts/v0.22.0/cellscript-registry-type-script new file mode 100755 index 00000000..9a756b8a Binary files /dev/null and b/contracts/registry-type-script/artifacts/v0.22.0/cellscript-registry-type-script differ diff --git a/contracts/registry-type-script/artifacts/v0.24.0/cellscript-registry-type-script b/contracts/registry-type-script/artifacts/v0.24.0/cellscript-registry-type-script new file mode 100755 index 00000000..623c30ac Binary files /dev/null and b/contracts/registry-type-script/artifacts/v0.24.0/cellscript-registry-type-script differ diff --git a/contracts/registry-type-script/build_canonical_container.sh b/contracts/registry-type-script/build_canonical_container.sh new file mode 100755 index 00000000..c37fdd3c --- /dev/null +++ b/contracts/registry-type-script/build_canonical_container.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +contract_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repository_root="$(cd "$contract_dir/../.." && pwd)" +canonical_target_dir="${CARGO_TARGET_DIR:-$contract_dir/target/canonical-container}" +builder_image="rust@sha256:77fac8b98f9f46062bb680b6d25d5bcaabfc400143952ebc572e924bcbedc3fa" + +mkdir -p "$canonical_target_dir" +canonical_target_dir="$(cd "$canonical_target_dir" && pwd)" + +docker run --rm --platform linux/amd64 \ + --user "$(id -u):$(id -g)" \ + --env CARGO_HOME=/contract-target/cargo-home \ + --env RUSTUP_HOME=/usr/local/rustup \ + --mount "type=bind,src=$repository_root,dst=/workspace,readonly" \ + --mount "type=bind,src=$canonical_target_dir,dst=/contract-target" \ + --workdir /workspace \ + "$builder_image" \ + bash -c ' + set -euo pipefail + toolchain_bin=/usr/local/rustup/toolchains/1.97.1-x86_64-unknown-linux-gnu/bin + export PATH="$toolchain_bin:/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + export RUSTC="$toolchain_bin/rustc" + export LD_LIBRARY_PATH="$toolchain_bin/../lib" + test "$(rustc -vV | sed -n "s/^host: //p")" = x86_64-unknown-linux-gnu + test -x "$(rustc --print sysroot)/lib/rustlib/x86_64-unknown-linux-gnu/bin/rust-objcopy" + rustc --print target-libdir --target riscv64imac-unknown-none-elf >/dev/null + CARGO_TARGET_DIR=/contract-target \ + CELLSCRIPT_HASH_TARGET_DIR=/contract-target/cellc \ + /workspace/contracts/registry-type-script/build_reproducible_release.sh + ' diff --git a/contracts/registry-type-script/build_reproducible_release.sh b/contracts/registry-type-script/build_reproducible_release.sh new file mode 100755 index 00000000..dce2254f --- /dev/null +++ b/contracts/registry-type-script/build_reproducible_release.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +set -euo pipefail + +contract_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repository_root="$(cd "$contract_dir/../.." && pwd)" +cargo_home_dir="${CARGO_HOME:-${HOME}/.cargo}" +target_dir="${CARGO_TARGET_DIR:-$contract_dir/target}" +hash_target_dir="${CELLSCRIPT_HASH_TARGET_DIR:-$repository_root/target}" +rust_sysroot="$(rustc --print sysroot)" +host_triple="$(rustc -vV | awk '/^host: / { print $2 }')" +rust_objcopy="$rust_sysroot/lib/rustlib/$host_triple/bin/rust-objcopy" +if [[ ! -x "$rust_objcopy" ]]; then + printf 'rust-objcopy not found; install llvm-tools-preview for the pinned toolchain\n' >&2 + exit 1 +fi + +sha256_file() { + local input_path="$1" + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$input_path" | awk '{ print $1 }' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$input_path" | awk '{ print $1 }' + else + printf 'SHA-256 tool not found; install sha256sum or shasum\n' >&2 + return 1 + fi +} + +mkdir -p "$target_dir" +target_dir="$(cd "$target_dir" && pwd)" +unit_separator=$'\x1f' +encoded_rustflags="-C${unit_separator}target-feature=+zba,+zbb,+zbc,+zbs" +encoded_rustflags+="${unit_separator}-C${unit_separator}passes=lower-atomic" +encoded_rustflags+="${unit_separator}--remap-path-prefix=$repository_root=/src/cellscript" +encoded_rustflags+="${unit_separator}--remap-path-prefix=$cargo_home_dir=/cargo" + +env -u RUSTFLAGS \ + CARGO_ENCODED_RUSTFLAGS="$encoded_rustflags" \ + CARGO_INCREMENTAL=0 \ + CARGO_TARGET_DIR="$target_dir" \ + cargo build \ + --locked \ + --manifest-path "$contract_dir/Cargo.toml" \ + --release \ + --target riscv64imac-unknown-none-elf \ + --features ckb-script \ + --bin cellscript-registry-type-script + +artifact="$target_dir/riscv64imac-unknown-none-elf/release/cellscript-registry-type-script" +host_artifact="$artifact.$host_triple.stripped" +"$rust_objcopy" --strip-all "$artifact" "$host_artifact" + +release_manifest="$contract_dir/release-manifest.json" +canonical_relative_path="$(sed -n 's/.*"artifact": "\([^"]*\)".*/\1/p' "$release_manifest")" +canonical_artifact="$contract_dir/$canonical_relative_path" +if [[ -z "$canonical_relative_path" || ! -f "$canonical_artifact" ]]; then + printf 'canonical Registry Type Script artifact is missing: %s\n' "$canonical_artifact" >&2 + exit 1 +fi + +sha256_hash="$(sha256_file "$canonical_artifact")" +artifact_bytes="$(wc -c < "$canonical_artifact" | tr -d ' ')" +ckb_data_hash="$(CARGO_TARGET_DIR="$hash_target_dir" cargo run --quiet --locked \ + --manifest-path "$contract_dir/Cargo.toml" \ + --features hash-tool \ + --bin cellscript-registry-type-script-hash \ + -- "$canonical_artifact")" +ckb_hash_json="$(printf '{\n "algorithm": "blake2b-256",\n "hash": "%s",\n "input_bytes": %s,\n "personalization": "ckb-default-hash",\n "status": "ok"\n}' \ + "$ckb_data_hash" "$artifact_bytes")" +expected_sha256="$(sed -n 's/.*"sha256": "\([0-9a-f]*\)".*/\1/p' "$release_manifest")" +expected_artifact_bytes="$(sed -n 's/.*"artifact_bytes": \([0-9]*\).*/\1/p' "$release_manifest")" +expected_ckb_data_hash="$(sed -n 's/.*"ckb_data_hash": "0x\([0-9a-f]*\)".*/\1/p' "$release_manifest")" +if [[ "$artifact_bytes" != "$expected_artifact_bytes" || "$sha256_hash" != "$expected_sha256" || "$ckb_data_hash" != "$expected_ckb_data_hash" ]]; then + printf 'Registry Type Script release identity mismatch\n' >&2 + printf 'expected bytes=%s sha256=%s ckb_data_hash=0x%s\n' "$expected_artifact_bytes" "$expected_sha256" "$expected_ckb_data_hash" >&2 + printf 'actual bytes=%s sha256=%s ckb_data_hash=0x%s\n' "$artifact_bytes" "$sha256_hash" "$ckb_data_hash" >&2 + exit 1 +fi + +host_sha256="$(sha256_file "$host_artifact")" +if [[ "$host_triple" == "x86_64-unknown-linux-gnu" ]]; then + if ! cmp -s "$host_artifact" "$canonical_artifact"; then + printf 'canonical x86_64 Linux rebuild does not match the tracked Registry Type Script artifact\n' >&2 + printf 'expected sha256=%s actual sha256=%s\n' "$sha256_hash" "$host_sha256" >&2 + exit 1 + fi + printf 'canonical_rebuild=matched\n' +else + printf 'canonical_rebuild=not_claimed host=%s host_sha256=%s\n' "$host_triple" "$host_sha256" +fi + +# Downstream tools always execute the exact tracked deployable bytes. A +# non-canonical host build is retained beside this path for inspection. +cp "$canonical_artifact" "$artifact" + +printf 'artifact=%s\n' "$artifact" +printf 'artifact_bytes=%s\n' "$artifact_bytes" +printf 'sha256=%s\n' "$sha256_hash" +printf '%s\n' "$ckb_hash_json" diff --git a/contracts/registry-type-script/release-manifest.json b/contracts/registry-type-script/release-manifest.json new file mode 100644 index 00000000..73466ccc --- /dev/null +++ b/contracts/registry-type-script/release-manifest.json @@ -0,0 +1,19 @@ +{ + "schema": "cellscript-registry-type-script-release-v1", + "version": "0.24.0", + "target": "riscv64imac-unknown-none-elf", + "artifact": "artifacts/v0.24.0/cellscript-registry-type-script", + "canonical_build_host": "x86_64-unknown-linux-gnu", + "canonical_builder_image": "rust@sha256:77fac8b98f9f46062bb680b6d25d5bcaabfc400143952ebc572e924bcbedc3fa", + "artifact_bytes": 3352, + "sha256": "0f48a8736360c121f6ae0f04ab4b0496834f6715d47e3284a0a07add609dede9", + "ckb_data_hash": "0x0dd596ade29e06e5bcc00f56abf36ecbe9afaa09f1b26a64436aa37854da622b", + "script_template": { + "code_hash": "0x0dd596ade29e06e5bcc00f56abf36ecbe9afaa09f1b26a64436aa37854da622b", + "hash_type": "data1", + "args_schema": "ckb_script_hash(custody_lock)", + "args_bytes": 32 + }, + "deployment_policy": "immutable_data_cell", + "custody_lock": "ckb_mainnet_secp256k1_blake160_sighash_all" +} diff --git a/contracts/registry-type-script/src/bin/ckb_data_hash.rs b/contracts/registry-type-script/src/bin/ckb_data_hash.rs new file mode 100644 index 00000000..e3bcf4e9 --- /dev/null +++ b/contracts/registry-type-script/src/bin/ckb_data_hash.rs @@ -0,0 +1,26 @@ +use std::{env, fs, process::ExitCode}; + +fn main() -> ExitCode { + let mut args = env::args_os(); + let _program = args.next(); + let Some(path) = args.next() else { + eprintln!("usage: cellscript-registry-type-script-hash "); + return ExitCode::from(2); + }; + if args.next().is_some() { + eprintln!("expected exactly one artifact path"); + return ExitCode::from(2); + } + let bytes = match fs::read(&path) { + Ok(bytes) => bytes, + Err(error) => { + eprintln!("failed to read {}: {error}", path.to_string_lossy()); + return ExitCode::FAILURE; + } + }; + for byte in ckb_hash::blake2b_256(bytes) { + print!("{byte:02x}"); + } + println!(); + ExitCode::SUCCESS +} diff --git a/contracts/registry-type-script/src/main.rs b/contracts/registry-type-script/src/main.rs new file mode 100644 index 00000000..ad0cda91 --- /dev/null +++ b/contracts/registry-type-script/src/main.rs @@ -0,0 +1,114 @@ +#![cfg_attr(not(test), no_std)] +#![cfg_attr(not(test), no_main)] + +#[cfg(not(test))] +ckb_std::entry!(program_entry); +ckb_std::default_alloc!(16_384, 1_258_306, 64); + +#[cfg(all(not(test), not(target_arch = "riscv64")))] +#[panic_handler] +fn host_panic_handler(_: &core::panic::PanicInfo<'_>) -> ! { + loop { + core::hint::spin_loop(); + } +} + +use ckb_std::{ + ckb_constants::{CellField, Source}, + error::SysError, + syscalls, +}; + +const COMMITMENT_MAGIC: &[u8; 7] = b"CSREGv1"; +const COMMITMENT_HASH_BYTES: usize = 32; +const COMMITMENT_DATA_BYTES: usize = COMMITMENT_MAGIC.len() + COMMITMENT_HASH_BYTES; +const CUSTODY_LOCK_HASH_BYTES: usize = 32; +// Molecule Script = total_size + 3 field offsets + code_hash + hash_type + +// args(Bytes length prefix + 32-byte payload). +const SCRIPT_BYTES_WITH_CUSTODY_HASH: usize = 4 + (3 * 4) + 32 + 1 + 4 + CUSTODY_LOCK_HASH_BYTES; +const SCRIPT_ARGS_OFFSET: usize = SCRIPT_BYTES_WITH_CUSTODY_HASH - CUSTODY_LOCK_HASH_BYTES; + +#[repr(i8)] +enum Error { + Syscall = 5, + NonCanonicalArgs = 6, + InvalidCommitmentData = 7, + InvalidCustodyLock = 8, + MissingCustodyInput = 9, +} + +impl From for Error { + fn from(_: SysError) -> Self { + Self::Syscall + } +} + +pub fn program_entry() -> i8 { + match validate() { + Ok(()) => 0, + Err(error) => error as i8, + } +} + +fn validate() -> Result<(), Error> { + let mut script = [0u8; SCRIPT_BYTES_WITH_CUSTODY_HASH]; + match syscalls::load_script(&mut script, 0) { + Ok(SCRIPT_BYTES_WITH_CUSTODY_HASH) => {} + Ok(_) | Err(SysError::LengthNotEnough(_)) => return Err(Error::NonCanonicalArgs), + Err(error) => return Err(error.into()), + } + let mut custody_lock_hash = [0u8; CUSTODY_LOCK_HASH_BYTES]; + custody_lock_hash.copy_from_slice(&script[SCRIPT_ARGS_OFFSET..]); + + validate_group(Source::GroupInput, &custody_lock_hash)?; + validate_group(Source::GroupOutput, &custody_lock_hash)?; + require_custody_input(&custody_lock_hash)?; + Ok(()) +} + +fn validate_group(source: Source, custody_lock_hash: &[u8; CUSTODY_LOCK_HASH_BYTES]) -> Result<(), Error> { + for index in 0.. { + let mut data = [0u8; COMMITMENT_DATA_BYTES]; + match syscalls::load_cell_data(&mut data, 0, index, source) { + Ok(COMMITMENT_DATA_BYTES) => { + validate_commitment_data(&data)?; + if &load_cell_lock_hash(index, source)? != custody_lock_hash { + return Err(Error::InvalidCustodyLock); + } + } + Ok(_) | Err(SysError::LengthNotEnough(_)) => return Err(Error::InvalidCommitmentData), + Err(SysError::IndexOutOfBound) => return Ok(()), + Err(error) => return Err(error.into()), + } + } + unreachable!() +} + +fn require_custody_input(custody_lock_hash: &[u8; CUSTODY_LOCK_HASH_BYTES]) -> Result<(), Error> { + for index in 0.. { + match load_cell_lock_hash(index, Source::Input) { + Ok(lock_hash) if &lock_hash == custody_lock_hash => return Ok(()), + Ok(_) => {} + Err(SysError::IndexOutOfBound) => return Err(Error::MissingCustodyInput), + Err(error) => return Err(error.into()), + } + } + unreachable!() +} + +fn load_cell_lock_hash(index: usize, source: Source) -> Result<[u8; CUSTODY_LOCK_HASH_BYTES], SysError> { + let mut lock_hash = [0u8; CUSTODY_LOCK_HASH_BYTES]; + match syscalls::load_cell_by_field(&mut lock_hash, 0, index, source, CellField::LockHash) { + Ok(CUSTODY_LOCK_HASH_BYTES) => Ok(lock_hash), + Ok(_) | Err(SysError::LengthNotEnough(_)) => Err(SysError::Encoding), + Err(error) => Err(error), + } +} + +fn validate_commitment_data(data: &[u8]) -> Result<(), Error> { + if data.len() == COMMITMENT_DATA_BYTES && data.starts_with(COMMITMENT_MAGIC) { + Ok(()) + } else { + Err(Error::InvalidCommitmentData) + } +} diff --git a/contracts/registry-type-script/tests/ckb_vm.rs b/contracts/registry-type-script/tests/ckb_vm.rs new file mode 100644 index 00000000..03226587 --- /dev/null +++ b/contracts/registry-type-script/tests/ckb_vm.rs @@ -0,0 +1,123 @@ +use std::path::PathBuf; + +use ckb_testtool::{ + builtin::ALWAYS_SUCCESS, + ckb_types::{ + bytes::Bytes, + core::TransactionBuilder, + packed::{CellInput, CellOutput, Script}, + prelude::*, + }, + context::Context, +}; + +const MAX_CYCLES: u64 = 10_000_000; +const CELL_CAPACITY: u64 = 20_000_000_000; + +struct Scripts { + context: Context, + lock: Script, + other_lock: Script, + registry_type: Script, +} + +fn contract_binary() -> Bytes { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("artifacts/v0.24.0/cellscript-registry-type-script"); + std::fs::read(&path).unwrap_or_else(|error| panic!("read tracked canonical artifact {}: {error}", path.display())).into() +} + +fn scripts(args: Option) -> Scripts { + let mut context = Context::default(); + let lock_out_point = context.deploy_cell(ALWAYS_SUCCESS.clone()); + let lock = context.build_script(&lock_out_point, Bytes::new()).expect("always-success lock"); + let other_lock = context.build_script(&lock_out_point, Bytes::from_static(&[1])).expect("alternate lock"); + let type_out_point = context.deploy_cell(contract_binary()); + let args = args.unwrap_or_else(|| Bytes::copy_from_slice(lock.calc_script_hash().as_slice())); + let registry_type = context.build_script(&type_out_point, args).expect("Registry Type Script"); + Scripts { context, lock, other_lock, registry_type } +} + +fn commitment(seed: u8) -> Bytes { + let mut data = b"CSREGv1".to_vec(); + data.extend([seed; 32]); + data.into() +} + +fn verify_creation(output_data: Bytes, args: Option, custody_input: bool, custody_output: bool) -> Result { + let Scripts { mut context, lock, other_lock, registry_type } = scripts(args); + let input_lock = if custody_input { lock.clone() } else { other_lock.clone() }; + let output_lock = if custody_output { lock } else { other_lock }; + let input_out_point = + context.create_cell(CellOutput::new_builder().capacity(CELL_CAPACITY).lock(input_lock).build(), Bytes::new()); + let input = CellInput::new_builder().previous_output(input_out_point).build(); + let output = CellOutput::new_builder().capacity(CELL_CAPACITY).lock(output_lock).type_(Some(registry_type).pack()).build(); + let transaction = TransactionBuilder::default().input(input).output(output).output_data(output_data.pack()).build(); + let transaction = context.complete_tx(transaction); + context.verify_tx(&transaction, MAX_CYCLES).map_err(|error| error.to_string()) +} + +#[test] +fn accepts_exact_commitment_data() { + verify_creation(commitment(0x11), None, true, true).expect("valid commitment"); +} + +#[test] +fn rejects_wrong_magic_short_hash_and_trailing_bytes() { + let mut wrong_magic = commitment(0x22).to_vec(); + wrong_magic[0] ^= 0xff; + assert!(verify_creation(wrong_magic.into(), None, true, true).is_err()); + + assert!(verify_creation(Bytes::from_static(b"CSREGv1"), None, true, true).is_err()); + + let mut trailing = commitment(0x33).to_vec(); + trailing.push(0); + assert!(verify_creation(trailing.into(), None, true, true).is_err()); +} + +#[test] +fn rejects_non_canonical_type_args() { + assert!(verify_creation(commitment(0x44), Some(Bytes::new()), true, true).is_err()); + assert!(verify_creation(commitment(0x44), Some(Bytes::from(vec![1; 31])), true, true).is_err()); + assert!(verify_creation(commitment(0x44), Some(Bytes::from(vec![1; 33])), true, true).is_err()); +} + +#[test] +fn requires_custody_authorization_and_custody_locked_outputs() { + assert!(verify_creation(commitment(0x45), None, false, true).is_err()); + assert!(verify_creation(commitment(0x46), None, true, false).is_err()); +} + +#[test] +fn accepts_replacement_and_destruction_but_rejects_malformed_input() { + for replacement in [Some(commitment(0x66)), None] { + let Scripts { mut context, lock, registry_type, .. } = scripts(None); + let input_out_point = context.create_cell( + CellOutput::new_builder().capacity(CELL_CAPACITY).lock(lock.clone()).type_(Some(registry_type.clone()).pack()).build(), + commitment(0x55), + ); + let input = CellInput::new_builder().previous_output(input_out_point).build(); + let mut builder = TransactionBuilder::default().input(input); + if let Some(data) = replacement { + builder = builder + .output( + CellOutput::new_builder() + .capacity(CELL_CAPACITY) + .lock(lock.clone()) + .type_(Some(registry_type.clone()).pack()) + .build(), + ) + .output_data(data.pack()); + } + let transaction = context.complete_tx(builder.build()); + context.verify_tx(&transaction, MAX_CYCLES).expect("valid lifecycle transition"); + } + + let Scripts { mut context, lock, registry_type, .. } = scripts(None); + let input_out_point = context.create_cell( + CellOutput::new_builder().capacity(CELL_CAPACITY).lock(lock).type_(Some(registry_type).pack()).build(), + Bytes::from_static(b"legacy-malformed"), + ); + let transaction = TransactionBuilder::default().input(CellInput::new_builder().previous_output(input_out_point).build()).build(); + let transaction = context.complete_tx(transaction); + assert!(context.verify_tx(&transaction, MAX_CYCLES).is_err()); +} diff --git a/crates/cellscript-artifact-checker/Cargo.toml b/crates/cellscript-artifact-checker/Cargo.toml new file mode 100644 index 00000000..16696998 --- /dev/null +++ b/crates/cellscript-artifact-checker/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "cellscript-artifact-checker" +version = "0.24.0" +edition = "2024" +rust-version = "1.97.1" +description = "Bounded independent verifier for CellScript lowering records and CKB RISC-V artifacts" +license = "MIT" +repository = "https://github.com/CellScript-Labs/CellScript" + +[[bin]] +name = "cellscript-artifact-checker" +path = "src/main.rs" + +[dependencies] +blake2b_simd = "1.0" +clap = { version = "=4.5.49", features = ["derive"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +[dev-dependencies] +tempfile = "3.10" diff --git a/crates/cellscript-artifact-checker/src/checker.rs b/crates/cellscript-artifact-checker/src/checker.rs new file mode 100644 index 00000000..e0ef8f5b --- /dev/null +++ b/crates/cellscript-artifact-checker/src/checker.rs @@ -0,0 +1,1099 @@ +use crate::elf::{parse_elf, DecodedControlFlowKind, ElfErrorKind, ElfParseError, ElfSummary, ParsedElf}; +use crate::schema::*; +use crate::{ckb_blake2b256, hex_encode}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum CheckerRejectionCode { + V2400BudgetExceeded, + V2401MalformedJson, + V2402NonCanonicalJson, + V2403UnsupportedSchema, + V2404CanonicalOrder, + V2405ReferentialIntegrity, + V2406CfgInvalid, + V2407AbiOrStackInvalid, + V2408ProofCoverageInvalid, + V2409ArtifactIdentityMismatch, + V2410MetadataBindingMismatch, + V2411ElfFormatInvalid, + V2412ElfSectionInvalid, + V2413InstructionInvalid, + V2414ControlFlowInvalid, + V2415BlockDigestMismatch, + V2416SourceMapInvalid, + V2417SyscallContractInvalid, + V2418RecursionPolicyInvalid, +} + +impl CheckerRejectionCode { + pub const fn as_str(self) -> &'static str { + match self { + Self::V2400BudgetExceeded => "V2400", + Self::V2401MalformedJson => "V2401", + Self::V2402NonCanonicalJson => "V2402", + Self::V2403UnsupportedSchema => "V2403", + Self::V2404CanonicalOrder => "V2404", + Self::V2405ReferentialIntegrity => "V2405", + Self::V2406CfgInvalid => "V2406", + Self::V2407AbiOrStackInvalid => "V2407", + Self::V2408ProofCoverageInvalid => "V2408", + Self::V2409ArtifactIdentityMismatch => "V2409", + Self::V2410MetadataBindingMismatch => "V2410", + Self::V2411ElfFormatInvalid => "V2411", + Self::V2412ElfSectionInvalid => "V2412", + Self::V2413InstructionInvalid => "V2413", + Self::V2414ControlFlowInvalid => "V2414", + Self::V2415BlockDigestMismatch => "V2415", + Self::V2416SourceMapInvalid => "V2416", + Self::V2417SyscallContractInvalid => "V2417", + Self::V2418RecursionPolicyInvalid => "V2418", + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct CheckerError { + pub code: CheckerRejectionCode, + pub message: String, +} + +impl CheckerError { + fn new(code: CheckerRejectionCode, message: impl Into) -> Self { + Self { code, message: message.into() } + } + + fn bounded(mut self, max_bytes: u32) -> Self { + let max_bytes = usize::try_from(max_bytes).unwrap_or(usize::MAX); + if self.message.len() > max_bytes { + let mut end = max_bytes.min(self.message.len()); + while end > 0 && !self.message.is_char_boundary(end) { + end -= 1; + } + self.message.truncate(end); + } + self + } +} + +impl std::fmt::Display for CheckerError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "{}: {}", self.code.as_str(), self.message) + } +} + +impl std::error::Error for CheckerError {} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum EvidenceState { + Verified, + NotProvided, + NotExecuted, + NotClaimed, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct CheckerReport { + pub schema: String, + pub checker_name: String, + pub checker_version: String, + pub checker_policy_schema: String, + pub artifact_hash: String, + pub lowering_record_hash: String, + pub source_map_hash: String, + pub binding_verification: EvidenceState, + pub structural_verification: EvidenceState, + pub lowering_record_verification: EvidenceState, + pub ckb_vm_evidence: EvidenceState, + pub chain_evidence: EvidenceState, + pub semantic_equivalence_claimed: bool, + pub elf: ElfSummary, +} + +pub fn canonical_bytes(value: &T) -> Result, CheckerError> { + serde_json::to_vec(value).map_err(|error| { + CheckerError::new(CheckerRejectionCode::V2401MalformedJson, format!("failed to serialize canonical checker value: {error}")) + }) +} + +pub fn canonical_hash(domain: &str, value: &T) -> Result { + let bytes = canonical_bytes(value)?; + let mut material = Vec::with_capacity(domain.len() + 1 + bytes.len()); + material.extend_from_slice(domain.as_bytes()); + material.push(0); + material.extend_from_slice(&bytes); + Ok(hex_encode(&ckb_blake2b256(&material))) +} + +pub fn parse_lowering_record(bytes: &[u8], budgets: &CheckerBudgets) -> Result { + ensure_byte_budget("lowering record", bytes.len(), budgets.record_bytes)?; + let record: VerifiedLoweringRecord = serde_json::from_slice(bytes).map_err(|error| { + CheckerError::new(CheckerRejectionCode::V2401MalformedJson, format!("failed to parse lowering record: {error}")) + })?; + ensure_canonical("lowering record", bytes, &record)?; + Ok(record) +} + +pub fn parse_source_map(bytes: &[u8], budgets: &CheckerBudgets) -> Result { + ensure_byte_budget("source map", bytes.len(), budgets.source_map_bytes)?; + let source_map: SourceArtifactMap = serde_json::from_slice(bytes).map_err(|error| { + CheckerError::new(CheckerRejectionCode::V2401MalformedJson, format!("failed to parse source map: {error}")) + })?; + ensure_canonical("source map", bytes, &source_map)?; + Ok(source_map) +} + +pub fn check_bundle( + artifact: &[u8], + metadata_bytes: &[u8], + lowering_record_bytes: &[u8], + source_map_bytes: &[u8], + budgets: &CheckerBudgets, +) -> Result { + let result = (|| { + if budgets.schema != CHECKER_POLICY_SCHEMA { + return Err(CheckerError::new( + CheckerRejectionCode::V2403UnsupportedSchema, + format!("unsupported checker policy schema '{}'", budgets.schema), + )); + } + ensure_byte_budget("artifact", artifact.len(), budgets.artifact_bytes)?; + let metadata: Value = serde_json::from_slice(metadata_bytes).map_err(|error| { + CheckerError::new(CheckerRejectionCode::V2401MalformedJson, format!("failed to parse compile metadata: {error}")) + })?; + let record = parse_lowering_record(lowering_record_bytes, budgets)?; + let source_map = parse_source_map(source_map_bytes, budgets)?; + check_bundle_values(artifact, &metadata, &record, &source_map, budgets) + })(); + result.map_err(|error| error.bounded(budgets.diagnostic_bytes)) +} + +pub fn check_bundle_values( + artifact: &[u8], + metadata: &Value, + record: &VerifiedLoweringRecord, + source_map: &SourceArtifactMap, + budgets: &CheckerBudgets, +) -> Result { + validate_record_schema(record)?; + validate_declared_limits(&record.limits, budgets)?; + validate_counts(record, source_map, budgets)?; + validate_metadata_binding(artifact, metadata, record, source_map)?; + validate_record_graph(record, budgets)?; + + let elf = parse_elf(artifact, budgets.instructions).map_err(map_elf_error)?; + validate_elf_binding(artifact, record, &elf)?; + validate_block_digests(artifact, record, &elf)?; + validate_control_flow(record, &elf)?; + validate_machine_terminators(record, &elf)?; + validate_stack_discipline(record, &elf)?; + validate_syscalls(record, &elf)?; + validate_source_map(source_map, record, artifact, &elf)?; + + Ok(CheckerReport { + schema: CHECKER_REPORT_SCHEMA.to_string(), + checker_name: "cellscript-artifact-checker".to_string(), + checker_version: CHECKER_VERSION.to_string(), + checker_policy_schema: budgets.schema.clone(), + artifact_hash: record.artifact_hash.clone(), + lowering_record_hash: canonical_hash(LOWERING_RECORD_SCHEMA, record)?, + source_map_hash: canonical_hash(SOURCE_MAP_SCHEMA, source_map)?, + binding_verification: EvidenceState::Verified, + structural_verification: EvidenceState::Verified, + lowering_record_verification: EvidenceState::Verified, + ckb_vm_evidence: EvidenceState::NotExecuted, + chain_evidence: EvidenceState::NotProvided, + semantic_equivalence_claimed: false, + elf: elf.summary(), + }) +} + +fn validate_record_schema(record: &VerifiedLoweringRecord) -> Result<(), CheckerError> { + if record.schema != LOWERING_RECORD_SCHEMA || record.version != LOWERING_RECORD_VERSION { + return Err(CheckerError::new( + CheckerRejectionCode::V2403UnsupportedSchema, + format!("unsupported lowering record '{}'/{}", record.schema, record.version), + )); + } + if record.claim.lowering_record != "binding-verified" + || record.claim.machine_code != "structurally-verified" + || record.claim.semantic_equivalence + { + return Err(CheckerError::new( + CheckerRejectionCode::V2403UnsupportedSchema, + "lowering record overclaims or mislabels the v1 verification boundary", + )); + } + if record.artifact_format != "RISC-V ELF" || record.target_profile != "ckb" { + return Err(CheckerError::new( + CheckerRejectionCode::V2403UnsupportedSchema, + "v1 checker accepts only the CKB RISC-V ELF profile", + )); + } + if record.compatibility_profile.target_profile != record.target_profile + || record.compatibility_profile.edition != record.edition + || record.compatibility_profile.raw_entry_witness_payload_compatible + { + return Err(CheckerError::new( + CheckerRejectionCode::V2410MetadataBindingMismatch, + "record compatibility profile disagrees with edition/target or accepts raw entry witnesses", + )); + } + let profile_hash = canonical_hash("cellscript-compatibility-profile-identity-v1", &record.compatibility_profile)?; + if profile_hash != record.compatibility_profile_hash { + return Err(CheckerError::new( + CheckerRejectionCode::V2410MetadataBindingMismatch, + "record compatibility profile hash does not match its canonical identity", + )); + } + Ok(()) +} + +fn validate_declared_limits(declared: &DeclaredLimits, budgets: &CheckerBudgets) -> Result<(), CheckerError> { + let checks = [ + ("artifact_bytes", declared.artifact_bytes, budgets.artifact_bytes), + ("record_bytes", declared.record_bytes, budgets.record_bytes), + ("source_map_bytes", declared.source_map_bytes, budgets.source_map_bytes), + ("entries", u64::from(declared.entries), u64::from(budgets.entries)), + ("blocks", u64::from(declared.blocks), u64::from(budgets.blocks)), + ("edges", u64::from(declared.edges), u64::from(budgets.edges)), + ("instructions", declared.instructions, budgets.instructions), + ("call_depth", u64::from(declared.call_depth), u64::from(budgets.call_depth)), + ("stack_frame_bytes", u64::from(declared.stack_frame_bytes), u64::from(budgets.stack_frame_bytes)), + ("proof_records", u64::from(declared.proof_records), u64::from(budgets.proof_records)), + ("source_map_intervals", u64::from(declared.source_map_intervals), u64::from(budgets.source_map_intervals)), + ("diagnostic_bytes", u64::from(declared.diagnostic_bytes), u64::from(budgets.diagnostic_bytes)), + ]; + for (name, value, limit) in checks { + if value > limit { + return Err(CheckerError::new( + CheckerRejectionCode::V2400BudgetExceeded, + format!("record-declared {name} limit {value} exceeds checker policy {limit}"), + )); + } + } + Ok(()) +} + +fn validate_counts( + record: &VerifiedLoweringRecord, + source_map: &SourceArtifactMap, + budgets: &CheckerBudgets, +) -> Result<(), CheckerError> { + ensure_count("entries", record.entries.len(), budgets.entries)?; + ensure_count("blocks", record.blocks.len(), budgets.blocks)?; + ensure_count("edges", record.edges.len(), budgets.edges)?; + ensure_count("proof records", record.proof_records.len(), budgets.proof_records)?; + ensure_count("source-map intervals", source_map.intervals.len(), budgets.source_map_intervals)?; + if artifact_declared_too_large(record.artifact_size_bytes, budgets.artifact_bytes) { + return Err(CheckerError::new( + CheckerRejectionCode::V2400BudgetExceeded, + "record-declared artifact size exceeds checker policy", + )); + } + Ok(()) +} + +fn validate_metadata_binding( + artifact: &[u8], + metadata: &Value, + record: &VerifiedLoweringRecord, + source_map: &SourceArtifactMap, +) -> Result<(), CheckerError> { + let artifact_hash = hex_encode(&ckb_blake2b256(artifact)); + if artifact_hash != record.artifact_hash || artifact.len() as u64 != record.artifact_size_bytes { + return Err(CheckerError::new( + CheckerRejectionCode::V2409ArtifactIdentityMismatch, + "artifact bytes do not match the lowering record identity", + )); + } + let record_hash = canonical_hash(LOWERING_RECORD_SCHEMA, record)?; + let source_map_hash = canonical_hash(SOURCE_MAP_SCHEMA, source_map)?; + let comparisons = [ + ("compiler_version", json_string(metadata, &["compiler_version"]), record.compiler_version.as_str()), + ("module", json_string(metadata, &["module"]), record.module.as_str()), + ("edition", json_string(metadata, &["edition"]), record.edition.as_str()), + ("target_profile.name", json_string(metadata, &["target_profile", "name"]), record.target_profile.as_str()), + ("artifact_format", json_string(metadata, &["artifact_format"]), record.artifact_format.as_str()), + ("artifact_hash", json_string(metadata, &["artifact_hash"]), record.artifact_hash.as_str()), + ("source_content_hash", json_string(metadata, &["source_content_hash"]), record.source_content_hash.as_str()), + ( + "verified_artifact.lowering_record_hash", + json_string(metadata, &["verified_artifact", "lowering_record_hash"]), + record_hash.as_str(), + ), + ( + "verified_artifact.source_map_hash", + json_string(metadata, &["verified_artifact", "source_map_hash"]), + source_map_hash.as_str(), + ), + ]; + for (field, actual, expected) in comparisons { + if actual != Some(expected) { + return Err(CheckerError::new( + CheckerRejectionCode::V2410MetadataBindingMismatch, + format!("compile metadata field '{field}' does not match lowering boundary"), + )); + } + } + if json_u64(metadata, &["artifact_size_bytes"]) != Some(record.artifact_size_bytes) { + return Err(CheckerError::new( + CheckerRejectionCode::V2410MetadataBindingMismatch, + "compile metadata artifact_size_bytes does not match lowering record", + )); + } + let profile_value = metadata.get("compatibility_profile").cloned().ok_or_else(|| { + CheckerError::new(CheckerRejectionCode::V2410MetadataBindingMismatch, "compile metadata has no compatibility_profile") + })?; + let profile: CompatibilityProfileIdentity = serde_json::from_value(profile_value).map_err(|error| { + CheckerError::new( + CheckerRejectionCode::V2410MetadataBindingMismatch, + format!("compile metadata compatibility_profile shape is invalid: {error}"), + ) + })?; + if profile != record.compatibility_profile { + return Err(CheckerError::new( + CheckerRejectionCode::V2410MetadataBindingMismatch, + "compile metadata compatibility profile differs from lowering record", + )); + } + if source_map.lowering_record_hash != record_hash + || source_map.artifact_hash != record.artifact_hash + || source_map.source_set_hash != record.source_set_hash + { + return Err(CheckerError::new( + CheckerRejectionCode::V2416SourceMapInvalid, + "source map identity does not bind to record, artifact, and source set", + )); + } + Ok(()) +} + +fn validate_record_graph(record: &VerifiedLoweringRecord, budgets: &CheckerBudgets) -> Result<(), CheckerError> { + if record.entries.is_empty() || record.blocks.is_empty() || record.text_range.is_empty() { + return Err(CheckerError::new( + CheckerRejectionCode::V2405ReferentialIntegrity, + "lowering record requires at least one entry, one block, and a non-empty text range", + )); + } + ensure_sorted_unique(&record.entries, |entry| entry.id.as_str(), "entry")?; + ensure_sorted_unique(&record.blocks, |block| block.id.as_str(), "block")?; + ensure_sorted_unique(&record.proof_records, |proof| proof.id.as_str(), "proof")?; + if !record.edges.windows(2).all(|pair| (&pair[0].from, &pair[0].kind, &pair[0].to) < (&pair[1].from, &pair[1].kind, &pair[1].to)) { + return Err(CheckerError::new(CheckerRejectionCode::V2404CanonicalOrder, "lowering edges are not strictly sorted and unique")); + } + + let entries = record.entries.iter().map(|entry| (entry.id.as_str(), entry)).collect::>(); + let blocks = record.blocks.iter().map(|block| (block.id.as_str(), block)).collect::>(); + let proofs = record.proof_records.iter().map(|proof| (proof.id.as_str(), proof)).collect::>(); + for entry in &record.entries { + let Some(block) = blocks.get(entry.entry_block.as_str()) else { + return Err(CheckerError::new( + CheckerRejectionCode::V2405ReferentialIntegrity, + format!("entry '{}' references missing block '{}'", entry.id, entry.entry_block), + )); + }; + if block.owner_entry != entry.id { + return Err(CheckerError::new( + CheckerRejectionCode::V2405ReferentialIntegrity, + format!("entry '{}' begins in block owned by '{}'", entry.id, block.owner_entry), + )); + } + validate_entry_abi(entry, budgets)?; + if !strictly_sorted(&entry.capabilities) + || entry.capabilities.iter().any(String::is_empty) + || !strictly_sorted(&entry.proof_ids) + { + return Err(CheckerError::new( + CheckerRejectionCode::V2404CanonicalOrder, + format!("entry '{}' has non-canonical capabilities or ProofPlan links", entry.id), + )); + } + for proof_id in &entry.proof_ids { + let Some(proof) = proofs.get(proof_id.as_str()) else { + return Err(CheckerError::new( + CheckerRejectionCode::V2408ProofCoverageInvalid, + format!("entry '{}' references missing proof '{}'", entry.id, proof_id), + )); + }; + if proof.entry_id != entry.id { + return Err(CheckerError::new( + CheckerRejectionCode::V2408ProofCoverageInvalid, + format!("proof '{}' is not owned by entry '{}'", proof_id, entry.id), + )); + } + } + } + for proof in &record.proof_records { + if !entries.contains_key(proof.entry_id.as_str()) || proof.obligation.is_empty() || proof.evidence_tier.is_empty() { + return Err(CheckerError::new( + CheckerRejectionCode::V2408ProofCoverageInvalid, + format!("proof '{}' has an invalid owner or empty enforcement fields", proof.id), + )); + } + } + + if !record + .runtime_error_exits + .windows(2) + .all(|pair| (&pair[0].block_id, pair[0].code, pair[0].address) < (&pair[1].block_id, pair[1].code, pair[1].address)) + { + return Err(CheckerError::new( + CheckerRejectionCode::V2404CanonicalOrder, + "runtime-error exits are not strictly sorted and unique", + )); + } + for exit in &record.runtime_error_exits { + if exit.code <= 0 + || exit.code > 255 + || exit.name.is_empty() + || blocks.get(exit.block_id.as_str()).is_none_or(|block| !block.range.contains(exit.address)) + { + return Err(CheckerError::new( + CheckerRejectionCode::V2406CfgInvalid, + format!("runtime-error exit {} ({}) is outside its declared block", exit.code, exit.name), + )); + } + } + + let mut expected_start = record.text_range.start; + for block in &record.blocks { + if !entries.contains_key(block.owner_entry.as_str()) { + return Err(CheckerError::new( + CheckerRejectionCode::V2405ReferentialIntegrity, + format!("block '{}' has missing owner '{}'", block.id, block.owner_entry), + )); + } + if block.range.start != expected_start || block.range.is_empty() || block.range.start % 4 != 0 || block.range.end % 4 != 0 { + return Err(CheckerError::new( + CheckerRejectionCode::V2406CfgInvalid, + format!("block '{}' does not form aligned contiguous text coverage", block.id), + )); + } + expected_start = block.range.end; + validate_block_abi(block, entries[block.owner_entry.as_str()], &proofs, budgets)?; + } + if expected_start != record.text_range.end { + return Err(CheckerError::new( + CheckerRejectionCode::V2406CfgInvalid, + "lowering blocks do not cover the declared text range exactly", + )); + } + + let mut outgoing = BTreeMap::<&str, Vec<&LoweringEdge>>::new(); + for edge in &record.edges { + if !blocks.contains_key(edge.from.as_str()) || !blocks.contains_key(edge.to.as_str()) { + return Err(CheckerError::new( + CheckerRejectionCode::V2405ReferentialIntegrity, + format!("edge '{} -> {}' references a missing block", edge.from, edge.to), + )); + } + outgoing.entry(edge.from.as_str()).or_default().push(edge); + } + for block in &record.blocks { + validate_terminator_edges(block, outgoing.get(block.id.as_str()).map(Vec::as_slice).unwrap_or(&[]))?; + } + validate_reachability(record, &outgoing)?; + validate_call_graph(record, &entries, &blocks, budgets.call_depth)?; + Ok(()) +} + +fn validate_entry_abi(entry: &LoweringEntry, budgets: &CheckerBudgets) -> Result<(), CheckerError> { + if entry.name.is_empty() + || entry.return_type.is_empty() + || entry.effect.is_empty() + || entry.frame_size_bytes > budgets.stack_frame_bytes + || entry.outgoing_argument_bytes > entry.frame_size_bytes + { + return Err(CheckerError::new( + CheckerRejectionCode::V2407AbiOrStackInvalid, + format!("entry '{}' has an invalid name/frame/outgoing-argument area", entry.id), + )); + } + let mut expected_index = 0u32; + for param in &entry.params { + if param.index != expected_index + || param.name.is_empty() + || param.ty.is_empty() + || param.width_bytes == 0 + || !valid_alignment(param.alignment_bytes) + { + return Err(CheckerError::new( + CheckerRejectionCode::V2407AbiOrStackInvalid, + format!("entry '{}' has an invalid typed parameter at index {}", entry.id, param.index), + )); + } + expected_index = expected_index.saturating_add(1); + } + Ok(()) +} + +fn validate_block_abi( + block: &LoweringBlock, + entry: &LoweringEntry, + proofs: &BTreeMap<&str, &ProofRecord>, + budgets: &CheckerBudgets, +) -> Result<(), CheckerError> { + if block.frame_size_bytes != entry.frame_size_bytes + || block.outgoing_argument_bytes != entry.outgoing_argument_bytes + || block.effect != entry.effect + || block.capabilities != entry.capabilities + || block.frame_size_bytes > budgets.stack_frame_bytes + || block.outgoing_argument_bytes > block.frame_size_bytes + { + return Err(CheckerError::new( + CheckerRejectionCode::V2407AbiOrStackInvalid, + format!("block '{}' frame contract disagrees with owner entry", block.id), + )); + } + let valid_registers = [ + "zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "s1", "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "s2", "s3", + "s4", "s5", "s6", "s7", "s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6", + ]; + if !strictly_sorted(&block.scratch_register_avoid) + || block.scratch_register_avoid.iter().any(|register| !valid_registers.contains(®ister.as_str())) + { + return Err(CheckerError::new( + CheckerRejectionCode::V2407AbiOrStackInvalid, + format!("block '{}' has invalid scratch-register declarations", block.id), + )); + } + let mut last_end = block.outgoing_argument_bytes; + for slot in &block.stack_slots { + if slot.name.is_empty() + || slot.width_bytes == 0 + || !valid_alignment(slot.alignment_bytes) + || slot.offset % slot.alignment_bytes != 0 + || slot.offset < last_end + || slot.offset.saturating_add(slot.width_bytes) > block.frame_size_bytes + { + return Err(CheckerError::new( + CheckerRejectionCode::V2407AbiOrStackInvalid, + format!("block '{}' has overlapping, misaligned, or out-of-frame stack slot '{}'", block.id, slot.name), + )); + } + last_end = slot.offset.saturating_add(slot.width_bytes); + } + if !strictly_sorted(&block.proof_ids) + || block.proof_ids.iter().any(|proof_id| proofs.get(proof_id.as_str()).is_none_or(|proof| proof.entry_id != block.owner_entry)) + { + return Err(CheckerError::new( + CheckerRejectionCode::V2408ProofCoverageInvalid, + format!("block '{}' has invalid ProofPlan links", block.id), + )); + } + Ok(()) +} + +fn validate_reachability(record: &VerifiedLoweringRecord, outgoing: &BTreeMap<&str, Vec<&LoweringEdge>>) -> Result<(), CheckerError> { + let mut reachable = BTreeSet::new(); + let mut pending = record.entries.iter().map(|entry| entry.entry_block.as_str()).collect::>(); + while let Some(block_id) = pending.pop() { + if !reachable.insert(block_id) { + continue; + } + if let Some(edges) = outgoing.get(block_id) { + pending.extend(edges.iter().map(|edge| edge.to.as_str())); + } + } + if let Some(block) = record.blocks.iter().find(|block| block.reachable != reachable.contains(block.id.as_str())) { + return Err(CheckerError::new( + CheckerRejectionCode::V2406CfgInvalid, + format!( + "block '{}' declared reachable={} but CFG reachability is {}", + block.id, + block.reachable, + reachable.contains(block.id.as_str()) + ), + )); + } + Ok(()) +} + +fn validate_terminator_edges(block: &LoweringBlock, edges: &[&LoweringEdge]) -> Result<(), CheckerError> { + let non_call = edges.iter().filter(|edge| edge.kind != EdgeKind::Call).map(|edge| edge.kind).collect::>(); + let valid = match block.terminator { + MachineTerminator::Fallthrough => non_call == [EdgeKind::Fallthrough], + MachineTerminator::Jump => non_call == [EdgeKind::Jump], + MachineTerminator::ConditionalBranch => { + non_call == [EdgeKind::ConditionalTaken, EdgeKind::ConditionalFallthrough] + || non_call == [EdgeKind::ConditionalFallthrough, EdgeKind::ConditionalTaken] + } + MachineTerminator::Return => non_call.is_empty(), + }; + if !valid { + return Err(CheckerError::new( + CheckerRejectionCode::V2406CfgInvalid, + format!("block '{}' terminator does not match its CFG edges", block.id), + )); + } + Ok(()) +} + +fn validate_call_graph( + record: &VerifiedLoweringRecord, + entries: &BTreeMap<&str, &LoweringEntry>, + blocks: &BTreeMap<&str, &LoweringBlock>, + max_depth: u32, +) -> Result<(), CheckerError> { + let mut graph = BTreeMap::<&str, BTreeSet<&str>>::new(); + for edge in record.edges.iter().filter(|edge| edge.kind == EdgeKind::Call) { + let from = blocks[edge.from.as_str()].owner_entry.as_str(); + let to = blocks[edge.to.as_str()].owner_entry.as_str(); + if from != to { + graph.entry(from).or_default().insert(to); + } + } + for root in entries.keys() { + let mut active = BTreeSet::new(); + validate_call_depth(root, &graph, &mut active, 1, max_depth)?; + } + Ok(()) +} + +fn validate_call_depth<'a>( + current: &'a str, + graph: &BTreeMap<&'a str, BTreeSet<&'a str>>, + active: &mut BTreeSet<&'a str>, + depth: u32, + max_depth: u32, +) -> Result<(), CheckerError> { + if depth > max_depth { + return Err(CheckerError::new( + CheckerRejectionCode::V2400BudgetExceeded, + format!("static call depth exceeds checker budget {max_depth}"), + )); + } + if !active.insert(current) { + return Err(CheckerError::new( + CheckerRejectionCode::V2418RecursionPolicyInvalid, + format!("recursive call cycle reaches entry '{current}'"), + )); + } + if let Some(children) = graph.get(current) { + for child in children { + validate_call_depth(child, graph, active, depth.saturating_add(1), max_depth)?; + } + } + active.remove(current); + Ok(()) +} + +fn validate_elf_binding(artifact: &[u8], record: &VerifiedLoweringRecord, elf: &ParsedElf) -> Result<(), CheckerError> { + if !elf.text.range().contains_range(record.text_range) { + return Err(CheckerError::new(CheckerRejectionCode::V2412ElfSectionInvalid, "record text range is outside ELF .text")); + } + if record.artifact_size_bytes != artifact.len() as u64 || record.text_range.start < elf.entry { + return Err(CheckerError::new( + CheckerRejectionCode::V2409ArtifactIdentityMismatch, + "record artifact size/text identity disagrees with ELF", + )); + } + Ok(()) +} + +fn validate_block_digests(artifact: &[u8], record: &VerifiedLoweringRecord, elf: &ParsedElf) -> Result<(), CheckerError> { + for block in &record.blocks { + let bytes = elf.bytes_for_range(artifact, block.range).map_err(map_elf_error)?; + let digest = domain_hash_bytes("cellscript-machine-block-v1", bytes); + if digest != block.byte_digest { + return Err(CheckerError::new( + CheckerRejectionCode::V2415BlockDigestMismatch, + format!("machine bytes for block '{}' do not match its digest", block.id), + )); + } + } + Ok(()) +} + +fn validate_control_flow(record: &VerifiedLoweringRecord, elf: &ParsedElf) -> Result<(), CheckerError> { + let blocks = record.blocks.iter().map(|block| (block.id.as_str(), block)).collect::>(); + let find_block = |address| record.blocks.iter().find(|block| block.range.contains(address)); + for flow in elf.control_flow.iter().filter(|flow| record.text_range.contains(flow.address)) { + let Some(from) = find_block(flow.address) else { + return Err(CheckerError::new( + CheckerRejectionCode::V2414ControlFlowInvalid, + format!("instruction at {:#x} is not covered by a lowering block", flow.address), + )); + }; + let Some(to) = find_block(flow.target) else { + return Err(CheckerError::new( + CheckerRejectionCode::V2414ControlFlowInvalid, + format!("target {:#x} is outside lowering blocks", flow.target), + )); + }; + let allowed_kinds: &[EdgeKind] = match flow.kind { + DecodedControlFlowKind::ConditionalBranch => { + &[EdgeKind::ConditionalTaken, EdgeKind::ConditionalFallthrough, EdgeKind::Fallthrough] + } + DecodedControlFlowKind::DirectJump => &[EdgeKind::Jump, EdgeKind::Call, EdgeKind::ConditionalTaken], + }; + let edge_exists = from.id == to.id + || record.edges.iter().any(|edge| edge.from == from.id && edge.to == to.id && allowed_kinds.contains(&edge.kind)); + if !edge_exists || !blocks.contains_key(to.id.as_str()) { + return Err(CheckerError::new( + CheckerRejectionCode::V2414ControlFlowInvalid, + format!("decoded flow '{} -> {}' is absent from the lowering CFG", from.id, to.id), + )); + } + } + Ok(()) +} + +fn validate_machine_terminators(record: &VerifiedLoweringRecord, elf: &ParsedElf) -> Result<(), CheckerError> { + let instructions = elf.instructions.iter().map(|instruction| (instruction.address, instruction.word)).collect::>(); + for block in &record.blocks { + let address = block.range.end.checked_sub(4).ok_or_else(|| { + CheckerError::new( + CheckerRejectionCode::V2414ControlFlowInvalid, + format!("block '{}' is too short for a terminator", block.id), + ) + })?; + let word = instructions.get(&address).copied().ok_or_else(|| { + CheckerError::new( + CheckerRejectionCode::V2414ControlFlowInvalid, + format!("block '{}' end does not address a decoded instruction", block.id), + ) + })?; + let opcode = word & 0x7f; + let rd = (word >> 7) & 0x1f; + let valid = match block.terminator { + MachineTerminator::Return => word == 0x0000_8067, + MachineTerminator::Jump => opcode == 0x6f && rd == 0, + MachineTerminator::ConditionalBranch => opcode == 0x63 || (opcode == 0x6f && rd == 0), + MachineTerminator::Fallthrough => word != 0x0000_8067 && !matches!(opcode, 0x63 | 0x6f), + }; + if !valid { + return Err(CheckerError::new( + CheckerRejectionCode::V2414ControlFlowInvalid, + format!("decoded final instruction of block '{}' disagrees with its terminator", block.id), + )); + } + } + Ok(()) +} + +fn validate_stack_discipline(record: &VerifiedLoweringRecord, elf: &ParsedElf) -> Result<(), CheckerError> { + let blocks = record.blocks.iter().map(|block| (block.id.as_str(), block)).collect::>(); + let mut outgoing = BTreeMap::<&str, Vec<&LoweringEdge>>::new(); + for edge in &record.edges { + outgoing.entry(edge.from.as_str()).or_default().push(edge); + } + let mut entry_delta = BTreeMap::<&str, i64>::new(); + let mut pending = record.entries.iter().map(|entry| (entry.entry_block.as_str(), 0_i64)).collect::>(); + while let Some((block_id, incoming_delta)) = pending.pop() { + if let Some(previous) = entry_delta.insert(block_id, incoming_delta) { + if previous != incoming_delta { + return Err(CheckerError::new( + CheckerRejectionCode::V2407AbiOrStackInvalid, + format!("block '{block_id}' has inconsistent incoming stack-pointer deltas {previous} and {incoming_delta}"), + )); + } + continue; + } + let block = blocks[block_id]; + let mut delta = incoming_delta; + for adjustment in elf.stack_adjustments.iter().filter(|adjustment| block.range.contains(adjustment.address)) { + delta = delta.checked_add(adjustment.delta).ok_or_else(|| { + CheckerError::new( + CheckerRejectionCode::V2407AbiOrStackInvalid, + format!("stack-pointer delta overflows in block '{block_id}'"), + ) + })?; + if delta > 0 || delta.unsigned_abs() > u64::from(block.frame_size_bytes) { + return Err(CheckerError::new( + CheckerRejectionCode::V2407AbiOrStackInvalid, + format!("stack-pointer delta {delta} in block '{block_id}' exceeds declared frame {}", block.frame_size_bytes), + )); + } + } + if block.terminator == MachineTerminator::Return && delta != 0 { + return Err(CheckerError::new( + CheckerRejectionCode::V2407AbiOrStackInvalid, + format!("return block '{block_id}' leaves stack-pointer delta {delta}"), + )); + } + for edge in outgoing.get(block_id).into_iter().flatten() { + pending.push((edge.to.as_str(), if edge.kind == EdgeKind::Call { 0 } else { delta })); + } + } + Ok(()) +} + +fn validate_syscalls(record: &VerifiedLoweringRecord, elf: &ParsedElf) -> Result<(), CheckerError> { + let actual = elf.syscall_addresses.iter().copied().filter(|address| record.text_range.contains(*address)).collect::>(); + let declared = record.syscall_sites.iter().map(|site| site.address).collect::>(); + if actual != declared { + return Err(CheckerError::new( + CheckerRejectionCode::V2417SyscallContractInvalid, + "declared syscall sites do not exactly match decoded ecall instructions", + )); + } + let blocks = record.blocks.iter().map(|block| (block.id.as_str(), block)).collect::>(); + for site in &record.syscall_sites { + if site.contract.is_empty() + || site.source_domain.is_empty() + || site.index_domain.is_empty() + || site.buffer_limit_bytes == 0 + || !site.return_code_checked + || blocks.get(site.block_id.as_str()).is_none_or(|block| !block.range.contains(site.address)) + { + return Err(CheckerError::new( + CheckerRejectionCode::V2417SyscallContractInvalid, + format!("syscall site at {:#x} has an invalid bounded contract", site.address), + )); + } + } + Ok(()) +} + +fn validate_source_map( + source_map: &SourceArtifactMap, + record: &VerifiedLoweringRecord, + artifact: &[u8], + elf: &ParsedElf, +) -> Result<(), CheckerError> { + if source_map.schema != SOURCE_MAP_SCHEMA + || source_map.version != SOURCE_MAP_VERSION + || source_map.module != record.module + || source_map.text_range != record.text_range + || source_map.coverage_claim.source_semantic_equivalence + || !source_map.coverage_claim.mapped_instruction_ranges_only + { + return Err(CheckerError::new( + CheckerRejectionCode::V2416SourceMapInvalid, + "source map schema, identity, or bounded claim is invalid", + )); + } + let blocks = record.blocks.iter().map(|block| (block.id.as_str(), block)).collect::>(); + let entries = record.entries.iter().map(|entry| entry.id.as_str()).collect::>(); + let mut previous_end = None; + let mut mapped_ranges = Vec::new(); + for interval in &source_map.intervals { + if !safe_source_path(&interval.source_path) + || interval.source_start > interval.source_end + || interval.machine_range.is_empty() + || interval.machine_range.start % 4 != 0 + || interval.machine_range.end % 4 != 0 + || previous_end.is_some_and(|end| interval.machine_range.start < end) + { + return Err(CheckerError::new( + CheckerRejectionCode::V2416SourceMapInvalid, + format!("source-map interval for block '{}' overlaps, escapes, or is malformed", interval.block_id), + )); + } + let Some(block) = blocks.get(interval.block_id.as_str()) else { + return Err(CheckerError::new( + CheckerRejectionCode::V2416SourceMapInvalid, + format!("source-map interval references missing block '{}'", interval.block_id), + )); + }; + if interval.entry_id != block.owner_entry + || !entries.contains(interval.entry_id.as_str()) + || !block.range.contains_range(interval.machine_range) + || interval.lowering_block_id != block.lowering_block_id + || interval.proof_ids.iter().any(|proof| !block.proof_ids.contains(proof)) + { + return Err(CheckerError::new( + CheckerRejectionCode::V2416SourceMapInvalid, + format!("source-map interval for '{}' disagrees with its lowering block", interval.block_id), + )); + } + elf.bytes_for_range(artifact, interval.machine_range).map_err(map_elf_error)?; + previous_end = Some(interval.machine_range.end); + mapped_ranges.push(interval.machine_range); + } + if source_map.coverage_claim.complete_text_coverage { + let mut expected = record.text_range.start; + for range in mapped_ranges { + if range.start != expected { + return Err(CheckerError::new( + CheckerRejectionCode::V2416SourceMapInvalid, + "source map claims complete text coverage but contains a gap", + )); + } + expected = range.end; + } + if expected != record.text_range.end { + return Err(CheckerError::new( + CheckerRejectionCode::V2416SourceMapInvalid, + "source map claims complete text coverage but does not reach text end", + )); + } + } + Ok(()) +} + +fn safe_source_path(path: &str) -> bool { + if path == "" { + return true; + } + if path.is_empty() || path.starts_with('/') || path.starts_with('\\') || path.contains('\\') { + return false; + } + if path.len() >= 2 && path.as_bytes()[1] == b':' { + return false; + } + path.split('/').all(|component| !matches!(component, "" | "." | "..")) +} + +fn ensure_canonical(label: &str, input: &[u8], value: &T) -> Result<(), CheckerError> { + let canonical = canonical_bytes(value)?; + if canonical != input { + return Err(CheckerError::new( + CheckerRejectionCode::V2402NonCanonicalJson, + format!("{label} is not byte-for-byte canonical JSON"), + )); + } + Ok(()) +} + +fn ensure_byte_budget(label: &str, actual: usize, limit: u64) -> Result<(), CheckerError> { + if actual as u64 > limit { + return Err(CheckerError::new( + CheckerRejectionCode::V2400BudgetExceeded, + format!("{label} bytes {actual} exceed budget {limit}"), + )); + } + Ok(()) +} + +fn ensure_count(label: &str, actual: usize, limit: u32) -> Result<(), CheckerError> { + if actual as u64 > u64::from(limit) { + return Err(CheckerError::new( + CheckerRejectionCode::V2400BudgetExceeded, + format!("{label} count {actual} exceeds budget {limit}"), + )); + } + Ok(()) +} + +fn ensure_sorted_unique<'a, T, F>(values: &'a [T], key: F, label: &str) -> Result<(), CheckerError> +where + F: Fn(&'a T) -> &'a str, +{ + if values.windows(2).all(|pair| key(&pair[0]) < key(&pair[1])) { + Ok(()) + } else { + Err(CheckerError::new( + CheckerRejectionCode::V2404CanonicalOrder, + format!("{label} identifiers are not strictly sorted and unique"), + )) + } +} + +fn strictly_sorted(values: &[T]) -> bool { + values.windows(2).all(|pair| pair[0] < pair[1]) +} + +fn valid_alignment(value: u32) -> bool { + value.is_power_of_two() && value <= 16 +} + +fn artifact_declared_too_large(actual: u64, limit: u64) -> bool { + actual > limit +} + +fn json_string<'a>(root: &'a Value, path: &[&str]) -> Option<&'a str> { + path.iter().try_fold(root, |value, key| value.get(*key)).and_then(Value::as_str) +} + +fn json_u64(root: &Value, path: &[&str]) -> Option { + path.iter().try_fold(root, |value, key| value.get(*key)).and_then(Value::as_u64) +} + +pub fn domain_hash_bytes(domain: &str, bytes: &[u8]) -> String { + let mut material = Vec::with_capacity(domain.len() + 1 + bytes.len()); + material.extend_from_slice(domain.as_bytes()); + material.push(0); + material.extend_from_slice(bytes); + hex_encode(&ckb_blake2b256(&material)) +} + +fn map_elf_error(error: ElfParseError) -> CheckerError { + let code = match error.kind { + ElfErrorKind::BudgetExceeded => CheckerRejectionCode::V2400BudgetExceeded, + ElfErrorKind::InvalidSection | ElfErrorKind::ProhibitedLinkState | ElfErrorKind::MissingText => { + CheckerRejectionCode::V2412ElfSectionInvalid + } + ElfErrorKind::InvalidInstruction => CheckerRejectionCode::V2413InstructionInvalid, + ElfErrorKind::InvalidBranchTarget => CheckerRejectionCode::V2414ControlFlowInvalid, + _ => CheckerRejectionCode::V2411ElfFormatInvalid, + }; + CheckerError::new(code, error.message) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn canonical_parser_rejects_whitespace_and_unknown_fields() { + let budgets = CheckerBudgets::default(); + let unknown = br#"{"schema":"cellscript-source-artifact-map-v1","version":1,"module":"m","artifact_hash":"h","lowering_record_hash":"r","source_set_hash":"s","text_range":{"start":1,"end":2},"intervals":[],"coverage_claim":{"mapped_instruction_ranges_only":true,"complete_text_coverage":false,"source_semantic_equivalence":false},"unknown":true}"#; + assert_eq!(parse_source_map(unknown, &budgets).unwrap_err().code, CheckerRejectionCode::V2401MalformedJson); + + let map = SourceArtifactMap { + schema: SOURCE_MAP_SCHEMA.to_string(), + version: SOURCE_MAP_VERSION, + module: "m".to_string(), + artifact_hash: "h".to_string(), + lowering_record_hash: "r".to_string(), + source_set_hash: "s".to_string(), + text_range: MachineRange { start: 1, end: 2 }, + intervals: Vec::new(), + coverage_claim: SourceMapCoverageClaim { + mapped_instruction_ranges_only: true, + complete_text_coverage: false, + source_semantic_equivalence: false, + }, + }; + let mut pretty = serde_json::to_vec_pretty(&map).unwrap(); + pretty.push(b'\n'); + assert_eq!(parse_source_map(&pretty, &budgets).unwrap_err().code, CheckerRejectionCode::V2402NonCanonicalJson); + } + + #[test] + fn checker_error_diagnostics_are_utf8_bounded() { + let error = CheckerError::new(CheckerRejectionCode::V2401MalformedJson, "边界".repeat(100)).bounded(10); + assert!(error.message.len() <= 10); + assert!(std::str::from_utf8(error.message.as_bytes()).is_ok()); + } + + #[test] + fn malformed_corpus_is_bounded_and_never_panics() { + let budgets = CheckerBudgets { + artifact_bytes: 4_096, + record_bytes: 4_096, + source_map_bytes: 4_096, + diagnostic_bytes: 64, + ..CheckerBudgets::default() + }; + let corpus = [ + Vec::new(), + vec![0xff], + b"{".to_vec(), + vec![b'{'; 4_097], + (0..4_096).map(|index| (index % 251) as u8).collect::>(), + ]; + for bytes in corpus { + let outcome = std::panic::catch_unwind(|| check_bundle(&bytes, &bytes, &bytes, &bytes, &budgets)); + let error = outcome.expect("checker must not panic on malformed bounded corpus").unwrap_err(); + assert!(error.message.len() <= budgets.diagnostic_bytes as usize); + } + } + + #[test] + fn source_paths_are_confined() { + assert!(safe_source_path("src/main.cell")); + assert!(safe_source_path("")); + assert!(!safe_source_path("../main.cell")); + assert!(!safe_source_path("/tmp/main.cell")); + assert!(!safe_source_path("C:/main.cell")); + } +} diff --git a/crates/cellscript-artifact-checker/src/elf.rs b/crates/cellscript-artifact-checker/src/elf.rs new file mode 100644 index 00000000..a169dae1 --- /dev/null +++ b/crates/cellscript-artifact-checker/src/elf.rs @@ -0,0 +1,631 @@ +use crate::schema::MachineRange; + +const ELF64_HEADER_SIZE: usize = 64; +const ELF64_PROGRAM_HEADER_SIZE: usize = 56; +const ELF64_SECTION_HEADER_SIZE: usize = 64; +const EM_RISCV: u16 = 243; +const ET_EXEC: u16 = 2; +const PT_LOAD: u32 = 1; +const PF_X: u32 = 1; +const PF_R: u32 = 4; +const SHF_ALLOC: u64 = 0x2; +const SHF_EXECINSTR: u64 = 0x4; +const SHT_PROGBITS: u32 = 1; +const SHT_STRTAB: u32 = 3; +const SHT_RELA: u32 = 4; +const SHT_DYNAMIC: u32 = 6; +const SHT_REL: u32 = 9; +const SHT_DYNSYM: u32 = 11; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ElfErrorKind { + Truncated, + InvalidHeader, + UnsupportedClass, + UnsupportedEndian, + UnsupportedType, + UnsupportedMachine, + InvalidTable, + InvalidSection, + ProhibitedLinkState, + MissingText, + InvalidInstruction, + InvalidBranchTarget, + BudgetExceeded, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ElfParseError { + pub kind: ElfErrorKind, + pub message: String, +} + +impl ElfParseError { + fn new(kind: ElfErrorKind, message: impl Into) -> Self { + Self { kind, message: message.into() } + } +} + +impl std::fmt::Display for ElfParseError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(&self.message) + } +} + +impl std::error::Error for ElfParseError {} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ElfSection { + pub name: String, + pub section_type: u32, + pub flags: u64, + pub address: u64, + pub offset: u64, + pub size: u64, +} + +impl ElfSection { + pub fn range(&self) -> MachineRange { + MachineRange { start: self.address, end: self.address.saturating_add(self.size) } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ElfSegment { + pub flags: u32, + pub offset: u64, + pub virtual_address: u64, + pub file_size: u64, + pub memory_size: u64, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DecodedControlFlow { + pub address: u64, + pub target: u64, + pub kind: DecodedControlFlowKind, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DecodedInstruction { + pub address: u64, + pub word: u32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct StackAdjustment { + pub address: u64, + pub delta: i64, +} + +struct DecodedText { + instructions: Vec, + stack_adjustments: Vec, + syscall_addresses: Vec, + control_flow: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DecodedControlFlowKind { + ConditionalBranch, + DirectJump, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParsedElf { + pub entry: u64, + pub sections: Vec, + pub segments: Vec, + pub text: ElfSection, + pub rodata: ElfSection, + pub instruction_count: u64, + pub instructions: Vec, + pub stack_adjustments: Vec, + pub syscall_addresses: Vec, + pub control_flow: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ElfSummary { + pub class: String, + pub endian: String, + pub machine: String, + pub entry: u64, + pub text_range: MachineRange, + pub text_size_bytes: u64, + pub rodata_range: MachineRange, + pub rodata_size_bytes: u64, + pub instruction_count: u64, + pub syscall_count: usize, + pub section_count: usize, + pub load_segment_count: usize, +} + +impl ParsedElf { + pub fn summary(&self) -> ElfSummary { + ElfSummary { + class: "ELF64".to_string(), + endian: "little".to_string(), + machine: "RISC-V".to_string(), + entry: self.entry, + text_range: self.text.range(), + text_size_bytes: self.text.size, + rodata_range: self.rodata.range(), + rodata_size_bytes: self.rodata.size, + instruction_count: self.instruction_count, + syscall_count: self.syscall_addresses.len(), + section_count: self.sections.len(), + load_segment_count: self.segments.len(), + } + } + + pub fn bytes_for_range<'a>(&self, artifact: &'a [u8], range: MachineRange) -> Result<&'a [u8], ElfParseError> { + let section = self.sections.iter().find(|section| section.range().contains_range(range)).ok_or_else(|| { + ElfParseError::new( + ElfErrorKind::InvalidSection, + format!("machine range {:#x}..{:#x} is outside all ELF sections", range.start, range.end), + ) + })?; + let relative = range + .start + .checked_sub(section.address) + .ok_or_else(|| ElfParseError::new(ElfErrorKind::InvalidSection, "machine range begins before its ELF section"))?; + let start = section + .offset + .checked_add(relative) + .and_then(|value| usize::try_from(value).ok()) + .ok_or_else(|| ElfParseError::new(ElfErrorKind::InvalidSection, "machine range file offset overflows usize"))?; + let len = usize::try_from(range.len()) + .map_err(|_| ElfParseError::new(ElfErrorKind::InvalidSection, "machine range length overflows usize"))?; + let end = start + .checked_add(len) + .ok_or_else(|| ElfParseError::new(ElfErrorKind::InvalidSection, "machine range end overflows usize"))?; + artifact.get(start..end).ok_or_else(|| ElfParseError::new(ElfErrorKind::Truncated, "machine range exceeds artifact bytes")) + } +} + +pub fn parse_elf(bytes: &[u8], max_instructions: u64) -> Result { + if bytes.len() < ELF64_HEADER_SIZE { + return Err(ElfParseError::new(ElfErrorKind::Truncated, "ELF header is truncated")); + } + if bytes.get(0..4) != Some(b"\x7fELF") { + return Err(ElfParseError::new(ElfErrorKind::InvalidHeader, "artifact is not ELF")); + } + if bytes[4] != 2 { + return Err(ElfParseError::new(ElfErrorKind::UnsupportedClass, "checker requires ELF64")); + } + if bytes[5] != 1 { + return Err(ElfParseError::new(ElfErrorKind::UnsupportedEndian, "checker requires little-endian ELF")); + } + if bytes[6] != 1 || bytes[7..16].iter().any(|byte| *byte != 0) || read_u32(bytes, 20)? != 1 || read_u32(bytes, 48)? != 0 { + return Err(ElfParseError::new(ElfErrorKind::InvalidHeader, "ELF version is not 1")); + } + if read_u16(bytes, 16)? != ET_EXEC { + return Err(ElfParseError::new(ElfErrorKind::UnsupportedType, "checker requires ET_EXEC")); + } + if read_u16(bytes, 18)? != EM_RISCV { + return Err(ElfParseError::new(ElfErrorKind::UnsupportedMachine, "checker requires EM_RISCV")); + } + if usize::from(read_u16(bytes, 52)?) != ELF64_HEADER_SIZE { + return Err(ElfParseError::new(ElfErrorKind::InvalidHeader, "unexpected ELF64 header size")); + } + + let entry = read_u64(bytes, 24)?; + let program_offset = read_u64(bytes, 32)?; + let section_offset = read_u64(bytes, 40)?; + let program_entry_size = usize::from(read_u16(bytes, 54)?); + let program_count = usize::from(read_u16(bytes, 56)?); + let section_entry_size = usize::from(read_u16(bytes, 58)?); + let section_count = usize::from(read_u16(bytes, 60)?); + let shstr_index = usize::from(read_u16(bytes, 62)?); + + if program_count == 0 || program_entry_size != ELF64_PROGRAM_HEADER_SIZE { + return Err(ElfParseError::new(ElfErrorKind::InvalidTable, "ELF must have standard ELF64 program headers")); + } + if section_count < 4 || section_entry_size != ELF64_SECTION_HEADER_SIZE || shstr_index >= section_count { + return Err(ElfParseError::new( + ElfErrorKind::InvalidTable, + "ELF must contain null, .text, .rodata, and .shstrtab section headers", + )); + } + + let program_table = checked_table(bytes, program_offset, program_entry_size, program_count, "program header")?; + let mut segments = Vec::new(); + for header in program_table.chunks_exact(program_entry_size) { + if read_u32(header, 0)? != PT_LOAD { + return Err(ElfParseError::new(ElfErrorKind::ProhibitedLinkState, "checker permits only PT_LOAD program headers")); + } + let segment = ElfSegment { + flags: read_u32(header, 4)?, + offset: read_u64(header, 8)?, + virtual_address: read_u64(header, 16)?, + file_size: read_u64(header, 32)?, + memory_size: read_u64(header, 40)?, + }; + checked_file_range(bytes, segment.offset, segment.file_size, "PT_LOAD")?; + if segment.memory_size < segment.file_size { + return Err(ElfParseError::new(ElfErrorKind::InvalidTable, "PT_LOAD memory size is smaller than file size")); + } + if segment.flags != PF_R | PF_X { + return Err(ElfParseError::new( + ElfErrorKind::ProhibitedLinkState, + "CellScript ELF PT_LOAD segments must be read/execute and never writable", + )); + } + segments.push(segment); + } + if segments.is_empty() || !segments.iter().any(|segment| segment.flags & PF_X != 0 && segment_contains(segment, entry)) { + return Err(ElfParseError::new(ElfErrorKind::InvalidTable, "ELF entry is not contained in an executable PT_LOAD segment")); + } + + let section_table = checked_table(bytes, section_offset, section_entry_size, section_count, "section header")?; + let string_header = section_table + .get(shstr_index * section_entry_size..(shstr_index + 1) * section_entry_size) + .ok_or_else(|| ElfParseError::new(ElfErrorKind::InvalidTable, "section string table header is missing"))?; + if read_u32(string_header, 4)? != SHT_STRTAB || read_u64(string_header, 8)? != 0 || read_u64(string_header, 16)? != 0 { + return Err(ElfParseError::new(ElfErrorKind::InvalidSection, "section-name table has an invalid type, flags, or address")); + } + let string_offset = read_u64(string_header, 24)?; + let string_size = read_u64(string_header, 32)?; + let strings = checked_file_range(bytes, string_offset, string_size, ".shstrtab")?; + + let mut sections = Vec::with_capacity(section_count.saturating_sub(1)); + for (index, header) in section_table.chunks_exact(section_entry_size).enumerate() { + if index == 0 { + if header.iter().any(|byte| *byte != 0) { + return Err(ElfParseError::new(ElfErrorKind::InvalidSection, "ELF null section header is not zero")); + } + continue; + } + let name_offset = usize::try_from(read_u32(header, 0)?) + .map_err(|_| ElfParseError::new(ElfErrorKind::InvalidSection, "section name offset overflows usize"))?; + let name = read_c_string(strings, name_offset)?; + let section = ElfSection { + name, + section_type: read_u32(header, 4)?, + flags: read_u64(header, 8)?, + address: read_u64(header, 16)?, + offset: read_u64(header, 24)?, + size: read_u64(header, 32)?, + }; + checked_file_range(bytes, section.offset, section.size, §ion.name)?; + if matches!(section.section_type, SHT_RELA | SHT_DYNAMIC | SHT_REL | SHT_DYNSYM) + || matches!(section.name.as_str(), ".dynamic" | ".dynsym" | ".dynstr" | ".interp" | ".plt" | ".got" | ".got.plt") + { + return Err(ElfParseError::new( + ElfErrorKind::ProhibitedLinkState, + format!("prohibited dynamic or relocation section '{}'", section.name), + )); + } + sections.push(section); + } + sections.sort_by(|a, b| a.name.cmp(&b.name)); + if sections.windows(2).any(|pair| pair[0].name == pair[1].name) { + return Err(ElfParseError::new(ElfErrorKind::InvalidSection, "ELF contains duplicate section names")); + } + if sections.len() != 3 + || sections.iter().map(|section| section.name.as_str()).collect::>() != [".rodata", ".shstrtab", ".text"] + { + return Err(ElfParseError::new( + ElfErrorKind::InvalidSection, + "checker permits exactly .text, .rodata, and .shstrtab sections", + )); + } + + let text = sections + .iter() + .find(|section| section.name == ".text") + .cloned() + .ok_or_else(|| ElfParseError::new(ElfErrorKind::MissingText, "ELF has no .text section"))?; + let rodata = sections + .iter() + .find(|section| section.name == ".rodata") + .cloned() + .ok_or_else(|| ElfParseError::new(ElfErrorKind::InvalidSection, "ELF has no .rodata section"))?; + if text.section_type != SHT_PROGBITS || text.flags != SHF_ALLOC | SHF_EXECINSTR || text.size == 0 || text.size % 4 != 0 { + return Err(ElfParseError::new(ElfErrorKind::InvalidSection, ".text must be non-empty, executable, and four-byte aligned")); + } + if rodata.section_type != SHT_PROGBITS || rodata.flags != SHF_ALLOC { + return Err(ElfParseError::new(ElfErrorKind::InvalidSection, ".rodata must not be executable")); + } + if !text.range().contains(entry) { + return Err(ElfParseError::new(ElfErrorKind::InvalidSection, "ELF entry is outside .text")); + } + for section in [&text, &rodata] { + if !segments.iter().any(|segment| segment_contains_range(segment, section.address, section.size)) { + return Err(ElfParseError::new( + ElfErrorKind::InvalidSection, + format!("ELF section '{}' is outside PT_LOAD mappings", section.name), + )); + } + } + + let instruction_count = text.size / 4; + if instruction_count > max_instructions { + return Err(ElfParseError::new( + ElfErrorKind::BudgetExceeded, + format!("ELF instruction count {} exceeds budget {}", instruction_count, max_instructions), + )); + } + let text_bytes = checked_file_range(bytes, text.offset, text.size, ".text")?; + let decoded = validate_instructions(text_bytes, text.address, text.range())?; + + Ok(ParsedElf { + entry, + sections, + segments, + text, + rodata, + instruction_count, + instructions: decoded.instructions, + stack_adjustments: decoded.stack_adjustments, + syscall_addresses: decoded.syscall_addresses, + control_flow: decoded.control_flow, + }) +} + +fn validate_instructions(bytes: &[u8], base: u64, text_range: MachineRange) -> Result { + let mut instructions = Vec::new(); + let mut stack_adjustments = Vec::new(); + let mut syscalls = Vec::new(); + let mut control_flow = Vec::new(); + for (index, chunk) in bytes.chunks_exact(4).enumerate() { + let word = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + let address = base + (index as u64) * 4; + let opcode = word & 0x7f; + if !instruction_is_allowed(word) { + return Err(ElfParseError::new( + ElfErrorKind::InvalidInstruction, + format!("instruction {:#010x} at {:#x} is outside the CellScript RV64 allowlist", word, address), + )); + } + validate_stack_pointer_write(bytes, index, word, address, &mut stack_adjustments)?; + instructions.push(DecodedInstruction { address, word }); + if word == 0x0000_0073 { + syscalls.push(address); + } + let target = match opcode { + 0x63 => Some((branch_target(address, word), DecodedControlFlowKind::ConditionalBranch)), + 0x6f => Some((jal_target(address, word), DecodedControlFlowKind::DirectJump)), + 0x67 if word != 0x0000_8067 => { + Some((decode_call_target(bytes, index, word, address)?, DecodedControlFlowKind::DirectJump)) + } + _ => None, + }; + if let Some((target, kind)) = target { + if target % 4 != 0 || !text_range.contains(target) { + return Err(ElfParseError::new( + ElfErrorKind::InvalidBranchTarget, + format!("control-flow target {:#x} from {:#x} is outside aligned .text", target, address), + )); + } + control_flow.push(DecodedControlFlow { address, target, kind }); + } + } + Ok(DecodedText { instructions, stack_adjustments, syscall_addresses: syscalls, control_flow }) +} + +fn decode_call_target(bytes: &[u8], index: usize, word: u32, address: u64) -> Result { + let rd = (word >> 7) & 0x1f; + let funct3 = (word >> 12) & 0x7; + let rs1 = (word >> 15) & 0x1f; + if rd != 1 || rs1 != 1 || funct3 != 0 || index == 0 { + return Err(ElfParseError::new( + ElfErrorKind::InvalidInstruction, + format!("jalr at {address:#x} is neither ret nor a canonical auipc/jalr call"), + )); + } + let previous = instruction_word(bytes, index - 1)?; + if previous & 0x7f != 0x17 || (previous >> 7) & 0x1f != 1 { + return Err(ElfParseError::new( + ElfErrorKind::InvalidInstruction, + format!("jalr call at {address:#x} is not immediately preceded by 'auipc ra'"), + )); + } + let high = sign_extend(previous & 0xffff_f000, 32); + let low = sign_extend(word >> 20, 12); + Ok(add_signed(address - 4, high.saturating_add(low)) & !1) +} + +fn validate_stack_pointer_write( + bytes: &[u8], + index: usize, + word: u32, + address: u64, + adjustments: &mut Vec, +) -> Result<(), ElfParseError> { + let opcode = word & 0x7f; + let rd = (word >> 7) & 0x1f; + if rd != 2 || !opcode_writes_rd(opcode) { + return Ok(()); + } + let rs1 = (word >> 15) & 0x1f; + let funct3 = (word >> 12) & 0x7; + if opcode == 0x13 && funct3 == 0 && rs1 == 2 { + adjustments.push(StackAdjustment { address, delta: sign_extend(word >> 20, 12) }); + return Ok(()); + } + let rs2 = (word >> 20) & 0x1f; + if opcode == 0x33 && funct3 == 0 && (word >> 25) & 0x7f == 0 && rs1 == 2 { + let delta = preceding_lui_addi_constant(bytes, index, rs2).ok_or_else(|| { + ElfParseError::new( + ElfErrorKind::InvalidInstruction, + format!("stack adjustment at {address:#x} does not use an immediately materialised bounded constant"), + ) + })?; + adjustments.push(StackAdjustment { address, delta }); + return Ok(()); + } + Err(ElfParseError::new( + ElfErrorKind::InvalidInstruction, + format!("instruction at {address:#x} writes sp outside the canonical frame-adjustment forms"), + )) +} + +fn preceding_lui_addi_constant(bytes: &[u8], index: usize, register: u32) -> Option { + if index < 2 { + return None; + } + let addi = instruction_word(bytes, index - 1).ok()?; + let lui = instruction_word(bytes, index - 2).ok()?; + if addi & 0x7f != 0x13 + || (addi >> 12) & 0x7 != 0 + || (addi >> 7) & 0x1f != register + || (addi >> 15) & 0x1f != register + || lui & 0x7f != 0x37 + || (lui >> 7) & 0x1f != register + { + return None; + } + Some(sign_extend(lui & 0xffff_f000, 32).saturating_add(sign_extend(addi >> 20, 12))) +} + +fn instruction_word(bytes: &[u8], index: usize) -> Result { + let offset = + index.checked_mul(4).ok_or_else(|| ElfParseError::new(ElfErrorKind::InvalidInstruction, "instruction offset overflows"))?; + read_u32(bytes, offset) +} + +fn opcode_writes_rd(opcode: u32) -> bool { + matches!(opcode, 0x03 | 0x13 | 0x17 | 0x1b | 0x33 | 0x37 | 0x3b | 0x67 | 0x6f) +} + +fn instruction_is_allowed(word: u32) -> bool { + let opcode = word & 0x7f; + let rd = (word >> 7) & 0x1f; + let funct3 = (word >> 12) & 0x7; + let funct7 = (word >> 25) & 0x7f; + let funct6 = (word >> 26) & 0x3f; + match opcode { + 0x03 => matches!(funct3, 3 | 4), + 0x13 => match funct3 { + 0 | 4 | 6 | 7 => true, + 1 => funct6 == 0, + // The emitted `seqz rd, rs` pseudo-instruction is exactly + // `sltiu rd, rs, 1`; arbitrary SLTIU immediates are not part of + // the current CellScript machine surface. + 3 => word >> 20 == 1, + 5 => matches!(funct6, 0 | 0x10), + _ => false, + }, + 0x17 | 0x37 => true, + 0x6f => matches!(rd, 0 | 1), + 0x1b => match funct3 { + 0 => true, + 1 => funct7 == 0, + 5 => matches!(funct7, 0 | 0x20), + _ => false, + }, + 0x23 => matches!(funct3, 0..=3), + 0x33 => match funct7 { + 0 | 1 => true, + 0x20 => matches!(funct3, 0 | 5), + _ => false, + }, + 0x3b => match funct7 { + 0 => matches!(funct3, 0 | 1 | 5), + 1 => matches!(funct3, 0 | 4 | 5 | 6 | 7), + 0x20 => matches!(funct3, 0 | 5), + _ => false, + }, + 0x63 => !matches!(funct3, 2 | 3), + 0x67 => funct3 == 0, + 0x73 => word == 0x0000_0073, + _ => false, + } +} + +fn branch_target(address: u64, word: u32) -> u64 { + let immediate = + (((word >> 31) & 0x1) << 12) | (((word >> 7) & 0x1) << 11) | (((word >> 25) & 0x3f) << 5) | (((word >> 8) & 0xf) << 1); + add_signed(address, sign_extend(immediate, 13)) +} + +fn jal_target(address: u64, word: u32) -> u64 { + let immediate = + (((word >> 31) & 0x1) << 20) | (((word >> 12) & 0xff) << 12) | (((word >> 20) & 0x1) << 11) | (((word >> 21) & 0x3ff) << 1); + add_signed(address, sign_extend(immediate, 21)) +} + +fn sign_extend(value: u32, bits: u32) -> i64 { + let shift = 64 - bits; + ((i64::from(value)) << shift) >> shift +} + +fn add_signed(base: u64, offset: i64) -> u64 { + if offset >= 0 { + base.saturating_add(offset as u64) + } else { + base.saturating_sub(offset.unsigned_abs()) + } +} + +fn segment_contains(segment: &ElfSegment, address: u64) -> bool { + segment.virtual_address <= address && address < segment.virtual_address.saturating_add(segment.memory_size) +} + +fn segment_contains_range(segment: &ElfSegment, address: u64, size: u64) -> bool { + segment.virtual_address <= address && address.saturating_add(size) <= segment.virtual_address.saturating_add(segment.memory_size) +} + +fn checked_table<'a>(bytes: &'a [u8], offset: u64, entry_size: usize, count: usize, label: &str) -> Result<&'a [u8], ElfParseError> { + let size = entry_size + .checked_mul(count) + .and_then(|size| u64::try_from(size).ok()) + .ok_or_else(|| ElfParseError::new(ElfErrorKind::InvalidTable, format!("{} table size overflows", label)))?; + checked_file_range(bytes, offset, size, label) +} + +fn checked_file_range<'a>(bytes: &'a [u8], offset: u64, size: u64, label: &str) -> Result<&'a [u8], ElfParseError> { + let start = usize::try_from(offset) + .map_err(|_| ElfParseError::new(ElfErrorKind::InvalidTable, format!("{} offset overflows usize", label)))?; + let len = usize::try_from(size) + .map_err(|_| ElfParseError::new(ElfErrorKind::InvalidTable, format!("{} size overflows usize", label)))?; + let end = start + .checked_add(len) + .ok_or_else(|| ElfParseError::new(ElfErrorKind::InvalidTable, format!("{} range overflows usize", label)))?; + bytes.get(start..end).ok_or_else(|| ElfParseError::new(ElfErrorKind::Truncated, format!("{} exceeds artifact bytes", label))) +} + +fn read_c_string(bytes: &[u8], offset: usize) -> Result { + let rest = bytes + .get(offset..) + .ok_or_else(|| ElfParseError::new(ElfErrorKind::InvalidSection, "section name offset exceeds .shstrtab"))?; + let end = rest + .iter() + .position(|byte| *byte == 0) + .ok_or_else(|| ElfParseError::new(ElfErrorKind::InvalidSection, "section name is not NUL terminated"))?; + let value = std::str::from_utf8(&rest[..end]) + .map_err(|_| ElfParseError::new(ElfErrorKind::InvalidSection, "section name is not UTF-8"))?; + Ok(value.to_string()) +} + +fn read_u16(bytes: &[u8], offset: usize) -> Result { + let slice = + bytes.get(offset..offset + 2).ok_or_else(|| ElfParseError::new(ElfErrorKind::Truncated, "ELF u16 field is truncated"))?; + Ok(u16::from_le_bytes([slice[0], slice[1]])) +} + +fn read_u32(bytes: &[u8], offset: usize) -> Result { + let slice = + bytes.get(offset..offset + 4).ok_or_else(|| ElfParseError::new(ElfErrorKind::Truncated, "ELF u32 field is truncated"))?; + Ok(u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]])) +} + +fn read_u64(bytes: &[u8], offset: usize) -> Result { + let slice = + bytes.get(offset..offset + 8).ok_or_else(|| ElfParseError::new(ElfErrorKind::Truncated, "ELF u64 field is truncated"))?; + Ok(u64::from_le_bytes([slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], slice[6], slice[7]])) +} + +#[cfg(test)] +mod tests { + use super::instruction_is_allowed; + + #[test] + fn allowlist_accepts_only_the_emitted_sltiu_seqz_form() { + assert!(instruction_is_allowed(0x0015_3e13)); + assert!(!instruction_is_allowed(0x0025_3e13)); + } +} diff --git a/crates/cellscript-artifact-checker/src/lib.rs b/crates/cellscript-artifact-checker/src/lib.rs new file mode 100644 index 00000000..c457fbe4 --- /dev/null +++ b/crates/cellscript-artifact-checker/src/lib.rs @@ -0,0 +1,29 @@ +mod checker; +mod elf; +mod schema; + +pub use checker::{ + canonical_bytes, canonical_hash, check_bundle, check_bundle_values, domain_hash_bytes, parse_lowering_record, parse_source_map, + CheckerError, CheckerRejectionCode, CheckerReport, EvidenceState, +}; +pub use elf::{parse_elf, ElfSummary, ParsedElf}; +pub use schema::*; + +pub const CKB_HASH_PERSONALIZATION: &[u8; 16] = b"ckb-default-hash"; + +pub fn ckb_blake2b256(data: &[u8]) -> [u8; 32] { + let digest = blake2b_simd::Params::new().hash_length(32).personal(CKB_HASH_PERSONALIZATION).hash(data); + let mut output = [0u8; 32]; + output.copy_from_slice(digest.as_bytes()); + output +} + +pub fn hex_encode(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut output = String::with_capacity(bytes.len() * 2); + for byte in bytes { + output.push(HEX[(byte >> 4) as usize] as char); + output.push(HEX[(byte & 0x0f) as usize] as char); + } + output +} diff --git a/crates/cellscript-artifact-checker/src/main.rs b/crates/cellscript-artifact-checker/src/main.rs new file mode 100644 index 00000000..c0c44127 --- /dev/null +++ b/crates/cellscript-artifact-checker/src/main.rs @@ -0,0 +1,63 @@ +use cellscript_artifact_checker::{check_bundle, CheckerBudgets}; +use clap::Parser; +use std::path::PathBuf; + +#[derive(Debug, Parser)] +#[command(name = "cellscript-artifact-checker")] +#[command(about = "Bounded independent CellScript lowering-record and CKB ELF checker")] +struct Args { + #[arg(long)] + artifact: PathBuf, + #[arg(long)] + metadata: PathBuf, + #[arg(long = "lowering-record")] + lowering_record: PathBuf, + #[arg(long = "source-map")] + source_map: PathBuf, + #[arg(long)] + policy: Option, +} + +fn main() { + let args = Args::parse(); + match run(args) { + Ok(report) => match serde_json::to_string(&report) { + Ok(json) => println!("{json}"), + Err(error) => { + eprintln!("V2401: failed to serialize checker report: {error}"); + std::process::exit(2); + } + }, + Err(error) => { + let json = serde_json::to_string(&error) + .unwrap_or_else(|_| format!(r#"{{"code":"{}","message":"checker rejection"}}"#, error.code.as_str())); + eprintln!("{json}"); + std::process::exit(1); + } + } +} + +fn run(args: Args) -> Result { + let budgets = match args.policy { + Some(path) => { + let bytes = std::fs::read(&path).map_err(|error| io_error("checker policy", &path, error))?; + serde_json::from_slice::(&bytes).map_err(|error| cellscript_artifact_checker::CheckerError { + code: cellscript_artifact_checker::CheckerRejectionCode::V2401MalformedJson, + message: format!("failed to parse checker policy '{}': {error}", path.display()), + })? + } + None => CheckerBudgets::default(), + }; + let artifact = std::fs::read(&args.artifact).map_err(|error| io_error("artifact", &args.artifact, error))?; + let metadata = std::fs::read(&args.metadata).map_err(|error| io_error("metadata", &args.metadata, error))?; + let record = std::fs::read(&args.lowering_record).map_err(|error| io_error("lowering record", &args.lowering_record, error))?; + let source_map = std::fs::read(&args.source_map).map_err(|error| io_error("source map", &args.source_map, error))?; + check_bundle(&artifact, &metadata, &record, &source_map, &budgets) +} + +fn io_error(label: &str, path: &std::path::Path, error: std::io::Error) -> cellscript_artifact_checker::CheckerError { + cellscript_artifact_checker::CheckerError { + code: cellscript_artifact_checker::CheckerRejectionCode::V2401MalformedJson, + message: format!("failed to read {label} '{}': {error}", path.display()), + } +} diff --git a/crates/cellscript-artifact-checker/src/schema.rs b/crates/cellscript-artifact-checker/src/schema.rs new file mode 100644 index 00000000..befd06ee --- /dev/null +++ b/crates/cellscript-artifact-checker/src/schema.rs @@ -0,0 +1,422 @@ +use serde::{Deserialize, Serialize}; + +pub const LOWERING_RECORD_SCHEMA: &str = "cellscript-verified-lowering-record-v1"; +pub const SOURCE_MAP_SCHEMA: &str = "cellscript-source-artifact-map-v1"; +pub const CHECKER_POLICY_SCHEMA: &str = "cellscript-artifact-checker-policy-v1"; +pub const CHECKER_REPORT_SCHEMA: &str = "cellscript-artifact-checker-report-v1"; +pub const LOWERING_RECORD_VERSION: u32 = 1; +pub const SOURCE_MAP_VERSION: u32 = 1; +pub const CHECKER_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct CompatibilityProfileIdentity { + pub schema: String, + pub id: String, + pub edition: String, + pub source_semantics: String, + pub target_profile: String, + pub primitive_assurance: String, + pub metadata_schema_version: u32, + pub source_metadata_schema_version: u32, + pub artifact_metadata_schema_version: u32, + pub constraints_metadata_schema_version: u32, + pub entry_witness_payload_abi: String, + pub entry_witness_placement_abi: String, + pub entry_witness_placement_field: String, + pub entry_witness_placement_source: String, + pub raw_entry_witness_payload_compatible: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct VerifiedLoweringRecord { + pub schema: String, + pub version: u32, + pub compiler_version: String, + pub module: String, + pub edition: String, + pub target_profile: String, + pub compatibility_profile: CompatibilityProfileIdentity, + pub compatibility_profile_hash: String, + pub source_set_hash: String, + pub source_content_hash: String, + pub artifact_format: String, + pub artifact_hash: String, + pub artifact_size_bytes: u64, + pub text_range: MachineRange, + pub entries: Vec, + pub blocks: Vec, + pub edges: Vec, + pub proof_records: Vec, + pub syscall_sites: Vec, + pub runtime_error_exits: Vec, + pub limits: DeclaredLimits, + pub claim: VerificationClaim, +} + +impl VerifiedLoweringRecord { + pub fn canonicalize(&mut self) { + self.entries.sort_by(|a, b| a.id.cmp(&b.id)); + for entry in &mut self.entries { + entry.params.sort_by_key(|param| param.index); + entry.proof_ids.sort(); + entry.proof_ids.dedup(); + entry.capabilities.sort(); + entry.capabilities.dedup(); + } + self.blocks.sort_by(|a, b| a.id.cmp(&b.id)); + for block in &mut self.blocks { + block.stack_slots.sort_by(|a, b| a.offset.cmp(&b.offset).then(a.name.cmp(&b.name))); + block.scratch_register_avoid.sort(); + block.scratch_register_avoid.dedup(); + block.proof_ids.sort(); + block.proof_ids.dedup(); + block.capabilities.sort(); + block.capabilities.dedup(); + } + self.edges.sort_by(|a, b| (&a.from, &a.kind, &a.to).cmp(&(&b.from, &b.kind, &b.to))); + self.edges.dedup_by(|a, b| a.from == b.from && a.kind == b.kind && a.to == b.to); + self.proof_records.sort_by(|a, b| a.id.cmp(&b.id)); + self.syscall_sites.sort_by(|a, b| (a.address, &a.block_id).cmp(&(b.address, &b.block_id))); + self.runtime_error_exits.sort_by(|a, b| (&a.block_id, a.code, a.address).cmp(&(&b.block_id, b.code, b.address))); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct LoweringEntry { + pub id: String, + pub kind: EntryKind, + pub name: String, + pub entry_block: String, + pub params: Vec, + pub return_type: String, + pub effect: String, + pub capabilities: Vec, + pub proof_ids: Vec, + pub frame_size_bytes: u32, + pub outgoing_argument_bytes: u32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum EntryKind { + Action, + Lock, + Helper, + Runtime, + Wrapper, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct TypedParameter { + pub index: u32, + pub name: String, + pub ty: String, + pub storage: StorageClass, + pub width_bytes: u32, + pub alignment_bytes: u32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum StorageClass { + Scalar, + FixedBytes, + SchemaPointer, + Reference, + Aggregate, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct LoweringBlock { + pub id: String, + pub owner_entry: String, + pub reachable: bool, + pub lowering_block_id: Option, + pub machine_label: Option, + pub terminator: MachineTerminator, + pub range: MachineRange, + pub byte_digest: String, + pub frame_size_bytes: u32, + pub outgoing_argument_bytes: u32, + pub stack_slots: Vec, + pub scratch_register_avoid: Vec, + pub effect: String, + pub capabilities: Vec, + pub proof_ids: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum MachineTerminator { + Fallthrough, + Jump, + ConditionalBranch, + Return, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MachineRange { + pub start: u64, + pub end: u64, +} + +impl MachineRange { + pub fn len(self) -> u64 { + self.end.saturating_sub(self.start) + } + + pub fn is_empty(self) -> bool { + self.start == self.end + } + + pub fn contains(self, address: u64) -> bool { + self.start <= address && address < self.end + } + + pub fn contains_range(self, other: Self) -> bool { + self.start <= other.start && other.end <= self.end + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct StackSlot { + pub name: String, + pub offset: u32, + pub width_bytes: u32, + pub alignment_bytes: u32, + pub kind: StorageClass, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct LoweringEdge { + pub from: String, + pub to: String, + pub kind: EdgeKind, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum EdgeKind { + Fallthrough, + Jump, + ConditionalTaken, + ConditionalFallthrough, + Call, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ProofRecord { + pub id: String, + pub entry_id: String, + pub obligation: String, + pub evidence_tier: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SyscallSite { + pub block_id: String, + pub address: u64, + pub syscall_number: Option, + pub contract: String, + pub source_domain: String, + pub index_domain: String, + pub return_code_checked: bool, + pub buffer_limit_bytes: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RuntimeErrorExit { + pub block_id: String, + pub address: u64, + pub code: i32, + pub name: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DeclaredLimits { + pub artifact_bytes: u64, + pub record_bytes: u64, + pub source_map_bytes: u64, + pub entries: u32, + pub blocks: u32, + pub edges: u32, + pub instructions: u64, + pub call_depth: u32, + pub stack_frame_bytes: u32, + pub proof_records: u32, + pub source_map_intervals: u32, + pub diagnostic_bytes: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct VerificationClaim { + pub lowering_record: String, + pub machine_code: String, + pub semantic_equivalence: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SourceArtifactMap { + pub schema: String, + pub version: u32, + pub module: String, + pub artifact_hash: String, + pub lowering_record_hash: String, + pub source_set_hash: String, + pub text_range: MachineRange, + pub intervals: Vec, + pub coverage_claim: SourceMapCoverageClaim, +} + +impl SourceArtifactMap { + pub fn canonicalize(&mut self) { + self.intervals.sort_by(|a, b| { + (a.machine_range.start, a.machine_range.end, &a.block_id, &a.source_path, a.source_start, a.source_end).cmp(&( + b.machine_range.start, + b.machine_range.end, + &b.block_id, + &b.source_path, + b.source_start, + b.source_end, + )) + }); + for interval in &mut self.intervals { + interval.proof_ids.sort(); + interval.proof_ids.dedup(); + interval.runtime_error_codes.sort(); + interval.runtime_error_codes.dedup(); + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SourceMapInterval { + pub source_path: String, + pub source_start: u32, + pub source_end: u32, + pub entry_id: String, + pub block_id: String, + pub lowering_block_id: Option, + pub machine_range: MachineRange, + pub proof_ids: Vec, + pub runtime_error_codes: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SourceMapCoverageClaim { + pub mapped_instruction_ranges_only: bool, + pub complete_text_coverage: bool, + pub source_semantic_equivalence: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct CheckerBudgets { + pub schema: String, + pub artifact_bytes: u64, + pub record_bytes: u64, + pub source_map_bytes: u64, + pub entries: u32, + pub blocks: u32, + pub edges: u32, + pub instructions: u64, + pub call_depth: u32, + pub stack_frame_bytes: u32, + pub proof_records: u32, + pub source_map_intervals: u32, + pub diagnostic_bytes: u32, +} + +impl Default for CheckerBudgets { + fn default() -> Self { + Self { + schema: CHECKER_POLICY_SCHEMA.to_string(), + artifact_bytes: 4 * 1024 * 1024, + record_bytes: 4 * 1024 * 1024, + source_map_bytes: 4 * 1024 * 1024, + entries: 2_048, + blocks: 65_536, + edges: 262_144, + instructions: 1_048_576, + call_depth: 256, + stack_frame_bytes: 1024 * 1024, + proof_records: 65_536, + source_map_intervals: 65_536, + diagnostic_bytes: 16 * 1024, + } + } +} + +impl CheckerBudgets { + pub fn as_declared_limits(&self) -> DeclaredLimits { + DeclaredLimits { + artifact_bytes: self.artifact_bytes, + record_bytes: self.record_bytes, + source_map_bytes: self.source_map_bytes, + entries: self.entries, + blocks: self.blocks, + edges: self.edges, + instructions: self.instructions, + call_depth: self.call_depth, + stack_frame_bytes: self.stack_frame_bytes, + proof_records: self.proof_records, + source_map_intervals: self.source_map_intervals, + diagnostic_bytes: self.diagnostic_bytes, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct VerifiedArtifactMetadata { + pub boundary_schema: String, + pub state: VerifiedArtifactState, + pub checker_name: String, + pub checker_version: String, + pub checker_policy_schema: String, + pub lowering_record_schema: String, + pub lowering_record_hash: Option, + pub source_map_schema: String, + pub source_map_hash: Option, + pub claim: String, +} + +impl Default for VerifiedArtifactMetadata { + fn default() -> Self { + Self { + boundary_schema: "cellscript-verified-artifact-boundary-v1".to_string(), + state: VerifiedArtifactState::NotEmittedNonElf, + checker_name: "cellscript-artifact-checker".to_string(), + checker_version: CHECKER_VERSION.to_string(), + checker_policy_schema: CHECKER_POLICY_SCHEMA.to_string(), + lowering_record_schema: LOWERING_RECORD_SCHEMA.to_string(), + lowering_record_hash: None, + source_map_schema: SOURCE_MAP_SCHEMA.to_string(), + source_map_hash: None, + claim: "unverified".to_string(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum VerifiedArtifactState { + Emitted, + NotEmittedNonElf, +} diff --git a/crates/cellscript-ckb-adapter/Cargo.toml b/crates/cellscript-ckb-adapter/Cargo.toml index 97f72ab1..5af32a89 100644 --- a/crates/cellscript-ckb-adapter/Cargo.toml +++ b/crates/cellscript-ckb-adapter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cellscript-ckb-adapter" -version = "0.22.0" +version = "0.24.0" edition = "2024" rust-version = "1.97.1" publish = false diff --git a/crates/cellscript-ckb-adapter/src/bin/cellscript-deploy.rs b/crates/cellscript-ckb-adapter/src/bin/cellscript-deploy.rs index 9f5ec483..6e023ebf 100644 --- a/crates/cellscript-ckb-adapter/src/bin/cellscript-deploy.rs +++ b/crates/cellscript-ckb-adapter/src/bin/cellscript-deploy.rs @@ -5,17 +5,17 @@ use cellscript_ckb_adapter::{ }; use ckb_types::{ bytes::Bytes, - core::ScriptHashType, - packed::{CellInput, OutPoint}, + core::{DepType, ScriptHashType}, + packed::{CellDep, CellInput, OutPoint}, prelude::*, H160, }; -use clap::{Parser, Subcommand}; +use clap::{Parser, Subcommand, ValueEnum}; use std::path::PathBuf; #[derive(Parser, Debug)] #[command(name = "cellscript-deploy")] -#[command(about = "CellScript CKB adapter CLI — deploy, act, and query on-chain state")] +#[command(about = "CellScript CKB adapter CLI — build mainnet deployment transactions, act, and query on-chain state")] #[command(version = env!("CARGO_PKG_VERSION"))] struct Cli { /// CKB node RPC URL @@ -32,10 +32,11 @@ struct Cli { #[derive(Subcommand, Debug)] enum Commands { - /// Deploy a compiled artifact as an on-chain code cell with TYPE_ID + /// Refuse unsigned submission and direct callers to the external-signing flow + #[command(hide = true)] Deploy(DeployArgs), - /// Build a headless deploy transaction without submitting + /// Build a mainnet unsigned deploy transaction for external signing BuildDeploy(BuildDeployArgs), /// Build a transaction from an action plan @@ -49,8 +50,8 @@ enum Commands { } #[derive(clap::Args, Debug)] -struct DeployArgs { - /// Artifact binary file path (.s or .cell) +struct DeploySpecArgs { + /// Compiled RISC-V ELF artifact path #[arg(long)] artifact: PathBuf, @@ -63,16 +64,30 @@ struct DeployArgs { name: String, /// Fee in shannons - #[arg(long, default_value_t = 1_000)] + #[arg(long, default_value_t = 10_000)] fee: u64, /// Capacity input out_point (format: 0x:) #[arg(long)] capacity_out_point: String, - /// Capacity input shannons - #[arg(long, default_value_t = 200_000_000_000)] - capacity_shannons: u64, + /// Code reference hash type: type creates a TYPE_ID cell; data variants create an immutable data cell + #[arg(long, value_enum, default_value_t = DeploymentHashType::Type)] + hash_type: DeploymentHashType, + + /// CellDep out_point for the input lock script (format: 0x:) + #[arg(long, default_value = "0x71a7ba8fc96349fea0ed3a5c47992e3b4084b031a42264a018e0072e8172e46c:0")] + lock_cell_dep_out_point: String, + + /// CellDep kind for the input lock script + #[arg(long, value_enum, default_value_t = CliDepType::DepGroup)] + lock_cell_dep_type: CliDepType, +} + +#[derive(clap::Args, Debug)] +struct DeployArgs { + #[command(flatten)] + spec: DeploySpecArgs, /// Max attempts to wait for commitment #[arg(long, default_value_t = 30)] @@ -89,29 +104,42 @@ struct DeployArgs { #[derive(clap::Args, Debug)] struct BuildDeployArgs { - /// Artifact binary file path - #[arg(long)] - artifact: PathBuf, - - /// Deployer lock script args (hex, 20 bytes for secp256k1-sighash) - #[arg(long)] - lock_arg: String, + #[command(flatten)] + spec: DeploySpecArgs, +} - /// Name for the deployment - #[arg(long, default_value = "cellscript-contract")] - name: String, +#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)] +enum DeploymentHashType { + Type, + Data, + Data1, + Data2, +} - /// Fee in shannons - #[arg(long, default_value_t = 1_000)] - fee: u64, +impl From for ScriptHashType { + fn from(value: DeploymentHashType) -> Self { + match value { + DeploymentHashType::Type => Self::Type, + DeploymentHashType::Data => Self::Data, + DeploymentHashType::Data1 => Self::Data1, + DeploymentHashType::Data2 => Self::Data2, + } + } +} - /// Capacity input out_point (format: 0x:) - #[arg(long)] - capacity_out_point: String, +#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)] +enum CliDepType { + Code, + DepGroup, +} - /// Capacity input shannons - #[arg(long, default_value_t = 200_000_000_000)] - capacity_shannons: u64, +impl From for DepType { + fn from(value: CliDepType) -> Self { + match value { + CliDepType::Code => Self::Code, + CliDepType::DepGroup => Self::DepGroup, + } + } } #[derive(clap::Args, Debug)] @@ -173,20 +201,13 @@ fn parse_lock_arg(s: &str) -> Result { } /// Shared spec builder for deploy and build-deploy. -fn build_deploy_spec( - artifact: PathBuf, - lock_arg: String, - name: String, - fee: u64, - capacity_out_point: String, - capacity_shannons: u64, -) -> Result { - let artifact_binary = std::fs::read(&artifact)?; +fn build_deploy_spec(adapter: &CellScriptAdapter, args: DeploySpecArgs) -> Result { + let artifact_binary = std::fs::read(&args.artifact)?; let artifact_binary = Bytes::from(artifact_binary); let artifact_hash = ckb_hash::blake2b_256(&artifact_binary).iter().map(|b| format!("{:02x}", b)).collect::(); - let lock_arg = parse_lock_arg(&lock_arg)?; - // Construct secp256k1-sighash lock script (code_hash for mainnet/devnet). + let lock_arg = parse_lock_arg(&args.lock_arg)?; + // Construct the mainnet secp256k1-sighash lock script. let lock_script = cellscript_ckb_adapter::construct_script(&cellscript_ckb_adapter::ScriptSpec::new( [ 0x9b, 0x81, 0x97, 0x34, 0x7e, 0x6e, 0x47, 0x1d, 0x7e, 0xa2, 0x8b, 0x52, 0x0c, 0x45, 0x3e, 0x18, 0x54, 0xf0, 0x96, 0x2e, @@ -196,97 +217,65 @@ fn build_deploy_spec( lock_arg.as_bytes().to_vec(), )); - let capacity_out_point = parse_out_point(&capacity_out_point)?; + let capacity_out_point = parse_out_point(&args.capacity_out_point)?; + let (capacity_input_shannons, capacity_input_data) = adapter.resolve_pure_capacity_input(&capacity_out_point, &lock_script)?; let capacity_input = CellInput::new_builder().previous_output(capacity_out_point).build(); + let lock_cell_dep = CellDep::new_builder() + .out_point(parse_out_point(&args.lock_cell_dep_out_point)?) + .dep_type(DepType::from(args.lock_cell_dep_type)) + .build(); Ok(DeployArtifactSpec { - name, + name: args.name, artifact_binary, artifact_hash, deployer_lock: lock_script, capacity_input, - capacity_input_shannons: capacity_shannons, - capacity_input_data: Bytes::new(), - type_id_hash_type: ScriptHashType::Type, + capacity_input_shannons, + capacity_input_data, + type_id_hash_type: args.hash_type.into(), type_script: None, - cell_deps: Vec::new(), + cell_deps: vec![lock_cell_dep], header_deps: Vec::new(), - fee_shannons: fee, + fee_shannons: args.fee, }) } -fn cmd_deploy(rpc: &str, json: bool, args: DeployArgs) -> Result<()> { - let spec = build_deploy_spec(args.artifact, args.lock_arg, args.name, args.fee, args.capacity_out_point, args.capacity_shannons)?; - let name = spec.name.clone(); - - let (tx, deploy_evidence) = build_deploy_transaction(&spec)?; - - // Connect and submit. - let adapter = CellScriptAdapter::connect(rpc)?; - - // Estimate cycles. - let estimate_cycles = adapter.estimate_cycles(&tx).ok().map(|e| e.cycles.value()); - - // Test tx-pool acceptance. - let tx_pool_accepted = adapter.test_tx_pool_accept(&tx).is_ok(); - - // Submit. - let tx_hash = adapter.submit_transaction(&tx)?; - eprintln!("submitted tx: 0x{}", hex::encode(tx_hash.as_bytes())); - - // Wait for commitment. - let committed = adapter.wait_for_commitment(&tx_hash, args.wait_attempts, args.wait_delay_ms)?; - - // Build manifest. - let mut hash_bytes = [0u8; 32]; - hash_bytes.copy_from_slice(tx_hash.as_bytes()); - let manifest = cellscript_ckb_adapter::build_deployment_manifest_from_evidence(&deploy_evidence, &hash_bytes, 0); - - // Write manifest if requested. - if let Some(ref path) = args.manifest_out { - let manifest_json = serde_json::to_string_pretty(&manifest)?; - std::fs::write(path, &manifest_json)?; - eprintln!("manifest written to {}", path.display()); - } - - if json { - let output = serde_json::json!({ - "tx_hash": format!("0x{}", hex::encode(tx_hash.as_bytes())), - "committed": true, - "block_hash": format!("0x{}", hex::encode(committed.block_hash.as_bytes())), - "estimate_cycles": estimate_cycles, - "tx_pool_accepted": tx_pool_accepted, - "manifest": manifest, - }); - println!("{}", serde_json::to_string_pretty(&output)?); - } else { - println!("deployed {} at tx 0x{}", name, hex::encode(tx_hash.as_bytes())); - println!(" committed in block 0x{}", hex::encode(committed.block_hash.as_bytes())); - if let Some(cycles) = estimate_cycles { - println!(" estimate_cycles: {cycles}"); - } - } - - Ok(()) +fn cmd_deploy(_rpc: &str, _json: bool, args: DeployArgs) -> Result<()> { + let _ = (args.spec, args.wait_attempts, args.wait_delay_ms, args.manifest_out); + bail!( + "direct deploy is disabled because this CLI does not hold or invoke a signer; use build-deploy, sign the returned transaction with a CKB wallet, then broadcast it" + ) } fn cmd_build_deploy(rpc: &str, json: bool, args: BuildDeployArgs) -> Result<()> { - let spec = build_deploy_spec(args.artifact, args.lock_arg, args.name, args.fee, args.capacity_out_point, args.capacity_shannons)?; + let adapter = CellScriptAdapter::connect(rpc)?; + adapter.require_mainnet()?; + let spec = build_deploy_spec(&adapter, args.spec)?; - let (tx, _evidence) = build_deploy_transaction(&spec)?; + let (tx, evidence) = build_deploy_transaction(&spec)?; - // Try to estimate cycles if node is available. - let estimate = CellScriptAdapter::connect(rpc).ok().and_then(|a| a.estimate_cycles(&tx).ok()).map(|e| e.cycles.value()); + // An unsigned secp transaction is expected to fail script verification, so + // cycle estimation remains informational until the external signer fills it. + let estimate = adapter.estimate_cycles(&tx).ok().map(|e| e.cycles.value()); if json { let tx_json = serde_json::to_value(cellscript_ckb_adapter::to_rpc_transaction(&tx))?; let output = serde_json::json!({ + "can_submit": false, + "signing_required": true, "transaction": tx_json, "estimate_cycles": estimate, + "evidence": evidence, }); println!("{}", serde_json::to_string_pretty(&output)?); } else { - println!("built deploy transaction ({} bytes)", tx.data().serialized_size_in_block()); + println!("built unsigned deploy transaction ({} bytes)", tx.data().serialized_size_in_block()); + println!(" can_submit: false"); + println!(" signing_required: true"); + println!(" hash_type: {}", evidence.hash_type); + println!(" code_hash: 0x{}", hex::encode(&evidence.code_hash)); + println!(" cell_deps: {}", evidence.cell_deps); if let Some(cycles) = estimate { println!(" estimate_cycles: {cycles}"); } @@ -407,3 +396,64 @@ fn cmd_info(rpc: &str, json: bool) -> Result<()> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + const INPUT: &str = "0x1111111111111111111111111111111111111111111111111111111111111111:0"; + + fn parse_build_deploy(extra: &[&str]) -> Cli { + let mut args = vec![ + "cellscript-deploy", + "build-deploy", + "--artifact", + "registry-type-script", + "--lock-arg", + "0x2222222222222222222222222222222222222222", + "--capacity-out-point", + INPUT, + ]; + args.extend_from_slice(extra); + Cli::try_parse_from(args).unwrap() + } + + #[test] + fn build_deploy_accepts_immutable_data1() { + let cli = parse_build_deploy(&["--hash-type", "data1"]); + let Commands::BuildDeploy(args) = cli.command else { + panic!("expected build-deploy command"); + }; + assert_eq!(args.spec.hash_type, DeploymentHashType::Data1); + assert_eq!(args.spec.fee, 10_000); + assert_eq!(args.spec.lock_cell_dep_type, CliDepType::DepGroup); + assert_eq!(args.spec.lock_cell_dep_out_point, "0x71a7ba8fc96349fea0ed3a5c47992e3b4084b031a42264a018e0072e8172e46c:0"); + } + + #[test] + fn build_deploy_defaults_to_type_id() { + let cli = parse_build_deploy(&[]); + let Commands::BuildDeploy(args) = cli.command else { + panic!("expected build-deploy command"); + }; + assert_eq!(args.spec.hash_type, DeploymentHashType::Type); + } + + #[test] + fn build_deploy_rejects_unknown_hash_type() { + let error = Cli::try_parse_from([ + "cellscript-deploy", + "build-deploy", + "--artifact", + "registry-type-script", + "--lock-arg", + "0x2222222222222222222222222222222222222222", + "--capacity-out-point", + INPUT, + "--hash-type", + "data3", + ]) + .unwrap_err(); + assert!(error.to_string().contains("invalid value 'data3'")); + } +} diff --git a/crates/cellscript-ckb-adapter/src/lib.rs b/crates/cellscript-ckb-adapter/src/lib.rs index 869b5c49..de44d390 100644 --- a/crates/cellscript-ckb-adapter/src/lib.rs +++ b/crates/cellscript-ckb-adapter/src/lib.rs @@ -343,11 +343,20 @@ pub struct ScriptCodeDepEvidence { pub dep_type: String, } +pub const ENTRY_WITNESS_PLACEMENT_ABI: &str = "cellscript-witnessargs-input-type-v2"; +pub const ENTRY_WITNESS_PAYLOAD_MAGIC: &[u8; 8] = b"CSARGv1\0"; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -pub enum WitnessPlacement { - Lock, - InputType, - OutputType, +pub enum EntryWitnessPlacementAbi { + WitnessArgsInputTypeV2, +} + +impl EntryWitnessPlacementAbi { + pub const fn name(self) -> &'static str { + match self { + Self::WitnessArgsInputTypeV2 => ENTRY_WITNESS_PLACEMENT_ABI, + } + } } #[derive(Debug, Clone, Serialize)] @@ -520,8 +529,9 @@ pub fn deployment_evidence(manifest: &DeploymentManifest) -> DeploymentEvidence /// Specification for deploying a compiled CellScript artifact as an on-chain code cell. /// /// The caller provides the artifact binary, the deployer lock script, and the -/// capacity input cell. The adapter computes TYPE_ID args, constructs the code -/// output, validates occupied capacity, and builds a headless CKB transaction. +/// capacity input cell. The adapter constructs either a TYPE_ID-backed code Cell +/// or an immutable data Cell, validates occupied capacity, and builds an unsigned +/// CKB transaction. #[derive(Debug, Clone)] pub struct DeployArtifactSpec { /// Name for the deployment (used in manifest and evidence). @@ -592,27 +602,30 @@ pub struct ResolvedDeployEvidence { pub tx_pool_acceptance: bool, } -/// Build a headless CKB transaction that deploys a CellScript artifact as an -/// on-chain code cell with TYPE_ID. +/// Build an unsigned CKB transaction that deploys a CellScript artifact as an +/// on-chain code Cell. /// /// The function: -/// 1. Computes TYPE_ID args from the first input tx_hash + output index 0. -/// 2. Constructs the type script (TYPE_ID) and lock script for the code cell. +/// 1. Computes TYPE_ID args when `type_id_hash_type` is `Type`. +/// 2. Constructs the optional Type Script and lock script for the code Cell. /// 3. Calculates occupied capacity for the code cell from artifact size. /// 4. Constructs a change output with remaining capacity minus fee. /// 5. Validates that both outputs meet occupied-capacity floors. /// 6. Assembles the transaction and returns evidence. /// /// This is headless: no RPC, no live-cell selection, no signing. The caller -/// provides a pre-resolved capacity input. Use `CkbSdkAcceptance` for node -/// interaction after building. +/// provides a pre-resolved capacity input and every required CellDep. The first +/// witness contains the standard 65-byte zeroed secp-sighash placeholder; an +/// external signer must replace it before submission. pub fn build_deploy_transaction(spec: &DeployArtifactSpec) -> Result<(TransactionView, ResolvedDeployEvidence)> { // Validate artifact is non-empty. if spec.artifact_binary.is_empty() { bail!("artifact binary must be non-empty"); } - if spec.artifact_hash.is_empty() { - bail!("artifact hash must be provided"); + let calculated_artifact_hash = hex::encode(blake2b_256(&spec.artifact_binary)); + let supplied_artifact_hash = spec.artifact_hash.strip_prefix("0x").unwrap_or(&spec.artifact_hash); + if !supplied_artifact_hash.eq_ignore_ascii_case(&calculated_artifact_hash) { + bail!("artifact hash mismatch: supplied {}, calculated {}", spec.artifact_hash, calculated_artifact_hash); } if spec.capacity_input_shannons == 0 { bail!("capacity input must have non-zero capacity"); @@ -631,7 +644,7 @@ pub fn build_deploy_transaction(spec: &DeployArtifactSpec) -> Result<(Transactio }; let type_id_args = type_script.as_ref().map(|script| script.args().raw_data().to_vec()).unwrap_or_default(); - // Step 3: Build code cell output with TYPE_ID type script. + // Step 3: Build the code Cell output with the optional Type Script. let code_data_capacity = Capacity::bytes(spec.artifact_binary.len())?; // We need to compute the actual code_hash which is blake2b of the artifact. let data_hash = blake2b_256(&spec.artifact_binary); @@ -685,12 +698,24 @@ pub fn build_deploy_transaction(spec: &DeployArtifactSpec) -> Result<(Transactio for dep in &spec.header_deps { builder.dedup_header_dep(dep.clone()); } - // Placeholder witness for the first input (required by CKB protocol). - let placeholder_witness = WitnessArgs::new_builder().build(); + // Standard secp256k1-sighash-all signing placeholder. External wallets sign + // against this shape and replace the zero bytes with a recoverable signature. + let placeholder_witness = WitnessArgs::new_builder().lock(Some(Bytes::from(vec![0u8; 65])).pack()).build(); builder.witness(placeholder_witness.as_bytes().pack()); let tx = builder.build(); - let serialized_tx_size_bytes = tx.data().as_slice().len(); + let serialized_tx_size_bytes = tx.data().serialized_size_in_block(); + // CKB's default relay policy is 1,000 shannons per 1,000 bytes, so the + // numeric minimum at that rate equals the serialized byte count. + let minimum_fee_shannons = u64::try_from(serialized_tx_size_bytes)?; + if spec.fee_shannons < minimum_fee_shannons { + bail!( + "fee {} shannons is below the 1,000 shannons/KB policy floor of {} shannons for a {}-byte transaction", + spec.fee_shannons, + minimum_fee_shannons, + serialized_tx_size_bytes + ); + } // Verify outputs/outputs_data pairing. assert_eq!(tx.outputs().len(), 2, "deploy tx must have 2 outputs"); @@ -710,7 +735,7 @@ pub fn build_deploy_transaction(spec: &DeployArtifactSpec) -> Result<(Transactio schema: DEPLOY_EVIDENCE_SCHEMA, state: "ResolvedDeployTx", name: spec.name.clone(), - artifact_hash: spec.artifact_hash.clone(), + artifact_hash: calculated_artifact_hash, code_output_index: 0, change_output_index: 1, type_id_args, @@ -1421,30 +1446,28 @@ pub fn require_script_code_dep(script: &Script, deps: &[ScriptCodeDep]) -> Resul Ok(dep.to_cell_dep()) } -pub fn place_entry_witness_payload(base: &WitnessArgs, placement: WitnessPlacement, payload: Bytes) -> Result { - if payload.is_empty() { - bail!("CellScript entry witness payload must be non-empty"); +/// Places a CellScript entry payload before any lock-script signing occurs. +/// +/// `base.lock` may contain an SDK placeholder, but it must not contain live +/// signatures. CKB lock signers commit to the complete serialized +/// `WitnessArgs`, including `input_type`; mutating this field after signing +/// invalidates the signatures. +pub fn place_entry_witness_payload_before_signing( + base: &WitnessArgs, + placement: EntryWitnessPlacementAbi, + payload: Bytes, +) -> Result { + if !payload.starts_with(ENTRY_WITNESS_PAYLOAD_MAGIC) { + bail!("CellScript entry witness payload must start with CSARGv1\\0"); } match placement { - WitnessPlacement::Lock => { - if base.lock().to_opt().is_some() { - bail!("refusing to overwrite WitnessArgs.lock; lock signatures must stay explicit"); - } - Ok(base.clone().as_builder().lock(Some(payload).pack()).build()) - } - WitnessPlacement::InputType => { + EntryWitnessPlacementAbi::WitnessArgsInputTypeV2 => { if base.input_type().to_opt().is_some() { bail!("refusing to overwrite WitnessArgs.input_type"); } Ok(base.clone().as_builder().input_type(Some(payload).pack()).build()) } - WitnessPlacement::OutputType => { - if base.output_type().to_opt().is_some() { - bail!("refusing to overwrite WitnessArgs.output_type"); - } - Ok(base.clone().as_builder().output_type(Some(payload).pack()).build()) - } } } @@ -1751,24 +1774,12 @@ pub fn signing_boundary_type() -> &'static str { /// /// ```no_run /// # fn main() -> anyhow::Result<()> { -/// use ckb_types::packed::Script; /// use cellscript_ckb_adapter::CellScriptAdapter; /// /// // Connect to a CKB node /// let adapter = CellScriptAdapter::connect("http://127.0.0.1:8114")?; -/// -/// // Deploy an artifact -/// let deployer_lock_script = Script::default(); -/// let (manifest, evidence) = adapter.deploy_artifact( -/// "my-token", -/// std::fs::read("artifact.bin")?.into(), -/// deployer_lock_script, -/// 1_000, // fee in shannons -/// )?; -/// -/// // Load an action plan and build a transaction -/// let plan = adapter.load_action_plan("action.json")?; -/// let resolved = adapter.resolve_action(&plan)?; +/// let tip = adapter.get_tip_block_number()?; +/// println!("CKB tip: {tip}"); /// # Ok(()) /// # } /// ``` @@ -1791,124 +1802,6 @@ impl CellScriptAdapter { Ok(Self { client }) } - // ---- Deploy workflow ---- - - /// Deploy a CellScript artifact as an on-chain code cell with TYPE_ID. - /// - /// This is the one-call deploy workflow that combines: - /// 1. Finding a spendable capacity cell from the node - /// 2. Building the deploy transaction (headless) - /// 3. Estimating cycles and testing tx-pool acceptance - /// 4. Submitting the transaction - /// 5. Waiting for commitment - /// 6. Building the deployment manifest - /// - /// Returns the `DeploymentManifest` and full `TransactionLifecycleEvidence`. - pub fn deploy_artifact( - &self, - name: &str, - artifact_binary: Bytes, - deployer_lock: Script, - fee_shannons: u64, - ) -> Result<(DeploymentManifest, TransactionLifecycleEvidence)> { - let artifact_hash = blake2b_256(&artifact_binary).iter().map(|b| format!("{:02x}", b)).collect::(); - - // Find a spendable capacity cell. - let capacity_input = self.find_capacity_for_deploy(&deployer_lock, &artifact_binary, fee_shannons)?; - - let spec = DeployArtifactSpec { - name: name.to_string(), - artifact_binary, - artifact_hash, - deployer_lock: deployer_lock.clone(), - capacity_input: capacity_input.input, - capacity_input_shannons: capacity_input.capacity_shannons, - capacity_input_data: capacity_input.data, - type_id_hash_type: ScriptHashType::Type, - type_script: None, - cell_deps: Vec::new(), - header_deps: Vec::new(), - fee_shannons, - }; - - let (tx, deploy_evidence) = build_deploy_transaction(&spec)?; - - // Estimate cycles. - let estimate = self.client.estimate_cycles(to_rpc_transaction(&tx)).ok(); - let estimate_cycles = estimate.as_ref().map(|e| e.cycles.value()); - - // Test tx-pool acceptance. - let tx_pool_accepted = self.client.test_tx_pool_accept(to_rpc_transaction(&tx), Some(OutputsValidator::Passthrough)).is_ok(); - - // Submit. - let submitted = self.client.send_transaction(to_rpc_transaction(&tx), Some(OutputsValidator::Passthrough)).is_ok(); - let tx_hash = self.client.send_transaction(to_rpc_transaction(&tx), Some(OutputsValidator::Passthrough)).ok(); - - // Wait for commitment. - let committed = if let Some(ref hash) = tx_hash { self.wait_for_commitment(hash, 30, 500).ok() } else { None }; - - // Build manifest from committed evidence. - let manifest = if let Some(ref hash) = tx_hash { - let mut hash_bytes = [0u8; 32]; - hash_bytes.copy_from_slice(hash.as_bytes()); - build_deployment_manifest_from_evidence(&deploy_evidence, &hash_bytes, 0) - } else { - build_deployment_manifest_from_evidence(&deploy_evidence, &[0u8; 32], 0) - }; - - let mut signing = SigningAdapter::new(vec!["deployer".to_string()]); - if submitted { - signing.mark_signed(); - } - - let lifecycle = TransactionLifecycleEvidence { - schema: "cellscript-ckb-tx-lifecycle-v0.19", - deploy_evidence: Some(deploy_evidence), - action_evidence: None, - signing: signing.evidence(), - capacity: Some(CapacityBridge::new(deployer_lock, 1000).evidence()), - estimate_cycles, - tx_pool_accepted, - submitted, - committed, - }; - - Ok((manifest, lifecycle)) - } - - /// Build a headless deploy transaction without submitting it. - /// - /// Use this when you want to inspect the transaction before submitting, - /// or when you need to add signing externally. - pub fn build_deploy( - &self, - name: &str, - artifact_binary: Bytes, - deployer_lock: Script, - fee_shannons: u64, - ) -> Result<(TransactionView, ResolvedDeployEvidence)> { - let artifact_hash = blake2b_256(&artifact_binary).iter().map(|b| format!("{:02x}", b)).collect::(); - - let capacity_input = self.find_capacity_for_deploy(&deployer_lock, &artifact_binary, fee_shannons)?; - - let spec = DeployArtifactSpec { - name: name.to_string(), - artifact_binary, - artifact_hash, - deployer_lock, - capacity_input: capacity_input.input, - capacity_input_shannons: capacity_input.capacity_shannons, - capacity_input_data: capacity_input.data, - type_id_hash_type: ScriptHashType::Type, - type_script: None, - cell_deps: Vec::new(), - header_deps: Vec::new(), - fee_shannons, - }; - - build_deploy_transaction(&spec) - } - // ---- Action workflow ---- /// Load an action plan from a file path. @@ -1968,22 +1861,45 @@ impl CellScriptAdapter { self.client.get_transaction(tx_hash.clone()) } - // ---- Internal helpers ---- + /// Fail closed unless the connected node is CKB mainnet. + pub fn require_mainnet(&self) -> Result<()> { + let consensus = self.client.get_consensus()?; + if consensus.genesis_hash != ckb_sdk::constants::GENESIS_BLOCK_HASH_MAINNET { + bail!( + "mainnet required: connected chain {} has genesis {}, expected {}", + consensus.id, + consensus.genesis_hash, + ckb_sdk::constants::GENESIS_BLOCK_HASH_MAINNET + ); + } + Ok(()) + } - fn find_capacity_for_deploy(&self, _lock: &Script, artifact: &[u8], fee: u64) -> Result { - // TODO: use CellCollector to find a real spendable cell. - // For now, requires the caller to provide capacity input manually - // via the lower-level `build_deploy_transaction` API. - let _ = (_lock, artifact, fee); - bail!("automatic live-cell collection is not yet implemented; use build_deploy_transaction() with a manually provided DeployArtifactSpec") + /// Resolve and validate a live, pure-capacity input owned by `expected_lock`. + /// + /// State-bearing Cells are rejected: the deployment flow must not silently + /// discard a Type Script or transform non-empty input data into untyped data. + pub fn resolve_pure_capacity_input(&self, out_point: &OutPoint, expected_lock: &Script) -> Result<(u64, Bytes)> { + let response = self.client.get_live_cell(out_point.clone().into(), true)?; + if response.status != "live" { + bail!("capacity input is not live (status: {})", response.status); + } + let cell = response.cell.ok_or_else(|| anyhow::anyhow!("live capacity input response is missing cell data"))?; + let output: CellOutput = cell.output.into(); + if output.lock() != *expected_lock { + bail!("capacity input lock does not match the requested deployer lock"); + } + if output.type_().to_opt().is_some() { + bail!("capacity input must not have a Type Script"); + } + let data = cell.data.ok_or_else(|| anyhow::anyhow!("capacity input RPC response omitted cell data"))?.content.into_bytes(); + if !data.is_empty() { + bail!("capacity input must have empty data"); + } + Ok((output.capacity().unpack(), data)) } -} -/// A found capacity input cell for deployment. -struct CapacityInput { - input: CellInput, - capacity_shannons: u64, - data: Bytes, + // ---- Internal helpers ---- } pub fn sample_resolved_action_tx() -> ResolvedActionTx { @@ -2352,16 +2268,24 @@ mod tests { } #[test] - fn places_cellscript_entry_payload_without_hiding_lock_signatures() { - let base = WitnessArgs::new_builder().lock(Some(Bytes::from(vec![0x77u8; 65])).pack()).build(); + fn places_cellscript_entry_payload_before_signing() { + let base = WitnessArgs::new_builder().lock(Some(Bytes::from(vec![0u8; 65])).pack()).build(); let payload = Bytes::from(b"CSARGv1\0\x4d\0\0\0\0\0\0\0".to_vec()); - let witness = place_entry_witness_payload(&base, WitnessPlacement::InputType, payload.clone()).unwrap(); + let placement = EntryWitnessPlacementAbi::WitnessArgsInputTypeV2; + assert_eq!(placement.name(), "cellscript-witnessargs-input-type-v2"); + let witness = place_entry_witness_payload_before_signing(&base, placement, payload.clone()).unwrap(); assert_eq!(witness.lock().to_opt().expect("lock preserved").raw_data().len(), 65); assert_eq!(witness.input_type().to_opt().expect("entry payload").raw_data(), payload); assert!(witness.output_type().to_opt().is_none()); - let error = place_entry_witness_payload(&base, WitnessPlacement::Lock, Bytes::from(vec![1u8])).unwrap_err().to_string(); - assert!(error.contains("lock signatures must stay explicit"), "{error}"); + let occupied = witness; + let error = place_entry_witness_payload_before_signing(&occupied, placement, payload.clone()).unwrap_err().to_string(); + assert!(error.contains("refusing to overwrite WitnessArgs.input_type"), "{error}"); + + let error = place_entry_witness_payload_before_signing(&base, placement, Bytes::from_static(b"not-cellscript")) + .unwrap_err() + .to_string(); + assert!(error.contains("must start with CSARGv1"), "{error}"); } #[test] @@ -2545,15 +2469,48 @@ mod tests { #[test] fn deploy_data_hash_type_uses_artifact_hash_and_no_type_script() { let mut spec = sample_deploy_spec(); - spec.type_id_hash_type = ScriptHashType::Data2; + spec.type_id_hash_type = ScriptHashType::Data1; let (tx, evidence) = build_deploy_transaction(&spec).unwrap(); assert!(tx.outputs().get(0).unwrap().type_().to_opt().is_none()); assert_eq!(evidence.code_hash, blake2b_256(&spec.artifact_binary).to_vec()); - assert_eq!(evidence.hash_type, "data2"); + assert_eq!(evidence.hash_type, "data1"); assert!(evidence.type_id_args.is_empty()); } + #[test] + fn deploy_rejects_artifact_hash_mismatch() { + let mut spec = sample_deploy_spec(); + spec.artifact_hash = "00".repeat(32); + let error = build_deploy_transaction(&spec).unwrap_err().to_string(); + assert!(error.contains("artifact hash mismatch"), "{error}"); + } + + #[test] + fn deploy_canonicalizes_equivalent_artifact_hash_text() { + let mut spec = sample_deploy_spec(); + spec.artifact_hash = format!("0x{}", spec.artifact_hash.to_ascii_uppercase()); + let (_, evidence) = build_deploy_transaction(&spec).unwrap(); + assert_eq!(evidence.artifact_hash, hex::encode(blake2b_256(&spec.artifact_binary))); + } + + #[test] + fn deploy_uses_standard_secp_signing_placeholder() { + let spec = sample_deploy_spec(); + let (tx, _) = build_deploy_transaction(&spec).unwrap(); + let witness = WitnessArgs::from_slice(tx.witnesses().get(0).unwrap().raw_data().as_ref()).unwrap(); + let lock = witness.lock().to_opt().expect("secp placeholder lock").raw_data(); + assert_eq!(lock.as_ref(), &[0u8; 65]); + } + + #[test] + fn deploy_rejects_fee_below_default_relay_floor() { + let mut spec = sample_deploy_spec(); + spec.fee_shannons = 1; + let error = build_deploy_transaction(&spec).unwrap_err().to_string(); + assert!(error.contains("policy floor"), "{error}"); + } + #[test] fn deploy_rejects_empty_artifact() { let mut spec = sample_deploy_spec(); diff --git a/crates/cellscript-fiber-adapter/Cargo.toml b/crates/cellscript-fiber-adapter/Cargo.toml index 41133ea6..0ec961fb 100644 --- a/crates/cellscript-fiber-adapter/Cargo.toml +++ b/crates/cellscript-fiber-adapter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cellscript-fiber-adapter" -version = "0.22.0" +version = "0.24.0" edition = "2024" rust-version = "1.97.1" publish = false @@ -23,7 +23,7 @@ regex = "1" reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -serde_yaml = "0.9" +serde_yaml_ng = "0.10" thiserror = "1.0" [dev-dependencies] diff --git a/crates/cellscript-fiber-adapter/src/deployment.rs b/crates/cellscript-fiber-adapter/src/deployment.rs index 0efcff1f..484f56de 100644 --- a/crates/cellscript-fiber-adapter/src/deployment.rs +++ b/crates/cellscript-fiber-adapter/src/deployment.rs @@ -491,7 +491,7 @@ mod tests { selected_type: "Asset".to_string(), selected_invariant: "supply".to_string(), selected_field: "quantity".to_string(), - compiler_version: "0.22.0".to_string(), + compiler_version: cellscript::VERSION.to_string(), metadata_schema_version: cellscript::METADATA_SCHEMA_VERSION, source_hash: format!("0x{}", "01".repeat(32)), artifact_hash: format!("0x{}", hex::encode(cellscript::ckb_blake2b256(data))), diff --git a/crates/cellscript-fiber-adapter/src/fiber_config.rs b/crates/cellscript-fiber-adapter/src/fiber_config.rs index 8734c29e..9fb23445 100644 --- a/crates/cellscript-fiber-adapter/src/fiber_config.rs +++ b/crates/cellscript-fiber-adapter/src/fiber_config.rs @@ -207,14 +207,14 @@ pub fn materialize_fiber_config(base_yaml: &str, configs: &[FiberUdtArgInfo]) -> for config in configs { config.validate()?; } - let mut document: serde_yaml::Value = serde_yaml::from_str(base_yaml)?; + let mut document: serde_yaml_ng::Value = serde_yaml_ng::from_str(base_yaml)?; let root = document.as_mapping_mut().ok_or_else(|| anyhow::anyhow!("Fiber config root must be a YAML mapping"))?; let ckb = root - .get_mut(serde_yaml::Value::String("ckb".to_string())) - .and_then(serde_yaml::Value::as_mapping_mut) + .get_mut(serde_yaml_ng::Value::String("ckb".to_string())) + .and_then(serde_yaml_ng::Value::as_mapping_mut) .ok_or_else(|| anyhow::anyhow!("Fiber config must contain a ckb mapping"))?; - ckb.insert(serde_yaml::Value::String("udt_whitelist".to_string()), serde_yaml::to_value(configs)?); - Ok(serde_yaml::to_string(&document)?) + ckb.insert(serde_yaml_ng::Value::String("udt_whitelist".to_string()), serde_yaml_ng::to_value(configs)?); + Ok(serde_yaml_ng::to_string(&document)?) } fn json_string(value: &str) -> anyhow::Result { @@ -307,7 +307,7 @@ mod tests { let (config, _) = build_fiber_udt_config("sample::Asset", &script, Some(42), vec![direct_dep()]).unwrap(); let base = "fiber:\n chain: dev.toml\nrpc:\n listening_addr: 127.0.0.1:21714\nckb:\n rpc_url: http://127.0.0.1:8114\n udt_whitelist:\n - name: stale\n"; let rendered = materialize_fiber_config(base, &[config]).unwrap(); - let parsed: serde_yaml::Value = serde_yaml::from_str(&rendered).unwrap(); + let parsed: serde_yaml_ng::Value = serde_yaml_ng::from_str(&rendered).unwrap(); assert_eq!(parsed["fiber"]["chain"].as_str(), Some("dev.toml")); assert_eq!(parsed["rpc"]["listening_addr"].as_str(), Some("127.0.0.1:21714")); assert_eq!(parsed["ckb"]["rpc_url"].as_str(), Some("http://127.0.0.1:8114")); diff --git a/crates/cellscript-tools/Cargo.toml b/crates/cellscript-tools/Cargo.toml new file mode 100644 index 00000000..93664e1f --- /dev/null +++ b/crates/cellscript-tools/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "cellscript-tools" +version = "0.24.0" +edition = "2024" +rust-version = "1.97.1" +publish = false +description = "Release, audit, and validation tooling for the CellScript workspace" +license = "MIT" + +[[bin]] +name = "cellscript-tools" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0" +blake2b-ref = "0.3" +ckb-jsonrpc-types = "1.0.0" +ckb-types = "1.0.0" +clap = { version = "=4.5.49", features = ["derive"] } +hex = "0.4" +hex-literal = "0.4" +k256 = { version = "0.13.4", default-features = false, features = ["schnorr"] } +percent-encoding = "2" +regex = "1" +reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sha2 = "0.10" +time = { version = "0.3", features = ["formatting", "local-offset"] } +toml = "0.8" +wait-timeout = "0.2" diff --git a/crates/cellscript-tools/fixtures/ckb_acceptance/transactions-v0.23.json b/crates/cellscript-tools/fixtures/ckb_acceptance/transactions-v0.23.json new file mode 100644 index 00000000..905f2cdd --- /dev/null +++ b/crates/cellscript-tools/fixtures/ckb_acceptance/transactions-v0.23.json @@ -0,0 +1,18177 @@ +{ + "schema": "cellscript-ckb-acceptance-transaction-recipes-v0.23", + "source_evidence": { + "compiler_edition": "2026", + "entry_witness_abi": "cellscript-entry-witness-v1", + "entry_witness_container": "ckb-molecule-witness-args-input-type", + "artifact_hashes_match_edition_2026_build_report": true, + "extracted_from_passed_local_devnet": true, + "production_hardening_gate_status": "passed", + "measurement_run_mode": "production" + }, + "transactions": { + "0x00409256e78106d58106d68a0fc529399530b444f5e73ebf74960616d208c2a4": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x006b521dc10d970574f21b5faf7d36e65b9242f7557bdf0f293020f759b2e568" + } + } + ], + "hash": "0x00409256e78106d58106d68a0fc529399530b444f5e73ebf74960616d208c2a4", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x6b1230cc7d06562c440c22b81e23c0cb7c253f5a1661ddfe23446ebe821353ba" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0xdf8475800", + "lock": { + "args": "0x", + "code_hash": "0x10f75e072356b123d3864ae553870a4759943c4ed71d373218ca17b611795020", + "hash_type": "data1" + }, + "type": { + "args": "0xa1", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x2540be400", + "lock": { + "args": "0x", + "code_hash": "0x6c3bf0d1bc162b2c71378ec3ea55d34121ab8c66480524e83cc8943de6b26555", + "hash_type": "data1" + }, + "type": { + "args": "0xa1", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x5354415445303031e8030000000000000500000000000000", + "0x05000000000000005354415445303031" + ], + "version": "0x0", + "witnesses": [ + "0x44000000100000001000000044000000300000004353415247763100592df79115780f5dc228bfc2e484ffd93922029e726eb4f1896cfc2bbb6af72c0500000000000000" + ] + }, + "0x014f954ce1b5dcd67562a3094bda3060e3807bd071a5da61a107a3c4c02716e2": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xa831ba0ff5d321de15b135872754b682e38d2ddd47c38d7315bce7f166e20ec4" + } + } + ], + "hash": "0x014f954ce1b5dcd67562a3094bda3060e3807bd071a5da61a107a3c4c02716e2", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x30596da5f51a5a9b2388bb40c5ae8011a74096b2a0f758784db69257d7b5b721" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0xba43b7400", + "lock": { + "args": "0x", + "code_hash": "0xee0a4d051aeb5c2c39df4eeeda6a55b8cb9ea79a964b905d3288c0d83138fabc", + "hash_type": "data1" + }, + "type": { + "args": "0x52", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x51", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0xff00000034000000540000005c0000007c0000007d0000009d000000a5000000a9000000aa000000ee000000f6000000fe000000081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd3080800000000000000be4bb273fc2295cd8466897942306079b3d1118c6150a000751ee6695e7d7c110064f944c22f0db7bfb29aa523b65cbb75a6a65b369febfbff0ffc17facacfe2dcf401000000000000000000000202000000be4bb273fc2295cd8466897942306079b3d1118c6150a000751ee6695e7d7c11edf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae1400000000000000d00700000000000000", + "0x8d00000018000000380000007c0000007d00000085000000081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd30802000000be4bb273fc2295cd8466897942306079b3d1118c6150a000751ee6695e7d7c11edf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae0200000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x0495a8b89852af2a653540ebe699453d04134738225430a174119ef7db3fa047": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xf99767aebf484c406de90a63140d8306eea1dbf509fb6b04f13f5594a27b4157" + } + } + ], + "hash": "0x0495a8b89852af2a653540ebe699453d04134738225430a174119ef7db3fa047", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xa9cef4087fceb9817020e4d1c51f0831a16fca7ec406021f4632886a98f0289b" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x7cda3da79fb46eaed62752444d9d4f666897861039c77bf94f0532fc43162a70", + "hash_type": "data1" + }, + "type": { + "args": "0x51", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x8d00000018000000380000007c0000007d00000085000000081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd308020000007d079c63043b805af33e1b72b25e83ba2897b47af215f9174932de79e9393d01edf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae0100000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x0538556ab99b85f0633cbb009edcc62be34efb26015f555c59d118424785c27b": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc1992e679cdcbc64bb722f94b5d226099b2b9ead0529e4ccf45833055f369b2a" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x2a9fbd7f43595d871d80e631baf1667f16d4e1cf6a44e85735c69684865db517" + } + } + ], + "hash": "0x0538556ab99b85f0633cbb009edcc62be34efb26015f555c59d118424785c27b", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x5e784733c02e52fe1d4c6996255dcfdbf2b792d69c36414970e539e90901d2b2" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x9502f9000", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0xf4", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x010000000000000086a76e2fc301d88cf9fed4a493b210edf5a21bc839886e7869bac99766ed6a5c280000000000000001" + ], + "version": "0x0", + "witnesses": [ + "0x4400000010000000100000004400000030000000435341524776310086a76e2fc301d88cf9fed4a493b210edf5a21bc839886e7869bac99766ed6a5c2800000000000000" + ] + }, + "0x073245d0e75464ee92f0f4a3264f989fe4ea3c11dac3e4a1abd6084ea8dc2305": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xdff9e4e52961c196f41c52c2d211468dfa6b3af8c4f194a02d4bc59bfe39d066" + } + } + ], + "hash": "0x073245d0e75464ee92f0f4a3264f989fe4ea3c11dac3e4a1abd6084ea8dc2305", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x30ec99674788122475be9ca8dc6a669a097535cac9d95a012e4cc78a1f6aba98" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xf7a0ff2f4e06b8af72ebbd3aa6a7e6ffe61d3ada8258397c6c224d217ae9d3cd", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x444444444444444444444444444444444444444444444444444444444444444411111111111111111111111111111111111111111111111111111111111111110001000000000000000000000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x08cbc0f0e4b4a1201e687be7ab5380399f3f971ab68bae873e7b6e5dde6dac8c": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x49ce2be6cbe254c0a78346d81c7b098e74f44601c4693a509ba36d4b3c681f69" + } + } + ], + "hash": "0x08cbc0f0e4b4a1201e687be7ab5380399f3f971ab68bae873e7b6e5dde6dac8c", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x1bb3cf00c66b3d7592593c6aa356d47b2c5d6dee93a8c759dfca50af1fad03ac" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x1ef8dbe9b2f531b18576d6ec194fae7e744ac501952706adcb6382d1deea04b4", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x665fa3d657391cb8819d2c9e91e3c0e6f82db7b371416f04e0b06332990457a511111111111111111111111111111111111111111111111111111111111111110064000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x09a58ddb0bb12c02eb2ca45b0d43f56eb40ebbd1a58fe5224831bb01d8f394f1": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xd3fd27a5c0ce54a627bc8b585760471badce4816d4839254b64ded850b60bfba" + } + } + ], + "hash": "0x09a58ddb0bb12c02eb2ca45b0d43f56eb40ebbd1a58fe5224831bb01d8f394f1", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x264f70265a964d3719a86579311a2f1adb0b6de22fcf524908c46b500dca2770" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xafc1309eef287471f5feb8e01a6bad53624851301ed7e5117b803290c2362c80", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "0x0b9fa823515ffce03746d1c4344db4e1e50bad3668c81088b7ca5eafc6040913": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x06fc2217647967fbbbb43852493f249d782b073b114cc29bbdca5e13bf830cfe" + } + } + ], + "hash": "0x0b9fa823515ffce03746d1c4344db4e1e50bad3668c81088b7ca5eafc6040913", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x21aaab6b34b6f7bd4c7672fe16baa2deacfe062cab95a9104c9c3d32de16165f" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x9502f9000", + "lock": { + "args": "0x60", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x61", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x70", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x62", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x71", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x62", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x60", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x62", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4c41554e434830311027000000000000e803000000000000", + "0x0a000000000000004c41554e43483031", + "0x14000000000000004c41554e43483031", + "0xca030000000000004c41554e43483031" + ], + "version": "0x0", + "witnesses": [ + "0xa40000001000000010000000a40000009000000043534152477631004c41554e434830311027000000000000e8030000000000006de4ed8e16e7400834559efc852ddca87762f738a5dd93853168a4cc390cdbf013e1988a98e068eb6128d8ad60febbec52113d16742917b29e9d0b753eb8e9710a000000000000006d7122fbd537293591fef620082e9213b875ee6f8a926497a533aee6f4ff93561400000000000000" + ] + }, + "0x0cba9e700c8a662884ab05066a027cc22b03e79ac0facfcbe0d002acd391bc7f": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x04ff3d5eebf352f6edd435d3c42bba62a2b84b65b79504643548e80b2d4d150c" + } + } + ], + "hash": "0x0cba9e700c8a662884ab05066a027cc22b03e79ac0facfcbe0d002acd391bc7f", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xfc58dabd5905ecf2b854962eb2da28fddf66936a79180d756cc0e027e0c315ed" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x67c60bfc34958c28b1d5277be6995af9920d480f60ded40806eaf173c704d10d", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [] + }, + "0x0ea342c485118cb69e4ecbc668e9c916b479fd6be1a284ef27db92c29f0b7141": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x2da50f9b01999a58735ab822ba3bc2b743d5d297b196e77f7cbbe84bc2766aac" + } + } + ], + "hash": "0x0ea342c485118cb69e4ecbc668e9c916b479fd6be1a284ef27db92c29f0b7141", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xb33e7360ff7ccaba5bcf51715aa8987ae9413998e5fe860f10b52b2f4fdff670" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xaebdd9d7c1cd1a9b581bc3a42a6c5f40d5b41f2ee637f7a1b4e4eef38500c349", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x8d00000018000000380000007c0000007d00000085000000444444444444444444444444444444444444444444444444444444444444444402000000111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222220200000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x0ed6bca9b7b257ea7fd5b25d4f686479d892da1349a94a24c20be92d396dcaa3": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xb402243a1be68cc9f3dd8f703010b6faadc4a98ac26dc19d4cca2703edb335a3" + } + } + ], + "hash": "0x0ed6bca9b7b257ea7fd5b25d4f686479d892da1349a94a24c20be92d396dcaa3", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x178476fefdc74a41929f6858dd8f6fe4094ee863ba6e8defbff459070e2f18dd" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x41", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc41256455354303030310a00000000000000640000000000000001" + ], + "version": "0x0", + "witnesses": [ + "0x550000001000000010000000550000004100000043534152477631004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc41256455354303030310a00000000000000640000000000000001" + ] + }, + "0x0fc3217536599ec792bf62408185b2a5c32103c994bd312fa19dbb7d02a957ac": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xec73cb2253c130c509a2fb0fa9557411c1bd607b51eb3ed20153393ca8c72157" + } + } + ], + "hash": "0x0fc3217536599ec792bf62408185b2a5c32103c994bd312fa19dbb7d02a957ac", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xcebe4151dcc7779bedbc9409ac44eca93448508b0770b287b1c9f8de763dfa30" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xa34564aa114e28106b02f63243b50f5135adc633ff7a74e735d27e9404bf0710", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [] + }, + "0x100622e71992fc7f45b3e46ff83c5f30748a75144c05ed015a39acaa2aefe84e": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x6aa5c60e30df163649a614d6637228dab6e507b00d3a8fd6bd93b5cc525163e3" + } + } + ], + "hash": "0x100622e71992fc7f45b3e46ff83c5f30748a75144c05ed015a39acaa2aefe84e", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xbf1baada9a3c1c4dcb21d5976d2a309d27e94129b531610ad17412013ebad36b" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xc5eb6cb03878c77e75b8462c561205189574fc4677e5538cc9136c10ab9921da", + "hash_type": "data1" + }, + "type": { + "args": "0x01", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xc5eb6cb03878c77e75b8462c561205189574fc4677e5538cc9136c10ab9921da", + "hash_type": "data1" + }, + "type": { + "args": "0x02", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xc5eb6cb03878c77e75b8462c561205189574fc4677e5538cc9136c10ab9921da", + "hash_type": "data1" + }, + "type": { + "args": "0x03", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc4126bd943617d9642f1dad174449a64bb34f325c84fc257eb8fff578e00a50b0a9c0000000000000000000000000000000000", + "0x544f4b454e3030312a000000000000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412", + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc4126bd943617d9642f1dad174449a64bb34f325c84fc257eb8fff578e00a50b0a9c000000000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x1028526cfa99595cebeff3b2745d0bd5a2ef4003cb96a974c3766829f80594d5": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x204eecf4d7006584af493c734f69488ee4ca52dd1c2e7dd7ac075f8f5be3ac1e" + } + } + ], + "hash": "0x1028526cfa99595cebeff3b2745d0bd5a2ef4003cb96a974c3766829f80594d5", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x7bdb141dc64f601e73012e4488dd97f98aba20178792f4f35f66ae5f6da31370" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x2eb610fca034a18303d192bcbf52ba0f68e6ee8b1cafa90b200d5edad55257ef", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x2eb610fca034a18303d192bcbf52ba0f68e6ee8b1cafa90b200d5edad55257ef", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x2eb610fca034a18303d192bcbf52ba0f68e6ee8b1cafa90b200d5edad55257ef", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x2eb610fca034a18303d192bcbf52ba0f68e6ee8b1cafa90b200d5edad55257ef", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc41205b4932b537bf8d7f006cb31700e23021bef615b652238322093b5534e7d71620064000000000000000000000000000000", + "0x61616161616161616161616161616161616161616161616161616161616161615151515151515151515151515151515151515151515151515151515151515151006e000000000000000000000000000000", + "0x626262626262626262626262626262626262626262626262626262626262626252525252525252525252525252525252525252525252525252525252525252520078000000000000000000000000000000", + "0x636363636363636363636363636363636363636363636363636363636363636353535353535353535353535353535353535353535353535353535353535353530082000000000000000000000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x3c01000010000000100000003c0100002801000043534152477631004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc41261616161616161616161616161616161616161616161616161616161616161616262626262626262626262626262626262626262626262626262626262626262636363636363636363636363636363636363636363636363636363636363636305b4932b537bf8d7f006cb31700e23021bef615b652238322093b5534e7d716251515151515151515151515151515151515151515151515151515151515151515252525252525252525252525252525252525252525252525252525252525252535353535353535353535353535353535353535353535353535353535353535364000000000000006e0000000000000078000000000000008200000000000000" + ] + }, + "0x10a51089ec0c33634bb2ed3d14f89840602faf9866fea6d2d4e51b5d7cf98b07": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc0bcb97f3c6a8c60d29eb5ed52c18597b102a5b43a1694a53a31d079a8814a95" + } + } + ], + "hash": "0x10a51089ec0c33634bb2ed3d14f89840602faf9866fea6d2d4e51b5d7cf98b07", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xd427ffbf8a602f10b6b1c845f50683fbc468d636c9749080b653c671827e22dd" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xc1e7dce634480aceedc7bd5dfbb7df1e5951cd945fb0d10cb8ea70968af0443b", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412cb1122061d0f5f32c0026f47daff56cec9b2941ce08f87e1c0d8f2bbb71561e80064000000000000000000000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x640000001000000010000000640000005000000043534152477631004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412cb1122061d0f5f32c0026f47daff56cec9b2941ce08f87e1c0d8f2bbb71561e86400000000000000" + ] + }, + "0x115b8ecbcb808b3b25b5f9cbc4883d27337aea43395ded9325a2db79ff74e71d": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x0ed6bca9b7b257ea7fd5b25d4f686479d892da1349a94a24c20be92d396dcaa3" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x8ad8c938473f108cf363d556d16de535af96f3ad0d3bc6be6893da0a11e8a96d" + } + } + ], + "hash": "0x115b8ecbcb808b3b25b5f9cbc4883d27337aea43395ded9325a2db79ff74e71d", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x8f31a148d525d4d627eda45d969275fb7966b3a1f0e425ba8bc1dbb441930b23" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x38a050dc2947e5bae1eb7526523ee43f60aecc0289e9a2e5c99ae5a46fd00e98" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x7902ead8098947fb0d9cf87ed357d821d21c0e85505621627fbd81d873290140", + "hash_type": "data1" + }, + "type": { + "args": "0x43", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x186046f747", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x007f263230cfbce124ee4756d36638c2577bb5e31459b4ecc8d1a3ca81acbceaea4d00000000000000000000000000000000000000000000000a0000000000000064000000000000005645535430303031", + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631007f263230cfbce124ee4756d36638c2577bb5e31459b4ecc8d1a3ca81acbceaea", + "0x" + ] + }, + "0x1213c894962b0b93e2fd49eb38ba5121b5df709d1d78ee888b4a060534f99ab9": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x99bd2cc55653377b2109baa3f88393a406c039e1fdf0703dcb782552e3ac16eb" + } + } + ], + "hash": "0x1213c894962b0b93e2fd49eb38ba5121b5df709d1d78ee888b4a060534f99ab9", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xd7667380b5d51d175c28daf8373ec12ed820e1fa9db6e6b4b0b8382e5ca9f8da" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x9da2791f3f0a46790e4b187a1d1dd15813f2c14883138631255c58b495a845d8", + "hash_type": "data1" + }, + "type": { + "args": "0x51", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x8d00000018000000380000007c0000007d00000085000000081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd308020000004d594af7f4d8c8158e0225a7d1a80903f7ea8df3f81bc67791af289b03ae879dedf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae0200000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x1231896def8739036e5e85f79df05e268e5900cf8285d1ed7601157a3e0cc38e": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x01c2a831918e3b54119d0952e4db1e3ebf65d73cec2c2bc3d9051fc0728f45c2" + } + } + ], + "hash": "0x1231896def8739036e5e85f79df05e268e5900cf8285d1ed7601157a3e0cc38e", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x1", + "tx_hash": "0x00409256e78106d58106d68a0fc529399530b444f5e73ebf74960616d208c2a4" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x2540be400", + "lock": { + "args": "0x", + "code_hash": "0x6c976866fb9c343bd28922b92aca893f292fed889e52b75a59a10f738b31f4e7", + "hash_type": "data1" + }, + "type": { + "args": "0xa1", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x05000000000000005354415445303031" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631000545322f195fd3db3fa6a39e1e053fef4e3fa14589f8a8c41fc2b58b4028afab" + ] + }, + "0x1535a036000b44931b13d0b9248ff807dcb50831b85c97aa6a6bb54e454c3d39": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xce13932ab95d93c1314a4d502849177e49ae562fef4b548150bba05bb04896b4" + } + } + ], + "hash": "0x1535a036000b44931b13d0b9248ff807dcb50831b85c97aa6a6bb54e454c3d39", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x3682c9124a707c7882a88d8e30916895d83e6989ee0538f9f4e3393cbed1c523" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x1", + "tx_hash": "0x3682c9124a707c7882a88d8e30916895d83e6989ee0538f9f4e3393cbed1c523" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x2", + "tx_hash": "0x3682c9124a707c7882a88d8e30916895d83e6989ee0538f9f4e3393cbed1c523" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x9502f9000", + "lock": { + "args": "0x", + "code_hash": "0xee46f625f63eaccefbc7c9bf1393a21295d1d21e712fc704523d6f349a39ca26", + "hash_type": "data1" + }, + "type": { + "args": "0x69", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x66", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x6a", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x0c2e7196a2c57e84d184146a4c2fae90ebbee6868385b48f6e790058d4cfb42149f070beeb4c781ae4867e862894a1678ed756948939c667bafbd6da9849e353414d4d4130303031414d4d42303030316e00000000000000dc000000000000004c040000000000001e00", + "0x64276f149001c22120e40a1153609d173a6125e6516694a465d0b1f0cb6d0ed264000000000000001c854ee8e7bc04b2afb2e79f831ead772e8f6c55bfb651f5fb27ed2417284f22" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631001c854ee8e7bc04b2afb2e79f831ead772e8f6c55bfb651f5fb27ed2417284f22", + "0x", + "0x" + ] + }, + "0x15da87cfff34c6bfc7a7012177b4845fbdd717f50dd145d2772678981f5a14e4": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x7385b0cd1428d6b3de24c02748cd013790f75530ae9fe8bd125b74ba6388f97c" + } + } + ], + "hash": "0x15da87cfff34c6bfc7a7012177b4845fbdd717f50dd145d2772678981f5a14e4", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xba81c768f00b3ec0f4021965d5063059779fcc464e57ded1ca69acb85a0607f8" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xc6c1fd22456162ae2457a61bb7fd4f0ad0ac92955adbc6a8da10a0caf1900362", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [] + }, + "0x1747065ef33181c2ef81ca3e925090b9f8d10d68db884951cb0d13d62bb8761a": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xb547f36f187c1934d41fdd9aa8a0e855842721d14c598386bdc850d6b17b5517" + } + } + ], + "hash": "0x1747065ef33181c2ef81ca3e925090b9f8d10d68db884951cb0d13d62bb8761a", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x5b11491c1b93c0e770b10e40426842c0e334f81178685b1578c2a8acd3fa1c76" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xa983e55b6bf70b5e24415864e1ab3640ccef502351a814d229b66d6738e0c83a", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0xff00000034000000540000005c0000007c0000007d0000009d000000a5000000a9000000aa000000ee000000f6000000fe000000444444444444444444444444444444444444444444444444444444444444444401000000000000001111111111111111111111111111111111111111111111111111111111111111003333333333333333333333333333333333333333333333333333333333333333f401000000000000000000000202000000111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222220a00000000000000d00700000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x17606461a3d98871a31a1d2dc71e0e81e47c2fb246665a0c19f207255b32f70a": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x7316d0640df6e12bf34469505237b41ef4ef81dd1d7cbe667d2bd929928a8ee9" + } + } + ], + "hash": "0x17606461a3d98871a31a1d2dc71e0e81e47c2fb246665a0c19f207255b32f70a", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x4a3e569f693934dcfca435644132ef1a44a29275ca54814354bb783db8a7ca7d" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x2e90edd000", + "lock": { + "args": "0x", + "code_hash": "0x540be7d1575ea9e60a6df5678ec2e4dc857edb687f1ad2f8359317be2288ff60", + "hash_type": "data1" + }, + "type": { + "args": "0xf1", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x8d00000018000000380000007c0000007d0000008500000065dfc0aa4a73f351b42a0bad8e1e57f7befd37622ec7fc56087c9cad029905930200000086a76e2fc301d88cf9fed4a493b210edf5a21bc839886e7869bac99766ed6a5cc7b799a057ff213011b152a94d895d9e5b1eeb05e376f7122464ff11bc93207a0200000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x8d00000010000000100000008d00000079000000435341524776310065dfc0aa4a73f351b42a0bad8e1e57f7befd37622ec7fc56087c9cad02990593440000000200000086a76e2fc301d88cf9fed4a493b210edf5a21bc839886e7869bac99766ed6a5cc7b799a057ff213011b152a94d895d9e5b1eeb05e376f7122464ff11bc93207a020a00000000000000" + ] + }, + "0x189e7a1b87b0df7c8fb2117191bb6fd6fa7f33b4aac9f4a53edc5ca5cb913a6c": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x85999c8371807a66812a6db192e23c22335b1faf2cc3bcf873658c827ba80570" + } + } + ], + "hash": "0x189e7a1b87b0df7c8fb2117191bb6fd6fa7f33b4aac9f4a53edc5ca5cb913a6c", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xd54b6d7cbe380752b64a1276382ab3a2113a300ec6f4b9e11d56d68c3361b739" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xb3e0825b2df698b051e0a98c6ab4c0a645bfa1c53d9b592cabcd43359ba41a41" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xf865c651a7698fc1cb23b2531990494fde954fb6ebce096dc16ce6737195f4f1" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xa25a0a55fe281903800eaceda49e70c8263ccf871ac163c4682b4c503486582e" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x5d21dba000", + "lock": { + "args": "0x", + "code_hash": "0xb6d00cb658e0732961e4c25b5322160074ac41e52182067bc326353930063479", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [] + }, + "0x19f683cc8c1d057780fae566d09ef252abb98559730bc9c17e6bebc703240968": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xbe466bab7cff1e51bbd15ce13c297ff867f1b089231d2f4797f3e656f9f2fcdd" + } + } + ], + "hash": "0x19f683cc8c1d057780fae566d09ef252abb98559730bc9c17e6bebc703240968", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x5debff60864d562bb94f7020d6a1180c7736815ee352e48c9905b920bc0cdf31" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x1", + "tx_hash": "0x5debff60864d562bb94f7020d6a1180c7736815ee352e48c9905b920bc0cdf31" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x2a00000000000000544f4b454e303031" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412", + "0x" + ] + }, + "0x1c5ec34e3022bdcee5b055f54102be7267a807b4baf9885b20efd665ba5ccbd9": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xf8ee78ee63762e2c05952e54e460b90a506c4160c9e4d420f83246162712be43" + } + } + ], + "hash": "0x1c5ec34e3022bdcee5b055f54102be7267a807b4baf9885b20efd665ba5ccbd9", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x32b4ee6800c6df5a7e795948fd42fdf5a21ecb453259903d0abad9a8706dadad" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x967d150b17ee92167fda6eb3b28e453cfc084f19c7cbfc773bb3f1b93d4dea61", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x860000001c000000350000003d0000005d000000650000006d00000015000000416363657074616e636520436f6c6c656374696f6e04000000414350543b27b52bcff21bb54b5ab2951b1ee1153fa29982ff473bf003e6097276b6d55c0a00000000000000e80300000000000015000000636b623a2f2f63656c6c7363726970742f6e66742f" + ], + "version": "0x0", + "witnesses": [] + }, + "0x1c8c4325505326f747420de5e8560c32794f3e1ef786c38d1bcd5b186c669784": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x92ba4f3a9f6ef2ef017253e99a5769579a4d9af3cb0b5bfeaf674c73f73e022f" + } + } + ], + "hash": "0x1c8c4325505326f747420de5e8560c32794f3e1ef786c38d1bcd5b186c669784", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x4dbde6eb4499b366a69afa2f677fe589f0cf9fd9d5892598771aecad786a4c38" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x9502f9000", + "lock": { + "args": "0x", + "code_hash": "0x10f75e072356b123d3864ae553870a4759943c4ed71d373218ca17b611795020", + "hash_type": "data1" + }, + "type": { + "args": "0x91", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0xa0", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x92", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0xa1", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x92", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0xa2", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x92", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0xa3", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x92", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x9502f9000", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x94", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x10f75e072356b123d3864ae553870a4759943c4ed71d373218ca17b611795020", + "hash_type": "data1" + }, + "type": { + "args": "0x95", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x10f75e072356b123d3864ae553870a4759943c4ed71d373218ca17b611795020", + "hash_type": "data1" + }, + "type": { + "args": "0x92", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4c41554e434830311027000000000000e803000000000000", + "0x0a000000000000004c41554e43483031", + "0x14000000000000004c41554e43483031", + "0x1e000000000000004c41554e43483031", + "0x28000000000000004c41554e43483031", + "0x0e837c401395a2f97c5b6c58fb3a7b3f989b392dc5811476208a5a05c6700503a7b2fc0390856f4faf9859a5fc0400e152e6885041ccbb362a2afba422d5636c4c41554e434830315041495230303031f401000000000000fa0000000000000061010000000000001e00", + "0x54c2bd6d1bbb50c7263f7bde6016ed68f7d316f4655b715730c2562117010c226101000000000000d13ecf0c119d1765ff5289703ae991ab33966c8b60d3cef4689c10e5e2a85bd0", + "0x90010000000000004c41554e43483031" + ], + "version": "0x0", + "witnesses": [ + "0xfe0000001000000010000000fe000000ea00000043534152477631004c41554e434830311027000000000000e803000000000000f4010000000000001e00d13ecf0c119d1765ff5289703ae991ab33966c8b60d3cef4689c10e5e2a85bd0e8dc66e3889a831192e3875bf3e7e515f58af8b54977e276cb9a48e5580215190a00000000000000a170ea85f6abdedfaf0a65e938edef518dc415a34d89e29f9040b9d3c310becd14000000000000009e9aa836257cc9fd6746e7ec5a1094ee6086bea1db3b0694de51dfb35eab1df01e00000000000000c161ea4e831cc80a99d06c05717f807385040bf90533147f9f8c65ac98669cee2800000000000000" + ] + }, + "0x1d50df7be4e0dc58deab8432cbcc769e2d8d00ff832dd9f6c55522f40c9360ed": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x6b76a471c376d588ebcc61b7ace0fd489d5015ce27ca1d261927a05e12c35e3f" + } + } + ], + "hash": "0x1d50df7be4e0dc58deab8432cbcc769e2d8d00ff832dd9f6c55522f40c9360ed", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x8e884dba5cfbc95c6e63d17866f4568b4fb2f28003b49d51d98ce98042d5c3b1" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x9502f9000", + "lock": { + "args": "0x60", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x61", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x70", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x62", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x71", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x62", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x72", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x62", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x73", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x62", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x9502f9000", + "lock": { + "args": "0x60", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x64", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x60", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x65", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x60", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x62", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4c41554e434830311027000000000000e803000000000000", + "0x0a000000000000004c41554e43483031", + "0x14000000000000004c41554e43483031", + "0x1e000000000000004c41554e43483031", + "0x28000000000000004c41554e43483031", + "0x6e764046853b3e6e5b2eb2f2d03e0f9fa119bf5da110166c48fdce579102826a0ba02773d63b6fa4b0ed6ce7a50816e8ca93d78005ca27c02a2d05b8e46f3c2c4c41554e434830315041495230303031f401000000000000fa0000000000000061010000000000001e00", + "0xbd925708cc9329a2ed2eef184ee313c1ec455027844c8ea227b3e589e5221a9a61010000000000006de4ed8e16e7400834559efc852ddca87762f738a5dd93853168a4cc390cdbf0", + "0x90010000000000004c41554e43483031" + ], + "version": "0x0", + "witnesses": [ + "0xfe0000001000000010000000fe000000ea00000043534152477631004c41554e434830311027000000000000e803000000000000f4010000000000001e006de4ed8e16e7400834559efc852ddca87762f738a5dd93853168a4cc390cdbf013e1988a98e068eb6128d8ad60febbec52113d16742917b29e9d0b753eb8e9710a000000000000006d7122fbd537293591fef620082e9213b875ee6f8a926497a533aee6f4ff935614000000000000000e7da160d8e77e9274a6fa6f8243153d2bae61ddf817d97c42e9cc7861e1f8301e000000000000002193d72a508a8145c089e2c41bf0566c81b2088349a3d2a3656f774dc0b1ef552800000000000000" + ] + }, + "0x1db48d9dedbc8fbf3feb809f32e63986b8e07ebe639b8dae8a2c7f9ed0134ba1": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc779774afe4bbb92248ad5e6f91ba71ddfac20bda122802790702264c6d8975f" + } + } + ], + "hash": "0x1db48d9dedbc8fbf3feb809f32e63986b8e07ebe639b8dae8a2c7f9ed0134ba1", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x40461a7a9e89d6574d81d1c496cd2ada9191d4a10684f6df1a632805c20fa75d" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x23", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x000000000000000000000000000000000000000000000000000000000000000005000000000000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc4129600000000000000c8000000000000005041594d3030303100" + ], + "version": "0x0", + "witnesses": [ + "0x7c00000010000000100000007c000000680000004353415247763100000000000000000000000000000000000000000000000000000000000000000005000000000000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc41296000000000000005041594d30303031c800000000000000" + ] + }, + "0x1f7b418973fc0723338fc7f2ff45fbd774b0afe291bca5e12e501388fc0d1f8a": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xac7777ae99467a32a740f9f89776f0a5c1a17808c49e29e69a1b193d9f751e1b" + } + } + ], + "hash": "0x1f7b418973fc0723338fc7f2ff45fbd774b0afe291bca5e12e501388fc0d1f8a", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x0856a83e4feca4395f9f76af9b0318351ccf128f10c85c68ec3da697594b8fea" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x6d3f4ff4a0012611cfad302ba4de186169643407d6fb5b302cbd11183f5b280e", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x000000000000000000000000000000000000000000000000000000000000000001000000000000001111111111111111111111111111111111111111111111111111111111111111f4010000000000000a0000000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x2119c94d3c5beaff73b1cd02bacc32f3f83a69baded172e851e65d5d8a52f3f4": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x9572797579e20683ff0f7c6fe0152a168a9c6bade9800dcb77751ff651df8478" + } + } + ], + "hash": "0x2119c94d3c5beaff73b1cd02bacc32f3f83a69baded172e851e65d5d8a52f3f4", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xcdbe4aeeaf08d0f6b320458cdcbaaf3bbb2479277afe0c16a4bfc643736a99a3" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xe383a15d1b2e326df64363cab5778e79bd5edef2bc97a2ae3d4c77ab072752e3", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x980000001c0000003c0000005c0000006b0000007300000097000000444444444444444444444444444444444444444444444444444444444444444411111111111111111111111111111111111111111111111111111111111111116f70657261746f72207265766965770a0000000000000001000000111111111111111111111111111111111111111111111111111111111111111100" + ], + "version": "0x0", + "witnesses": [] + }, + "0x216dde4df2ea8fe1425edc0dedca51a7e00dd08d33941db55d205a18314c34af": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xaa5563e32d88035679d005517839675d1717431123ecccac41442e008f201abc" + } + } + ], + "hash": "0x216dde4df2ea8fe1425edc0dedca51a7e00dd08d33941db55d205a18314c34af", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x3318719ba10143a600ebda5a3f0b3a563c8f1d94d4c791831497295a4abcac74" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xe1350ee31a467cf4c84e39edfe45476a1ef4a77734cb0e48c4ef4e4e5c32d93b" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x513c43aa3e994cbdf9bcd7903f1e220882873ad830b3eb024cdeefe2ca3afcb3", + "hash_type": "data1" + }, + "type": { + "args": "0xd3", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0xd5", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0xd2", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x3850aba1ee6b423273975d12fb627dde7af42441026f269f09c44615a1a85b0845e5adc64633f8c455d0a0257610c5915b8223518dfa9beb1ae64502628b36b7414d4d4130303031414d4d42303030310a000000000000000f000000000000000c000000000000001e00", + "0x0300000000000000414d4d4230303031" + ], + "version": "0x0", + "witnesses": [ + "0x4400000010000000100000004400000030000000435341524776310002000000000000005019e51dad76aeffb28bfca8e1b6a9126043e66d35869f1610ce5f39d4014441", + "0x" + ] + }, + "0x226c0a2e34cedaa363a9d4b223982d2daf4fc419d302115e05e98396b3d68c9b": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x4d0c0cc1df3a9620a55de0fb0691025fb805e651cda66536e620aa7ff04bd2ed" + } + } + ], + "hash": "0x226c0a2e34cedaa363a9d4b223982d2daf4fc419d302115e05e98396b3d68c9b", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x43398ca152e78764ed64cf42b6a5f61da59b38f9087e9714c4cbb8a070f642db" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x104c533c00", + "lock": { + "args": "0x", + "code_hash": "0x204920209dab2137b0f75335063856cc1e84d28b41d1e31c2160614832de50ec", + "hash_type": "data1" + }, + "type": { + "args": "0x51", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x204920209dab2137b0f75335063856cc1e84d28b41d1e31c2160614832de50ec", + "hash_type": "data1" + }, + "type": { + "args": "0x52", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x8d00000018000000380000007c0000007d00000085000000000000000000000000000000000000000000000000000000000000000000000002000000cf8cbc02d90b9e9bca7eb320ce67edfcf62400cd2a8bfb81a8104cf3942bca74edf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae0201000000000000000a00000000000000", + "0xdf00000034000000540000005c0000007c0000007d0000009d000000a5000000c9000000ca000000ce000000d6000000de00000000000000000000000000000000000000000000000000000000000000000000000100000000000000cf8cbc02d90b9e9bca7eb320ce67edfcf62400cd2a8bfb81a8104cf3942bca7401d07ba215cc89ea5a359c4a8ab2a8a483caf0f466281ce46d2d5b862e8d8dcc3d000000000000000020000000d07ba215cc89ea5a359c4a8ab2a8a483caf0f466281ce46d2d5b862e8d8dcc3d02000000001400000000000000b40500000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x64000000100000001000000064000000500000004353415247763100cf8cbc02d90b9e9bca7eb320ce67edfcf62400cd2a8bfb81a8104cf3942bca74d07ba215cc89ea5a359c4a8ab2a8a483caf0f466281ce46d2d5b862e8d8dcc3d1400000000000000" + ] + }, + "0x264f70265a964d3719a86579311a2f1adb0b6de22fcf524908c46b500dca2770": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xd3fd27a5c0ce54a627bc8b585760471badce4816d4839254b64ded850b60bfba" + } + } + ], + "hash": "0x264f70265a964d3719a86579311a2f1adb0b6de22fcf524908c46b500dca2770", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xf81b8f3fa28f224801721f45024613cd91ff6d64ef6a1de494e265be03b2e9af" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xafc1309eef287471f5feb8e01a6bad53624851301ed7e5117b803290c2362c80", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x000000000000000000000000000000000000000000000000000000000000000002000000000000009097611a54d809e6d5cd011ef4511e4259dd6c5594dc8670598498cfd6f33551000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412fa00" + ], + "version": "0x0", + "witnesses": [] + }, + "0x2746ae89bf4c9c652cebc4119bbd5a9df9f721f9eedd64182d188bbc17e5d489": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xdb30f9d5f126ed97836bb953e06415433f7367a42dd24b0a6b53f934259b70c9" + } + } + ], + "hash": "0x2746ae89bf4c9c652cebc4119bbd5a9df9f721f9eedd64182d188bbc17e5d489", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xbf6aa2ba40fb3d8804ff896f05be4c8ef70e848d321b208c7136fbfefe9a3616" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xb68bc21ce6610e2236c25abb71e5dfef7272083f069416c9c9942040d792b13d" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x214893fb9dd10fe10e4a92ef80b06c126808256f524d308629c828a49c4327de", + "hash_type": "data1" + }, + "type": null + }, + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x214893fb9dd10fe10e4a92ef80b06c126808256f524d308629c828a49c4327de", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x544f4b454e30303164000000000000004444444444444444444444444444444444444444444444444444444444444444", + "0x444444444444444444444444444444444444444444444444444444444444444411111111111111111111111111111111111111111111111111111111111111110064000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x2ad1120afda308f8aabe45f3ace721125f268138917137a0e2681c435e47b6c6": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xd32db7375c2ca9b9df46c33c6d3c6ae4f1f86236633a3ad794086f2fa708f2e4" + } + } + ], + "hash": "0x2ad1120afda308f8aabe45f3ace721125f268138917137a0e2681c435e47b6c6", + "header_deps": [ + "0xb35487c7b0d7a3c1351f9bdfdf76e178b01c7f93d4cfbbb84e1d07e800090bec" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x4a2f379980234301b6755cdb05bbcf5d46f407f31a8fe7228bf2cce0c29cbc7c" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x318333f71531adb7109813cd89f757d76d7a1a8aebb79e93d800df8f4f0bc3c3", + "hash_type": "data1" + }, + "type": { + "args": "0x45", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x318333f71531adb7109813cd89f757d76d7a1a8aebb79e93d800df8f4f0bc3c3", + "hash_type": "data1" + }, + "type": { + "args": "0x43", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x50000000000000005645535430303031", + "0x01b250afae197267ab716da7baa7d3077d66d0bfb6286f19ab4d0698a90737666e640000000000000064000000000000000000000000000000000000000000000001000000000000005645535430303031" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "0x2b6f560e02fd1d710f9e47d3fb9771d37ae3b88362ebca48a822e3bee8e318f8": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x8c6940241808971b02b84d9bae41658d771003f1c281eb929b3aebd456b637d1" + } + } + ], + "hash": "0x2b6f560e02fd1d710f9e47d3fb9771d37ae3b88362ebca48a822e3bee8e318f8", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x331e2838307293ed62c8cc61101d0afe7de0e5e1cb0e14d1d369524fada9de22" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xc13b702287bf01246fd600189482d58fb1e8bdadce497124858e010750a4b4cd", + "hash_type": "data1" + }, + "type": null + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x", + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412a4dec0a82af4fefe10a664b9821c9e481efc2c7ca085bc82ed029a18282243140000000000000000000000000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x2b965ae1e6b62320a3cf421dc790d3a585e91e990f098ee61a63f21f8edfb669": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xdb690265e0a48d6b81c1b38a4af3366976ec7d73515a63128a97e320a4664175" + } + } + ], + "hash": "0x2b965ae1e6b62320a3cf421dc790d3a585e91e990f098ee61a63f21f8edfb669", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xd955f7836227960c50e27364ab37f530f14adb8b1f47dc683539619b3db9b7fb" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x4e52ecbe274e6cefae5fdb1eba4f1cee15d92370a2a991bce607e86bd12c2cae", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0xff00000034000000540000005c0000007c0000007d0000009d000000a5000000a9000000aa000000ee000000f6000000fe000000444444444444444444444444444444444444444444444444444444444444444401000000000000001111111111111111111111111111111111111111111111111111111111111111003333333333333333333333333333333333333333333333333333333333333333f401000000000000000000000202000000111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222220a00000000000000d00700000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x2c4b0dd0bcfb2f67d16e2dd6d86136b2d4c67596a13e419fed44981d1181bdf1": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x5722b21ca2e67ba87092e0fd80580aec5df50e30c37fb60efe7dcd24c426bca5" + } + } + ], + "hash": "0x2c4b0dd0bcfb2f67d16e2dd6d86136b2d4c67596a13e419fed44981d1181bdf1", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x216dde4df2ea8fe1425edc0dedca51a7e00dd08d33941db55d205a18314c34af" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x1", + "tx_hash": "0x3318719ba10143a600ebda5a3f0b3a563c8f1d94d4c791831497295a4abcac74" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x96e685a42e14cd65be8a3b9f6b63f1da1f37852c82ace8d9f711077b3754de48" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0xd3", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x513c43aa3e994cbdf9bcd7903f1e220882873ad830b3eb024cdeefe2ca3afcb3", + "hash_type": "data1" + }, + "type": { + "args": "0xd1", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x513c43aa3e994cbdf9bcd7903f1e220882873ad830b3eb024cdeefe2ca3afcb3", + "hash_type": "data1" + }, + "type": { + "args": "0xd2", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x16ed8284f2", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x3850aba1ee6b423273975d12fb627dde7af42441026f269f09c44615a1a85b0845e5adc64633f8c455d0a0257610c5915b8223518dfa9beb1ae64502628b36b7414d4d4130303031414d4d42303030310500000000000000080000000000000006000000000000001e00", + "0x0500000000000000414d4d4130303031", + "0x0700000000000000414d4d4230303031", + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c000000280000004353415247763100ed765278a6a68d4a9135c503f9ce0fc652eaad62fbb857b24bfdf9d6e0955a3c", + "0x", + "0x" + ] + }, + "0x2f2a53ea7336cf1d1b199a59b22148e7357d2e12846050a664cce5bf73094e80": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x361ddd4cf352f5a027b10ac34cde394aaf28cb92f71dc04f00e4837643111170" + } + } + ], + "hash": "0x2f2a53ea7336cf1d1b199a59b22148e7357d2e12846050a664cce5bf73094e80", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xcdee5c008e17b8d31dbc8472c5f3771a959ed40826789829f30c56062390104f" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xc5eb6cb03878c77e75b8462c561205189574fc4677e5538cc9136c10ab9921da", + "hash_type": "data1" + }, + "type": { + "args": "0xb1", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x7d84ad3da83e8ccab86a945f8bb0b74ebaa3d29369b01c5370dff1f340078ac06bd943617d9642f1dad174449a64bb34f325c84fc257eb8fff578e00a50b0a9c000b000000000000000000000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x640000001000000010000000640000005000000043534152477631007d84ad3da83e8ccab86a945f8bb0b74ebaa3d29369b01c5370dff1f340078ac06bd943617d9642f1dad174449a64bb34f325c84fc257eb8fff578e00a50b0a9c0b00000000000000" + ] + }, + "0x303ddda76da0fffdac25e53f5e93c2bfca6617c7281d37afd216d8f64410ceb3": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x969891936e2843de5c335b05af807ebe3ffc6d6f3e371e129c150b7944389b27" + } + } + ], + "hash": "0x303ddda76da0fffdac25e53f5e93c2bfca6617c7281d37afd216d8f64410ceb3", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xf8fad0a7c22671360bcbb8f74064995f7fc5dd371dcf699289d4e486aac23d0f" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x87c083fadb4ec6e5823e9bd76b000f7b98f4b374cea82ed6a528915317d8a59e", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x860000001c000000350000003d0000005d000000650000006d00000015000000416363657074616e636520436f6c6c656374696f6e040000004143505411111111111111111111111111111111111111111111111111111111111111110100000000000000e80300000000000015000000636b623a2f2f63656c6c7363726970742f6e66742f" + ], + "version": "0x0", + "witnesses": [] + }, + "0x31aa013f1e5e95cef0f8a5cc0a4dc6c069ad72d6a2989a7aa02e13c7d30576c8": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x1d3726f0eb930917dbb02cb08aa68622494014245a885c60e4d1df758f245b49" + } + } + ], + "hash": "0x31aa013f1e5e95cef0f8a5cc0a4dc6c069ad72d6a2989a7aa02e13c7d30576c8", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x29787f481c71ee1f761d3d7566fe53c6c08bb84fb99cdb5a1b59e4589d6bb866" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xa80dc0eca06d915541974ed828b671ef3583818828c8fd86aaf8922a8d282f64", + "hash_type": "data1" + }, + "type": { + "args": "0x11", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xa80dc0eca06d915541974ed828b671ef3583818828c8fd86aaf8922a8d282f64", + "hash_type": "data1" + }, + "type": { + "args": "0x12", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xa80dc0eca06d915541974ed828b671ef3583818828c8fd86aaf8922a8d282f64", + "hash_type": "data1" + }, + "type": { + "args": "0x13", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc4125baf451ed7373f0615c332dd7cf063955378dcf10b7a6fc35b0146cf4ca5182b00f4010000000000000000000000000000", + "0x544f4b454e3030312a000000000000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412", + "0xbe0000001c0000003c0000005c0000007100000079000000bd0000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc4125baf451ed7373f0615c332dd7cf063955378dcf10b7a6fc35b0146cf4ca5182b11000000656d657267656e63792072656c656173650000000000000000020000004242424242424242424242424242424242424242424242424242424242424242434343434343434343434343434343434343434343434343434343434343434300" + ], + "version": "0x0", + "witnesses": [] + }, + "0x328af8fa27cee70d6009d30c6b9ce494b1cd01e8f30f36e7c0e8b1031056850b": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x4d298843298431d70021bb66737e15abfe84b67851ce6a99787c28941caee507" + } + } + ], + "hash": "0x328af8fa27cee70d6009d30c6b9ce494b1cd01e8f30f36e7c0e8b1031056850b", + "header_deps": [ + "0xe3351eef7f5486f5e5199cceb0953a762e70ff1fde6fe6419eb1b3ea1af366dd" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xb7978d739c6d861ab1902226dbc51bfad44edf6dcfdee1dc303f50606f4c62ea" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0xcc321278301291afc3c43ae43d5e3e3c10ce9cf0658c3063587b0123cdce7cef", + "hash_type": "data1" + }, + "type": { + "args": "0x45", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0xcc321278301291afc3c43ae43d5e3e3c10ce9cf0658c3063587b0123cdce7cef", + "hash_type": "data1" + }, + "type": { + "args": "0x43", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x1e000000000000005645535430303031", + "0x00d51ef4e28799d94e9046a2873a2974c4fbe51572f6fd1d579cd43f4bf679fb87640000000000000032000000000000000000000000000000000000000000000002000000000000005645535430303031" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "0x32a7a73a12207334bbb3966a636e22d5ea60ee3dbcf4b8f0d757c90e3cc282b6": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xdb690265e0a48d6b81c1b38a4af3366976ec7d73515a63128a97e320a4664175" + } + } + ], + "hash": "0x32a7a73a12207334bbb3966a636e22d5ea60ee3dbcf4b8f0d757c90e3cc282b6", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x336a97dc34dcfb9fd522d94f3a0653fb50f2aaf7d4cba9278462fe43e92a70c9" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x240000001000000010000000240000001000000043534152477631006400000000000000" + ] + }, + "0x330bd555021ad2b154510d81eeccf8cbd8e6e62ebae7c456e5fabc2831e5eb26": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xdb73dd1332931d73fa333bb3e2b9ad2b0b93f3350e75420154005ba23a2d7d9d" + } + } + ], + "hash": "0x330bd555021ad2b154510d81eeccf8cbd8e6e62ebae7c456e5fabc2831e5eb26", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x375d660a387685e9e2fb3694a6329a5598d2879a50a794c0ce458f723c2788aa" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x1", + "tx_hash": "0x375d660a387685e9e2fb3694a6329a5598d2879a50a794c0ce458f723c2788aa" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x2", + "tx_hash": "0x375d660a387685e9e2fb3694a6329a5598d2879a50a794c0ce458f723c2788aa" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xa80dc0eca06d915541974ed828b671ef3583818828c8fd86aaf8922a8d282f64", + "hash_type": "data1" + }, + "type": { + "args": "0x15", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xa80dc0eca06d915541974ed828b671ef3583818828c8fd86aaf8922a8d282f64", + "hash_type": "data1" + }, + "type": { + "args": "0x14", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x2a00000000000000544f4b454e303031", + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc41200000000000000005baf451ed7373f0615c332dd7cf063955378dcf10b7a6fc35b0146cf4ca5182b" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631005baf451ed7373f0615c332dd7cf063955378dcf10b7a6fc35b0146cf4ca5182b", + "0x", + "0x" + ] + }, + "0x3318719ba10143a600ebda5a3f0b3a563c8f1d94d4c791831497295a4abcac74": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xa641762dced489313320a33d0a25ad81848a3cfdf3e057d37e5313f5aa7bff7a" + } + } + ], + "hash": "0x3318719ba10143a600ebda5a3f0b3a563c8f1d94d4c791831497295a4abcac74", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x69a432d0677efdd81acdd9ee50ac097f3cfe6e4dc981c86cb3bf7b090d32b833" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x1552617977bba10cb1d8df84ccaba2a68deaeac7eb39fc08462ecc5b9feec933" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x1", + "tx_hash": "0x1552617977bba10cb1d8df84ccaba2a68deaeac7eb39fc08462ecc5b9feec933" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0xa3bdc8e44991a1790d469db884797763ce5dd7ef8631b77f45c2d925633dbbd9", + "hash_type": "data1" + }, + "type": { + "args": "0xd3", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x513c43aa3e994cbdf9bcd7903f1e220882873ad830b3eb024cdeefe2ca3afcb3", + "hash_type": "data1" + }, + "type": { + "args": "0xd4", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x3850aba1ee6b423273975d12fb627dde7af42441026f269f09c44615a1a85b0845e5adc64633f8c455d0a0257610c5915b8223518dfa9beb1ae64502628b36b7414d4d4130303031414d4d4230303031080000000000000012000000000000000c000000000000001e00", + "0xe8564582cfb212e256ae6674d1224e7e09759592eb5dd7c1227430e9bf7132b40600000000000000ed765278a6a68d4a9135c503f9ce0fc652eaad62fbb857b24bfdf9d6e0955a3c" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c000000280000004353415247763100ed765278a6a68d4a9135c503f9ce0fc652eaad62fbb857b24bfdf9d6e0955a3c", + "0x", + "0x" + ] + }, + "0x33611509a83b78a19cf2ce0980216ef2aa22a359ffb7d07bf31fbd73c339444c": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xb58deece93c4942aa5ab1e0722ebfceaa8f9fabe3c6e8eb01dff0f2bd44b176d" + } + } + ], + "hash": "0x33611509a83b78a19cf2ce0980216ef2aa22a359ffb7d07bf31fbd73c339444c", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x856f0d3868b6c46e2834ddd850509f63354c2041a60d3c48fb673727f339b185" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x734c3177a05355d83a3be6c68309e377cfa444fc62d5d6504b3664ea090e9b02", + "hash_type": "data1" + }, + "type": null + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x", + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc41294ad7e819f84f5e26539b71e3e49ce236802e80d09e5ad2f3a0c8a17264b144200f4010000000000000000000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x336a97dc34dcfb9fd522d94f3a0653fb50f2aaf7d4cba9278462fe43e92a70c9": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xdb690265e0a48d6b81c1b38a4af3366976ec7d73515a63128a97e320a4664175" + } + } + ], + "hash": "0x336a97dc34dcfb9fd522d94f3a0653fb50f2aaf7d4cba9278462fe43e92a70c9", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x48629d4a9cc3fc983106af03deb7b963ce23b7766cda59a21f07af3922e08714" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x4e52ecbe274e6cefae5fdb1eba4f1cee15d92370a2a991bce607e86bd12c2cae", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0xff00000034000000540000005c0000007c0000007d0000009d000000a5000000a9000000aa000000ee000000f6000000fe000000444444444444444444444444444444444444444444444444444444444444444401000000000000001111111111111111111111111111111111111111111111111111111111111111003333333333333333333333333333333333333333333333333333333333333333f401000000000000000000000202000000111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222220a00000000000000d00700000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x349aa74d03c45384b56f8dbc5aef108759d23116eeba59a92774cfc08682bf67": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc8d09aa1bd1628fbcbaf36c8708d86a6ae276bbada0a5b3f032f3c4188bcc9f2" + } + } + ], + "hash": "0x349aa74d03c45384b56f8dbc5aef108759d23116eeba59a92774cfc08682bf67", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x93aa183f3781d22f64bdb65338ccc34ad23cd53b7565f2d38d2c09fac6480085" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x828a52262139378ce50e40ea7a24d4e8d8219cb2b9ae8f0e2a2cde2a8712a546", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [] + }, + "0x352b275582f167c4a2332d05c5bab89ffb39f2053dcc899f5d42f57a9f075234": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x1", + "tx_hash": "0x2b6f560e02fd1d710f9e47d3fb9771d37ae3b88362ebca48a822e3bee8e318f8" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x8c6940241808971b02b84d9bae41658d771003f1c281eb929b3aebd456b637d1" + } + } + ], + "hash": "0x352b275582f167c4a2332d05c5bab89ffb39f2053dcc899f5d42f57a9f075234", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x2b6f560e02fd1d710f9e47d3fb9771d37ae3b88362ebca48a822e3bee8e318f8" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xc13b702287bf01246fd600189482d58fb1e8bdadce497124858e010750a4b4cd", + "hash_type": "data1" + }, + "type": { + "args": "0x21", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x104c533c00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412a4dec0a82af4fefe10a664b9821c9e481efc2c7ca085bc82ed029a1828224314000000000000000000", + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c000000280000004353415247763100a4dec0a82af4fefe10a664b9821c9e481efc2c7ca085bc82ed029a1828224314" + ] + }, + "0x359f39061473e35c6dbab0c66794d75a41382022924c32e1f8adb6b7e18bc87a": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x59e35a159719575477e11dfd15b9b1c0c0c895495034a74e23e8f89a884cfa44" + } + } + ], + "hash": "0x359f39061473e35c6dbab0c66794d75a41382022924c32e1f8adb6b7e18bc87a", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xdec8a1e44fc58f622184065d886f6bc08e932ba83a27375cdec8f9a63a5bc5fb" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x67cf56e6c2bcd82506436ade5d8a220f9b51ad099c307d7cb59de6629cca279e", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x444444444444444444444444444444444444444444444444444444444444444411111111111111111111111111111111111111111111111111111111111111110064000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x3682c9124a707c7882a88d8e30916895d83e6989ee0538f9f4e3393cbed1c523": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xce13932ab95d93c1314a4d502849177e49ae562fef4b548150bba05bb04896b4" + } + } + ], + "hash": "0x3682c9124a707c7882a88d8e30916895d83e6989ee0538f9f4e3393cbed1c523", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x2bb928938ac2f0268e5832c69104527e4d29a9e2b84d3202377da542cabccc9f" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x9502f9000", + "lock": { + "args": "0x", + "code_hash": "0xee46f625f63eaccefbc7c9bf1393a21295d1d21e712fc704523d6f349a39ca26", + "hash_type": "data1" + }, + "type": { + "args": "0x69", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0xee46f625f63eaccefbc7c9bf1393a21295d1d21e712fc704523d6f349a39ca26", + "hash_type": "data1" + }, + "type": { + "args": "0x67", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0xee46f625f63eaccefbc7c9bf1393a21295d1d21e712fc704523d6f349a39ca26", + "hash_type": "data1" + }, + "type": { + "args": "0x68", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x0c2e7196a2c57e84d184146a4c2fae90ebbee6868385b48f6e790058d4cfb42149f070beeb4c781ae4867e862894a1678ed756948939c667bafbd6da9849e353414d4d4130303031414d4d42303030316400000000000000c800000000000000e8030000000000001e00", + "0x0a00000000000000414d4d4130303031", + "0x1400000000000000414d4d4230303031" + ], + "version": "0x0", + "witnesses": [] + }, + "0x39d8620d7cdab5fe38e1f273955366ec78088bb03ca244c8e652ca01eda72ffd": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x1e601e8f4a43db4216118fd235a8c21841e611d4d32208c6f8745d6ff5049d74" + } + } + ], + "hash": "0x39d8620d7cdab5fe38e1f273955366ec78088bb03ca244c8e652ca01eda72ffd", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x800bd3b016078a651b0e8291abfd3b01892cc2125e43c607d6d818e4d0986b7b" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xe47eaf52f41e951f3eb7ae9bfd1173ca26b884ecff101bfa931b5a75ddf1be70", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x000000000000000000000000000000000000000000000000000000000000000001000000000000002222222222222222222222222222222222222222222222222222222222222222f401000000000000d0070000000000005041594d3030303100" + ], + "version": "0x0", + "witnesses": [] + }, + "0x3b5f601fb0d58eec101f8758734ca3adaa979411bc16d348e7dad955ae10f23d": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc1992e679cdcbc64bb722f94b5d226099b2b9ead0529e4ccf45833055f369b2a" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xf0738d58ce079764795b431bbfd979cb1e39fa1672b01a35e6c50648a7831211" + } + } + ], + "hash": "0x3b5f601fb0d58eec101f8758734ca3adaa979411bc16d348e7dad955ae10f23d", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x1", + "tx_hash": "0xc1992e679cdcbc64bb722f94b5d226099b2b9ead0529e4ccf45833055f369b2a" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x1bf08eb000", + "lock": { + "args": "0x", + "code_hash": "0xb5e6f61a513204507f6cf508cbc23b449a60d91ef6ebdfde79fcf886bf4cb020", + "hash_type": "data1" + }, + "type": { + "args": "0xf2", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0xf3", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0xdf00000034000000540000005c0000007c0000007d0000009d000000a5000000a9000000aa000000ce000000d6000000de00000065dfc0aa4a73f351b42a0bad8e1e57f7befd37622ec7fc56087c9cad02990593010000000000000086a76e2fc301d88cf9fed4a493b210edf5a21bc839886e7869bac99766ed6a5c003664eabff06921f646efbb743006b9544de3a96a399db5870777e5e8d78a7b2df40100000000000000000000020100000086a76e2fc301d88cf9fed4a493b210edf5a21bc839886e7869bac99766ed6a5c1400000000000000b40500000000000000", + "0x010000000000000086a76e2fc301d88cf9fed4a493b210edf5a21bc839886e7869bac99766ed6a5c1e00000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x4400000010000000100000004400000030000000435341524776310086a76e2fc301d88cf9fed4a493b210edf5a21bc839886e7869bac99766ed6a5c1e00000000000000" + ] + }, + "0x3c849919043c16e3e898eda183516a34bbcdc02458f05c8d208cf134cf0ed8f0": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x59e35a159719575477e11dfd15b9b1c0c0c895495034a74e23e8f89a884cfa44" + } + } + ], + "hash": "0x3c849919043c16e3e898eda183516a34bbcdc02458f05c8d208cf134cf0ed8f0", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x359f39061473e35c6dbab0c66794d75a41382022924c32e1f8adb6b7e18bc87a" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631001111111111111111111111111111111111111111111111111111111111111111" + ] + }, + "0x3e1251358de881931f81bfe6f8a80a66309befa2db94fef5889a81b57334bc13": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x757f318b25b23b441765bcbfac6ae04d0986ffcbdbc86cc58ebdd7eef79c65fc" + } + } + ], + "hash": "0x3e1251358de881931f81bfe6f8a80a66309befa2db94fef5889a81b57334bc13", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x5784153f26770fa3021edb7050dc2555d2f43f08ffaed620bb1ff9a76abaa993" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631001111111111111111111111111111111111111111111111111111111111111111" + ] + }, + "0x402005d53808fc439e58003f9ffecbc77ab2cf229dbf93bff3888221c05c37cd": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x5a42e270ccd43a33e96ba446dc3305288ff81717caf6d657da2f20c5cfda25d8" + } + } + ], + "hash": "0x402005d53808fc439e58003f9ffecbc77ab2cf229dbf93bff3888221c05c37cd", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xc88b941d4c707c59ee2b460199746ffe18e32fd9b784d06b6a6245e0f477cea6" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x204920209dab2137b0f75335063856cc1e84d28b41d1e31c2160614832de50ec", + "hash_type": "data1" + }, + "type": { + "args": "0x51", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x8d00000018000000380000007c0000007d00000085000000081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd30802000000cf8cbc02d90b9e9bca7eb320ce67edfcf62400cd2a8bfb81a8104cf3942bca74edf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae0200000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x402f7e5dd680c1d6dc63abfc07b59a2583aa577503c506bef3d00a3a9318608b": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x2f2a53ea7336cf1d1b199a59b22148e7357d2e12846050a664cce5bf73094e80" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x4e37ef1b4ef9ce4d4bf6e391a520856f1646deec3fd27518ee4b3fd932f3cde7" + } + } + ], + "hash": "0x402f7e5dd680c1d6dc63abfc07b59a2583aa577503c506bef3d00a3a9318608b", + "header_deps": [ + "0x690c44e7f3605a4c984edfe17dc953047114aff5e42ae1b2f108dc042a37a34d" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x9081136c24df469f162ee5d2f811b20c93c9ab55686d7dc94a3c5396185d42e2" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xc5eb6cb03878c77e75b8462c561205189574fc4677e5538cc9136c10ab9921da", + "hash_type": "data1" + }, + "type": { + "args": "0xb3", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x104c533c00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x7d84ad3da83e8ccab86a945f8bb0b74ebaa3d29369b01c5370dff1f340078ac06bd943617d9642f1dad174449a64bb34f325c84fc257eb8fff578e00a50b0a9c0b0000000000000000", + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631006bd943617d9642f1dad174449a64bb34f325c84fc257eb8fff578e00a50b0a9c" + ] + }, + "0x403c6468185cda532c013187efc8a8037ddab784faa77ffa2f598d8aac72eb71": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc922cbc382b9e65ed9852d188f4eac36d7b7e47c518639c0b6e39899aa32d440" + } + } + ], + "hash": "0x403c6468185cda532c013187efc8a8037ddab784faa77ffa2f598d8aac72eb71", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x0bb7448780f83f057e6175331a11fd4e901208c9d6d25387b8fdff8e5790b0eb" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0xdebeeebed4b050592aeeda4cd26e530632ce8236e4a0481c150ee8264cfcdffb", + "hash_type": "data1" + }, + "type": { + "args": "0x44", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x41", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4d000000000000005645535430303031", + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc41256455354303030310a00000000000000640000000000000001" + ], + "version": "0x0", + "witnesses": [] + }, + "0x40461a7a9e89d6574d81d1c496cd2ada9191d4a10684f6df1a632805c20fa75d": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc779774afe4bbb92248ad5e6f91ba71ddfac20bda122802790702264c6d8975f" + } + } + ], + "hash": "0x40461a7a9e89d6574d81d1c496cd2ada9191d4a10684f6df1a632805c20fa75d", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x3ff86d30f57d900405a644add9552daab4483d43befe2c3e75c1da4272e488ae" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xb961548b7fb156288df5e3c472a626d1791f0753906579980f1c75f1069698a1", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [] + }, + "0x42897b5ae6adaa91365deb19c8ce0fa269befa90f83fbb2d1aa06e7a3f64a131": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x1", + "tx_hash": "0xae0bc2b1731b075e540a5fd07c59b6bb03278cac9e259d21528e4b0e543ef2f0" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xf3587c0b234657d49a8060ead24d3c0c6746964524c281738238c2eee58261cc" + } + } + ], + "hash": "0x42897b5ae6adaa91365deb19c8ce0fa269befa90f83fbb2d1aa06e7a3f64a131", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xae0bc2b1731b075e540a5fd07c59b6bb03278cac9e259d21528e4b0e543ef2f0" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0xb68a0aa00", + "lock": { + "args": "0x", + "code_hash": "0xf51e1060b13ba495ab2cac42ab5102d7ff6bb2c00df50f115846b5fcc9429298", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c000000280000004353415247763100fbf9445aa019c17dbdd072cae5c7840097cba346940a427fc4269257922e5bf7" + ] + }, + "0x43aa34c172d01d43c427650825c520f05f0775b6691bb9448488bd542475db62": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc293f43936132ce8cb8f8a4a760f1de03c82dfd7464533a73b586d1867b92349" + } + } + ], + "hash": "0x43aa34c172d01d43c427650825c520f05f0775b6691bb9448488bd542475db62", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x2196617ba13ed7b92b5decac17cf16f966d584e267a76e3f5e64cd17669ccb22" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x9c1a6fed0b778db0b8006116e9d9a6213d2b5dadf1ac391118790c44a9ffe275", + "hash_type": "data1" + }, + "type": { + "args": "0x21", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x9c1a6fed0b778db0b8006116e9d9a6213d2b5dadf1ac391118790c44a9ffe275", + "hash_type": "data1" + }, + "type": { + "args": "0x24", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x9c1a6fed0b778db0b8006116e9d9a6213d2b5dadf1ac391118790c44a9ffe275", + "hash_type": "data1" + }, + "type": { + "args": "0x24", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0xba43b7400", + "lock": { + "args": "0x", + "code_hash": "0x9c1a6fed0b778db0b8006116e9d9a6213d2b5dadf1ac391118790c44a9ffe275", + "hash_type": "data1" + }, + "type": { + "args": "0x23", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x00000000000000000000000000000000000000000000000000000000000000000700000000000000b185b58da000c8f9180f3ab6f8dc6b1c907fd1f44f8ace7fdbe46b7ea7ab3db4000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412fa00", + "0xfa000000000000005041594d30303031", + "0x16260000000000005041594d30303031", + "0x000000000000000000000000000000000000000000000000000000000000000007000000000000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc4121027000000000000c8000000000000005041594d3030303100" + ], + "version": "0x0", + "witnesses": [] + }, + "0x444ee91ba47e5385db975ecd93a4a7ddbca24280a7b63c7ff4898461e206388a": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x4549c1c05bb03fe8c884c329830b6ba4cbc4fe2f560fa5db94addb128fc14015" + } + } + ], + "hash": "0x444ee91ba47e5385db975ecd93a4a7ddbca24280a7b63c7ff4898461e206388a", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xeaeed3d06f30198c983882e0221720faf11c02473429adb757bd570b910363f9" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "0x44e53c1d471b7bdfbda6816b72152f0c83550a5296edac7313b645ccde8527dc": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x339055295d20077427f346e209218b486acf729ef51fab4051a6c08999fdf40a" + } + } + ], + "hash": "0x44e53c1d471b7bdfbda6816b72152f0c83550a5296edac7313b645ccde8527dc", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xfdca9e7b25faf9b61892db5728fab459dab21c39aeb6396438a4f321389d5327" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x6c3bf0d1bc162b2c71378ec3ea55d34121ab8c66480524e83cc8943de6b26555", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x2a00000000000000544f4b454e303031" + ], + "version": "0x0", + "witnesses": [] + }, + "0x4a2f379980234301b6755cdb05bbcf5d46f407f31a8fe7228bf2cce0c29cbc7c": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xd32db7375c2ca9b9df46c33c6d3c6ae4f1f86236633a3ad794086f2fa708f2e4" + } + } + ], + "hash": "0x4a2f379980234301b6755cdb05bbcf5d46f407f31a8fe7228bf2cce0c29cbc7c", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x8e35ffbe3c9554b756703995206443214379ee30e0377466223286b0d86de771" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0xba43b7400", + "lock": { + "args": "0x", + "code_hash": "0x318333f71531adb7109813cd89f757d76d7a1a8aebb79e93d800df8f4f0bc3c3", + "hash_type": "data1" + }, + "type": { + "args": "0x43", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x00b250afae197267ab716da7baa7d3077d66d0bfb6286f19ab4d0698a90737666e640000000000000014000000000000000000000000000000000000000000000001000000000000005645535430303031" + ], + "version": "0x0", + "witnesses": [] + }, + "0x4b79a85846e54477f3689cc8fa64e3488e1f6c7da2ad02b12b4d7a623a8093f4": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x605ff9349d7a02a281af3488d3f7eeedea672de6d61f015759297b95cec97b33" + } + } + ], + "hash": "0x4b79a85846e54477f3689cc8fa64e3488e1f6c7da2ad02b12b4d7a623a8093f4", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x19cde1adb4b5e9fed5e5e2837c79e19630eb2e3641809c73668aeef5847d2740" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x2ca9877f46bc3e89b31d76e256714e075ad57732d7fcd489fdc6aa636772f93d", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x00000000000000000000000000000000000000000000000000000000000000000100000000000000ff63e89620213deea694117ec085da42e12c77ac38c0c86eeb6d2202ecb78edd000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412fa00" + ], + "version": "0x0", + "witnesses": [] + }, + "0x4cc2653d8451a590de947172988ec164e7bf5a4fe457fdb4e2908b77e66c9d6d": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x1f4e697b9f5155338b31392abc0794fe3e262a65e8ca61cc6eeea35fb8aa30f6" + } + } + ], + "hash": "0x4cc2653d8451a590de947172988ec164e7bf5a4fe457fdb4e2908b77e66c9d6d", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x73f7a36fbcffdd8b1001ae6cae57f5e3ff92992199c2fb83584503bd1898d3df" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xd305deeaaf5715086b9ca367189279b7709450c3df4a6b73620f841578e1195f", + "hash_type": "data1" + }, + "type": { + "args": "0x21", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0xd305deeaaf5715086b9ca367189279b7709450c3df4a6b73620f841578e1195f", + "hash_type": "data1" + }, + "type": { + "args": "0x24", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0xd305deeaaf5715086b9ca367189279b7709450c3df4a6b73620f841578e1195f", + "hash_type": "data1" + }, + "type": { + "args": "0x24", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0xba43b7400", + "lock": { + "args": "0x", + "code_hash": "0xd305deeaaf5715086b9ca367189279b7709450c3df4a6b73620f841578e1195f", + "hash_type": "data1" + }, + "type": { + "args": "0x22", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x000000000000000000000000000000000000000000000000000000000000000006000000000000007ec54f4d397c9e0406cb21db151b5b2844123b83ba7e8eb9ce9c0a3c3759b5fb000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412fa00", + "0xfa000000000000005041594d30303031", + "0x16260000000000005041594d30303031", + "0x000000000000000000000000000000000000000000000000000000000000000006000000000000007ec54f4d397c9e0406cb21db151b5b2844123b83ba7e8eb9ce9c0a3c3759b5fb1027000000000000460000000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x4d6d94bf0b85a090775f7c8c7127e5b0b4d334547d299080282dac7fc94eafd9": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xb547f36f187c1934d41fdd9aa8a0e855842721d14c598386bdc850d6b17b5517" + } + } + ], + "hash": "0x4d6d94bf0b85a090775f7c8c7127e5b0b4d334547d299080282dac7fc94eafd9", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x1747065ef33181c2ef81ca3e925090b9f8d10d68db884951cb0d13d62bb8761a" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x240000001000000010000000240000001000000043534152477631006400000000000000" + ] + }, + "0x4fd12d9427983bb4486b499152aa8b7cc9051c0e83f9baabe005a380bafbad07": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xf90754033d45778b16034a348f5757b56b8578eab5fd81bd2707a4fa43572a7f" + } + } + ], + "hash": "0x4fd12d9427983bb4486b499152aa8b7cc9051c0e83f9baabe005a380bafbad07", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xbb4544c75dd32136bfbc6eeab20b2a7ca0539d8e059d3fadfbca9393629a94e8" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x10f75e072356b123d3864ae553870a4759943c4ed71d373218ca17b611795020", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x2540be400", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x544f4b454e303031e8030000000000000f00000000000000", + "0x0500000000000000544f4b454e303031" + ], + "version": "0x0", + "witnesses": [ + "0x440000001000000010000000440000003000000043534152477631004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc4120500000000000000" + ] + }, + "0x515a67864ac1959d9cd4fa108ba421b195a4410f1878832bd01208b5d86e7fcd": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xba786ad1ae914446151de4ce6258fc3d780be1d17424330bbd6a36b6b87f30a1" + } + } + ], + "hash": "0x515a67864ac1959d9cd4fa108ba421b195a4410f1878832bd01208b5d86e7fcd", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x5dd59331ecc0a98010fba90d44799e98e082e54eff3b4b386afed335afaa42d4" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0xba43b7400", + "lock": { + "args": "0x", + "code_hash": "0x7902ead8098947fb0d9cf87ed357d821d21c0e85505621627fbd81d873290140", + "hash_type": "data1" + }, + "type": { + "args": "0x43", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x41", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x00d714763e4a1855490e72ed7282ee94abb4ad846e79662f8423d83b4f5aca0c35640000000000000014000000000000000000000000000000000000000000000001000000000000005645535430303031", + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc41256455354303030310a00000000000000640000000000000001" + ], + "version": "0x0", + "witnesses": [] + }, + "0x536a1329df3e98119af6bc48f9b8894650d7c85a852a37ae461fc28cd59ea098": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x1f4e697b9f5155338b31392abc0794fe3e262a65e8ca61cc6eeea35fb8aa30f6" + } + } + ], + "hash": "0x536a1329df3e98119af6bc48f9b8894650d7c85a852a37ae461fc28cd59ea098", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x4cc2653d8451a590de947172988ec164e7bf5a4fe457fdb4e2908b77e66c9d6d" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x1", + "tx_hash": "0x4cc2653d8451a590de947172988ec164e7bf5a4fe457fdb4e2908b77e66c9d6d" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x2", + "tx_hash": "0x4cc2653d8451a590de947172988ec164e7bf5a4fe457fdb4e2908b77e66c9d6d" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x3", + "tx_hash": "0x4cc2653d8451a590de947172988ec164e7bf5a4fe457fdb4e2908b77e66c9d6d" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xd305deeaaf5715086b9ca367189279b7709450c3df4a6b73620f841578e1195f", + "hash_type": "data1" + }, + "type": { + "args": "0x21", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x24", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0xd305deeaaf5715086b9ca367189279b7709450c3df4a6b73620f841578e1195f", + "hash_type": "data1" + }, + "type": { + "args": "0x24", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x000000000000000000000000000000000000000000000000000000000000000006000000000000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412fa00", + "0xfa000000000000005041594d30303031", + "0x16260000000000005041594d30303031" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412", + "0x", + "0x", + "0x" + ] + }, + "0x558ddcd2b7e2faf8b3e72f03235cee6ae1ab465b76732cfede9c6967ceb130ec": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x08a319c4fe820d63319732392e166c200c4f7eb811244ac8a4dd433065e8400c" + } + } + ], + "hash": "0x558ddcd2b7e2faf8b3e72f03235cee6ae1ab465b76732cfede9c6967ceb130ec", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x1231896def8739036e5e85f79df05e268e5900cf8285d1ed7601157a3e0cc38e" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x1", + "tx_hash": "0xe9680f21dbf851055f0cb2fcc4cd51a05b5e2f6846b22b08462ffa12e7dc7d2d" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x1b82b117ab4e41de9f6652a54f2f2ee24abad190b04a935cf90075cf9f3da237", + "hash_type": "data1" + }, + "type": { + "args": "0xa1", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x0c000000000000005354415445303031" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c000000280000004353415247763100a200e27c78437ddcab853984e34033b1031f475d67a7ba28a463b7e9979cf7d8", + "0x" + ] + }, + "0x563bdf20a03aa85513c4c052b7e8c1489f5c47d97ad0377084d898aabb32be0c": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x1", + "tx_hash": "0xbf23c0724ab21b007a7d3b7832a662b934e41b1c656f84b2544c87aacd5d8c48" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xbcad341f60c752aa595c93250be7968b9073f5c09b3e9c645fd115dec67eeb88" + } + } + ], + "hash": "0x563bdf20a03aa85513c4c052b7e8c1489f5c47d97ad0377084d898aabb32be0c", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xbf23c0724ab21b007a7d3b7832a662b934e41b1c656f84b2544c87aacd5d8c48" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x734c3177a05355d83a3be6c68309e377cfa444fc62d5d6504b3664ea090e9b02", + "hash_type": "data1" + }, + "type": { + "args": "0x22", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x104c533c00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x7e0000001c0000003c0000005c00000071000000790000007d0000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc41294ad7e819f84f5e26539b71e3e49ce236802e80d09e5ad2f3a0c8a17264b144211000000656d657267656e63792072656c6561736500000000000000000000000000", + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x5500000010000000100000005500000041000000435341524776310094ad7e819f84f5e26539b71e3e49ce236802e80d09e5ad2f3a0c8a17264b14421500000011000000656d657267656e63792072656c65617365" + ] + }, + "0x5784153f26770fa3021edb7050dc2555d2f43f08ffaed620bb1ff9a76abaa993": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x757f318b25b23b441765bcbfac6ae04d0986ffcbdbc86cc58ebdd7eef79c65fc" + } + } + ], + "hash": "0x5784153f26770fa3021edb7050dc2555d2f43f08ffaed620bb1ff9a76abaa993", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x63a414a50f75bc973e2a1cf27953132d279058cfeb47d253134f4389a424e1fd" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x0d708753715b0c50502a06fea8c0d48b890212a5217b4330b7a77805a71f3dd7", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0xff00000034000000540000005c0000007c0000007d0000009d000000a5000000a9000000aa000000ee000000f6000000fe000000444444444444444444444444444444444444444444444444444444444444444401000000000000001111111111111111111111111111111111111111111111111111111111111111003333333333333333333333333333333333333333333333333333333333333333f401000000000000000000000202000000111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222220a00000000000000d00700000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x58389e2aa2f07b207b69c854bb471cbfa9e980b3fb0f5c5acde5d16fe4163f05": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x1e601e8f4a43db4216118fd235a8c21841e611d4d32208c6f8745d6ff5049d74" + } + } + ], + "hash": "0x58389e2aa2f07b207b69c854bb471cbfa9e980b3fb0f5c5acde5d16fe4163f05", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x8565bc40dac39e76130ebe55ea770cb776cb0892c0f2561d20de4b8db3b4e8e1" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xe47eaf52f41e951f3eb7ae9bfd1173ca26b884ecff101bfa931b5a75ddf1be70", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x000000000000000000000000000000000000000000000000000000000000000001000000000000002222222222222222222222222222222222222222222222222222222222222222f401000000000000d0070000000000005041594d3030303100" + ], + "version": "0x0", + "witnesses": [] + }, + "0x58e74715d125d7cbd4c7a98b8aad1518cfaaf118e9e0defca5728b9430946249": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc71e873453ffa750d9ef781214a1e015b9fe92ba751672b9592f3593750cb2fd" + } + } + ], + "hash": "0x58e74715d125d7cbd4c7a98b8aad1518cfaaf118e9e0defca5728b9430946249", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x9c6275bfed126d72f67238e8617ef96a7d9f101a2efed8076d448c731ec8416e" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x9b4ed74a5469e89dd428a35929c57c901ef5dee0ea5a3e1979ac81d53dc2ea4e", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x111111111111111111111111111111111111111111111111111111111111111156455354303030310a00000000000000640000000000000001" + ], + "version": "0x0", + "witnesses": [] + }, + "0x5a8ff906574c1edf1e5fbd1487c723f67038565705582d8ac112264d57cbfe07": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x0ed6bca9b7b257ea7fd5b25d4f686479d892da1349a94a24c20be92d396dcaa3" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xe2bfd1c340bd2b8f529bc256d9c58274f2e5c65e443ca77fc78a26d4904e4969" + } + } + ], + "hash": "0x5a8ff906574c1edf1e5fbd1487c723f67038565705582d8ac112264d57cbfe07", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x115b8ecbcb808b3b25b5f9cbc4883d27337aea43395ded9325a2db79ff74e71d" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x37e11d600", + "lock": { + "args": "0x", + "code_hash": "0x7902ead8098947fb0d9cf87ed357d821d21c0e85505621627fbd81d873290140", + "hash_type": "data1" + }, + "type": { + "args": "0x44", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x37e11d600", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x44", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x00000000000000005645535430303031", + "0x4d000000000000005645535430303031" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412" + ] + }, + "0x5debff60864d562bb94f7020d6a1180c7736815ee352e48c9905b920bc0cdf31": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xbe466bab7cff1e51bbd15ce13c297ff867f1b089231d2f4797f3e656f9f2fcdd" + } + } + ], + "hash": "0x5debff60864d562bb94f7020d6a1180c7736815ee352e48c9905b920bc0cdf31", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xa27c6e786f9e6d95b23f3db81ae171b79cd438041bcc74fce0f9382398849b92" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x6c976866fb9c343bd28922b92aca893f292fed889e52b75a59a10f738b31f4e7", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x37e11d600", + "lock": { + "args": "0x", + "code_hash": "0x6c976866fb9c343bd28922b92aca893f292fed889e52b75a59a10f738b31f4e7", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x2800000000000000544f4b454e303031", + "0x0200000000000000544f4b454e303031" + ], + "version": "0x0", + "witnesses": [] + }, + "0x5e784733c02e52fe1d4c6996255dcfdbf2b792d69c36414970e539e90901d2b2": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc1992e679cdcbc64bb722f94b5d226099b2b9ead0529e4ccf45833055f369b2a" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xf0738d58ce079764795b431bbfd979cb1e39fa1672b01a35e6c50648a7831211" + } + } + ], + "hash": "0x5e784733c02e52fe1d4c6996255dcfdbf2b792d69c36414970e539e90901d2b2", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x3b5f601fb0d58eec101f8758734ca3adaa979411bc16d348e7dad955ae10f23d" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x14f46b0400", + "lock": { + "args": "0x", + "code_hash": "0xee0a4d051aeb5c2c39df4eeeda6a55b8cb9ea79a964b905d3288c0d83138fabc", + "hash_type": "data1" + }, + "type": { + "args": "0xf2", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0xf3", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0xff00000034000000540000005c0000007c0000007d0000009d000000a5000000a9000000aa000000ee000000f6000000fe00000065dfc0aa4a73f351b42a0bad8e1e57f7befd37622ec7fc56087c9cad02990593010000000000000086a76e2fc301d88cf9fed4a493b210edf5a21bc839886e7869bac99766ed6a5c003664eabff06921f646efbb743006b9544de3a96a399db5870777e5e8d78a7b2df40100000000000000000000020200000086a76e2fc301d88cf9fed4a493b210edf5a21bc839886e7869bac99766ed6a5cc7b799a057ff213011b152a94d895d9e5b1eeb05e376f7122464ff11bc93207a1400000000000000b40500000000000000", + "0x0100000000000000c7b799a057ff213011b152a94d895d9e5b1eeb05e376f7122464ff11bc93207a1f00000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x44000000100000001000000044000000300000004353415247763100c7b799a057ff213011b152a94d895d9e5b1eeb05e376f7122464ff11bc93207a1f00000000000000" + ] + }, + "0x5e9cccf3c3feeef58ad7e21e3b611b765752e45370870fbdcb2363fe645e714d": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x1d3726f0eb930917dbb02cb08aa68622494014245a885c60e4d1df758f245b49" + } + } + ], + "hash": "0x5e9cccf3c3feeef58ad7e21e3b611b765752e45370870fbdcb2363fe645e714d", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x31aa013f1e5e95cef0f8a5cc0a4dc6c069ad72d6a2989a7aa02e13c7d30576c8" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x1", + "tx_hash": "0x31aa013f1e5e95cef0f8a5cc0a4dc6c069ad72d6a2989a7aa02e13c7d30576c8" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x2", + "tx_hash": "0x31aa013f1e5e95cef0f8a5cc0a4dc6c069ad72d6a2989a7aa02e13c7d30576c8" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xa80dc0eca06d915541974ed828b671ef3583818828c8fd86aaf8922a8d282f64", + "hash_type": "data1" + }, + "type": { + "args": "0x15", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xa80dc0eca06d915541974ed828b671ef3583818828c8fd86aaf8922a8d282f64", + "hash_type": "data1" + }, + "type": { + "args": "0x14", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x2a00000000000000544f4b454e303031", + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc41200000000000000005baf451ed7373f0615c332dd7cf063955378dcf10b7a6fc35b0146cf4ca5182b" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631005baf451ed7373f0615c332dd7cf063955378dcf10b7a6fc35b0146cf4ca5182b", + "0x", + "0x" + ] + }, + "0x5fae038da17633b4994474ccfab8cd4769b9670ca984573a047d8e73fa1321f9": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x1fc61e5ec8572c8853a001a40fa7acef0190c6833da6ec3e407bd2863c986a45" + } + } + ], + "hash": "0x5fae038da17633b4994474ccfab8cd4769b9670ca984573a047d8e73fa1321f9", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xe2caa37ca2e0591eb4662a9c263d9bbd2fb0c1717253b0e909e24658f5d5dbf9" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x104c533c00", + "lock": { + "args": "0x", + "code_hash": "0x540be7d1575ea9e60a6df5678ec2e4dc857edb687f1ad2f8359317be2288ff60", + "hash_type": "data1" + }, + "type": { + "args": "0x51", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x540be7d1575ea9e60a6df5678ec2e4dc857edb687f1ad2f8359317be2288ff60", + "hash_type": "data1" + }, + "type": { + "args": "0x52", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x8d00000018000000380000007c0000007d00000085000000081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd3080200000086a76e2fc301d88cf9fed4a493b210edf5a21bc839886e7869bac99766ed6a5cedf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae0201000000000000000a00000000000000", + "0xbf00000034000000540000005c0000007c0000007d0000009d000000a5000000a9000000aa000000ae000000b6000000be000000081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd308010000000000000086a76e2fc301d88cf9fed4a493b210edf5a21bc839886e7869bac99766ed6a5c0064f944c22f0db7bfb29aa523b65cbb75a6a65b369febfbff0ffc17facacfe2dcf4010000000000000000000002000000001400000000000000b40500000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x6c00000010000000100000006c00000058000000435341524776310086a76e2fc301d88cf9fed4a493b210edf5a21bc839886e7869bac99766ed6a5c64f944c22f0db7bfb29aa523b65cbb75a6a65b369febfbff0ffc17facacfe2dcf4010000000000001400000000000000" + ] + }, + "0x638614460e8f5f22cc0aff1077c4ab63559f16b230b4230bc0767abad961fd13": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x4549c1c05bb03fe8c884c329830b6ba4cbc4fe2f560fa5db94addb128fc14015" + } + } + ], + "hash": "0x638614460e8f5f22cc0aff1077c4ab63559f16b230b4230bc0767abad961fd13", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x14a53cee63255d44aa2c329fdf93f911de92ece53bfc45773039df1d26b4005d" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xc39605e25bec7c9fe1297e640cf6ef42128dff83cec50c6e94bcc1eeb7aa268a", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0xdf00000034000000540000005c0000007c0000007d0000009d000000a5000000a9000000aa000000ce000000d6000000de000000444444444444444444444444444444444444444444444444444444444444444401000000000000001111111111111111111111111111111111111111111111111111111111111111003333333333333333333333333333333333333333333333333333333333333333f40100000000000000000000020100000011111111111111111111111111111111111111111111111111111111111111110a00000000000000d00700000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x6643966a91792a95d2fa6dc1fa6e1cf1a1c1c677930c6d95df344e7edbbab27b": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x3ee712eb9ce234366e17d006c3a022f164cd052b1739c8d0b1ddfaae7fdab1b2" + } + } + ], + "hash": "0x6643966a91792a95d2fa6dc1fa6e1cf1a1c1c677930c6d95df344e7edbbab27b", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x1", + "tx_hash": "0xef62d924ff6af766acc21727b36c6e6483fac2768527599bf597348eb3c55a91" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x0a0b8c20de2b149b74926989ccef6f20ab3984e666b923dfb78610c569e753bc" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x1", + "tx_hash": "0x0a0b8c20de2b149b74926989ccef6f20ab3984e666b923dfb78610c569e753bc" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xc67554cbd1c3973fe04e014c2271023a82d9874a4b84ec38bda8bca1f9a65b26" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0xc5", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0xc2", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x37e11d600", + "lock": { + "args": "0x", + "code_hash": "0x967d150b17ee92167fda6eb3b28e453cfc084f19c7cbfc773bb3f1b93d4dea61", + "hash_type": "data1" + }, + "type": { + "args": "0xc4", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x37e11d600", + "lock": { + "args": "0x", + "code_hash": "0xd305deeaaf5715086b9ca367189279b7709450c3df4a6b73620f841578e1195f", + "hash_type": "data1" + }, + "type": { + "args": "0xc4", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x01134f3e5fc0e711c8860ae1fe92cdf6797bb68e4059cf057a4ea9eae6b9e2d00100000000000000619e5495bb676cb83755e01c2a2e40f8d285eef0051bb7b6ccbec3d9810e023833333333333333333333333333333333333333333333333333333333333333333b27b52bcff21bb54b5ab2951b1ee1153fa29982ff473bf003e6097276b6d55cfa00", + "0xfa000000000000005041594d30303031", + "0x16260000000000005041594d30303031" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c000000280000004353415247763100619e5495bb676cb83755e01c2a2e40f8d285eef0051bb7b6ccbec3d9810e0238", + "0x", + "0x", + "0x" + ] + }, + "0x67be331af3ce7812f3b9acc4dd3f4e6fdda26bce7daaf7e97250aa7a018214ce": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xac7777ae99467a32a740f9f89776f0a5c1a17808c49e29e69a1b193d9f751e1b" + } + } + ], + "hash": "0x67be331af3ce7812f3b9acc4dd3f4e6fdda26bce7daaf7e97250aa7a018214ce", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x1f7b418973fc0723338fc7f2ff45fbd774b0afe291bca5e12e501388fc0d1f8a" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631001111111111111111111111111111111111111111111111111111111111111111" + ] + }, + "0x69a432d0677efdd81acdd9ee50ac097f3cfe6e4dc981c86cb3bf7b090d32b833": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x86c3b29ad0bba4281c2d58d64030f41ad9242dcce7416f20c67130a0df8b5e46" + } + } + ], + "hash": "0x69a432d0677efdd81acdd9ee50ac097f3cfe6e4dc981c86cb3bf7b090d32b833", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x3a4ef8679d5d77f3e7cc52e77b60c86533a2bcb68dc4fd32b00e947a15a8aaa9" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x1", + "tx_hash": "0x3a4ef8679d5d77f3e7cc52e77b60c86533a2bcb68dc4fd32b00e947a15a8aaa9" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0xee46f625f63eaccefbc7c9bf1393a21295d1d21e712fc704523d6f349a39ca26", + "hash_type": "data1" + }, + "type": { + "args": "0xd3", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x513c43aa3e994cbdf9bcd7903f1e220882873ad830b3eb024cdeefe2ca3afcb3", + "hash_type": "data1" + }, + "type": { + "args": "0xd4", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x3850aba1ee6b423273975d12fb627dde7af42441026f269f09c44615a1a85b0845e5adc64633f8c455d0a0257610c5915b8223518dfa9beb1ae64502628b36b7414d4d4130303031414d4d42303030310400000000000000090000000000000006000000000000001e00", + "0xe8564582cfb212e256ae6674d1224e7e09759592eb5dd7c1227430e9bf7132b40600000000000000ed765278a6a68d4a9135c503f9ce0fc652eaad62fbb857b24bfdf9d6e0955a3c" + ], + "version": "0x0", + "witnesses": [ + "0x3e00000010000000100000003e0000002a00000043534152477631001e00ed765278a6a68d4a9135c503f9ce0fc652eaad62fbb857b24bfdf9d6e0955a3c", + "0x" + ] + }, + "0x69d4ed19143215adfaec3dd6a0030b59200a21d8931079cd8504c0726bbe866c": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xf0f807aecb16aaf7820fdb2c70d719912dd02d8274f76343fc42eebdbc3bcc02" + } + } + ], + "hash": "0x69d4ed19143215adfaec3dd6a0030b59200a21d8931079cd8504c0726bbe866c", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x7a476dcd9c82d876e0f00b36dfd4fc1854b923a7bf7fd2a26150ab0513213348" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x18758b10cc53dcf2fcd775462ec3ad8052ca19f21158a315eedc926cd085d520", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x00000000000000000000000000000000000000000000000000000000000000000100000000000000111111111111111111111111111111111111111111111111111111111111111144444444444444444444444444444444444444444444444444444444444444442222222222222222222222222222222222222222222222222222222222222222fa00" + ], + "version": "0x0", + "witnesses": [] + }, + "0x6a3d32809ed36e7835e40e027dc05ee5adc36c9f04cc84866dfd3b58fe0f3043": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc71e873453ffa750d9ef781214a1e015b9fe92ba751672b9592f3593750cb2fd" + } + } + ], + "hash": "0x6a3d32809ed36e7835e40e027dc05ee5adc36c9f04cc84866dfd3b58fe0f3043", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x9b21a90dc923eaf2dc8abc0242fecb36d549408e6226138714851cab22d5e336" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x9b4ed74a5469e89dd428a35929c57c901ef5dee0ea5a3e1979ac81d53dc2ea4e", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x111111111111111111111111111111111111111111111111111111111111111156455354303030310a00000000000000640000000000000001" + ], + "version": "0x0", + "witnesses": [] + }, + "0x6f6ed0c878e8dd8d80724a1b65adbc3ff9509f1727f2432790be4d5aebafb7ff": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc8d09aa1bd1628fbcbaf36c8708d86a6ae276bbada0a5b3f032f3c4188bcc9f2" + } + } + ], + "hash": "0x6f6ed0c878e8dd8d80724a1b65adbc3ff9509f1727f2432790be4d5aebafb7ff", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x349aa74d03c45384b56f8dbc5aef108759d23116eeba59a92774cfc08682bf67" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x828a52262139378ce50e40ea7a24d4e8d8219cb2b9ae8f0e2a2cde2a8712a546", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc4128755508da7aa1fd862ab37b0e6ef59a8cb026c9bacc0c07e838683cb6cf3404e0119000000000000000000000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x640000001000000010000000640000005000000043534152477631004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc4128755508da7aa1fd862ab37b0e6ef59a8cb026c9bacc0c07e838683cb6cf3404e1900000000000000" + ] + }, + "0x715c3e373c2d4cc35c03c86a41031d7f8be2bc768e644461eac57e5eab004d28": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x54ea0c5e8948e5691f98bebe41b5071a4a8c762ca435c622761508af8cd4e51d" + } + } + ], + "hash": "0x715c3e373c2d4cc35c03c86a41031d7f8be2bc768e644461eac57e5eab004d28", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xc1a6e6f593ae6b52c28cab12c650db48041bbcb1ae6a7e06b800c199c6d8a4da" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x93f05f4fb67225c694ab2cb4c4b57125136e08fcff490458f42fa63e26086bdd", + "hash_type": "data1" + }, + "type": { + "args": "0x23", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0xbe0000001c0000003c0000005c0000007100000079000000bd0000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412414141414141414141414141414141414141414141414141414141414141414111000000656d657267656e63792072656c656173657800000000000000020000004242424242424242424242424242424242424242424242424242424242424242a5fa3ab929ac363193de55b810aaac99277989f8550a29501c8d393e11a16ee500" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c000000280000004353415247763100a5fa3ab929ac363193de55b810aaac99277989f8550a29501c8d393e11a16ee5" + ] + }, + "0x71ebb2e3086c8a436787a22c176235cc45e7b75779ba47d34bd390f41b4b9af5": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xac7777ae99467a32a740f9f89776f0a5c1a17808c49e29e69a1b193d9f751e1b" + } + } + ], + "hash": "0x71ebb2e3086c8a436787a22c176235cc45e7b75779ba47d34bd390f41b4b9af5", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x8795ebb4c7d5d03abcc518a9b13121a77c850e6fa9e6b62e108c3c22b40b1cc6" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x6d3f4ff4a0012611cfad302ba4de186169643407d6fb5b302cbd11183f5b280e", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x000000000000000000000000000000000000000000000000000000000000000001000000000000001111111111111111111111111111111111111111111111111111111111111111f4010000000000000a0000000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x73908c7815c16a3a45f876d8695355d173f8d1ab68c8b7e74d2bd6d398d440ae": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x5a42e270ccd43a33e96ba446dc3305288ff81717caf6d657da2f20c5cfda25d8" + } + } + ], + "hash": "0x73908c7815c16a3a45f876d8695355d173f8d1ab68c8b7e74d2bd6d398d440ae", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x402005d53808fc439e58003f9ffecbc77ab2cf229dbf93bff3888221c05c37cd" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x104c533c00", + "lock": { + "args": "0x", + "code_hash": "0x204920209dab2137b0f75335063856cc1e84d28b41d1e31c2160614832de50ec", + "hash_type": "data1" + }, + "type": { + "args": "0x51", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x204920209dab2137b0f75335063856cc1e84d28b41d1e31c2160614832de50ec", + "hash_type": "data1" + }, + "type": { + "args": "0x52", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x8d00000018000000380000007c0000007d00000085000000081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd30802000000cf8cbc02d90b9e9bca7eb320ce67edfcf62400cd2a8bfb81a8104cf3942bca74edf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae0201000000000000000a00000000000000", + "0xdf00000034000000540000005c0000007c0000007d0000009d000000a5000000c9000000ca000000ce000000d6000000de000000081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd3080100000000000000cf8cbc02d90b9e9bca7eb320ce67edfcf62400cd2a8bfb81a8104cf3942bca7401d07ba215cc89ea5a359c4a8ab2a8a483caf0f466281ce46d2d5b862e8d8dcc3d000000000000000020000000d07ba215cc89ea5a359c4a8ab2a8a483caf0f466281ce46d2d5b862e8d8dcc3d02000000001400000000000000b40500000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x64000000100000001000000064000000500000004353415247763100cf8cbc02d90b9e9bca7eb320ce67edfcf62400cd2a8bfb81a8104cf3942bca74d07ba215cc89ea5a359c4a8ab2a8a483caf0f466281ce46d2d5b862e8d8dcc3d1400000000000000" + ] + }, + "0x740f68ba912a942022541741c57d332680fb0d6d73fece3763ce2f1a1a4d0628": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x297acc94d2c6e532490f039bfbfeed7c2e494fef06b7adb6cf00a4287dca0a73" + } + } + ], + "hash": "0x740f68ba912a942022541741c57d332680fb0d6d73fece3763ce2f1a1a4d0628", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x1fb33087bb84449f9219ff127824406cc1ad70f99c4f04d583d6e4a80753cf3b" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x93f05f4fb67225c694ab2cb4c4b57125136e08fcff490458f42fa63e26086bdd", + "hash_type": "data1" + }, + "type": { + "args": "0x23", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x9e0000001c0000003c0000005c00000071000000790000009d0000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412414141414141414141414141414141414141414141414141414141414141414111000000656d657267656e63792072656c65617365780000000000000001000000424242424242424242424242424242424242424242424242424242424242424200" + ], + "version": "0x0", + "witnesses": [] + }, + "0x7a8b320dab64745045b14d0bc21679b0d6b136775eae11033b5041b6f2912c5a": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xeb13566917d6910918b1ccecac0c80f748dd0947169e3771684db5322187b986" + } + } + ], + "hash": "0x7a8b320dab64745045b14d0bc21679b0d6b136775eae11033b5041b6f2912c5a", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xb5bb69474889615326f43e030a432ec50a00f3a71c093557029ac47e171bb34d" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x1", + "tx_hash": "0xb5bb69474889615326f43e030a432ec50a00f3a71c093557029ac47e171bb34d" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x2", + "tx_hash": "0xb5bb69474889615326f43e030a432ec50a00f3a71c093557029ac47e171bb34d" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x3", + "tx_hash": "0xb5bb69474889615326f43e030a432ec50a00f3a71c093557029ac47e171bb34d" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x9c1a6fed0b778db0b8006116e9d9a6213d2b5dadf1ac391118790c44a9ffe275", + "hash_type": "data1" + }, + "type": { + "args": "0x21", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x24", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x9c1a6fed0b778db0b8006116e9d9a6213d2b5dadf1ac391118790c44a9ffe275", + "hash_type": "data1" + }, + "type": { + "args": "0x24", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x000000000000000000000000000000000000000000000000000000000000000007000000000000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412fa00", + "0xfa000000000000005041594d30303031", + "0x16260000000000005041594d30303031" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100", + "0x", + "0x", + "0x" + ] + }, + "0x7c08591b593710f6481af4afcbbdb671fa46332b64a890850192409e7a7242c2": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x16fe2ced0417b0a62f56bffaea8082d4901f2327c2b3f0e8e6f7d867575a1ee4" + } + } + ], + "hash": "0x7c08591b593710f6481af4afcbbdb671fa46332b64a890850192409e7a7242c2", + "header_deps": [ + "0x8ddb85198d040fa97fc5d43e6d725e5b5ed0bf285022cc66a10e9bb1379bfecb" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x049df337a2dff720c87c75a9aee3508694c52030e387d1820a4afa28a14b8254" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0xcc321278301291afc3c43ae43d5e3e3c10ce9cf0658c3063587b0123cdce7cef", + "hash_type": "data1" + }, + "type": { + "args": "0x45", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0xcc321278301291afc3c43ae43d5e3e3c10ce9cf0658c3063587b0123cdce7cef", + "hash_type": "data1" + }, + "type": { + "args": "0x43", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x1e000000000000005645535430303031", + "0x00d51ef4e28799d94e9046a2873a2974c4fbe51572f6fd1d579cd43f4bf679fb87640000000000000032000000000000000000000000000000000000000000000016000000000000005645535430303031" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "0x7dbcaed57f3138633b094969af7e098cfc7e5615d0822585ef1bc3d2a0621514": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xd0b58fa147e5809651e0e3ec1edf1ebd5d25f78cd62529195d11c23bebab6f72" + } + } + ], + "hash": "0x7dbcaed57f3138633b094969af7e098cfc7e5615d0822585ef1bc3d2a0621514", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x5065689f1cfd78aac7cf05f9f31ac375afbb37740bde275f017eb66b26aa2973" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x582102f2b60c104220dfb5607341b748d7a0651050af5d2ee44f3a5fc540ce1d", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x444444444444444444444444444444444444444444444444444444444444444411111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x7e40aa9553c4f2c63d5ae3732a4d57a9e697e14b8bf8428dcd99574709a66b9e": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xd0b58fa147e5809651e0e3ec1edf1ebd5d25f78cd62529195d11c23bebab6f72" + } + } + ], + "hash": "0x7e40aa9553c4f2c63d5ae3732a4d57a9e697e14b8bf8428dcd99574709a66b9e", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x7dbcaed57f3138633b094969af7e098cfc7e5615d0822585ef1bc3d2a0621514" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "0x7ebc3eee04e3daaf2d17874dcb156ce493376dfb56327782ad398567d2d154ee": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x7f27bfaffe26061a6317a13ef25b9a6c7aa5ace6f31f4463fec22eb89aed6d18" + } + } + ], + "hash": "0x7ebc3eee04e3daaf2d17874dcb156ce493376dfb56327782ad398567d2d154ee", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x66714451088a178f55c3f7a67c56800a817ec3314fabd8ed14f3b8172b43f8d9" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x078a625eb933f34dee98290d89c9cd6b57ab95d8bb1a89f254f422649a972954", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc4128a166edfc898dd17d9c3968fdee59e903e0b98ddf23d77c870dee1585cce89020064000000000000000000000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x8128c4595a00a0cc220271dded5f787a8106cbefee1554c9719e586e76b9893f": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x2da50f9b01999a58735ab822ba3bc2b743d5d297b196e77f7cbbe84bc2766aac" + } + } + ], + "hash": "0x8128c4595a00a0cc220271dded5f787a8106cbefee1554c9719e586e76b9893f", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x835773fb38ff3537ca5a342019797b8ac9f66e92beaf0308bd39cf93eec19ad0" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631001111111111111111111111111111111111111111111111111111111111111111" + ] + }, + "0x835773fb38ff3537ca5a342019797b8ac9f66e92beaf0308bd39cf93eec19ad0": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x2da50f9b01999a58735ab822ba3bc2b743d5d297b196e77f7cbbe84bc2766aac" + } + } + ], + "hash": "0x835773fb38ff3537ca5a342019797b8ac9f66e92beaf0308bd39cf93eec19ad0", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xe698912826f745a5cd946be2a3714100a90cb83a80f7fd6d9563b819d96f5714" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xaebdd9d7c1cd1a9b581bc3a42a6c5f40d5b41f2ee637f7a1b4e4eef38500c349", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x8d00000018000000380000007c0000007d00000085000000444444444444444444444444444444444444444444444444444444444444444402000000111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222220200000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x86a24cb3b26a8379df76a852b569c5148b51988ba34b411062d49a545fb0d876": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x757f318b25b23b441765bcbfac6ae04d0986ffcbdbc86cc58ebdd7eef79c65fc" + } + } + ], + "hash": "0x86a24cb3b26a8379df76a852b569c5148b51988ba34b411062d49a545fb0d876", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xabeea9b1c46e3715dd21a1fdbbc297ef86423d8fb3d2f10adfc60487cb3f7a49" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x0d708753715b0c50502a06fea8c0d48b890212a5217b4330b7a77805a71f3dd7", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0xff00000034000000540000005c0000007c0000007d0000009d000000a5000000a9000000aa000000ee000000f6000000fe000000444444444444444444444444444444444444444444444444444444444444444401000000000000001111111111111111111111111111111111111111111111111111111111111111003333333333333333333333333333333333333333333333333333333333333333f401000000000000000000000202000000111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222220a00000000000000d00700000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x8729c54e1abbda37d62f0a446976f690613c634292e5e895b063547c5caa8e70": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xec73cb2253c130c509a2fb0fa9557411c1bd607b51eb3ed20153393ca8c72157" + } + } + ], + "hash": "0x8729c54e1abbda37d62f0a446976f690613c634292e5e895b063547c5caa8e70", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x0fc3217536599ec792bf62408185b2a5c32103c994bd312fa19dbb7d02a957ac" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xa34564aa114e28106b02f63243b50f5135adc633ff7a74e735d27e9404bf0710", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x860000001c000000350000003d0000005d000000650000006d00000015000000416363657074616e636520436f6c6c656374696f6e0400000041435054318db5041f6426dc6a98825d53904d3a1e578215ffdc93ce6322fa53b18cd3610000000000000000c80000000000000015000000636b623a2f2f63656c6c7363726970742f6e66742f" + ], + "version": "0x0", + "witnesses": [ + "0x8a00000010000000100000008a000000760000004353415247763100318db5041f6426dc6a98825d53904d3a1e578215ffdc93ce6322fa53b18cd361c8000000000000001900000015000000416363657074616e636520436f6c6c656374696f6e0800000004000000414350541900000015000000636b623a2f2f63656c6c7363726970742f6e66742f" + ] + }, + "0x899bac1c9e02a9ca6cd47386e6fd5470d06ba1348a018b2acd8d3d0bbde2bade": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc3d498167f8fa254bdaed6029f276aacea9d662a5cd393b4cc19cffa2889fe25" + } + } + ], + "hash": "0x899bac1c9e02a9ca6cd47386e6fd5470d06ba1348a018b2acd8d3d0bbde2bade", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xb72fc8f97ab673b26bfb904541e07cd3820cd528c0df3166619168b72f79bc37" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x9502f9000", + "lock": { + "args": "0x", + "code_hash": "0xa3bdc8e44991a1790d469db884797763ce5dd7ef8631b77f45c2d925633dbbd9", + "hash_type": "data1" + }, + "type": { + "args": "0x73", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0xa3bdc8e44991a1790d469db884797763ce5dd7ef8631b77f45c2d925633dbbd9", + "hash_type": "data1" + }, + "type": { + "args": "0x71", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x6d7122fbd537293591fef620082e9213b875ee6f8a926497a533aee6f4ff93560e7da160d8e77e9274a6fa6f8243153d2bae61ddf817d97c42e9cc7861e1f830414d4d4130303031414d4d42303030311027000000000000204e00000000000010270000000000001e00", + "0xe803000000000000414d4d4130303031" + ], + "version": "0x0", + "witnesses": [] + }, + "0x8b4922b49150481d756b6c3af4236357618d9dcbff0eee4e164ff3288640e9f5": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xe483d497e40139e1da27c2904f8438c0682a4ff9578d41a24eed218fa5ff76fd" + } + } + ], + "hash": "0x8b4922b49150481d756b6c3af4236357618d9dcbff0eee4e164ff3288640e9f5", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x29d306ab03fdfef7ef8fe68ab222e5b318f9a82732f3126627e691b40b2994fe" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x104c533c00", + "lock": { + "args": "0x", + "code_hash": "0x7cda3da79fb46eaed62752444d9d4f666897861039c77bf94f0532fc43162a70", + "hash_type": "data1" + }, + "type": { + "args": "0x51", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x7cda3da79fb46eaed62752444d9d4f666897861039c77bf94f0532fc43162a70", + "hash_type": "data1" + }, + "type": { + "args": "0x52", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x8d00000018000000380000007c0000007d000000850000000000000000000000000000000000000000000000000000000000000000000000020000007d079c63043b805af33e1b72b25e83ba2897b47af215f9174932de79e9393d01edf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae0101000000000000000a00000000000000", + "0xbf00000034000000540000005c0000007c0000007d0000009d000000a5000000a9000000aa000000ae000000b6000000be000000000000000000000000000000000000000000000000000000000000000000000001000000000000007d079c63043b805af33e1b72b25e83ba2897b47af215f9174932de79e9393d0102edf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae00000000000000000000000001000000001400000000000000b40500000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x640000001000000010000000640000005000000043534152477631007d079c63043b805af33e1b72b25e83ba2897b47af215f9174932de79e9393d01edf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae1400000000000000" + ] + }, + "0x8b85a45b4b3c0da4633ca155697290d15ec99bb27068b1a2f611d49214869704": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x6988a589235f9fd830f970f1302dbeb1685104a75ff5c95e13fa8f833fa67f84" + } + } + ], + "hash": "0x8b85a45b4b3c0da4633ca155697290d15ec99bb27068b1a2f611d49214869704", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x49806df28e0ddba29d6678861a68552f533ef8be66a3a554e12bfb4ff3228337" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x13814f1b2b6fa776385ebf4a63ee7ec94ebc806957ac34dc565d573e7f9b0f4f", + "hash_type": "data1" + }, + "type": { + "args": "0x62", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x13814f1b2b6fa776385ebf4a63ee7ec94ebc806957ac34dc565d573e7f9b0f4f", + "hash_type": "data1" + }, + "type": { + "args": "0x63", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x0400000000000000414d4d4130303031", + "0x0900000000000000414d4d4230303031" + ], + "version": "0x0", + "witnesses": [] + }, + "0x8cfc356815eccbcae35af59d0034cdda7af6094bb3a6e95e1b89b799f5d4cca5": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xfc01255f8d4c79d2307cbf689795022d46555c16027b3c954bc9969ec7387d81" + } + } + ], + "hash": "0x8cfc356815eccbcae35af59d0034cdda7af6094bb3a6e95e1b89b799f5d4cca5", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x8e6c407a748b66fd7f9a77896102faaa945086fafdf0d3e01c4d595ff8834bbe" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xca92833ef77901845dd672a356fa9e568a3d46f179a1df5469dbbc05b7313427", + "hash_type": "data1" + }, + "type": { + "args": "0x1f", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x2a00000000000000544f4b454e303031", + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412156c2221f53a61d6d5a035d0d8ee30837f4503dd0cca1f1901317c357fdcacd800f4010000000000000100000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x8e884dba5cfbc95c6e63d17866f4568b4fb2f28003b49d51d98ce98042d5c3b1": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x6b76a471c376d588ebcc61b7ace0fd489d5015ce27ca1d261927a05e12c35e3f" + } + } + ], + "hash": "0x8e884dba5cfbc95c6e63d17866f4568b4fb2f28003b49d51d98ce98042d5c3b1", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x276185522c8293f202a9cd62aa08de0d351a79ad52619900408546cca3ffb5f8" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xa082732704b5885b30b692c60030e4fc137fe6be30a8f0dc9a024458ac97f783" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x7141eb99856d0a6a3923546546cdd2c8dc894c542348e4f27acf6b20b52213fb" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xdbd009c29602a2a2cce27bdf67345b95a9950473b5d2abdb64508903b9092503" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x5d21dba000", + "lock": { + "args": "0x", + "code_hash": "0x450a496151c4111710cd56ffe7558b63b2a5e22e0ad7fd33edcf52173121c440", + "hash_type": "data1" + }, + "type": { + "args": "0x63", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0xfa000000000000005041495230303031" + ], + "version": "0x0", + "witnesses": [] + }, + "0x90885689172ed74eedb55cca655df84188643e6cd752f1aa350dc8cb9679dd88": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xe274608e446e15ce0f9ec8680954950c72336954fb025575fb4a310bad2c3d63" + } + } + ], + "hash": "0x90885689172ed74eedb55cca655df84188643e6cd752f1aa350dc8cb9679dd88", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x5f7e68e39b7606ffe8fb6730ed62c594e8b84ad854fdb45df92b1e05ee199124" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x2ca9877f46bc3e89b31d76e256714e075ad57732d7fcd489fdc6aa636772f93d", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x000000000000000000000000000000000000000000000000000000000000000001000000000000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412fa00" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412" + ] + }, + "0x95cf642ae48c517930fe6e3c737fd3981c2f910242f22d5dee7ab53f44c20d9f": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x4e6498bb05ab2acef4f3dc7aca48bea59b65a76ba1be2359d334621a701672c0" + } + } + ], + "hash": "0x95cf642ae48c517930fe6e3c737fd3981c2f910242f22d5dee7ab53f44c20d9f", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x56c800e60d9f2a4a012acf29ebfa72256bbfa55276cacb9ba6c4e828372975f3" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x9502f9000", + "lock": { + "args": "0x", + "code_hash": "0x513c43aa3e994cbdf9bcd7903f1e220882873ad830b3eb024cdeefe2ca3afcb3", + "hash_type": "data1" + }, + "type": { + "args": "0x6e", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0xdf8475800", + "lock": { + "args": "0x", + "code_hash": "0x513c43aa3e994cbdf9bcd7903f1e220882873ad830b3eb024cdeefe2ca3afcb3", + "hash_type": "data1" + }, + "type": { + "args": "0x6f", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x0358b5b4af3799ee4f7fbb489135f67e4b316c3a6bc5ef6e31a7b80958ea1569ee44736c2a40bb9b927c93b719db9a3681696c42caa38f292b3a0423234fff93414d4d4130303031414d4d42303030316400000000000000c800000000000000e8030000000000001e00", + "0x1613b7f52b423c70c7351fd7417b8b1532df3b07313b23ccd595f51552ccf0e164000000000000005d4eec43082abf0f7a62b2f9682051adeb7e017c32f00756482c17985bef0bd6" + ], + "version": "0x0", + "witnesses": [] + }, + "0x964480ea9d113044f45b7aa836999dc9486a95c1f3786c7ffaa6df2a1457cc0c": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xdff9e4e52961c196f41c52c2d211468dfa6b3af8c4f194a02d4bc59bfe39d066" + } + } + ], + "hash": "0x964480ea9d113044f45b7aa836999dc9486a95c1f3786c7ffaa6df2a1457cc0c", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x073245d0e75464ee92f0f4a3264f989fe4ea3c11dac3e4a1abd6084ea8dc2305" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "0x96729af7693292d31397d4ebdf3624d743424a305cb8c4f7c6bdc5ad63111c9e": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc145bbbef86e1441c587b6a24a8007c687becdb42b503a349b06475e8a86de48" + } + } + ], + "hash": "0x96729af7693292d31397d4ebdf3624d743424a305cb8c4f7c6bdc5ad63111c9e", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x75b5f01c9b1084b6555ac61833d40d2860594ffd8f04544d09f94e4818bed2d8" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0xba43b7400", + "lock": { + "args": "0x", + "code_hash": "0xf51e1060b13ba495ab2cac42ab5102d7ff6bb2c00df50f115846b5fcc9429298", + "hash_type": "data1" + }, + "type": { + "args": "0x52", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x51", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0xbf00000034000000540000005c0000007c0000007d0000009d000000a5000000a9000000aa000000ae000000b6000000be000000081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd3080900000000000000fbf9445aa019c17dbdd072cae5c7840097cba346940a427fc4269257922e5bf70064f944c22f0db7bfb29aa523b65cbb75a6a65b369febfbff0ffc17facacfe2dcf4010000000000000000000002000000001400000000000000d00700000000000000", + "0x8d00000018000000380000007c0000007d00000085000000081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd30802000000fbf9445aa019c17dbdd072cae5c7840097cba346940a427fc4269257922e5bf7edf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae0200000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x98b4b13e3fc4f920bf1ae1626c1959f156e3d8ba0b608e4742e6e8ef998a149f": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xdb30f9d5f126ed97836bb953e06415433f7367a42dd24b0a6b53f934259b70c9" + } + } + ], + "hash": "0x98b4b13e3fc4f920bf1ae1626c1959f156e3d8ba0b608e4742e6e8ef998a149f", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x03d0a2e99d75a22d0d4751e49817d0da8584223a20bfdca8ec32e4da959a0d9f" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x31d33d47ea68158d86bae548d378ef89d9ea6d80a7b8cded0320296ac8ff0a83" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x214893fb9dd10fe10e4a92ef80b06c126808256f524d308629c828a49c4327de", + "hash_type": "data1" + }, + "type": null + }, + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x214893fb9dd10fe10e4a92ef80b06c126808256f524d308629c828a49c4327de", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x544f4b454e30303164000000000000005555555555555555555555555555555555555555555555555555555555555555", + "0x444444444444444444444444444444444444444444444444444444444444444411111111111111111111111111111111111111111111111111111111111111110064000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x9c2c24f15cb3583f2f36a4bf4febc0fed09c369a71f5c3cd2148e206b8d788ee": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x7f27bfaffe26061a6317a13ef25b9a6c7aa5ace6f31f4463fec22eb89aed6d18" + } + } + ], + "hash": "0x9c2c24f15cb3583f2f36a4bf4febc0fed09c369a71f5c3cd2148e206b8d788ee", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x7ebc3eee04e3daaf2d17874dcb156ce493376dfb56327782ad398567d2d154ee" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x078a625eb933f34dee98290d89c9cd6b57ab95d8bb1a89f254f422649a972954", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc4128a166edfc898dd17d9c3968fdee59e903e0b98ddf23d77c870dee1585cce8902006e000000000000000000000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x440000001000000010000000440000003000000043534152477631000a000000000000008a166edfc898dd17d9c3968fdee59e903e0b98ddf23d77c870dee1585cce8902" + ] + }, + "0x9cc87bc8882895ab82b3cc4c91c1a6da4a0831019bad2b9454fb7195d703420f": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x339055295d20077427f346e209218b486acf729ef51fab4051a6c08999fdf40a" + } + } + ], + "hash": "0x9cc87bc8882895ab82b3cc4c91c1a6da4a0831019bad2b9454fb7195d703420f", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x44e53c1d471b7bdfbda6816b72152f0c83550a5296edac7313b645ccde8527dc" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x2a00000000000000544f4b454e303031" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412" + ] + }, + "0xa0ad32ea6682bbe84f394ed740b42c4412c497273adb2955e6cbbba2d6d25137": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xef6eddcdb6a4500202839de5d3929cdcb7ade4274dee72021b5612314ad01235" + } + } + ], + "hash": "0xa0ad32ea6682bbe84f394ed740b42c4412c497273adb2955e6cbbba2d6d25137", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x3e7ce34e4208d96287e089a8e7cf9e706fac3c0735d0951c7669b1f4191b92c5" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x2f95e743beef860d851bf2277fc56c036c5be0b956db68627791b404b45067e9", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x00000000000000000000000000000000000000000000000000000000000000000100000000000000111111111111111111111111111111111111111111111111111111111111111144444444444444444444444444444444444444444444444444444444444444442222222222222222222222222222222222222222222222222222222222222222fa00" + ], + "version": "0x0", + "witnesses": [] + }, + "0xa0d20ba71c2ee983d8b2ce0c261dad1978f0c078122ec9cf711ece0d24d6b223": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x605ff9349d7a02a281af3488d3f7eeedea672de6d61f015759297b95cec97b33" + } + } + ], + "hash": "0xa0d20ba71c2ee983d8b2ce0c261dad1978f0c078122ec9cf711ece0d24d6b223", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x4b79a85846e54477f3689cc8fa64e3488e1f6c7da2ad02b12b4d7a623a8093f4" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x2ca9877f46bc3e89b31d76e256714e075ad57732d7fcd489fdc6aa636772f93d", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x000000000000000000000000000000000000000000000000000000000000000001000000000000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412fa00" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412" + ] + }, + "0xa120df0ec121ee17c99593ca6475b3874bd6fff05693e1e579f03c878e6b6303": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x969891936e2843de5c335b05af807ebe3ffc6d6f3e371e129c150b7944389b27" + } + } + ], + "hash": "0xa120df0ec121ee17c99593ca6475b3874bd6fff05693e1e579f03c878e6b6303", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xcc6acde163e8d23052be1f9fa08173cc664713ef99f8921d8bfb8b71f9f6a9a6" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x87c083fadb4ec6e5823e9bd76b000f7b98f4b374cea82ed6a528915317d8a59e", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x860000001c000000350000003d0000005d000000650000006d00000015000000416363657074616e636520436f6c6c656374696f6e040000004143505411111111111111111111111111111111111111111111111111111111111111110100000000000000e80300000000000015000000636b623a2f2f63656c6c7363726970742f6e66742f" + ], + "version": "0x0", + "witnesses": [] + }, + "0xa278863a1589ef75f641ead8c869a21b4f426a167f4814927af651f01de54cea": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xfa1320af6eff6f2b2b69e30391ca3a027c259318a86dca32e3238884311b84d7" + } + } + ], + "hash": "0xa278863a1589ef75f641ead8c869a21b4f426a167f4814927af651f01de54cea", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x238f2ceedd51e0575705f189111340520552a861e09666e47b4517ef10757b01" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x12a05f2000", + "lock": { + "args": "0x", + "code_hash": "0x967d150b17ee92167fda6eb3b28e453cfc084f19c7cbfc773bb3f1b93d4dea61", + "hash_type": "data1" + }, + "type": { + "args": "0xc1", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x8d0000001c000000330000003b0000005b000000630000006b00000013000000537461746566756c20436f6c6c656374696f6e04000000534e46543b27b52bcff21bb54b5ab2951b1ee1153fa29982ff473bf003e6097276b6d55c0000000000000000c8000000000000001e000000636b623a2f2f63656c6c7363726970742f737461746566756c2d6e66742f" + ], + "version": "0x0", + "witnesses": [ + "0x910000001000000010000000910000007d00000043534152477631003b27b52bcff21bb54b5ab2951b1ee1153fa29982ff473bf003e6097276b6d55cc8000000000000001700000013000000537461746566756c20436f6c6c656374696f6e0800000004000000534e4654220000001e000000636b623a2f2f63656c6c7363726970742f737461746566756c2d6e66742f" + ] + }, + "0xa74c6e001ecc03a1e0432afe27307efcfb85090f1ad2734deca603240a2da157": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x9f02df0a573644347b6f73102ec88a9c6be51b35fb36c6305e17048c3f13ec0d" + } + } + ], + "hash": "0xa74c6e001ecc03a1e0432afe27307efcfb85090f1ad2734deca603240a2da157", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xadd93fd1d69b52f2de36bfa1d108d8d143b137d7c043622fe1d1175589f748ee" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x078a625eb933f34dee98290d89c9cd6b57ab95d8bb1a89f254f422649a972954", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc4128a166edfc898dd17d9c3968fdee59e903e0b98ddf23d77c870dee1585cce8902006e000000000000000000000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x440000001000000010000000440000003000000043534152477631000a000000000000008a166edfc898dd17d9c3968fdee59e903e0b98ddf23d77c870dee1585cce8902" + ] + }, + "0xa8e8c60bbed4ebf0747eb82243ce7c6644d925a506d822cdc080dfc26a067dd2": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x2f2a53ea7336cf1d1b199a59b22148e7357d2e12846050a664cce5bf73094e80" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x17fdc71ba9532d39718b8f52c521c40c1f5c19b7194bad896326abddc303c7bb" + } + } + ], + "hash": "0xa8e8c60bbed4ebf0747eb82243ce7c6644d925a506d822cdc080dfc26a067dd2", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x0f7ef307cd4762342d70d780814b26f305dd44d149a9e91c37443b622b72e34b" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xc5eb6cb03878c77e75b8462c561205189574fc4677e5538cc9136c10ab9921da", + "hash_type": "data1" + }, + "type": { + "args": "0xb2", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x104c533c00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x544f4b454e3030312a000000000000007d84ad3da83e8ccab86a945f8bb0b74ebaa3d29369b01c5370dff1f340078ac0", + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "0xab0f22960f33ac447c3075073190a7127930d337a090125a7279a8544f3d28d5": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x84949d0ac6b772fbe9eddc7aaecf1527609fb591c42129deea687f59d3bde57b" + } + } + ], + "hash": "0xab0f22960f33ac447c3075073190a7127930d337a090125a7279a8544f3d28d5", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xa8ec24a7d804f12cdf165ba7019d8af530517535622350a74dd0d7c3acff1843" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x8f6887aa0f2b4aa9e92983daca7c37343c9594fba07a73d56c4a1dbd71c7c10d" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x3a35294400", + "lock": { + "args": "0x", + "code_hash": "0x0a4cb03cd7e44ca4449ded0f8c6014c0f919819072badeb07bd51e0add611f35", + "hash_type": "data1" + }, + "type": { + "args": "0x25", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x860000001c000000350000003d0000005d000000650000006d00000015000000416363657074616e636520436f6c6c656374696f6e0400000041435054ad2efee9b2aa7cf537c64f1adbbf7bf4d35accce0f9c25dc9e7b111ea4dc87ea1400000000000000e80300000000000015000000636b623a2f2f63656c6c7363726970742f6e66742f" + ], + "version": "0x0", + "witnesses": [] + }, + "0xabf216907540017b954863b29e925febb092f833c52f4b0c4678603a0c60cfd7": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xd0b58fa147e5809651e0e3ec1edf1ebd5d25f78cd62529195d11c23bebab6f72" + } + } + ], + "hash": "0xabf216907540017b954863b29e925febb092f833c52f4b0c4678603a0c60cfd7", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x166d2f0593e62b5bd99da592327fdf69f784a0624f95140a614a41f0ec048c6f" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x582102f2b60c104220dfb5607341b748d7a0651050af5d2ee44f3a5fc540ce1d", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x444444444444444444444444444444444444444444444444444444444444444411111111111111111111111111111111111111111111111111111111111111110001000000000000000000000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0xac9d28c6d3ff7bbf0357a655c0aac471c77bb9ad97374dbc2d507eae0541a733": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x9b6af43c1c3e7556bbb8d2b570bf4c0c29bfc13989a59bc601a05810d7a78d85" + } + } + ], + "hash": "0xac9d28c6d3ff7bbf0357a655c0aac471c77bb9ad97374dbc2d507eae0541a733", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x40d1840247d7ff684bec814254db3e3a8d2515f59a3c02c6bd95a99120f10c20" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x104c533c00", + "lock": { + "args": "0x", + "code_hash": "0x9da2791f3f0a46790e4b187a1d1dd15813f2c14883138631255c58b495a845d8", + "hash_type": "data1" + }, + "type": { + "args": "0x51", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x9da2791f3f0a46790e4b187a1d1dd15813f2c14883138631255c58b495a845d8", + "hash_type": "data1" + }, + "type": { + "args": "0x52", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x8d00000018000000380000007c0000007d000000850000000000000000000000000000000000000000000000000000000000000000000000020000004d594af7f4d8c8158e0225a7d1a80903f7ea8df3f81bc67791af289b03ae879dedf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae0201000000000000000a00000000000000", + "0xc000000034000000540000005c0000007c0000007d0000009d000000a5000000aa000000ab000000af000000b7000000bf000000000000000000000000000000000000000000000000000000000000000000000001000000000000004d594af7f4d8c8158e0225a7d1a80903f7ea8df3f81bc67791af289b03ae879d0300000000000000000000000000000000000000000000000000000000000000000200000000000000010000000202000000001400000000000000b40500000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x450000001000000010000000450000003100000043534152477631004d594af7f4d8c8158e0225a7d1a80903f7ea8df3f81bc67791af289b03ae879d021400000000000000" + ] + }, + "0xad8a03597cf6aeceb16aa4a16ccc2828bcbd49b1aa2861b5fa01440d6fe803e3": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x1", + "tx_hash": "0x515a67864ac1959d9cd4fa108ba421b195a4410f1878832bd01208b5d86e7fcd" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xba786ad1ae914446151de4ce6258fc3d780be1d17424330bbd6a36b6b87f30a1" + } + } + ], + "hash": "0xad8a03597cf6aeceb16aa4a16ccc2828bcbd49b1aa2861b5fa01440d6fe803e3", + "header_deps": [ + "0x933f1ca9e878cbe88f51849a169762b1d11758cf7d62db67824490dfdb39d8e1" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x515a67864ac1959d9cd4fa108ba421b195a4410f1878832bd01208b5d86e7fcd" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x42", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x45", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x45", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x50000000000000005645535430303031", + "0x00000000000000005645535430303031" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412", + "0x" + ] + }, + "0xaf62b59e32e627da106edf13d40c811ce3dec551c1d67ea8831100cf3862c90f": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x969891936e2843de5c335b05af807ebe3ffc6d6f3e371e129c150b7944389b27" + } + } + ], + "hash": "0xaf62b59e32e627da106edf13d40c811ce3dec551c1d67ea8831100cf3862c90f", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xa120df0ec121ee17c99593ca6475b3874bd6fff05693e1e579f03c878e6b6303" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631001111111111111111111111111111111111111111111111111111111111111111" + ] + }, + "0xb112c9cde54c7772d740ce548093c97278a1c295fd05a60d2999dbab9ef7186c": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x1", + "tx_hash": "0x33611509a83b78a19cf2ce0980216ef2aa22a359ffb7d07bf31fbd73c339444c" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xb58deece93c4942aa5ab1e0722ebfceaa8f9fabe3c6e8eb01dff0f2bd44b176d" + } + } + ], + "hash": "0xb112c9cde54c7772d740ce548093c97278a1c295fd05a60d2999dbab9ef7186c", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x33611509a83b78a19cf2ce0980216ef2aa22a359ffb7d07bf31fbd73c339444c" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x734c3177a05355d83a3be6c68309e377cfa444fc62d5d6504b3664ea090e9b02", + "hash_type": "data1" + }, + "type": { + "args": "0x22", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x104c533c00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x7e0000001c0000003c0000005c00000071000000790000007d0000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc41294ad7e819f84f5e26539b71e3e49ce236802e80d09e5ad2f3a0c8a17264b144211000000656d657267656e63792072656c6561736500000000000000000000000000", + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x5500000010000000100000005500000041000000435341524776310094ad7e819f84f5e26539b71e3e49ce236802e80d09e5ad2f3a0c8a17264b14421500000011000000656d657267656e63792072656c65617365" + ] + }, + "0xb492fefbdce3c5a93e58b60643f9b3f851703401e75a5f6070e6e016bb1b6e48": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xf0f807aecb16aaf7820fdb2c70d719912dd02d8274f76343fc42eebdbc3bcc02" + } + } + ], + "hash": "0xb492fefbdce3c5a93e58b60643f9b3f851703401e75a5f6070e6e016bb1b6e48", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xc0ca46dcc6d0c1c6ba5944656d1e5650b877f934911cb540e2b52a1f191f85b9" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631001111111111111111111111111111111111111111111111111111111111111111" + ] + }, + "0xb629ee7fb29df995ea9fae1d02db475f878ffea6657cea91d21fe1986779692f": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x14e410f98eb197fc6a336f68534cee7ce181ab0d6ea6bc28a8f66acb6a3c8c44" + } + } + ], + "hash": "0xb629ee7fb29df995ea9fae1d02db475f878ffea6657cea91d21fe1986779692f", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x0f7efec9879eec40f3a9034d15db163c4a51adbf41ac67eb171d8e3944cd680c" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x2540be400", + "lock": { + "args": "0x", + "code_hash": "0x1b82b117ab4e41de9f6652a54f2f2ee24abad190b04a935cf90075cf9f3da237", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x0700000000000000544f4b454e303031" + ], + "version": "0x0", + "witnesses": [] + }, + "0xb74d691e2b3b09ba70b33cae3a78c04ab723fede031598d5ebbb30f3f79c8442": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x04ff3d5eebf352f6edd435d3c42bba62a2b84b65b79504643548e80b2d4d150c" + } + } + ], + "hash": "0xb74d691e2b3b09ba70b33cae3a78c04ab723fede031598d5ebbb30f3f79c8442", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x0cba9e700c8a662884ab05066a027cc22b03e79ac0facfcbe0d002acd391bc7f" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x67c60bfc34958c28b1d5277be6995af9920d480f60ded40806eaf173c704d10d", + "hash_type": "data1" + }, + "type": { + "args": "0x51", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x8d00000018000000380000007c0000007d00000085000000081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd30802000000d3c12a90f9db949dcb505dbc44a10f1ea772924a1629c2084e30f136ed7c1cbeedf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae0200000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x8d00000010000000100000008d000000790000004353415247763100081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd3084400000002000000d3c12a90f9db949dcb505dbc44a10f1ea772924a1629c2084e30f136ed7c1cbeedf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae020a00000000000000" + ] + }, + "0xb7978d739c6d861ab1902226dbc51bfad44edf6dcfdee1dc303f50606f4c62ea": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x4d298843298431d70021bb66737e15abfe84b67851ce6a99787c28941caee507" + } + } + ], + "hash": "0xb7978d739c6d861ab1902226dbc51bfad44edf6dcfdee1dc303f50606f4c62ea", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x031bde521ff6cd99aab9a6b71a0326c4e5b19b5f4d2ec63346bde69977b814c8" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0xba43b7400", + "lock": { + "args": "0x", + "code_hash": "0xcc321278301291afc3c43ae43d5e3e3c10ce9cf0658c3063587b0123cdce7cef", + "hash_type": "data1" + }, + "type": { + "args": "0x43", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x00d51ef4e28799d94e9046a2873a2974c4fbe51572f6fd1d579cd43f4bf679fb87640000000000000014000000000000000000000000000000000000000000000002000000000000005645535430303031" + ], + "version": "0x0", + "witnesses": [] + }, + "0xb81a922893475d9f7bb43877d52d7b7c15c1bc1db63a4cbdc5aa0a5d08abf784": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x05bdf82334e9817b9e706495e1e0897548dad8e635a07d19ae0dfb2551ed84e9" + } + } + ], + "hash": "0xb81a922893475d9f7bb43877d52d7b7c15c1bc1db63a4cbdc5aa0a5d08abf784", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x78e2458a9ec57a0cd427c4090f9903efcdc634d18a35c6850fab8c3c3a1f14c5" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x30999ca44c3638eceddd4d707a01242294bd06473c9e34f92f5cdea23c2bdb75", + "hash_type": "data1" + }, + "type": { + "args": "0x22", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x0000000000000000000000000000000000000000000000000000000000000000040000000000000068e0283c1fd1128451c56b1271048ce1bdbadaaab224ea219c1277ee4cbabe7778000000000000003c0000000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0xb83abaee23854733da3a985f90440de3c831022c65e128ae2b0b1c0b2ca82850": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc3d498167f8fa254bdaed6029f276aacea9d662a5cd393b4cc19cffa2889fe25" + } + } + ], + "hash": "0xb83abaee23854733da3a985f90440de3c831022c65e128ae2b0b1c0b2ca82850", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x899bac1c9e02a9ca6cd47386e6fd5470d06ba1348a018b2acd8d3d0bbde2bade" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x1", + "tx_hash": "0x899bac1c9e02a9ca6cd47386e6fd5470d06ba1348a018b2acd8d3d0bbde2bade" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x9502f9000", + "lock": { + "args": "0x", + "code_hash": "0xa3bdc8e44991a1790d469db884797763ce5dd7ef8631b77f45c2d925633dbbd9", + "hash_type": "data1" + }, + "type": { + "args": "0x73", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x70", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x72", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x6d7122fbd537293591fef620082e9213b875ee6f8a926497a533aee6f4ff93560e7da160d8e77e9274a6fa6f8243153d2bae61ddf817d97c42e9cc7861e1f830414d4d4130303031414d4d4230303031f82a0000000000000b4700000000000010270000000000001e00", + "0x1507000000000000414d4d4230303031" + ], + "version": "0x0", + "witnesses": [ + "0x44000000100000001000000044000000300000004353415247763100140700000000000013e1988a98e068eb6128d8ad60febbec52113d16742917b29e9d0b753eb8e971", + "0x" + ] + }, + "0xb97f4c75e0015d8deae35de3cc121201aae47742a6e57687c1c8b5049796759c": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x84949d0ac6b772fbe9eddc7aaecf1527609fb591c42129deea687f59d3bde57b" + } + } + ], + "hash": "0xb97f4c75e0015d8deae35de3cc121201aae47742a6e57687c1c8b5049796759c", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xab0f22960f33ac447c3075073190a7127930d337a090125a7279a8544f3d28d5" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x0a4cb03cd7e44ca4449ded0f8c6014c0f919819072badeb07bd51e0add611f35", + "hash_type": "data1" + }, + "type": { + "args": "0x25", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x5d21dba00", + "lock": { + "args": "0x", + "code_hash": "0x0a4cb03cd7e44ca4449ded0f8c6014c0f919819072badeb07bd51e0add611f35", + "hash_type": "data1" + }, + "type": { + "args": "0x21", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x5d21dba00", + "lock": { + "args": "0x", + "code_hash": "0x0a4cb03cd7e44ca4449ded0f8c6014c0f919819072badeb07bd51e0add611f35", + "hash_type": "data1" + }, + "type": { + "args": "0x21", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x5d21dba00", + "lock": { + "args": "0x", + "code_hash": "0x0a4cb03cd7e44ca4449ded0f8c6014c0f919819072badeb07bd51e0add611f35", + "hash_type": "data1" + }, + "type": { + "args": "0x21", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x5d21dba00", + "lock": { + "args": "0x", + "code_hash": "0x0a4cb03cd7e44ca4449ded0f8c6014c0f919819072badeb07bd51e0add611f35", + "hash_type": "data1" + }, + "type": { + "args": "0x21", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x860000001c000000350000003d0000005d000000650000006d00000015000000416363657074616e636520436f6c6c656374696f6e0400000041435054ad2efee9b2aa7cf537c64f1adbbf7bf4d35accce0f9c25dc9e7b111ea4dc87ea1800000000000000e80300000000000015000000636b623a2f2f63656c6c7363726970742f6e66742f", + "0x95adf7ef6067c00ecc240badc9e98ac4ee50b6b6a7cecdf150c0cfc15d54e3fd15000000000000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fad2efee9b2aa7cf537c64f1adbbf7bf4d35accce0f9c25dc9e7b111ea4dc87eafa00", + "0x95adf7ef6067c00ecc240badc9e98ac4ee50b6b6a7cecdf150c0cfc15d54e3fd160000000000000031313131313131313131313131313131313131313131313131313131313131314141414141414141414141414141414141414141414141414141414141414141ad2efee9b2aa7cf537c64f1adbbf7bf4d35accce0f9c25dc9e7b111ea4dc87eafa00", + "0x95adf7ef6067c00ecc240badc9e98ac4ee50b6b6a7cecdf150c0cfc15d54e3fd170000000000000032323232323232323232323232323232323232323232323232323232323232324242424242424242424242424242424242424242424242424242424242424242ad2efee9b2aa7cf537c64f1adbbf7bf4d35accce0f9c25dc9e7b111ea4dc87eafa00", + "0x95adf7ef6067c00ecc240badc9e98ac4ee50b6b6a7cecdf150c0cfc15d54e3fd180000000000000033333333333333333333333333333333333333333333333333333333333333334343434343434343434343434343434343434343434343434343434343434343ad2efee9b2aa7cf537c64f1adbbf7bf4d35accce0f9c25dc9e7b111ea4dc87eafa00" + ], + "version": "0x0", + "witnesses": [ + "0x1c01000010000000100000001c0100000801000043534152477631004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412313131313131313131313131313131313131313131313131313131313131313132323232323232323232323232323232323232323232323232323232323232323333333333333333333333333333333333333333333333333333333333333333000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f414141414141414141414141414141414141414141414141414141414141414142424242424242424242424242424242424242424242424242424242424242424343434343434343434343434343434343434343434343434343434343434343" + ] + }, + "0xb9f8fd253e6da658201c7d51eb1d64c53ae37639fc94ead37dce09dc7b8a122f": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x9572797579e20683ff0f7c6fe0152a168a9c6bade9800dcb77751ff651df8478" + } + } + ], + "hash": "0xb9f8fd253e6da658201c7d51eb1d64c53ae37639fc94ead37dce09dc7b8a122f", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x16fb3f1a0ef711b6deb76b0595105e225d61ca83d2a609474c0e12683e399f39" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xe383a15d1b2e326df64363cab5778e79bd5edef2bc97a2ae3d4c77ab072752e3", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0xb80000001c0000003c0000005c0000006b00000073000000b7000000444444444444444444444444444444444444444444444444444444444444444411111111111111111111111111111111111111111111111111111111111111116f70657261746f72207265766965770a00000000000000020000001111111111111111111111111111111111111111111111111111111111111111222222222222222222222222222222222222222222222222222222222222222200" + ], + "version": "0x0", + "witnesses": [] + }, + "0xba87194e3b5862bb583ac228ca3b79b0230c2667d71c095fb5c8e33f375c4006": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x7385b0cd1428d6b3de24c02748cd013790f75530ae9fe8bd125b74ba6388f97c" + } + } + ], + "hash": "0xba87194e3b5862bb583ac228ca3b79b0230c2667d71c095fb5c8e33f375c4006", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x15da87cfff34c6bfc7a7012177b4845fbdd717f50dd145d2772678981f5a14e4" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x41", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc41256455354303030310a00000000000000640000000000000001" + ], + "version": "0x0", + "witnesses": [ + "0x550000001000000010000000550000004100000043534152477631004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc41256455354303030310a00000000000000640000000000000001" + ] + }, + "0xbb4544c75dd32136bfbc6eeab20b2a7ca0539d8e059d3fadfbca9393629a94e8": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xf90754033d45778b16034a348f5757b56b8578eab5fd81bd2707a4fa43572a7f" + } + } + ], + "hash": "0xbb4544c75dd32136bfbc6eeab20b2a7ca0539d8e059d3fadfbca9393629a94e8", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x7e440ad501421b27a482372d706506acb9652daa062b70d002f0f316402003a5" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x10f75e072356b123d3864ae553870a4759943c4ed71d373218ca17b611795020", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x544f4b454e303031e8030000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0xc0ca46dcc6d0c1c6ba5944656d1e5650b877f934911cb540e2b52a1f191f85b9": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xf0f807aecb16aaf7820fdb2c70d719912dd02d8274f76343fc42eebdbc3bcc02" + } + } + ], + "hash": "0xc0ca46dcc6d0c1c6ba5944656d1e5650b877f934911cb540e2b52a1f191f85b9", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xed6ef86b9918c6e673cfdbe410350eeb3e3e9f0fc480fc2fe2273c3da3308917" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x18758b10cc53dcf2fcd775462ec3ad8052ca19f21158a315eedc926cd085d520", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x00000000000000000000000000000000000000000000000000000000000000000100000000000000111111111111111111111111111111111111111111111111111111111111111144444444444444444444444444444444444444444444444444444444444444442222222222222222222222222222222222222222222222222222222222222222fa00" + ], + "version": "0x0", + "witnesses": [] + }, + "0xc1027a7241ef72189a265f99ee6df274cffd53983ff85f0fc6e51b3372bb47a7": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x85999c8371807a66812a6db192e23c22335b1faf2cc3bcf873658c827ba80570" + } + } + ], + "hash": "0xc1027a7241ef72189a265f99ee6df274cffd53983ff85f0fc6e51b3372bb47a7", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x189e7a1b87b0df7c8fb2117191bb6fd6fa7f33b4aac9f4a53edc5ca5cb913a6c" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x9502f9000", + "lock": { + "args": "0x60", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x61", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x70", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x62", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x71", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x62", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x60", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x62", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4c41554e434830311027000000000000e803000000000000", + "0x0a000000000000004c41554e43483031", + "0x14000000000000004c41554e43483031", + "0xca030000000000004c41554e43483031" + ], + "version": "0x0", + "witnesses": [ + "0xa40000001000000010000000a40000009000000043534152477631004c41554e434830311027000000000000e8030000000000006de4ed8e16e7400834559efc852ddca87762f738a5dd93853168a4cc390cdbf013e1988a98e068eb6128d8ad60febbec52113d16742917b29e9d0b753eb8e9710a000000000000006d7122fbd537293591fef620082e9213b875ee6f8a926497a533aee6f4ff93561400000000000000" + ] + }, + "0xc10773a7ba18c8d32e4deab978518e5d1f4665d833b6361ccc306d5382387f7a": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x14a44b8c532b2bb73f71cbaee290f3e62ae5a4c3b2b083dacfa2018de393dc3a" + } + } + ], + "hash": "0xc10773a7ba18c8d32e4deab978518e5d1f4665d833b6361ccc306d5382387f7a", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xeeec2214dbcab6baf139332667732583af8de6f940dad430156d5b6ff2c494b5" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xb5e6f61a513204507f6cf508cbc23b449a60d91ef6ebdfde79fcf886bf4cb020", + "hash_type": "data1" + }, + "type": { + "args": "0x52", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0xba43b7400", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x51", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0xdf00000034000000540000005c0000007c0000007d0000009d000000a5000000a9000000aa000000ce000000d6000000de000000081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd308070000000000000054c5b8ead560f70812f047450079e4d44d711bd08a9fc8068bf1278932736b840064f944c22f0db7bfb29aa523b65cbb75a6a65b369febfbff0ffc17facacfe2dcf40100000000000000000000020100000054c5b8ead560f70812f047450079e4d44d711bd08a9fc8068bf1278932736b841400000000000000d00700000000000000", + "0x8d00000018000000380000007c0000007d00000085000000081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd3080200000054c5b8ead560f70812f047450079e4d44d711bd08a9fc8068bf1278932736b84edf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae0200000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0xc1992e679cdcbc64bb722f94b5d226099b2b9ead0529e4ccf45833055f369b2a": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xfe8eb1d61e9167f5864fa5b417edb0c6976b8e3729c172ac6ec50382bd634b61" + } + } + ], + "hash": "0xc1992e679cdcbc64bb722f94b5d226099b2b9ead0529e4ccf45833055f369b2a", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x17606461a3d98871a31a1d2dc71e0e81e47c2fb246665a0c19f207255b32f70a" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0xba43b7400", + "lock": { + "args": "0x", + "code_hash": "0x540be7d1575ea9e60a6df5678ec2e4dc857edb687f1ad2f8359317be2288ff60", + "hash_type": "data1" + }, + "type": { + "args": "0xf1", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x22ecb25c00", + "lock": { + "args": "0x", + "code_hash": "0xb5e6f61a513204507f6cf508cbc23b449a60d91ef6ebdfde79fcf886bf4cb020", + "hash_type": "data1" + }, + "type": { + "args": "0xf2", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x8d00000018000000380000007c0000007d0000008500000065dfc0aa4a73f351b42a0bad8e1e57f7befd37622ec7fc56087c9cad029905930200000086a76e2fc301d88cf9fed4a493b210edf5a21bc839886e7869bac99766ed6a5cc7b799a057ff213011b152a94d895d9e5b1eeb05e376f7122464ff11bc93207a0201000000000000000a00000000000000", + "0xbf00000034000000540000005c0000007c0000007d0000009d000000a5000000a9000000aa000000ae000000b6000000be00000065dfc0aa4a73f351b42a0bad8e1e57f7befd37622ec7fc56087c9cad02990593010000000000000086a76e2fc301d88cf9fed4a493b210edf5a21bc839886e7869bac99766ed6a5c003664eabff06921f646efbb743006b9544de3a96a399db5870777e5e8d78a7b2df4010000000000000000000002000000001400000000000000b40500000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x6c00000010000000100000006c00000058000000435341524776310086a76e2fc301d88cf9fed4a493b210edf5a21bc839886e7869bac99766ed6a5c3664eabff06921f646efbb743006b9544de3a96a399db5870777e5e8d78a7b2df4010000000000001400000000000000" + ] + }, + "0xc390427394790da202c9564f872551a36bcee7747e19fcc58d0eac765d7bbaae": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xf99767aebf484c406de90a63140d8306eea1dbf509fb6b04f13f5594a27b4157" + } + } + ], + "hash": "0xc390427394790da202c9564f872551a36bcee7747e19fcc58d0eac765d7bbaae", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x0495a8b89852af2a653540ebe699453d04134738225430a174119ef7db3fa047" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x104c533c00", + "lock": { + "args": "0x", + "code_hash": "0x7cda3da79fb46eaed62752444d9d4f666897861039c77bf94f0532fc43162a70", + "hash_type": "data1" + }, + "type": { + "args": "0x51", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x7cda3da79fb46eaed62752444d9d4f666897861039c77bf94f0532fc43162a70", + "hash_type": "data1" + }, + "type": { + "args": "0x52", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x8d00000018000000380000007c0000007d00000085000000081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd308020000007d079c63043b805af33e1b72b25e83ba2897b47af215f9174932de79e9393d01edf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae0101000000000000000a00000000000000", + "0xbf00000034000000540000005c0000007c0000007d0000009d000000a5000000a9000000aa000000ae000000b6000000be000000081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd30801000000000000007d079c63043b805af33e1b72b25e83ba2897b47af215f9174932de79e9393d0102edf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae00000000000000000000000001000000001400000000000000b40500000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x640000001000000010000000640000005000000043534152477631007d079c63043b805af33e1b72b25e83ba2897b47af215f9174932de79e9393d01edf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae1400000000000000" + ] + }, + "0xc5f4a0ba516ae824a4b48b2c604120abd7d5140c18b0f27ac2b13dba0aec548a": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x8713577264f34e7acd5e5d74b494dbfb1c09c70af8f30a7fb1c3570aa4cbf1d4" + } + } + ], + "hash": "0xc5f4a0ba516ae824a4b48b2c604120abd7d5140c18b0f27ac2b13dba0aec548a", + "header_deps": [ + "0x88ce0e52d92e34dad6b737cc8c81d31badc9eaf981c783b52e3082c663d48093" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x967df738576d6c83283ed92ef71a0e174e45556fed0ac222f2b4e450150f91a3" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x318333f71531adb7109813cd89f757d76d7a1a8aebb79e93d800df8f4f0bc3c3", + "hash_type": "data1" + }, + "type": { + "args": "0x45", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x318333f71531adb7109813cd89f757d76d7a1a8aebb79e93d800df8f4f0bc3c3", + "hash_type": "data1" + }, + "type": { + "args": "0x43", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x50000000000000005645535430303031", + "0x01b250afae197267ab716da7baa7d3077d66d0bfb6286f19ab4d0698a90737666e64000000000000006400000000000000000000000000000000000000000000000b000000000000005645535430303031" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "0xc67554cbd1c3973fe04e014c2271023a82d9874a4b84ec38bda8bca1f9a65b26": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x1", + "tx_hash": "0xef62d924ff6af766acc21727b36c6e6483fac2768527599bf597348eb3c55a91" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x165cc6ad0c8d376ed93c10aea3246877f219e1a0cf40d9bd33bb4ebdd3c49bb8" + } + } + ], + "hash": "0xc67554cbd1c3973fe04e014c2271023a82d9874a4b84ec38bda8bca1f9a65b26", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x5a8483ebb69040dee1659744182b76fe71c973610ea4d31cc84d86f171d9dda9" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xd305deeaaf5715086b9ca367189279b7709450c3df4a6b73620f841578e1195f", + "hash_type": "data1" + }, + "type": { + "args": "0xc3", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x01134f3e5fc0e711c8860ae1fe92cdf6797bb68e4059cf057a4ea9eae6b9e2d001000000000000007ec54f4d397c9e0406cb21db151b5b2844123b83ba7e8eb9ce9c0a3c3759b5fb1027000000000000000000000000000000", + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x240000001000000010000000240000001000000043534152477631001027000000000000" + ] + }, + "0xc8a5c0e66095d60b6955962dd47327012167b91791b6f762684de515b9c1354e": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x297acc94d2c6e532490f039bfbfeed7c2e494fef06b7adb6cf00a4287dca0a73" + } + } + ], + "hash": "0xc8a5c0e66095d60b6955962dd47327012167b91791b6f762684de515b9c1354e", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x740f68ba912a942022541741c57d332680fb0d6d73fece3763ce2f1a1a4d0628" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x93f05f4fb67225c694ab2cb4c4b57125136e08fcff490458f42fa63e26086bdd", + "hash_type": "data1" + }, + "type": { + "args": "0x23", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0xbe0000001c0000003c0000005c0000007100000079000000bd0000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412414141414141414141414141414141414141414141414141414141414141414111000000656d657267656e63792072656c656173657800000000000000020000004242424242424242424242424242424242424242424242424242424242424242a5fa3ab929ac363193de55b810aaac99277989f8550a29501c8d393e11a16ee500" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c000000280000004353415247763100a5fa3ab929ac363193de55b810aaac99277989f8550a29501c8d393e11a16ee5" + ] + }, + "0xc962300d035f0d3e1a401d57d0420ee6e984c4cabc0da7cf8beae28bb3b0c040": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x1", + "tx_hash": "0xc10773a7ba18c8d32e4deab978518e5d1f4665d833b6361ccc306d5382387f7a" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x14a44b8c532b2bb73f71cbaee290f3e62ae5a4c3b2b083dacfa2018de393dc3a" + } + } + ], + "hash": "0xc962300d035f0d3e1a401d57d0420ee6e984c4cabc0da7cf8beae28bb3b0c040", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xc10773a7ba18c8d32e4deab978518e5d1f4665d833b6361ccc306d5382387f7a" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0xdf8475800", + "lock": { + "args": "0x", + "code_hash": "0xb5e6f61a513204507f6cf508cbc23b449a60d91ef6ebdfde79fcf886bf4cb020", + "hash_type": "data1" + }, + "type": { + "args": "0x52", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xb5e6f61a513204507f6cf508cbc23b449a60d91ef6ebdfde79fcf886bf4cb020", + "hash_type": "data1" + }, + "type": { + "args": "0x53", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0xff00000034000000540000005c0000007c0000007d0000009d000000a5000000a9000000aa000000ee000000f6000000fe000000081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd308070000000000000054c5b8ead560f70812f047450079e4d44d711bd08a9fc8068bf1278932736b840064f944c22f0db7bfb29aa523b65cbb75a6a65b369febfbff0ffc17facacfe2dcf40100000000000000000000020200000054c5b8ead560f70812f047450079e4d44d711bd08a9fc8068bf1278932736b84edf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae1400000000000000d00700000000000000", + "0x0700000000000000edf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae1e00000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x44000000100000001000000044000000300000004353415247763100edf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae1e00000000000000" + ] + }, + "0xcaf1e3fead81946aed95a54b532f739b4c68b6fbae7165a5f1ff919c8f8b3756": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x4e6498bb05ab2acef4f3dc7aca48bea59b65a76ba1be2359d334621a701672c0" + } + } + ], + "hash": "0xcaf1e3fead81946aed95a54b532f739b4c68b6fbae7165a5f1ff919c8f8b3756", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x95cf642ae48c517930fe6e3c737fd3981c2f910242f22d5dee7ab53f44c20d9f" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x1", + "tx_hash": "0x95cf642ae48c517930fe6e3c737fd3981c2f910242f22d5dee7ab53f44c20d9f" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x9502f9000", + "lock": { + "args": "0x", + "code_hash": "0x513c43aa3e994cbdf9bcd7903f1e220882873ad830b3eb024cdeefe2ca3afcb3", + "hash_type": "data1" + }, + "type": { + "args": "0x6e", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x6b", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x6c", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x6b", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x6d", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x0358b5b4af3799ee4f7fbb489135f67e4b316c3a6bc5ef6e31a7b80958ea1569ee44736c2a40bb9b927c93b719db9a3681696c42caa38f292b3a0423234fff93414d4d4130303031414d4d42303030315a00000000000000b40000000000000084030000000000001e00", + "0x0a00000000000000414d4d4130303031", + "0x1400000000000000414d4d4230303031" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631005d4eec43082abf0f7a62b2f9682051adeb7e017c32f00756482c17985bef0bd6", + "0x" + ] + }, + "0xceaaabab7b6cb8b1b1a6332e8f0624978e76fa9931a2e5097dbb48abebb09df2": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x59e35a159719575477e11dfd15b9b1c0c0c895495034a74e23e8f89a884cfa44" + } + } + ], + "hash": "0xceaaabab7b6cb8b1b1a6332e8f0624978e76fa9931a2e5097dbb48abebb09df2", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xa7a79ee082961201fac154d42dd5c86501e52e1b53d360444c7a3393264cffa6" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x67cf56e6c2bcd82506436ade5d8a220f9b51ad099c307d7cb59de6629cca279e", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x444444444444444444444444444444444444444444444444444444444444444411111111111111111111111111111111111111111111111111111111111111110064000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0xd097c95c41a0970b66c253c9abe6b3f276282b2a8f6126dda3cf18494340b2c3": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x1e601e8f4a43db4216118fd235a8c21841e611d4d32208c6f8745d6ff5049d74" + } + } + ], + "hash": "0xd097c95c41a0970b66c253c9abe6b3f276282b2a8f6126dda3cf18494340b2c3", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x58389e2aa2f07b207b69c854bb471cbfa9e980b3fb0f5c5acde5d16fe4163f05" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631002222222222222222222222222222222222222222222222222222222222222222" + ] + }, + "0xd12b75240c9745693c87a94242cc383e3f4facb87b3d5a0e23a9f4e8242d9c5c": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x1", + "tx_hash": "0xebdaef4d2108c25778301a3237fbdcf71a260c384e1d8d3f738d16b65d7a66b8" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x5c75c5ca82dabee1d0aece80ed61f066c6afd349accbfedd76aa203a1e447cf6" + } + } + ], + "hash": "0xd12b75240c9745693c87a94242cc383e3f4facb87b3d5a0e23a9f4e8242d9c5c", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xebdaef4d2108c25778301a3237fbdcf71a260c384e1d8d3f738d16b65d7a66b8" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xaaf97b20c59720fa641958ba0c26eb2bfd3ed6c23444975811f4f1c08989c368", + "hash_type": "data1" + }, + "type": { + "args": "0x22", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x104c533c00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x000000000000000000000000000000000000000000000000000000000000000003000000000000003a5455bdce07967f3d95167e932e52a7f094a6778ce77761b4d3e5c2d51e332e6400000000000000000000000000000000", + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x240000001000000010000000240000001000000043534152477631006400000000000000" + ] + }, + "0xd427ffbf8a602f10b6b1c845f50683fbc468d636c9749080b653c671827e22dd": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc0bcb97f3c6a8c60d29eb5ed52c18597b102a5b43a1694a53a31d079a8814a95" + } + } + ], + "hash": "0xd427ffbf8a602f10b6b1c845f50683fbc468d636c9749080b653c671827e22dd", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x76401b481b980abeb6e3ca4cc414ef2dc28819eff96b4e4c32d26f1febc6ae20" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xc1e7dce634480aceedc7bd5dfbb7df1e5951cd945fb0d10cb8ea70968af0443b", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [] + }, + "0xd44187944519beb8fb0d67544e148c80880dc5e12336bae473be6e788ff980c2": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x71eff92809d8a4981a72e97209d0b726be408aefa00d1508a26c8b1fff164552" + } + } + ], + "hash": "0xd44187944519beb8fb0d67544e148c80880dc5e12336bae473be6e788ff980c2", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x1c8c4325505326f747420de5e8560c32794f3e1ef786c38d1bcd5b186c669784" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x10f75e072356b123d3864ae553870a4759943c4ed71d373218ca17b611795020", + "hash_type": "data1" + }, + "type": { + "args": "0x91", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x2540be400", + "lock": { + "args": "0xa4", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x92", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4c41554e4348303110270000000000000104000000000000", + "0x19000000000000004c41554e43483031" + ], + "version": "0x0", + "witnesses": [ + "0x4400000010000000100000004400000030000000435341524776310096fce7ed113ae01b9c4b1d6b3065804825a5708ba868b624ed12e30c5665d1e61900000000000000" + ] + }, + "0xd5fa5dcfd1dc5ac7749aac58e2ccc70953e8e86adcbbd315e7d28eac991c6bbc": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x6aa5c60e30df163649a614d6637228dab6e507b00d3a8fd6bd93b5cc525163e3" + } + } + ], + "hash": "0xd5fa5dcfd1dc5ac7749aac58e2ccc70953e8e86adcbbd315e7d28eac991c6bbc", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x100622e71992fc7f45b3e46ff83c5f30748a75144c05ed015a39acaa2aefe84e" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x1", + "tx_hash": "0x100622e71992fc7f45b3e46ff83c5f30748a75144c05ed015a39acaa2aefe84e" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x2", + "tx_hash": "0x100622e71992fc7f45b3e46ff83c5f30748a75144c05ed015a39acaa2aefe84e" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xc5eb6cb03878c77e75b8462c561205189574fc4677e5538cc9136c10ab9921da", + "hash_type": "data1" + }, + "type": { + "args": "0x05", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xc5eb6cb03878c77e75b8462c561205189574fc4677e5538cc9136c10ab9921da", + "hash_type": "data1" + }, + "type": { + "args": "0x04", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x2a00000000000000544f4b454e303031", + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc41200000000000000006bd943617d9642f1dad174449a64bb34f325c84fc257eb8fff578e00a50b0a9c" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631006bd943617d9642f1dad174449a64bb34f325c84fc257eb8fff578e00a50b0a9c", + "0x", + "0x" + ] + }, + "0xd6d214048b0d486197d309dcd9317e52a42e3e20cc4f049c4e87281d1f5a6d5a": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x49cc066a2d2f6275cc83080d71ede68d3ae540573353901dfabd8d031fc528c6" + } + } + ], + "hash": "0xd6d214048b0d486197d309dcd9317e52a42e3e20cc4f049c4e87281d1f5a6d5a", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xc516c050781b8fe2ba57c8c4fd4a54969299ffb31a0377fc9f497cf2bf4f9fbe" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x22ecb25c00", + "lock": { + "args": "0x", + "code_hash": "0x2eb610fca034a18303d192bcbf52ba0f68e6ee8b1cafa90b200d5edad55257ef", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [] + }, + "0xd7d1822d5820493f4a5c03812e71ecf7a2943734611dfe351598146890453059": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x80ec8fc6e4986da8bc215946af429bbdb6fe26ab543bc6735377b480fcc8418a" + } + } + ], + "hash": "0xd7d1822d5820493f4a5c03812e71ecf7a2943734611dfe351598146890453059", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x078d6b393501851b72052db4c6f1e8b439ede7a80a15ed84a8fa2f777eed8054" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x30999ca44c3638eceddd4d707a01242294bd06473c9e34f92f5cdea23c2bdb75", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "0xd816ab4444154f8550b94676b6195160a7a09d0ba5d9d42ccee11c4d34370f1e": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xef6eddcdb6a4500202839de5d3929cdcb7ade4274dee72021b5612314ad01235" + } + } + ], + "hash": "0xd816ab4444154f8550b94676b6195160a7a09d0ba5d9d42ccee11c4d34370f1e", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xa0ad32ea6682bbe84f394ed740b42c4412c497273adb2955e6cbbba2d6d25137" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "0xd8c0053e479d1e7c9f45e2abc8c2f082194cd47634c2d6388f4e2e7a240366a7": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xbdba2f98f29414b88797bc5942b6c00d6a887dffeee6e3af43579372ea4d612e" + } + } + ], + "hash": "0xd8c0053e479d1e7c9f45e2abc8c2f082194cd47634c2d6388f4e2e7a240366a7", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xda0a4d13e2a3cabdaa0001fee1acb48209ee68b4d786311af2629448b52dcfc2" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x23", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x000000000000000000000000000000000000000000000000000000000000000005000000000000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc4129600000000000000c8000000000000005041594d3030303100" + ], + "version": "0x0", + "witnesses": [ + "0x7c00000010000000100000007c000000680000004353415247763100000000000000000000000000000000000000000000000000000000000000000005000000000000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc41296000000000000005041594d30303031c800000000000000" + ] + }, + "0xd8e30c66d1da8a5af3a43c6e7514e691948b6aea907874ed80858eaae0201caf": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x6988a589235f9fd830f970f1302dbeb1685104a75ff5c95e13fa8f833fa67f84" + } + } + ], + "hash": "0xd8e30c66d1da8a5af3a43c6e7514e691948b6aea907874ed80858eaae0201caf", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x8b85a45b4b3c0da4633ca155697290d15ec99bb27068b1a2f611d49214869704" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x1", + "tx_hash": "0x8b85a45b4b3c0da4633ca155697290d15ec99bb27068b1a2f611d49214869704" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x64", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x61", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x65", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x6e764046853b3e6e5b2eb2f2d03e0f9fa119bf5da110166c48fdce579102826a0ba02773d63b6fa4b0ed6ce7a50816e8ca93d78005ca27c02a2d05b8e46f3c2c414d4d4130303031414d4d42303030310400000000000000090000000000000006000000000000001e00", + "0xbd925708cc9329a2ed2eef184ee313c1ec455027844c8ea227b3e589e5221a9a0600000000000000a2159af3fb001c55e6c3e8fbfe034c52699451a5e88086ebb684fdcdac2d6748" + ], + "version": "0x0", + "witnesses": [ + "0x3e00000010000000100000003e0000002a00000043534152477631001e00a2159af3fb001c55e6c3e8fbfe034c52699451a5e88086ebb684fdcdac2d6748", + "0x" + ] + }, + "0xdb5b770c97e55457e5b576a9612fa4a5ba8867c9c11899060f2193129e51d923": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x5f2c3eb45b63be5422acd84352c33591c0c51bdbc2bcdaa28b541c4dcbe6ec1d" + } + } + ], + "hash": "0xdb5b770c97e55457e5b576a9612fa4a5ba8867c9c11899060f2193129e51d923", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x696b5e896adf0a8d68ed777f00d8549883fb64692d00257693656264ce126e65" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x828a52262139378ce50e40ea7a24d4e8d8219cb2b9ae8f0e2a2cde2a8712a546", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc4128755508da7aa1fd862ab37b0e6ef59a8cb026c9bacc0c07e838683cb6cf3404e0119000000000000000000000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x640000001000000010000000640000005000000043534152477631004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc4128755508da7aa1fd862ab37b0e6ef59a8cb026c9bacc0c07e838683cb6cf3404e1900000000000000" + ] + }, + "0xdf6318fdf0dc8c8103363dc325dff2676b363c85405355debbdd24a41e424998": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x49cc066a2d2f6275cc83080d71ede68d3ae540573353901dfabd8d031fc528c6" + } + } + ], + "hash": "0xdf6318fdf0dc8c8103363dc325dff2676b363c85405355debbdd24a41e424998", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xd6d214048b0d486197d309dcd9317e52a42e3e20cc4f049c4e87281d1f5a6d5a" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x2eb610fca034a18303d192bcbf52ba0f68e6ee8b1cafa90b200d5edad55257ef", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x2eb610fca034a18303d192bcbf52ba0f68e6ee8b1cafa90b200d5edad55257ef", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x2eb610fca034a18303d192bcbf52ba0f68e6ee8b1cafa90b200d5edad55257ef", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x2eb610fca034a18303d192bcbf52ba0f68e6ee8b1cafa90b200d5edad55257ef", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc41205b4932b537bf8d7f006cb31700e23021bef615b652238322093b5534e7d71620064000000000000000000000000000000", + "0x61616161616161616161616161616161616161616161616161616161616161615151515151515151515151515151515151515151515151515151515151515151006e000000000000000000000000000000", + "0x626262626262626262626262626262626262626262626262626262626262626252525252525252525252525252525252525252525252525252525252525252520078000000000000000000000000000000", + "0x636363636363636363636363636363636363636363636363636363636363636353535353535353535353535353535353535353535353535353535353535353530082000000000000000000000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x3c01000010000000100000003c0100002801000043534152477631004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc41261616161616161616161616161616161616161616161616161616161616161616262626262626262626262626262626262626262626262626262626262626262636363636363636363636363636363636363636363636363636363636363636305b4932b537bf8d7f006cb31700e23021bef615b652238322093b5534e7d716251515151515151515151515151515151515151515151515151515151515151515252525252525252525252525252525252525252525252525252525252525252535353535353535353535353535353535353535353535353535353535353535364000000000000006e0000000000000078000000000000008200000000000000" + ] + }, + "0xdfb65c7699a692c39bdba73ec0647d99c56398310465d1db372c8a63369c0c93": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x99bd2cc55653377b2109baa3f88393a406c039e1fdf0703dcb782552e3ac16eb" + } + } + ], + "hash": "0xdfb65c7699a692c39bdba73ec0647d99c56398310465d1db372c8a63369c0c93", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x1213c894962b0b93e2fd49eb38ba5121b5df709d1d78ee888b4a060534f99ab9" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x104c533c00", + "lock": { + "args": "0x", + "code_hash": "0x9da2791f3f0a46790e4b187a1d1dd15813f2c14883138631255c58b495a845d8", + "hash_type": "data1" + }, + "type": { + "args": "0x51", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x9da2791f3f0a46790e4b187a1d1dd15813f2c14883138631255c58b495a845d8", + "hash_type": "data1" + }, + "type": { + "args": "0x52", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x8d00000018000000380000007c0000007d00000085000000081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd308020000004d594af7f4d8c8158e0225a7d1a80903f7ea8df3f81bc67791af289b03ae879dedf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae0201000000000000000a00000000000000", + "0xc000000034000000540000005c0000007c0000007d0000009d000000a5000000aa000000ab000000af000000b7000000bf000000081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd30801000000000000004d594af7f4d8c8158e0225a7d1a80903f7ea8df3f81bc67791af289b03ae879d0300000000000000000000000000000000000000000000000000000000000000000200000000000000010000000202000000001400000000000000b40500000000000000" + ], + "version": "0x0", + "witnesses": [ + "0x450000001000000010000000450000003100000043534152477631004d594af7f4d8c8158e0225a7d1a80903f7ea8df3f81bc67791af289b03ae879d021400000000000000" + ] + }, + "0xe2caa37ca2e0591eb4662a9c263d9bbd2fb0c1717253b0e909e24658f5d5dbf9": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x1fc61e5ec8572c8853a001a40fa7acef0190c6833da6ec3e407bd2863c986a45" + } + } + ], + "hash": "0xe2caa37ca2e0591eb4662a9c263d9bbd2fb0c1717253b0e909e24658f5d5dbf9", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x0d0cd5183c0b46372c241150800abd62f029fdb3a378118e9b6cb40742e885f0" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x540be7d1575ea9e60a6df5678ec2e4dc857edb687f1ad2f8359317be2288ff60", + "hash_type": "data1" + }, + "type": { + "args": "0x51", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x8d00000018000000380000007c0000007d00000085000000081a5dcf3714186936153512187ce7c99c78f3280f00ae5a985a3d1cddacd3080200000086a76e2fc301d88cf9fed4a493b210edf5a21bc839886e7869bac99766ed6a5cedf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae0200000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0xe32ba198cf261e9245eeb097056d69457006704701255e84c64181d8115d1fea": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xf6c1dea3d39f795519ada0030abe07e5524aa5b99b89b86733664a7e038c7d96" + } + } + ], + "hash": "0xe32ba198cf261e9245eeb097056d69457006704701255e84c64181d8115d1fea", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x558ddcd2b7e2faf8b3e72f03235cee6ae1ab465b76732cfede9c6967ceb130ec" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "0xe36126e163f21a6ca37cad04416acdee8215a45efb9627b21209c0d73f8e70aa": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xef6eddcdb6a4500202839de5d3929cdcb7ade4274dee72021b5612314ad01235" + } + } + ], + "hash": "0xe36126e163f21a6ca37cad04416acdee8215a45efb9627b21209c0d73f8e70aa", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x3e92751afe9579893059de61e15723d1664c34d6b00e1c6dfbf81fe795395494" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x2f95e743beef860d851bf2277fc56c036c5be0b956db68627791b404b45067e9", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x00000000000000000000000000000000000000000000000000000000000000000100000000000000111111111111111111111111111111111111111111111111111111111111111144444444444444444444444444444444444444444444444444444444444444442222222222222222222222222222222222222222222222222222222222222222e903" + ], + "version": "0x0", + "witnesses": [] + }, + "0xe5d28d78e2c97cfb5cd0fb6d236304cd6b66cbeb235ca2b5cf7468378817844a": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xd0734aa42e646234c69b1fc13a8352a746230eb748a3b4f0ed577b37a59c97a6" + } + } + ], + "hash": "0xe5d28d78e2c97cfb5cd0fb6d236304cd6b66cbeb235ca2b5cf7468378817844a", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x8e0dc6fb8bde56d6fa372501bab5f137df3c09dcd0ae455d6396cd38a1bc3e18" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x0a4cb03cd7e44ca4449ded0f8c6014c0f919819072badeb07bd51e0add611f35", + "hash_type": "data1" + }, + "type": { + "args": "0x25", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x5d21dba00", + "lock": { + "args": "0x", + "code_hash": "0x0a4cb03cd7e44ca4449ded0f8c6014c0f919819072badeb07bd51e0add611f35", + "hash_type": "data1" + }, + "type": { + "args": "0x21", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x5d21dba00", + "lock": { + "args": "0x", + "code_hash": "0x0a4cb03cd7e44ca4449ded0f8c6014c0f919819072badeb07bd51e0add611f35", + "hash_type": "data1" + }, + "type": { + "args": "0x21", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x5d21dba00", + "lock": { + "args": "0x", + "code_hash": "0x0a4cb03cd7e44ca4449ded0f8c6014c0f919819072badeb07bd51e0add611f35", + "hash_type": "data1" + }, + "type": { + "args": "0x21", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x5d21dba00", + "lock": { + "args": "0x", + "code_hash": "0x0a4cb03cd7e44ca4449ded0f8c6014c0f919819072badeb07bd51e0add611f35", + "hash_type": "data1" + }, + "type": { + "args": "0x21", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x860000001c000000350000003d0000005d000000650000006d00000015000000416363657074616e636520436f6c6c656374696f6e0400000041435054ad2efee9b2aa7cf537c64f1adbbf7bf4d35accce0f9c25dc9e7b111ea4dc87ea1800000000000000e80300000000000015000000636b623a2f2f63656c6c7363726970742f6e66742f", + "0x95adf7ef6067c00ecc240badc9e98ac4ee50b6b6a7cecdf150c0cfc15d54e3fd15000000000000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fad2efee9b2aa7cf537c64f1adbbf7bf4d35accce0f9c25dc9e7b111ea4dc87eafa00", + "0x95adf7ef6067c00ecc240badc9e98ac4ee50b6b6a7cecdf150c0cfc15d54e3fd160000000000000031313131313131313131313131313131313131313131313131313131313131314141414141414141414141414141414141414141414141414141414141414141ad2efee9b2aa7cf537c64f1adbbf7bf4d35accce0f9c25dc9e7b111ea4dc87eafa00", + "0x95adf7ef6067c00ecc240badc9e98ac4ee50b6b6a7cecdf150c0cfc15d54e3fd170000000000000032323232323232323232323232323232323232323232323232323232323232324242424242424242424242424242424242424242424242424242424242424242ad2efee9b2aa7cf537c64f1adbbf7bf4d35accce0f9c25dc9e7b111ea4dc87eafa00", + "0x95adf7ef6067c00ecc240badc9e98ac4ee50b6b6a7cecdf150c0cfc15d54e3fd180000000000000033333333333333333333333333333333333333333333333333333333333333334343434343434343434343434343434343434343434343434343434343434343ad2efee9b2aa7cf537c64f1adbbf7bf4d35accce0f9c25dc9e7b111ea4dc87eafa00" + ], + "version": "0x0", + "witnesses": [ + "0x1c01000010000000100000001c0100000801000043534152477631004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412313131313131313131313131313131313131313131313131313131313131313132323232323232323232323232323232323232323232323232323232323232323333333333333333333333333333333333333333333333333333333333333333000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f414141414141414141414141414141414141414141414141414141414141414142424242424242424242424242424242424242424242424242424242424242424343434343434343434343434343434343434343434343434343434343434343" + ] + }, + "0xe60611e6f8611fb019ebb0dac2c76cfa5081ef8d05ae8b57c44c3e887cd656dd": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x05bdf82334e9817b9e706495e1e0897548dad8e635a07d19ae0dfb2551ed84e9" + } + } + ], + "hash": "0xe60611e6f8611fb019ebb0dac2c76cfa5081ef8d05ae8b57c44c3e887cd656dd", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xb81a922893475d9f7bb43877d52d7b7c15c1bc1db63a4cbdc5aa0a5d08abf784" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x30999ca44c3638eceddd4d707a01242294bd06473c9e34f92f5cdea23c2bdb75", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "0xe712cd2c89aeadb85ad178be1df80fa32c72c563007d02022307da44d2b10f17": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc71e873453ffa750d9ef781214a1e015b9fe92ba751672b9592f3593750cb2fd" + } + } + ], + "hash": "0xe712cd2c89aeadb85ad178be1df80fa32c72c563007d02022307da44d2b10f17", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x6a3d32809ed36e7835e40e027dc05ee5adc36c9f04cc84866dfd3b58fe0f3043" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631001111111111111111111111111111111111111111111111111111111111111111" + ] + }, + "0xe795d5d96599dbae3f86bf88419c29ea037bd479b9339acb1fa189f5ebd5d259": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x1", + "tx_hash": "0x8cfc356815eccbcae35af59d0034cdda7af6094bb3a6e95e1b89b799f5d4cca5" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xfc01255f8d4c79d2307cbf689795022d46555c16027b3c954bc9969ec7387d81" + } + } + ], + "hash": "0xe795d5d96599dbae3f86bf88419c29ea037bd479b9339acb1fa189f5ebd5d259", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x8cfc356815eccbcae35af59d0034cdda7af6094bb3a6e95e1b89b799f5d4cca5" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xca92833ef77901845dd672a356fa9e568a3d46f179a1df5469dbbc05b7313427", + "hash_type": "data1" + }, + "type": { + "args": "0x20", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x104c533c00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x544f4b454e3030312a000000000000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412", + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "0xe9680f21dbf851055f0cb2fcc4cd51a05b5e2f6846b22b08462ffa12e7dc7d2d": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x006b521dc10d970574f21b5faf7d36e65b9242f7557bdf0f293020f759b2e568" + } + } + ], + "hash": "0xe9680f21dbf851055f0cb2fcc4cd51a05b5e2f6846b22b08462ffa12e7dc7d2d", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x00409256e78106d58106d68a0fc529399530b444f5e73ebf74960616d208c2a4" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0xba43b7400", + "lock": { + "args": "0x", + "code_hash": "0x10f75e072356b123d3864ae553870a4759943c4ed71d373218ca17b611795020", + "hash_type": "data1" + }, + "type": { + "args": "0xa1", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x2540be400", + "lock": { + "args": "0x", + "code_hash": "0x6c976866fb9c343bd28922b92aca893f292fed889e52b75a59a10f738b31f4e7", + "hash_type": "data1" + }, + "type": { + "args": "0xa1", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x5354415445303031e8030000000000000c00000000000000", + "0x07000000000000005354415445303031" + ], + "version": "0x0", + "witnesses": [ + "0x440000001000000010000000440000003000000043534152477631000545322f195fd3db3fa6a39e1e053fef4e3fa14589f8a8c41fc2b58b4028afab0700000000000000" + ] + }, + "0xe9f918cc4cd4842ac8cc6f54c0bd1c2158ceb0105d1b71b97c22524acef3e33f": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc293f43936132ce8cb8f8a4a760f1de03c82dfd7464533a73b586d1867b92349" + } + } + ], + "hash": "0xe9f918cc4cd4842ac8cc6f54c0bd1c2158ceb0105d1b71b97c22524acef3e33f", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x43aa34c172d01d43c427650825c520f05f0775b6691bb9448488bd542475db62" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x1", + "tx_hash": "0x43aa34c172d01d43c427650825c520f05f0775b6691bb9448488bd542475db62" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x2", + "tx_hash": "0x43aa34c172d01d43c427650825c520f05f0775b6691bb9448488bd542475db62" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x3", + "tx_hash": "0x43aa34c172d01d43c427650825c520f05f0775b6691bb9448488bd542475db62" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x9c1a6fed0b778db0b8006116e9d9a6213d2b5dadf1ac391118790c44a9ffe275", + "hash_type": "data1" + }, + "type": { + "args": "0x21", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x24", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x9c1a6fed0b778db0b8006116e9d9a6213d2b5dadf1ac391118790c44a9ffe275", + "hash_type": "data1" + }, + "type": { + "args": "0x24", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x000000000000000000000000000000000000000000000000000000000000000007000000000000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412fa00", + "0xfa000000000000005041594d30303031", + "0x16260000000000005041594d30303031" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100", + "0x", + "0x", + "0x" + ] + }, + "0xeaeed3d06f30198c983882e0221720faf11c02473429adb757bd570b910363f9": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x4549c1c05bb03fe8c884c329830b6ba4cbc4fe2f560fa5db94addb128fc14015" + } + } + ], + "hash": "0xeaeed3d06f30198c983882e0221720faf11c02473429adb757bd570b910363f9", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x16abacfaf5234b72cb013ac1485f1b92e39d1153ffee2019af44716d17ab1fdc" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xc39605e25bec7c9fe1297e640cf6ef42128dff83cec50c6e94bcc1eeb7aa268a", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0xff00000034000000540000005c0000007c0000007d0000009d000000a5000000a9000000aa000000ee000000f6000000fe000000444444444444444444444444444444444444444444444444444444444444444401000000000000001111111111111111111111111111111111111111111111111111111111111111003333333333333333333333333333333333333333333333333333333333333333f401000000000000000000000202000000111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222220a00000000000000d00700000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0xebdaef4d2108c25778301a3237fbdcf71a260c384e1d8d3f738d16b65d7a66b8": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x5c75c5ca82dabee1d0aece80ed61f066c6afd349accbfedd76aa203a1e447cf6" + } + } + ], + "hash": "0xebdaef4d2108c25778301a3237fbdcf71a260c384e1d8d3f738d16b65d7a66b8", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xd7290b2b90834c131df36fabd447cc248f102a94cb5cefb0d12f78df742c836b" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xaaf97b20c59720fa641958ba0c26eb2bfd3ed6c23444975811f4f1c08989c368", + "hash_type": "data1" + }, + "type": null + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x21", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x", + "0x000000000000000000000000000000000000000000000000000000000000000003000000000000003a5455bdce07967f3d95167e932e52a7f094a6778ce77761b4d3e5c2d51e332e000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412fa00" + ], + "version": "0x0", + "witnesses": [] + }, + "0xec6798ce41a5f6e605ff145156155055fa3de7d9d3ae339a7a362f91fdc060c9": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x1", + "tx_hash": "0x403c6468185cda532c013187efc8a8037ddab784faa77ffa2f598d8aac72eb71" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc922cbc382b9e65ed9852d188f4eac36d7b7e47c518639c0b6e39899aa32d440" + } + } + ], + "hash": "0xec6798ce41a5f6e605ff145156155055fa3de7d9d3ae339a7a362f91fdc060c9", + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x403c6468185cda532c013187efc8a8037ddab784faa77ffa2f598d8aac72eb71" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x48ae74b55eea7d34804fb2aab0a86fb3bd2a193051315e9c4a16cdd0aaccda91" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x42", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x43", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x337b00807f", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x00d714763e4a1855490e72ed7282ee94abb4ad846e79662f8423d83b4f5aca0c354d00000000000000000000000000000000000000000000000a0000000000000064000000000000005645535430303031", + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c000000280000004353415247763100d714763e4a1855490e72ed7282ee94abb4ad846e79662f8423d83b4f5aca0c35" + ] + }, + "0xed204f9b9fa736fae8691f41c9674b625c5a831a7d6fa0d5d2b50c1d4ce5e142": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xdff9e4e52961c196f41c52c2d211468dfa6b3af8c4f194a02d4bc59bfe39d066" + } + } + ], + "hash": "0xed204f9b9fa736fae8691f41c9674b625c5a831a7d6fa0d5d2b50c1d4ce5e142", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x7a756cb6c7c9658d5f5285e65e36c3f513227686918ae70c9fad31ca8064b26a" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xf7a0ff2f4e06b8af72ebbd3aa6a7e6ffe61d3ada8258397c6c224d217ae9d3cd", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x444444444444444444444444444444444444444444444444444444444444444411111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0xef62d924ff6af766acc21727b36c6e6483fac2768527599bf597348eb3c55a91": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x7e9657ed8c1aabb75e70fe5a6f3e2b06aa9dc8c78551e69b967d148803ef9f0e" + } + } + ], + "hash": "0xef62d924ff6af766acc21727b36c6e6483fac2768527599bf597348eb3c55a91", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xa278863a1589ef75f641ead8c869a21b4f426a167f4814927af651f01de54cea" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0xba43b7400", + "lock": { + "args": "0x", + "code_hash": "0x967d150b17ee92167fda6eb3b28e453cfc084f19c7cbfc773bb3f1b93d4dea61", + "hash_type": "data1" + }, + "type": { + "args": "0xc1", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xd305deeaaf5715086b9ca367189279b7709450c3df4a6b73620f841578e1195f", + "hash_type": "data1" + }, + "type": { + "args": "0xc2", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x8d0000001c000000330000003b0000005b000000630000006b00000013000000537461746566756c20436f6c6c656374696f6e04000000534e46543b27b52bcff21bb54b5ab2951b1ee1153fa29982ff473bf003e6097276b6d55c0100000000000000c8000000000000001e000000636b623a2f2f63656c6c7363726970742f737461746566756c2d6e66742f", + "0x01134f3e5fc0e711c8860ae1fe92cdf6797bb68e4059cf057a4ea9eae6b9e2d001000000000000007ec54f4d397c9e0406cb21db151b5b2844123b83ba7e8eb9ce9c0a3c3759b5fb33333333333333333333333333333333333333333333333333333333333333333b27b52bcff21bb54b5ab2951b1ee1153fa29982ff473bf003e6097276b6d55cfa00" + ], + "version": "0x0", + "witnesses": [ + "0x5c00000010000000100000005c0000004800000043534152477631007ec54f4d397c9e0406cb21db151b5b2844123b83ba7e8eb9ce9c0a3c3759b5fb3333333333333333333333333333333333333333333333333333333333333333" + ] + }, + "0xf0d43d47f20b19cd70aed45f330cbb4c98a5898d4ad3096cf5b588b5dfe6f834": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xb547f36f187c1934d41fdd9aa8a0e855842721d14c598386bdc850d6b17b5517" + } + } + ], + "hash": "0xf0d43d47f20b19cd70aed45f330cbb4c98a5898d4ad3096cf5b588b5dfe6f834", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x40467f4762c9f86e4f0c6d4b05ce196efd2b10594db8648a8599ec48e661dbab" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xa983e55b6bf70b5e24415864e1ab3640ccef502351a814d229b66d6738e0c83a", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0xff00000034000000540000005c0000007c0000007d0000009d000000a5000000a9000000aa000000ee000000f6000000fe000000444444444444444444444444444444444444444444444444444444444444444401000000000000001111111111111111111111111111111111111111111111111111111111111111003333333333333333333333333333333333333333333333333333333333333333f401000000000000000000000202000000111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222220a00000000000000d00700000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0xf18073c9dd4436dfca5146f8b7aac0e4bfe4398b8b6f91f1727873aeef202c9d": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x1", + "tx_hash": "0x014f954ce1b5dcd67562a3094bda3060e3807bd071a5da61a107a3c4c02716e2" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xa831ba0ff5d321de15b135872754b682e38d2ddd47c38d7315bce7f166e20ec4" + } + } + ], + "hash": "0xf18073c9dd4436dfca5146f8b7aac0e4bfe4398b8b6f91f1727873aeef202c9d", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x014f954ce1b5dcd67562a3094bda3060e3807bd071a5da61a107a3c4c02716e2" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0xee0a4d051aeb5c2c39df4eeeda6a55b8cb9ea79a964b905d3288c0d83138fabc", + "hash_type": "data1" + }, + "type": { + "args": "0x54", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x0800000000000000be4bb273fc2295cd8466897942306079b3d1118c6150a000751ee6695e7d7c11280000000000000001" + ], + "version": "0x0", + "witnesses": [ + "0x44000000100000001000000044000000300000004353415247763100be4bb273fc2295cd8466897942306079b3d1118c6150a000751ee6695e7d7c112800000000000000" + ] + }, + "0xf212913e057843c248eea6ca642ad7cd8c1d930865fdea6604f4588b07f5e9f4": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xf8ee78ee63762e2c05952e54e460b90a506c4160c9e4d420f83246162712be43" + } + } + ], + "hash": "0xf212913e057843c248eea6ca642ad7cd8c1d930865fdea6604f4588b07f5e9f4", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x1c5ec34e3022bdcee5b055f54102be7267a807b4baf9885b20efd665ba5ccbd9" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x967d150b17ee92167fda6eb3b28e453cfc084f19c7cbfc773bb3f1b93d4dea61", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x860000001c000000350000003d0000005d000000650000006d00000015000000416363657074616e636520436f6c6c656374696f6e04000000414350543b27b52bcff21bb54b5ab2951b1ee1153fa29982ff473bf003e6097276b6d55c0b00000000000000e80300000000000015000000636b623a2f2f63656c6c7363726970742f6e66742f", + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc4120b000000000000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f3b27b52bcff21bb54b5ab2951b1ee1153fa29982ff473bf003e6097276b6d55cfa00" + ], + "version": "0x0", + "witnesses": [ + "0x5c00000010000000100000005c0000004800000043534152477631004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + ] + }, + "0xf222cd329af79ea45c70e20dca573edbfb9d93769d15c1cf06d0c6d30f572804": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x1", + "tx_hash": "0x96729af7693292d31397d4ebdf3624d743424a305cb8c4f7c6bdc5ad63111c9e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc145bbbef86e1441c587b6a24a8007c687becdb42b503a349b06475e8a86de48" + } + } + ], + "hash": "0xf222cd329af79ea45c70e20dca573edbfb9d93769d15c1cf06d0c6d30f572804", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x96729af7693292d31397d4ebdf3624d743424a305cb8c4f7c6bdc5ad63111c9e" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0xb68a0aa00", + "lock": { + "args": "0x", + "code_hash": "0xf51e1060b13ba495ab2cac42ab5102d7ff6bb2c00df50f115846b5fcc9429298", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c000000280000004353415247763100fbf9445aa019c17dbdd072cae5c7840097cba346940a427fc4269257922e5bf7" + ] + }, + "0xf36de341cb16e3887aa7fca0f4421e35bc3bd224f9e39d215606a49f73dabee4": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x75810e2bb00c39358795f31d647c7aab850fef67bf4c17be74391898f2699887" + } + } + ], + "hash": "0xf36de341cb16e3887aa7fca0f4421e35bc3bd224f9e39d215606a49f73dabee4", + "header_deps": [ + "0x690c44e7f3605a4c984edfe17dc953047114aff5e42ae1b2f108dc042a37a34d" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x2f2a53ea7336cf1d1b199a59b22148e7357d2e12846050a664cce5bf73094e80" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xa8e8c60bbed4ebf0747eb82243ce7c6644d925a506d822cdc080dfc26a067dd2" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x402f7e5dd680c1d6dc63abfc07b59a2583aa577503c506bef3d00a3a9318608b" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xc5eb6cb03878c77e75b8462c561205189574fc4677e5538cc9136c10ab9921da", + "hash_type": "data1" + }, + "type": { + "args": "0xb5", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0xb4", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x2a00000000000000544f4b454e303031", + "0x7d84ad3da83e8ccab86a945f8bb0b74ebaa3d29369b01c5370dff1f340078ac00b000000000000006bd943617d9642f1dad174449a64bb34f325c84fc257eb8fff578e00a50b0a9c" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631006bd943617d9642f1dad174449a64bb34f325c84fc257eb8fff578e00a50b0a9c", + "0x", + "0x" + ] + }, + "0xf784caf50f8ff3466cfb61ca40b3fecb90bff03f1dfb1916ca749f33b54d216d": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x49ce2be6cbe254c0a78346d81c7b098e74f44601c4693a509ba36d4b3c681f69" + } + } + ], + "hash": "0xf784caf50f8ff3466cfb61ca40b3fecb90bff03f1dfb1916ca749f33b54d216d", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x08cbc0f0e4b4a1201e687be7ab5380399f3f971ab68bae873e7b6e5dde6dac8c" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631006666666666666666666666666666666666666666666666666666666666666666" + ] + }, + "0xf7a35513fabdaa0d83307fa67eb92badabb850a2b71ec2a2f67b49229ed9dc59": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x49ce2be6cbe254c0a78346d81c7b098e74f44601c4693a509ba36d4b3c681f69" + } + } + ], + "hash": "0xf7a35513fabdaa0d83307fa67eb92badabb850a2b71ec2a2f67b49229ed9dc59", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xc189d2e59e3097eaa6265afdb58616ac0e806fbf8e57efeaf08c455a1f63a0c5" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x1ef8dbe9b2f531b18576d6ec194fae7e744ac501952706adcb6382d1deea04b4", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x665fa3d657391cb8819d2c9e91e3c0e6f82db7b371416f04e0b06332990457a511111111111111111111111111111111111111111111111111111111111111110064000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0xfa2e16ceccde2faf8a2ddcd8bedd2cbdbf0438cedb74d8e2684fea9e6ad98496": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x14e410f98eb197fc6a336f68534cee7ce181ab0d6ea6bc28a8f66acb6a3c8c44" + } + } + ], + "hash": "0xfa2e16ceccde2faf8a2ddcd8bedd2cbdbf0438cedb74d8e2684fea9e6ad98496", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xb629ee7fb29df995ea9fae1d02db475f878ffea6657cea91d21fe1986779692f" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x2540be400", + "lock": { + "args": "0x", + "code_hash": "0x1b82b117ab4e41de9f6652a54f2f2ee24abad190b04a935cf90075cf9f3da237", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "0xfc4b81ff774304b41f674c3e53c374264a3ec988118144a8d49ebb87e66e2f00": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x9572797579e20683ff0f7c6fe0152a168a9c6bade9800dcb77751ff651df8478" + } + } + ], + "hash": "0xfc4b81ff774304b41f674c3e53c374264a3ec988118144a8d49ebb87e66e2f00", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xb9f8fd253e6da658201c7d51eb1d64c53ae37639fc94ead37dce09dc7b8a122f" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "0xfe15000508fa702953f7966b7da93a3ee01952d7fbf7968c831b4d4103a1f587": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x1", + "tx_hash": "0x2746ae89bf4c9c652cebc4119bbd5a9df9f721f9eedd64182d188bbc17e5d489" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xdb30f9d5f126ed97836bb953e06415433f7367a42dd24b0a6b53f934259b70c9" + } + } + ], + "hash": "0xfe15000508fa702953f7966b7da93a3ee01952d7fbf7968c831b4d4103a1f587", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x2746ae89bf4c9c652cebc4119bbd5a9df9f721f9eedd64182d188bbc17e5d489" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100", + "0x" + ] + }, + "0xffe1382e9db1645da25b1602fba1f38b7df1a11b2e72bc98420e9e7353a4ae27": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x628d5d167bfdc69330f8a4f4e972147c622618d8bc847d5bb4f52d4446ba2f48" + } + } + ], + "hash": "0xffe1382e9db1645da25b1602fba1f38b7df1a11b2e72bc98420e9e7353a4ae27", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xa7bfa9832a57afa6de3ba0566d10e89e25c051df7f69a13fa66d0938dfa2a176" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xafc1309eef287471f5feb8e01a6bad53624851301ed7e5117b803290c2362c80", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "0x049df337a2dff720c87c75a9aee3508694c52030e387d1820a4afa28a14b8254": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x16fe2ced0417b0a62f56bffaea8082d4901f2327c2b3f0e8e6f7d867575a1ee4" + } + } + ], + "hash": "0x049df337a2dff720c87c75a9aee3508694c52030e387d1820a4afa28a14b8254", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xbb14cef29b87d090c2c61031652ca3d70c2e1919990f018ae285bb30c15998bc" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0xba43b7400", + "lock": { + "args": "0x", + "code_hash": "0xcc321278301291afc3c43ae43d5e3e3c10ce9cf0658c3063587b0123cdce7cef", + "hash_type": "data1" + }, + "type": { + "args": "0x43", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x00d51ef4e28799d94e9046a2873a2974c4fbe51572f6fd1d579cd43f4bf679fb87640000000000000014000000000000000000000000000000000000000000000016000000000000005645535430303031" + ], + "version": "0x0", + "witnesses": [] + }, + "0x078d6b393501851b72052db4c6f1e8b439ede7a80a15ed84a8fa2f777eed8054": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x80ec8fc6e4986da8bc215946af429bbdb6fe26ab543bc6735377b480fcc8418a" + } + } + ], + "hash": "0x078d6b393501851b72052db4c6f1e8b439ede7a80a15ed84a8fa2f777eed8054", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xec8e911ac2a4e9edc0c1412df26ec7cd9d7b2147e4f0cd3f2d542fb6fecb829b" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x30999ca44c3638eceddd4d707a01242294bd06473c9e34f92f5cdea23c2bdb75", + "hash_type": "data1" + }, + "type": { + "args": "0x22", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x0000000000000000000000000000000000000000000000000000000000000000040000000000000068e0283c1fd1128451c56b1271048ce1bdbadaaab224ea219c1277ee4cbabe7778000000000000003c0000000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x0a0b8c20de2b149b74926989ccef6f20ab3984e666b923dfb78610c569e753bc": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x3ee712eb9ce234366e17d006c3a022f164cd052b1739c8d0b1ddfaae7fdab1b2" + } + } + ], + "hash": "0x0a0b8c20de2b149b74926989ccef6f20ab3984e666b923dfb78610c569e753bc", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x170908af85e2cfd612ba186fe782233e08dab212177255e3e15e236df1f2b526" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0xd305deeaaf5715086b9ca367189279b7709450c3df4a6b73620f841578e1195f", + "hash_type": "data1" + }, + "type": { + "args": "0xc4", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0xd305deeaaf5715086b9ca367189279b7709450c3df4a6b73620f841578e1195f", + "hash_type": "data1" + }, + "type": { + "args": "0xc4", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0xfa000000000000005041594d30303031", + "0x16260000000000005041594d30303031" + ], + "version": "0x0", + "witnesses": [] + }, + "0x0f7ef307cd4762342d70d780814b26f305dd44d149a9e91c37443b622b72e34b": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x17fdc71ba9532d39718b8f52c521c40c1f5c19b7194bad896326abddc303c7bb" + } + } + ], + "hash": "0x0f7ef307cd4762342d70d780814b26f305dd44d149a9e91c37443b622b72e34b", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x1f20dc94201a9ba20df40dd1a35a5c272817b83e2dec88e13e12670034c573a0" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xca92833ef77901845dd672a356fa9e568a3d46f179a1df5469dbbc05b7313427", + "hash_type": "data1" + }, + "type": { + "args": "0xb5", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x2a00000000000000544f4b454e303031" + ], + "version": "0x0", + "witnesses": [] + }, + "0x1552617977bba10cb1d8df84ccaba2a68deaeac7eb39fc08462ecc5b9feec933": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xa641762dced489313320a33d0a25ad81848a3cfdf3e057d37e5313f5aa7bff7a" + } + } + ], + "hash": "0x1552617977bba10cb1d8df84ccaba2a68deaeac7eb39fc08462ecc5b9feec933", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x268eeff71c5758b0a41cd7a4264d01ee0ef1bfaea8bb5cae50ebc17317c991a9" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0xee46f625f63eaccefbc7c9bf1393a21295d1d21e712fc704523d6f349a39ca26", + "hash_type": "data1" + }, + "type": { + "args": "0xd1", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0xee46f625f63eaccefbc7c9bf1393a21295d1d21e712fc704523d6f349a39ca26", + "hash_type": "data1" + }, + "type": { + "args": "0xd2", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x0400000000000000414d4d4130303031", + "0x0900000000000000414d4d4230303031" + ], + "version": "0x0", + "witnesses": [] + }, + "0x178476fefdc74a41929f6858dd8f6fe4094ee863ba6e8defbff459070e2f18dd": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xb402243a1be68cc9f3dd8f703010b6faadc4a98ac26dc19d4cca2703edb335a3" + } + } + ], + "hash": "0x178476fefdc74a41929f6858dd8f6fe4094ee863ba6e8defbff459070e2f18dd", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x1a0161c46f8d9218cea883867b37c3ad655c5dd7533a56a374ac1b046cab598f" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xc6c1fd22456162ae2457a61bb7fd4f0ad0ac92955adbc6a8da10a0caf1900362", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [] + }, + "0x21aaab6b34b6f7bd4c7672fe16baa2deacfe062cab95a9104c9c3d32de16165f": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x06fc2217647967fbbbb43852493f249d782b073b114cc29bbdca5e13bf830cfe" + } + } + ], + "hash": "0x21aaab6b34b6f7bd4c7672fe16baa2deacfe062cab95a9104c9c3d32de16165f", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x134f2b5f75013ce63a904e32e4aca008207118fe843570cb8af04243850b35fa" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x1f82090965f2d38d5fd0f1f654345a0c146476db869bf5452392702eeab55bff" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x0b3ac2602d34b52a0f8d258890d4fe3a2a0c86c8e7c5062442abd00106adeb98" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xe64d0051791f2f2d161d51734ee363180b843910429ae5de3c45381c57126855" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x5d21dba000", + "lock": { + "args": "0x", + "code_hash": "0xb6d00cb658e0732961e4c25b5322160074ac41e52182067bc326353930063479", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [] + }, + "0x238f2ceedd51e0575705f189111340520552a861e09666e47b4517ef10757b01": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xfa1320af6eff6f2b2b69e30391ca3a027c259318a86dca32e3238884311b84d7" + } + } + ], + "hash": "0x238f2ceedd51e0575705f189111340520552a861e09666e47b4517ef10757b01", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x56b2587c1da48156ff216bfa71a78ac16845676c4205a92347e0b0a8951ef475" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x14f46b0400", + "lock": { + "args": "0x", + "code_hash": "0xa34564aa114e28106b02f63243b50f5135adc633ff7a74e735d27e9404bf0710", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [] + }, + "0x29d306ab03fdfef7ef8fe68ab222e5b318f9a82732f3126627e691b40b2994fe": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xe483d497e40139e1da27c2904f8438c0682a4ff9578d41a24eed218fa5ff76fd" + } + } + ], + "hash": "0x29d306ab03fdfef7ef8fe68ab222e5b318f9a82732f3126627e691b40b2994fe", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xb9d6137b85cd3fd51d1aab0521d70016c0085e5fbdabe1c48cc7ea21399ef7c5" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x7cda3da79fb46eaed62752444d9d4f666897861039c77bf94f0532fc43162a70", + "hash_type": "data1" + }, + "type": { + "args": "0x51", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x8d00000018000000380000007c0000007d000000850000000000000000000000000000000000000000000000000000000000000000000000020000007d079c63043b805af33e1b72b25e83ba2897b47af215f9174932de79e9393d01edf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae0100000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x375d660a387685e9e2fb3694a6329a5598d2879a50a794c0ce458f723c2788aa": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xdb73dd1332931d73fa333bb3e2b9ad2b0b93f3350e75420154005ba23a2d7d9d" + } + } + ], + "hash": "0x375d660a387685e9e2fb3694a6329a5598d2879a50a794c0ce458f723c2788aa", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xa8f6c62104531f95cecf9575b1bf69b8aa65f5fa918fc67dba08aeb3bd06d7f1" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xa80dc0eca06d915541974ed828b671ef3583818828c8fd86aaf8922a8d282f64", + "hash_type": "data1" + }, + "type": { + "args": "0x11", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xa80dc0eca06d915541974ed828b671ef3583818828c8fd86aaf8922a8d282f64", + "hash_type": "data1" + }, + "type": { + "args": "0x12", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0xa80dc0eca06d915541974ed828b671ef3583818828c8fd86aaf8922a8d282f64", + "hash_type": "data1" + }, + "type": { + "args": "0x13", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc4125baf451ed7373f0615c332dd7cf063955378dcf10b7a6fc35b0146cf4ca5182b00f4010000000000000000000000000000", + "0x544f4b454e3030312a000000000000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412", + "0xbe0000001c0000003c0000005c0000007100000079000000bd0000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc4125baf451ed7373f0615c332dd7cf063955378dcf10b7a6fc35b0146cf4ca5182b11000000656d657267656e63792072656c656173650000000000000000020000004242424242424242424242424242424242424242424242424242424242424242434343434343434343434343434343434343434343434343434343434343434300" + ], + "version": "0x0", + "witnesses": [] + }, + "0x3a4ef8679d5d77f3e7cc52e77b60c86533a2bcb68dc4fd32b00e947a15a8aaa9": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x86c3b29ad0bba4281c2d58d64030f41ad9242dcce7416f20c67130a0df8b5e46" + } + } + ], + "hash": "0x3a4ef8679d5d77f3e7cc52e77b60c86533a2bcb68dc4fd32b00e947a15a8aaa9", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xb49cf495a3c4ee97ce38cf01f2473446d47eb46398df16d1c8b44d6ccf1dad4e" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x13814f1b2b6fa776385ebf4a63ee7ec94ebc806957ac34dc565d573e7f9b0f4f", + "hash_type": "data1" + }, + "type": { + "args": "0xd1", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x13814f1b2b6fa776385ebf4a63ee7ec94ebc806957ac34dc565d573e7f9b0f4f", + "hash_type": "data1" + }, + "type": { + "args": "0xd2", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x0400000000000000414d4d4130303031", + "0x0900000000000000414d4d4230303031" + ], + "version": "0x0", + "witnesses": [] + }, + "0x40d1840247d7ff684bec814254db3e3a8d2515f59a3c02c6bd95a99120f10c20": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x9b6af43c1c3e7556bbb8d2b570bf4c0c29bfc13989a59bc601a05810d7a78d85" + } + } + ], + "hash": "0x40d1840247d7ff684bec814254db3e3a8d2515f59a3c02c6bd95a99120f10c20", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xbd4be51e8f1b998cee8782c11b81bdb58fd93c400679a7d974bb748cbb65a453" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x9da2791f3f0a46790e4b187a1d1dd15813f2c14883138631255c58b495a845d8", + "hash_type": "data1" + }, + "type": { + "args": "0x51", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x8d00000018000000380000007c0000007d000000850000000000000000000000000000000000000000000000000000000000000000000000020000004d594af7f4d8c8158e0225a7d1a80903f7ea8df3f81bc67791af289b03ae879dedf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae0200000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x43398ca152e78764ed64cf42b6a5f61da59b38f9087e9714c4cbb8a070f642db": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x4d0c0cc1df3a9620a55de0fb0691025fb805e651cda66536e620aa7ff04bd2ed" + } + } + ], + "hash": "0x43398ca152e78764ed64cf42b6a5f61da59b38f9087e9714c4cbb8a070f642db", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xc33a319da6ef4a430f15bad1a102aefe90e7fd0da81a3852c1c0e0627b2e8575" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x204920209dab2137b0f75335063856cc1e84d28b41d1e31c2160614832de50ec", + "hash_type": "data1" + }, + "type": { + "args": "0x51", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x8d00000018000000380000007c0000007d00000085000000000000000000000000000000000000000000000000000000000000000000000002000000cf8cbc02d90b9e9bca7eb320ce67edfcf62400cd2a8bfb81a8104cf3942bca74edf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae0200000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x4a3e569f693934dcfca435644132ef1a44a29275ca54814354bb783db8a7ca7d": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x7316d0640df6e12bf34469505237b41ef4ef81dd1d7cbe667d2bd929928a8ee9" + } + } + ], + "hash": "0x4a3e569f693934dcfca435644132ef1a44a29275ca54814354bb783db8a7ca7d", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x6eabbb604a1c3deae9c66db4ddf0808656fd8719a7f74066f9e023e6dbd14c54" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x56243615bf571a99518758be09271fe4246d7a980607b2a46d0a8f5041de3593" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x2e90edd000", + "lock": { + "args": "0x", + "code_hash": "0x67c60bfc34958c28b1d5277be6995af9920d480f60ded40806eaf173c704d10d", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [] + }, + "0x4dbde6eb4499b366a69afa2f677fe589f0cf9fd9d5892598771aecad786a4c38": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x92ba4f3a9f6ef2ef017253e99a5769579a4d9af3cb0b5bfeaf674c73f73e022f" + } + } + ], + "hash": "0x4dbde6eb4499b366a69afa2f677fe589f0cf9fd9d5892598771aecad786a4c38", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x6e63e49770c170c81862de53e554b17c85275ee24e3eadb44f349c8b4bf2d162" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xfa97e4b195e88b2e154d7d260df3f1d27a35618b4aaeff97ba01eb9911a15901" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xcb4fe44af248bedef5c7264fca05dc1bc480add1d06f1bcb90dfcd0910810b1e" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xaee2bfa44ca0e72a6e48400e8c88c3dab0ff606940465e89846849b1087f25b9" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x5d21dba000", + "lock": { + "args": "0x", + "code_hash": "0x450a496151c4111710cd56ffe7558b63b2a5e22e0ad7fd33edcf52173121c440", + "hash_type": "data1" + }, + "type": { + "args": "0x93", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0xfa000000000000005041495230303031" + ], + "version": "0x0", + "witnesses": [] + }, + "0x5a8483ebb69040dee1659744182b76fe71c973610ea4d31cc84d86f171d9dda9": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x165cc6ad0c8d376ed93c10aea3246877f219e1a0cf40d9bd33bb4ebdd3c49bb8" + } + } + ], + "hash": "0x5a8483ebb69040dee1659744182b76fe71c973610ea4d31cc84d86f171d9dda9", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x539786ed07dc7e15fc8970663c2204577798f45340780faf130b707ae6667f55" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0xba43b7400", + "lock": { + "args": "0x", + "code_hash": "0xaaf97b20c59720fa641958ba0c26eb2bfd3ed6c23444975811f4f1c08989c368", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [] + }, + "0x5f7e68e39b7606ffe8fb6730ed62c594e8b84ad854fdb45df92b1e05ee199124": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xe274608e446e15ce0f9ec8680954950c72336954fb025575fb4a310bad2c3d63" + } + } + ], + "hash": "0x5f7e68e39b7606ffe8fb6730ed62c594e8b84ad854fdb45df92b1e05ee199124", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xc3584e126f722a6908b615f3a9ce62a15e5470951aaba467b9610cf9cc929ec2" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x2ca9877f46bc3e89b31d76e256714e075ad57732d7fcd489fdc6aa636772f93d", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x00000000000000000000000000000000000000000000000000000000000000000100000000000000ff63e89620213deea694117ec085da42e12c77ac38c0c86eeb6d2202ecb78edd000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412fa00" + ], + "version": "0x0", + "witnesses": [] + }, + "0x696b5e896adf0a8d68ed777f00d8549883fb64692d00257693656264ce126e65": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x5f2c3eb45b63be5422acd84352c33591c0c51bdbc2bcdaa28b541c4dcbe6ec1d" + } + } + ], + "hash": "0x696b5e896adf0a8d68ed777f00d8549883fb64692d00257693656264ce126e65", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x1229e1587b3c431f1590ed6d18911dc6f451f656f3d6da52fd66440c5acef798" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x828a52262139378ce50e40ea7a24d4e8d8219cb2b9ae8f0e2a2cde2a8712a546", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [] + }, + "0x6b1230cc7d06562c440c22b81e23c0cb7c253f5a1661ddfe23446ebe821353ba": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x006b521dc10d970574f21b5faf7d36e65b9242f7557bdf0f293020f759b2e568" + } + } + ], + "hash": "0x6b1230cc7d06562c440c22b81e23c0cb7c253f5a1661ddfe23446ebe821353ba", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xff2efd4828d6c567ffe542ed50c0296a5604711eb7ecc4130581eab4346eb97a" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x104c533c00", + "lock": { + "args": "0x", + "code_hash": "0x10f75e072356b123d3864ae553870a4759943c4ed71d373218ca17b611795020", + "hash_type": "data1" + }, + "type": { + "args": "0xa1", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x5354415445303031e8030000000000000000000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0x7bdb141dc64f601e73012e4488dd97f98aba20178792f4f35f66ae5f6da31370": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x204eecf4d7006584af493c734f69488ee4ca52dd1c2e7dd7ac075f8f5be3ac1e" + } + } + ], + "hash": "0x7bdb141dc64f601e73012e4488dd97f98aba20178792f4f35f66ae5f6da31370", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x40fc9998ddbecc43260cf84806c7c7e71494a4161dee1d05314457af0d58c5ea" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xfb48437de0a39605beb256eff10ddaf2923e2bcc880af813371bade22152f464" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x22ecb25c00", + "lock": { + "args": "0x", + "code_hash": "0x2eb610fca034a18303d192bcbf52ba0f68e6ee8b1cafa90b200d5edad55257ef", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [] + }, + "0x8e0dc6fb8bde56d6fa372501bab5f137df3c09dcd0ae455d6396cd38a1bc3e18": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xd0734aa42e646234c69b1fc13a8352a746230eb748a3b4f0ed577b37a59c97a6" + } + } + ], + "hash": "0x8e0dc6fb8bde56d6fa372501bab5f137df3c09dcd0ae455d6396cd38a1bc3e18", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x8452495fa641e5ea1654b9f5c1f388bd94e1c51aeeb51bc1fc4b3bc824a2fccd" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xf8feaa6faa28c75b4743f98c90340855926a41147ca443dca6d0d9d18636afb9" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x542086a6c5151ff4a2781b2e169f56b148125f0d389351411a14cee4819c1033" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x3a35294400", + "lock": { + "args": "0x", + "code_hash": "0x0a4cb03cd7e44ca4449ded0f8c6014c0f919819072badeb07bd51e0add611f35", + "hash_type": "data1" + }, + "type": { + "args": "0x25", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x860000001c000000350000003d0000005d000000650000006d00000015000000416363657074616e636520436f6c6c656374696f6e0400000041435054ad2efee9b2aa7cf537c64f1adbbf7bf4d35accce0f9c25dc9e7b111ea4dc87ea1400000000000000e80300000000000015000000636b623a2f2f63656c6c7363726970742f6e66742f" + ], + "version": "0x0", + "witnesses": [] + }, + "0x8f31a148d525d4d627eda45d969275fb7966b3a1f0e425ba8bc1dbb441930b23": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x8ad8c938473f108cf363d556d16de535af96f3ad0d3bc6be6893da0a11e8a96d" + } + } + ], + "hash": "0x8f31a148d525d4d627eda45d969275fb7966b3a1f0e425ba8bc1dbb441930b23", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xa22db1026c273393d0405ab6cd1fcc23705cfca478f4b9ea9b0540d804b322b2" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0xdebeeebed4b050592aeeda4cd26e530632ce8236e4a0481c150ee8264cfcdffb", + "hash_type": "data1" + }, + "type": { + "args": "0x44", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4d000000000000005645535430303031" + ], + "version": "0x0", + "witnesses": [] + }, + "0x9081136c24df469f162ee5d2f811b20c93c9ab55686d7dc94a3c5396185d42e2": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x4e37ef1b4ef9ce4d4bf6e391a520856f1646deec3fd27518ee4b3fd932f3cde7" + } + } + ], + "hash": "0x9081136c24df469f162ee5d2f811b20c93c9ab55686d7dc94a3c5396185d42e2", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xf86ab2de6450c0c56de9e9e88ad52b3be1e65ab42d89573cdb91ff56baf5fd58" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xc13b702287bf01246fd600189482d58fb1e8bdadce497124858e010750a4b4cd", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [] + }, + "0x967df738576d6c83283ed92ef71a0e174e45556fed0ac222f2b4e450150f91a3": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x8713577264f34e7acd5e5d74b494dbfb1c09c70af8f30a7fb1c3570aa4cbf1d4" + } + } + ], + "hash": "0x967df738576d6c83283ed92ef71a0e174e45556fed0ac222f2b4e450150f91a3", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x081b01df9d673856b1b6171d4731be5338d1df8efdc29220615bd99db2506708" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0xba43b7400", + "lock": { + "args": "0x", + "code_hash": "0x318333f71531adb7109813cd89f757d76d7a1a8aebb79e93d800df8f4f0bc3c3", + "hash_type": "data1" + }, + "type": { + "args": "0x43", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x00b250afae197267ab716da7baa7d3077d66d0bfb6286f19ab4d0698a90737666e64000000000000001400000000000000000000000000000000000000000000000b000000000000005645535430303031" + ], + "version": "0x0", + "witnesses": [] + }, + "0xa7bfa9832a57afa6de3ba0566d10e89e25c051df7f69a13fa66d0938dfa2a176": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x628d5d167bfdc69330f8a4f4e972147c622618d8bc847d5bb4f52d4446ba2f48" + } + } + ], + "hash": "0xa7bfa9832a57afa6de3ba0566d10e89e25c051df7f69a13fa66d0938dfa2a176", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xf7011418b95f6ac5cc75eecc9b2ef3ef8e4e45113ccde9eb1fc01e9d73ea4ecc" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xafc1309eef287471f5feb8e01a6bad53624851301ed7e5117b803290c2362c80", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x000000000000000000000000000000000000000000000000000000000000000002000000000000009097611a54d809e6d5cd011ef4511e4259dd6c5594dc8670598498cfd6f33551000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412fa00" + ], + "version": "0x0", + "witnesses": [] + }, + "0xadd93fd1d69b52f2de36bfa1d108d8d143b137d7c043622fe1d1175589f748ee": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x9f02df0a573644347b6f73102ec88a9c6be51b35fb36c6305e17048c3f13ec0d" + } + } + ], + "hash": "0xadd93fd1d69b52f2de36bfa1d108d8d143b137d7c043622fe1d1175589f748ee", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xd3da18803e0dbf16680e781495d8d5a052aee2145d6d31a9915c1b11d26eb5d0" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x078a625eb933f34dee98290d89c9cd6b57ab95d8bb1a89f254f422649a972954", + "hash_type": "data1" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc4128a166edfc898dd17d9c3968fdee59e903e0b98ddf23d77c870dee1585cce89020064000000000000000000000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0xae0bc2b1731b075e540a5fd07c59b6bb03278cac9e259d21528e4b0e543ef2f0": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xf3587c0b234657d49a8060ead24d3c0c6746964524c281738238c2eee58261cc" + } + } + ], + "hash": "0xae0bc2b1731b075e540a5fd07c59b6bb03278cac9e259d21528e4b0e543ef2f0", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xce59b12e61da7e64eb59e3aa77c07a5a2e00b427ddfe09cd41ecc932ca007b96" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0xba43b7400", + "lock": { + "args": "0x", + "code_hash": "0xf51e1060b13ba495ab2cac42ab5102d7ff6bb2c00df50f115846b5fcc9429298", + "hash_type": "data1" + }, + "type": { + "args": "0x52", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x51", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0xbf00000034000000540000005c0000007c0000007d0000009d000000a5000000a9000000aa000000ae000000b6000000be00000000000000000000000000000000000000000000000000000000000000000000000900000000000000fbf9445aa019c17dbdd072cae5c7840097cba346940a427fc4269257922e5bf70064f944c22f0db7bfb29aa523b65cbb75a6a65b369febfbff0ffc17facacfe2dcf4010000000000000000000002000000001400000000000000d00700000000000000", + "0x8d00000018000000380000007c0000007d00000085000000000000000000000000000000000000000000000000000000000000000000000002000000fbf9445aa019c17dbdd072cae5c7840097cba346940a427fc4269257922e5bf7edf9f440e51fb1f87aec693c7e925c2d99c8f80582115bb8d68ed68da2c50eae0200000000000000000a00000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0xb5bb69474889615326f43e030a432ec50a00f3a71c093557029ac47e171bb34d": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xeb13566917d6910918b1ccecac0c80f748dd0947169e3771684db5322187b986" + } + } + ], + "hash": "0xb5bb69474889615326f43e030a432ec50a00f3a71c093557029ac47e171bb34d", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x601c3f2e01908dfea7fd1a3e1fb40e788559e0776307d785f867b44adbdd5282" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x936f404b8bb2d8004894245c812d2aab8eea81718b0c98a695f8013f4c85e2a2" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x9c1a6fed0b778db0b8006116e9d9a6213d2b5dadf1ac391118790c44a9ffe275", + "hash_type": "data1" + }, + "type": { + "args": "0x21", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x9c1a6fed0b778db0b8006116e9d9a6213d2b5dadf1ac391118790c44a9ffe275", + "hash_type": "data1" + }, + "type": { + "args": "0x24", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0x9c1a6fed0b778db0b8006116e9d9a6213d2b5dadf1ac391118790c44a9ffe275", + "hash_type": "data1" + }, + "type": { + "args": "0x24", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + }, + { + "capacity": "0xba43b7400", + "lock": { + "args": "0x", + "code_hash": "0x9c1a6fed0b778db0b8006116e9d9a6213d2b5dadf1ac391118790c44a9ffe275", + "hash_type": "data1" + }, + "type": { + "args": "0x23", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x00000000000000000000000000000000000000000000000000000000000000000700000000000000b185b58da000c8f9180f3ab6f8dc6b1c907fd1f44f8ace7fdbe46b7ea7ab3db4000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412fa00", + "0xfa000000000000005041594d30303031", + "0x16260000000000005041594d30303031", + "0x000000000000000000000000000000000000000000000000000000000000000007000000000000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc4121027000000000000c8000000000000005041594d3030303100" + ], + "version": "0x0", + "witnesses": [] + }, + "0xbf23c0724ab21b007a7d3b7832a662b934e41b1c656f84b2544c87aacd5d8c48": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xbcad341f60c752aa595c93250be7968b9073f5c09b3e9c645fd115dec67eeb88" + } + } + ], + "hash": "0xbf23c0724ab21b007a7d3b7832a662b934e41b1c656f84b2544c87aacd5d8c48", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x9941f70304f5a63b2d27792e7ef9da7905c27f901753b32e34a159eb59c6df5f" + }, + "since": "0x0" + }, + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xe1e1f0891c918064293563725caa2e611df3a63a85b2881504b7caba679c50d2" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x734c3177a05355d83a3be6c68309e377cfa444fc62d5d6504b3664ea090e9b02", + "hash_type": "data1" + }, + "type": null + }, + { + "capacity": "0x6fc23ac00", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x", + "0x4ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc41294ad7e819f84f5e26539b71e3e49ce236802e80d09e5ad2f3a0c8a17264b144200f4010000000000000000000000000000" + ], + "version": "0x0", + "witnesses": [] + }, + "0xc1a6e6f593ae6b52c28cab12c650db48041bbcb1ae6a7e06b800c199c6d8a4da": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x54ea0c5e8948e5691f98bebe41b5071a4a8c762ca435c622761508af8cd4e51d" + } + } + ], + "hash": "0xc1a6e6f593ae6b52c28cab12c650db48041bbcb1ae6a7e06b800c199c6d8a4da", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x3203d951615a5ddd3662a45c4981f21ad1a6f8d9d64c77897afbdf62868e8d0a" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x93f05f4fb67225c694ab2cb4c4b57125136e08fcff490458f42fa63e26086bdd", + "hash_type": "data1" + }, + "type": { + "args": "0x23", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x9e0000001c0000003c0000005c00000071000000790000009d0000004ceaa32f692948413e213ce6f3a83337145bde6e11fd8cb94377ce2637dcc412414141414141414141414141414141414141414141414141414141414141414111000000656d657267656e63792072656c65617365780000000000000001000000424242424242424242424242424242424242424242424242424242424242424200" + ], + "version": "0x0", + "witnesses": [] + }, + "0xcdee5c008e17b8d31dbc8472c5f3771a959ed40826789829f30c56062390104f": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x361ddd4cf352f5a027b10ac34cde394aaf28cb92f71dc04f00e4837643111170" + } + } + ], + "hash": "0xcdee5c008e17b8d31dbc8472c5f3771a959ed40826789829f30c56062390104f", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x077d5b8fd1645ca64627fce7e7811836529ebda1ab14ec01222cc221ec59b8fa" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0xba43b7400", + "lock": { + "args": "0x", + "code_hash": "0xc1e7dce634480aceedc7bd5dfbb7df1e5951cd945fb0d10cb8ea70968af0443b", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [] + }, + "0xda0a4d13e2a3cabdaa0001fee1acb48209ee68b4d786311af2629448b52dcfc2": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xbdba2f98f29414b88797bc5942b6c00d6a887dffeee6e3af43579372ea4d612e" + } + } + ], + "hash": "0xda0a4d13e2a3cabdaa0001fee1acb48209ee68b4d786311af2629448b52dcfc2", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xaf97e8864072e46943255a3337698dc111a3efcfc193a6529ea82365abc44e94" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0xb961548b7fb156288df5e3c472a626d1791f0753906579980f1c75f1069698a1", + "hash_type": "data1" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [] + }, + "0xe1350ee31a467cf4c84e39edfe45476a1ef4a77734cb0e48c4ef4e4e5c32d93b": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xaa5563e32d88035679d005517839675d1717431123ecccac41442e008f201abc" + } + } + ], + "hash": "0xe1350ee31a467cf4c84e39edfe45476a1ef4a77734cb0e48c4ef4e4e5c32d93b", + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x1d0d2bf472e7601333557354d681513aa86a587fb5084243cf1a27ff5988c904" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x4a817c800", + "lock": { + "args": "0x", + "code_hash": "0xa3bdc8e44991a1790d469db884797763ce5dd7ef8631b77f45c2d925633dbbd9", + "hash_type": "data1" + }, + "type": { + "args": "0xd1", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + } + } + ], + "outputs_data": [ + "0x0200000000000000414d4d4130303031" + ], + "version": "0x0", + "witnesses": [] + } + }, + "cell_deps": { + "0x006b521dc10d970574f21b5faf7d36e65b9242f7557bdf0f293020f759b2e568:0x0": { + "data_hash": "0x10f75e072356b123d3864ae553870a4759943c4ed71d373218ca17b611795020" + }, + "0x014f954ce1b5dcd67562a3094bda3060e3807bd071a5da61a107a3c4c02716e2:0x1": { + "data_hash": "0x236ce882a6f9c2ec9ef0fd90e96f581fe711c10ccdf9cd39d178fa84a9c2bbc8" + }, + "0x01c2a831918e3b54119d0952e4db1e3ebf65d73cec2c2bc3d9051fc0728f45c2:0x0": { + "data_hash": "0x6c3bf0d1bc162b2c71378ec3ea55d34121ab8c66480524e83cc8943de6b26555" + }, + "0x04ff3d5eebf352f6edd435d3c42bba62a2b84b65b79504643548e80b2d4d150c:0x0": { + "data_hash": "0x67c60bfc34958c28b1d5277be6995af9920d480f60ded40806eaf173c704d10d" + }, + "0x05bdf82334e9817b9e706495e1e0897548dad8e635a07d19ae0dfb2551ed84e9:0x0": { + "data_hash": "0x30999ca44c3638eceddd4d707a01242294bd06473c9e34f92f5cdea23c2bdb75" + }, + "0x06fc2217647967fbbbb43852493f249d782b073b114cc29bbdca5e13bf830cfe:0x0": { + "data_hash": "0xb6d00cb658e0732961e4c25b5322160074ac41e52182067bc326353930063479" + }, + "0x08a319c4fe820d63319732392e166c200c4f7eb811244ac8a4dd433065e8400c:0x0": { + "data_hash": "0x6c976866fb9c343bd28922b92aca893f292fed889e52b75a59a10f738b31f4e7" + }, + "0x0ed6bca9b7b257ea7fd5b25d4f686479d892da1349a94a24c20be92d396dcaa3:0x0": { + "data_hash": "0xbb4e6287d83e99d7184fbca2c277f0326b25eee7917e5815e0817cf79f2d4fe5" + }, + "0x14a44b8c532b2bb73f71cbaee290f3e62ae5a4c3b2b083dacfa2018de393dc3a:0x0": { + "data_hash": "0xb5e6f61a513204507f6cf508cbc23b449a60d91ef6ebdfde79fcf886bf4cb020" + }, + "0x14e410f98eb197fc6a336f68534cee7ce181ab0d6ea6bc28a8f66acb6a3c8c44:0x0": { + "data_hash": "0x1b82b117ab4e41de9f6652a54f2f2ee24abad190b04a935cf90075cf9f3da237" + }, + "0x165cc6ad0c8d376ed93c10aea3246877f219e1a0cf40d9bd33bb4ebdd3c49bb8:0x0": { + "data_hash": "0xaaf97b20c59720fa641958ba0c26eb2bfd3ed6c23444975811f4f1c08989c368" + }, + "0x16fe2ced0417b0a62f56bffaea8082d4901f2327c2b3f0e8e6f7d867575a1ee4:0x0": { + "data_hash": "0xcc321278301291afc3c43ae43d5e3e3c10ce9cf0658c3063587b0123cdce7cef" + }, + "0x17fdc71ba9532d39718b8f52c521c40c1f5c19b7194bad896326abddc303c7bb:0x0": { + "data_hash": "0xca92833ef77901845dd672a356fa9e568a3d46f179a1df5469dbbc05b7313427" + }, + "0x1d3726f0eb930917dbb02cb08aa68622494014245a885c60e4d1df758f245b49:0x0": { + "data_hash": "0xa80dc0eca06d915541974ed828b671ef3583818828c8fd86aaf8922a8d282f64" + }, + "0x1e601e8f4a43db4216118fd235a8c21841e611d4d32208c6f8745d6ff5049d74:0x0": { + "data_hash": "0xe47eaf52f41e951f3eb7ae9bfd1173ca26b884ecff101bfa931b5a75ddf1be70" + }, + "0x1f4e697b9f5155338b31392abc0794fe3e262a65e8ca61cc6eeea35fb8aa30f6:0x0": { + "data_hash": "0xd305deeaaf5715086b9ca367189279b7709450c3df4a6b73620f841578e1195f" + }, + "0x1fc61e5ec8572c8853a001a40fa7acef0190c6833da6ec3e407bd2863c986a45:0x0": { + "data_hash": "0x540be7d1575ea9e60a6df5678ec2e4dc857edb687f1ad2f8359317be2288ff60" + }, + "0x204eecf4d7006584af493c734f69488ee4ca52dd1c2e7dd7ac075f8f5be3ac1e:0x0": { + "data_hash": "0x2eb610fca034a18303d192bcbf52ba0f68e6ee8b1cafa90b200d5edad55257ef" + }, + "0x2746ae89bf4c9c652cebc4119bbd5a9df9f721f9eedd64182d188bbc17e5d489:0x1": { + "data_hash": "0x3e7d3fe3d81dd97dd69bbd3df405b56a165e54fa37415fbb148caa1a16dfa70a" + }, + "0x297acc94d2c6e532490f039bfbfeed7c2e494fef06b7adb6cf00a4287dca0a73:0x0": { + "data_hash": "0x93f05f4fb67225c694ab2cb4c4b57125136e08fcff490458f42fa63e26086bdd" + }, + "0x2a9fbd7f43595d871d80e631baf1667f16d4e1cf6a44e85735c69684865db517:0x0": { + "data_hash": "0xee0a4d051aeb5c2c39df4eeeda6a55b8cb9ea79a964b905d3288c0d83138fabc" + }, + "0x2b6f560e02fd1d710f9e47d3fb9771d37ae3b88362ebca48a822e3bee8e318f8:0x1": { + "data_hash": "0xfafb763bf3b8d90faf46356618babfef4aefe9003fff84129a9d322fdd1d32f1" + }, + "0x2da50f9b01999a58735ab822ba3bc2b743d5d297b196e77f7cbbe84bc2766aac:0x0": { + "data_hash": "0xaebdd9d7c1cd1a9b581bc3a42a6c5f40d5b41f2ee637f7a1b4e4eef38500c349" + }, + "0x2f2a53ea7336cf1d1b199a59b22148e7357d2e12846050a664cce5bf73094e80:0x0": { + "data_hash": null + }, + "0x33611509a83b78a19cf2ce0980216ef2aa22a359ffb7d07bf31fbd73c339444c:0x1": { + "data_hash": "0xabb8fe08184a7964042c7ebd5817749c066dfdfc506d88f6bb1e60b4552b65ac" + }, + "0x339055295d20077427f346e209218b486acf729ef51fab4051a6c08999fdf40a:0x0": { + "data_hash": "0x6c3bf0d1bc162b2c71378ec3ea55d34121ab8c66480524e83cc8943de6b26555" + }, + "0x361ddd4cf352f5a027b10ac34cde394aaf28cb92f71dc04f00e4837643111170:0x0": { + "data_hash": "0xc1e7dce634480aceedc7bd5dfbb7df1e5951cd945fb0d10cb8ea70968af0443b" + }, + "0x3ee712eb9ce234366e17d006c3a022f164cd052b1739c8d0b1ddfaae7fdab1b2:0x0": { + "data_hash": "0xd305deeaaf5715086b9ca367189279b7709450c3df4a6b73620f841578e1195f" + }, + "0x403c6468185cda532c013187efc8a8037ddab784faa77ffa2f598d8aac72eb71:0x1": { + "data_hash": "0xbb4e6287d83e99d7184fbca2c277f0326b25eee7917e5815e0817cf79f2d4fe5" + }, + "0x4549c1c05bb03fe8c884c329830b6ba4cbc4fe2f560fa5db94addb128fc14015:0x0": { + "data_hash": "0xc39605e25bec7c9fe1297e640cf6ef42128dff83cec50c6e94bcc1eeb7aa268a" + }, + "0x49cc066a2d2f6275cc83080d71ede68d3ae540573353901dfabd8d031fc528c6:0x0": { + "data_hash": "0x2eb610fca034a18303d192bcbf52ba0f68e6ee8b1cafa90b200d5edad55257ef" + }, + "0x49ce2be6cbe254c0a78346d81c7b098e74f44601c4693a509ba36d4b3c681f69:0x0": { + "data_hash": "0x1ef8dbe9b2f531b18576d6ec194fae7e744ac501952706adcb6382d1deea04b4" + }, + "0x4d0c0cc1df3a9620a55de0fb0691025fb805e651cda66536e620aa7ff04bd2ed:0x0": { + "data_hash": "0x204920209dab2137b0f75335063856cc1e84d28b41d1e31c2160614832de50ec" + }, + "0x4d298843298431d70021bb66737e15abfe84b67851ce6a99787c28941caee507:0x0": { + "data_hash": "0xcc321278301291afc3c43ae43d5e3e3c10ce9cf0658c3063587b0123cdce7cef" + }, + "0x4e37ef1b4ef9ce4d4bf6e391a520856f1646deec3fd27518ee4b3fd932f3cde7:0x0": { + "data_hash": "0xc13b702287bf01246fd600189482d58fb1e8bdadce497124858e010750a4b4cd" + }, + "0x4e6498bb05ab2acef4f3dc7aca48bea59b65a76ba1be2359d334621a701672c0:0x0": { + "data_hash": "0x513c43aa3e994cbdf9bcd7903f1e220882873ad830b3eb024cdeefe2ca3afcb3" + }, + "0x515a67864ac1959d9cd4fa108ba421b195a4410f1878832bd01208b5d86e7fcd:0x1": { + "data_hash": "0xbb4e6287d83e99d7184fbca2c277f0326b25eee7917e5815e0817cf79f2d4fe5" + }, + "0x54ea0c5e8948e5691f98bebe41b5071a4a8c762ca435c622761508af8cd4e51d:0x0": { + "data_hash": "0x93f05f4fb67225c694ab2cb4c4b57125136e08fcff490458f42fa63e26086bdd" + }, + "0x5722b21ca2e67ba87092e0fd80580aec5df50e30c37fb60efe7dcd24c426bca5:0x0": { + "data_hash": "0x513c43aa3e994cbdf9bcd7903f1e220882873ad830b3eb024cdeefe2ca3afcb3" + }, + "0x59e35a159719575477e11dfd15b9b1c0c0c895495034a74e23e8f89a884cfa44:0x0": { + "data_hash": "0x67cf56e6c2bcd82506436ade5d8a220f9b51ad099c307d7cb59de6629cca279e" + }, + "0x5a42e270ccd43a33e96ba446dc3305288ff81717caf6d657da2f20c5cfda25d8:0x0": { + "data_hash": "0x204920209dab2137b0f75335063856cc1e84d28b41d1e31c2160614832de50ec" + }, + "0x5c75c5ca82dabee1d0aece80ed61f066c6afd349accbfedd76aa203a1e447cf6:0x0": { + "data_hash": "0xaaf97b20c59720fa641958ba0c26eb2bfd3ed6c23444975811f4f1c08989c368" + }, + "0x5f2c3eb45b63be5422acd84352c33591c0c51bdbc2bcdaa28b541c4dcbe6ec1d:0x0": { + "data_hash": "0x828a52262139378ce50e40ea7a24d4e8d8219cb2b9ae8f0e2a2cde2a8712a546" + }, + "0x605ff9349d7a02a281af3488d3f7eeedea672de6d61f015759297b95cec97b33:0x0": { + "data_hash": "0x2ca9877f46bc3e89b31d76e256714e075ad57732d7fcd489fdc6aa636772f93d" + }, + "0x628d5d167bfdc69330f8a4f4e972147c622618d8bc847d5bb4f52d4446ba2f48:0x0": { + "data_hash": "0xafc1309eef287471f5feb8e01a6bad53624851301ed7e5117b803290c2362c80" + }, + "0x6988a589235f9fd830f970f1302dbeb1685104a75ff5c95e13fa8f833fa67f84:0x0": { + "data_hash": "0x13814f1b2b6fa776385ebf4a63ee7ec94ebc806957ac34dc565d573e7f9b0f4f" + }, + "0x6aa5c60e30df163649a614d6637228dab6e507b00d3a8fd6bd93b5cc525163e3:0x0": { + "data_hash": "0xc5eb6cb03878c77e75b8462c561205189574fc4677e5538cc9136c10ab9921da" + }, + "0x6b76a471c376d588ebcc61b7ace0fd489d5015ce27ca1d261927a05e12c35e3f:0x0": { + "data_hash": "0x450a496151c4111710cd56ffe7558b63b2a5e22e0ad7fd33edcf52173121c440" + }, + "0x71eff92809d8a4981a72e97209d0b726be408aefa00d1508a26c8b1fff164552:0x0": { + "data_hash": "0x10f75e072356b123d3864ae553870a4759943c4ed71d373218ca17b611795020" + }, + "0x7316d0640df6e12bf34469505237b41ef4ef81dd1d7cbe667d2bd929928a8ee9:0x0": { + "data_hash": "0x67c60bfc34958c28b1d5277be6995af9920d480f60ded40806eaf173c704d10d" + }, + "0x7385b0cd1428d6b3de24c02748cd013790f75530ae9fe8bd125b74ba6388f97c:0x0": { + "data_hash": "0xc6c1fd22456162ae2457a61bb7fd4f0ad0ac92955adbc6a8da10a0caf1900362" + }, + "0x757f318b25b23b441765bcbfac6ae04d0986ffcbdbc86cc58ebdd7eef79c65fc:0x0": { + "data_hash": "0x0d708753715b0c50502a06fea8c0d48b890212a5217b4330b7a77805a71f3dd7" + }, + "0x75810e2bb00c39358795f31d647c7aab850fef67bf4c17be74391898f2699887:0x0": { + "data_hash": "0xc5eb6cb03878c77e75b8462c561205189574fc4677e5538cc9136c10ab9921da" + }, + "0x7e9657ed8c1aabb75e70fe5a6f3e2b06aa9dc8c78551e69b967d148803ef9f0e:0x0": { + "data_hash": "0x967d150b17ee92167fda6eb3b28e453cfc084f19c7cbfc773bb3f1b93d4dea61" + }, + "0x7f27bfaffe26061a6317a13ef25b9a6c7aa5ace6f31f4463fec22eb89aed6d18:0x0": { + "data_hash": "0x078a625eb933f34dee98290d89c9cd6b57ab95d8bb1a89f254f422649a972954" + }, + "0x80ec8fc6e4986da8bc215946af429bbdb6fe26ab543bc6735377b480fcc8418a:0x0": { + "data_hash": "0x30999ca44c3638eceddd4d707a01242294bd06473c9e34f92f5cdea23c2bdb75" + }, + "0x84949d0ac6b772fbe9eddc7aaecf1527609fb591c42129deea687f59d3bde57b:0x0": { + "data_hash": "0x0a4cb03cd7e44ca4449ded0f8c6014c0f919819072badeb07bd51e0add611f35" + }, + "0x85999c8371807a66812a6db192e23c22335b1faf2cc3bcf873658c827ba80570:0x0": { + "data_hash": "0xb6d00cb658e0732961e4c25b5322160074ac41e52182067bc326353930063479" + }, + "0x86c3b29ad0bba4281c2d58d64030f41ad9242dcce7416f20c67130a0df8b5e46:0x0": { + "data_hash": "0x13814f1b2b6fa776385ebf4a63ee7ec94ebc806957ac34dc565d573e7f9b0f4f" + }, + "0x8713577264f34e7acd5e5d74b494dbfb1c09c70af8f30a7fb1c3570aa4cbf1d4:0x0": { + "data_hash": "0x318333f71531adb7109813cd89f757d76d7a1a8aebb79e93d800df8f4f0bc3c3" + }, + "0x8ad8c938473f108cf363d556d16de535af96f3ad0d3bc6be6893da0a11e8a96d:0x0": { + "data_hash": "0xdebeeebed4b050592aeeda4cd26e530632ce8236e4a0481c150ee8264cfcdffb" + }, + "0x8c6940241808971b02b84d9bae41658d771003f1c281eb929b3aebd456b637d1:0x0": { + "data_hash": "0xc13b702287bf01246fd600189482d58fb1e8bdadce497124858e010750a4b4cd" + }, + "0x8cfc356815eccbcae35af59d0034cdda7af6094bb3a6e95e1b89b799f5d4cca5:0x1": { + "data_hash": "0x8ca88d88c4ccb8cdfc2645226faf2aa49f6f9b52c7dbae3ce5cc6ced0500229b" + }, + "0x92ba4f3a9f6ef2ef017253e99a5769579a4d9af3cb0b5bfeaf674c73f73e022f:0x0": { + "data_hash": "0x450a496151c4111710cd56ffe7558b63b2a5e22e0ad7fd33edcf52173121c440" + }, + "0x9572797579e20683ff0f7c6fe0152a168a9c6bade9800dcb77751ff651df8478:0x0": { + "data_hash": "0xe383a15d1b2e326df64363cab5778e79bd5edef2bc97a2ae3d4c77ab072752e3" + }, + "0x96729af7693292d31397d4ebdf3624d743424a305cb8c4f7c6bdc5ad63111c9e:0x1": { + "data_hash": "0x54ff9579c276449e10cf3ab6189cc3e82a911b83eb3ec89b2940bbf692d1106b" + }, + "0x969891936e2843de5c335b05af807ebe3ffc6d6f3e371e129c150b7944389b27:0x0": { + "data_hash": "0x87c083fadb4ec6e5823e9bd76b000f7b98f4b374cea82ed6a528915317d8a59e" + }, + "0x99bd2cc55653377b2109baa3f88393a406c039e1fdf0703dcb782552e3ac16eb:0x0": { + "data_hash": "0x9da2791f3f0a46790e4b187a1d1dd15813f2c14883138631255c58b495a845d8" + }, + "0x9b6af43c1c3e7556bbb8d2b570bf4c0c29bfc13989a59bc601a05810d7a78d85:0x0": { + "data_hash": "0x9da2791f3f0a46790e4b187a1d1dd15813f2c14883138631255c58b495a845d8" + }, + "0x9f02df0a573644347b6f73102ec88a9c6be51b35fb36c6305e17048c3f13ec0d:0x0": { + "data_hash": "0x078a625eb933f34dee98290d89c9cd6b57ab95d8bb1a89f254f422649a972954" + }, + "0xa641762dced489313320a33d0a25ad81848a3cfdf3e057d37e5313f5aa7bff7a:0x0": { + "data_hash": "0xee46f625f63eaccefbc7c9bf1393a21295d1d21e712fc704523d6f349a39ca26" + }, + "0xa831ba0ff5d321de15b135872754b682e38d2ddd47c38d7315bce7f166e20ec4:0x0": { + "data_hash": "0xee0a4d051aeb5c2c39df4eeeda6a55b8cb9ea79a964b905d3288c0d83138fabc" + }, + "0xaa5563e32d88035679d005517839675d1717431123ecccac41442e008f201abc:0x0": { + "data_hash": "0xa3bdc8e44991a1790d469db884797763ce5dd7ef8631b77f45c2d925633dbbd9" + }, + "0xac7777ae99467a32a740f9f89776f0a5c1a17808c49e29e69a1b193d9f751e1b:0x0": { + "data_hash": "0x6d3f4ff4a0012611cfad302ba4de186169643407d6fb5b302cbd11183f5b280e" + }, + "0xae0bc2b1731b075e540a5fd07c59b6bb03278cac9e259d21528e4b0e543ef2f0:0x1": { + "data_hash": "0x8ad83f727b350baccd6804275e333b8168861f8ee098d35119f5e32f2f298f55" + }, + "0xb402243a1be68cc9f3dd8f703010b6faadc4a98ac26dc19d4cca2703edb335a3:0x0": { + "data_hash": "0xc6c1fd22456162ae2457a61bb7fd4f0ad0ac92955adbc6a8da10a0caf1900362" + }, + "0xb547f36f187c1934d41fdd9aa8a0e855842721d14c598386bdc850d6b17b5517:0x0": { + "data_hash": "0xa983e55b6bf70b5e24415864e1ab3640ccef502351a814d229b66d6738e0c83a" + }, + "0xb58deece93c4942aa5ab1e0722ebfceaa8f9fabe3c6e8eb01dff0f2bd44b176d:0x0": { + "data_hash": "0x734c3177a05355d83a3be6c68309e377cfa444fc62d5d6504b3664ea090e9b02" + }, + "0xba786ad1ae914446151de4ce6258fc3d780be1d17424330bbd6a36b6b87f30a1:0x0": { + "data_hash": "0x7902ead8098947fb0d9cf87ed357d821d21c0e85505621627fbd81d873290140" + }, + "0xbcad341f60c752aa595c93250be7968b9073f5c09b3e9c645fd115dec67eeb88:0x0": { + "data_hash": "0x734c3177a05355d83a3be6c68309e377cfa444fc62d5d6504b3664ea090e9b02" + }, + "0xbdba2f98f29414b88797bc5942b6c00d6a887dffeee6e3af43579372ea4d612e:0x0": { + "data_hash": "0xb961548b7fb156288df5e3c472a626d1791f0753906579980f1c75f1069698a1" + }, + "0xbe466bab7cff1e51bbd15ce13c297ff867f1b089231d2f4797f3e656f9f2fcdd:0x0": { + "data_hash": "0x6c976866fb9c343bd28922b92aca893f292fed889e52b75a59a10f738b31f4e7" + }, + "0xbf23c0724ab21b007a7d3b7832a662b934e41b1c656f84b2544c87aacd5d8c48:0x1": { + "data_hash": "0xabb8fe08184a7964042c7ebd5817749c066dfdfc506d88f6bb1e60b4552b65ac" + }, + "0xc0bcb97f3c6a8c60d29eb5ed52c18597b102a5b43a1694a53a31d079a8814a95:0x0": { + "data_hash": "0xc1e7dce634480aceedc7bd5dfbb7df1e5951cd945fb0d10cb8ea70968af0443b" + }, + "0xc10773a7ba18c8d32e4deab978518e5d1f4665d833b6361ccc306d5382387f7a:0x1": { + "data_hash": "0x4d8f78c8205152c06842e724696959f882e8ed7c35a738f6a43f271ddbdaaf47" + }, + "0xc145bbbef86e1441c587b6a24a8007c687becdb42b503a349b06475e8a86de48:0x0": { + "data_hash": "0xf51e1060b13ba495ab2cac42ab5102d7ff6bb2c00df50f115846b5fcc9429298" + }, + "0xc1992e679cdcbc64bb722f94b5d226099b2b9ead0529e4ccf45833055f369b2a:0x0": { + "data_hash": "0x506f0fcad78f1aac2f1d95006a2a62b4dfbbfa2334a0838fd43f4610547b275e" + }, + "0xc293f43936132ce8cb8f8a4a760f1de03c82dfd7464533a73b586d1867b92349:0x0": { + "data_hash": "0x9c1a6fed0b778db0b8006116e9d9a6213d2b5dadf1ac391118790c44a9ffe275" + }, + "0xc3d498167f8fa254bdaed6029f276aacea9d662a5cd393b4cc19cffa2889fe25:0x0": { + "data_hash": "0xa3bdc8e44991a1790d469db884797763ce5dd7ef8631b77f45c2d925633dbbd9" + }, + "0xc71e873453ffa750d9ef781214a1e015b9fe92ba751672b9592f3593750cb2fd:0x0": { + "data_hash": "0x9b4ed74a5469e89dd428a35929c57c901ef5dee0ea5a3e1979ac81d53dc2ea4e" + }, + "0xc779774afe4bbb92248ad5e6f91ba71ddfac20bda122802790702264c6d8975f:0x0": { + "data_hash": "0xb961548b7fb156288df5e3c472a626d1791f0753906579980f1c75f1069698a1" + }, + "0xc8d09aa1bd1628fbcbaf36c8708d86a6ae276bbada0a5b3f032f3c4188bcc9f2:0x0": { + "data_hash": "0x828a52262139378ce50e40ea7a24d4e8d8219cb2b9ae8f0e2a2cde2a8712a546" + }, + "0xc922cbc382b9e65ed9852d188f4eac36d7b7e47c518639c0b6e39899aa32d440:0x0": { + "data_hash": "0xdebeeebed4b050592aeeda4cd26e530632ce8236e4a0481c150ee8264cfcdffb" + }, + "0xce13932ab95d93c1314a4d502849177e49ae562fef4b548150bba05bb04896b4:0x0": { + "data_hash": "0xee46f625f63eaccefbc7c9bf1393a21295d1d21e712fc704523d6f349a39ca26" + }, + "0xd0734aa42e646234c69b1fc13a8352a746230eb748a3b4f0ed577b37a59c97a6:0x0": { + "data_hash": "0x0a4cb03cd7e44ca4449ded0f8c6014c0f919819072badeb07bd51e0add611f35" + }, + "0xd0b58fa147e5809651e0e3ec1edf1ebd5d25f78cd62529195d11c23bebab6f72:0x0": { + "data_hash": "0x582102f2b60c104220dfb5607341b748d7a0651050af5d2ee44f3a5fc540ce1d" + }, + "0xd32db7375c2ca9b9df46c33c6d3c6ae4f1f86236633a3ad794086f2fa708f2e4:0x0": { + "data_hash": "0x318333f71531adb7109813cd89f757d76d7a1a8aebb79e93d800df8f4f0bc3c3" + }, + "0xd3fd27a5c0ce54a627bc8b585760471badce4816d4839254b64ded850b60bfba:0x0": { + "data_hash": "0xafc1309eef287471f5feb8e01a6bad53624851301ed7e5117b803290c2362c80" + }, + "0xdb30f9d5f126ed97836bb953e06415433f7367a42dd24b0a6b53f934259b70c9:0x0": { + "data_hash": "0x214893fb9dd10fe10e4a92ef80b06c126808256f524d308629c828a49c4327de" + }, + "0xdb690265e0a48d6b81c1b38a4af3366976ec7d73515a63128a97e320a4664175:0x0": { + "data_hash": "0x4e52ecbe274e6cefae5fdb1eba4f1cee15d92370a2a991bce607e86bd12c2cae" + }, + "0xdb73dd1332931d73fa333bb3e2b9ad2b0b93f3350e75420154005ba23a2d7d9d:0x0": { + "data_hash": "0xa80dc0eca06d915541974ed828b671ef3583818828c8fd86aaf8922a8d282f64" + }, + "0xdff9e4e52961c196f41c52c2d211468dfa6b3af8c4f194a02d4bc59bfe39d066:0x0": { + "data_hash": "0xf7a0ff2f4e06b8af72ebbd3aa6a7e6ffe61d3ada8258397c6c224d217ae9d3cd" + }, + "0xe274608e446e15ce0f9ec8680954950c72336954fb025575fb4a310bad2c3d63:0x0": { + "data_hash": "0x2ca9877f46bc3e89b31d76e256714e075ad57732d7fcd489fdc6aa636772f93d" + }, + "0xe2bfd1c340bd2b8f529bc256d9c58274f2e5c65e443ca77fc78a26d4904e4969:0x0": { + "data_hash": "0x7902ead8098947fb0d9cf87ed357d821d21c0e85505621627fbd81d873290140" + }, + "0xe483d497e40139e1da27c2904f8438c0682a4ff9578d41a24eed218fa5ff76fd:0x0": { + "data_hash": "0x7cda3da79fb46eaed62752444d9d4f666897861039c77bf94f0532fc43162a70" + }, + "0xeb13566917d6910918b1ccecac0c80f748dd0947169e3771684db5322187b986:0x0": { + "data_hash": "0x9c1a6fed0b778db0b8006116e9d9a6213d2b5dadf1ac391118790c44a9ffe275" + }, + "0xebdaef4d2108c25778301a3237fbdcf71a260c384e1d8d3f738d16b65d7a66b8:0x1": { + "data_hash": "0xc735c3a898cf40f0f97cab804ca6b5f54b2d324994af8396ddd1e1bb4ceb5d99" + }, + "0xec73cb2253c130c509a2fb0fa9557411c1bd607b51eb3ed20153393ca8c72157:0x0": { + "data_hash": "0xa34564aa114e28106b02f63243b50f5135adc633ff7a74e735d27e9404bf0710" + }, + "0xef62d924ff6af766acc21727b36c6e6483fac2768527599bf597348eb3c55a91:0x1": { + "data_hash": null + }, + "0xef6eddcdb6a4500202839de5d3929cdcb7ade4274dee72021b5612314ad01235:0x0": { + "data_hash": "0x2f95e743beef860d851bf2277fc56c036c5be0b956db68627791b404b45067e9" + }, + "0xf0738d58ce079764795b431bbfd979cb1e39fa1672b01a35e6c50648a7831211:0x0": { + "data_hash": "0xb5e6f61a513204507f6cf508cbc23b449a60d91ef6ebdfde79fcf886bf4cb020" + }, + "0xf0f807aecb16aaf7820fdb2c70d719912dd02d8274f76343fc42eebdbc3bcc02:0x0": { + "data_hash": "0x18758b10cc53dcf2fcd775462ec3ad8052ca19f21158a315eedc926cd085d520" + }, + "0xf3587c0b234657d49a8060ead24d3c0c6746964524c281738238c2eee58261cc:0x0": { + "data_hash": "0xf51e1060b13ba495ab2cac42ab5102d7ff6bb2c00df50f115846b5fcc9429298" + }, + "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e:0x5": { + "data_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5" + }, + "0xf6c1dea3d39f795519ada0030abe07e5524aa5b99b89b86733664a7e038c7d96:0x0": { + "data_hash": "0x1b82b117ab4e41de9f6652a54f2f2ee24abad190b04a935cf90075cf9f3da237" + }, + "0xf8ee78ee63762e2c05952e54e460b90a506c4160c9e4d420f83246162712be43:0x0": { + "data_hash": "0x967d150b17ee92167fda6eb3b28e453cfc084f19c7cbfc773bb3f1b93d4dea61" + }, + "0xf90754033d45778b16034a348f5757b56b8578eab5fd81bd2707a4fa43572a7f:0x0": { + "data_hash": "0x10f75e072356b123d3864ae553870a4759943c4ed71d373218ca17b611795020" + }, + "0xf99767aebf484c406de90a63140d8306eea1dbf509fb6b04f13f5594a27b4157:0x0": { + "data_hash": "0x7cda3da79fb46eaed62752444d9d4f666897861039c77bf94f0532fc43162a70" + }, + "0xfa1320af6eff6f2b2b69e30391ca3a027c259318a86dca32e3238884311b84d7:0x0": { + "data_hash": "0xa34564aa114e28106b02f63243b50f5135adc633ff7a74e735d27e9404bf0710" + }, + "0xfc01255f8d4c79d2307cbf689795022d46555c16027b3c954bc9969ec7387d81:0x0": { + "data_hash": "0xca92833ef77901845dd672a356fa9e568a3d46f179a1df5469dbbc05b7313427" + }, + "0xfe8eb1d61e9167f5864fa5b417edb0c6976b8e3729c172ac6ec50382bd634b61:0x0": { + "data_hash": "0x540be7d1575ea9e60a6df5678ec2e4dc857edb687f1ad2f8359317be2288ff60" + } + }, + "headers": { + "0x690c44e7f3605a4c984edfe17dc953047114aff5e42ae1b2f108dc042a37a34d": { + "number": "0x4a38", + "epoch": "0x708000000000b", + "timestamp": "0x19f98075357" + }, + "0x88ce0e52d92e34dad6b737cc8c81d31badc9eaf981c783b52e3082c663d48093": { + "number": "0x4d6e", + "epoch": "0x708033600000b", + "timestamp": "0x19f9807a56b" + }, + "0x8ddb85198d040fa97fc5d43e6d725e5b5ed0bf285022cc66a10e9bb1379bfecb": { + "number": "0x4d8d", + "epoch": "0x708035500000b", + "timestamp": "0x19f9807a840" + }, + "0x933f1ca9e878cbe88f51849a169762b1d11758cf7d62db67824490dfdb39d8e1": { + "number": "0x411", + "epoch": "0x7080029000001", + "timestamp": "0x19f9802b6fb" + }, + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5": { + "number": "0x0", + "epoch": "0x0", + "timestamp": "0x0" + }, + "0xb35487c7b0d7a3c1351f9bdfdf76e178b01c7f93d4cfbbb84e1d07e800090bec": { + "number": "0x3fa", + "epoch": "0x7080012000001", + "timestamp": "0x19f9802b42b" + }, + "0xe3351eef7f5486f5e5199cceb0953a762e70ff1fde6fe6419eb1b3ea1af366dd": { + "number": "0x3e8", + "epoch": "0x7080000000001", + "timestamp": "0x19f9802b205" + } + }, + "action_cases": [ + { + "name": "token.cell:mint_with_authority", + "action": "mint_with_authority", + "artifact_data_hash": "0x10f75e072356b123d3864ae553870a4759943c4ed71d373218ca17b611795020", + "initial_tx": "0xbb4544c75dd32136bfbc6eeab20b2a7ca0539d8e059d3fadfbca9393629a94e8", + "valid_tx": "0x4fd12d9427983bb4486b499152aa8b7cc9051c0e83f9baabe005a380bafbad07", + "acceptance_harness_name": "token-action-builder-v1", + "acceptance_harness_implementation": "token-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 586, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1255, + "measured_cycles": 10306, + "measured_output_capacity_shannons": [ + 20000000000, + 10000000000 + ], + "occupied_capacity_shannons": 18800000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 30000000000, + "output_count": 2, + "output_data_bytes": 40, + "output_occupied_capacity_shannons": [ + 9800000000, + 9000000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 68, + "witness_count": 1 + } + }, + { + "name": "token.cell:transfer_token", + "action": "transfer_token", + "artifact_data_hash": "0x6c3bf0d1bc162b2c71378ec3ea55d34121ab8c66480524e83cc8943de6b26555", + "initial_tx": "0x44e53c1d471b7bdfbda6816b72152f0c83550a5296edac7313b645ccde8527dc", + "valid_tx": "0x9cc87bc8882895ab82b3cc4c91c1a6da4a0831019bad2b9454fb7195d703420f", + "acceptance_harness_name": "token-action-builder-v1", + "acceptance_harness_implementation": "token-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 412, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 916, + "measured_cycles": 6901, + "measured_output_capacity_shannons": [ + 20000000000 + ], + "occupied_capacity_shannons": 9000000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 20000000000, + "output_count": 1, + "output_data_bytes": 16, + "output_occupied_capacity_shannons": [ + 9000000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 1 + } + }, + { + "name": "token.cell:burn", + "action": "burn", + "artifact_data_hash": "0x1b82b117ab4e41de9f6652a54f2f2ee24abad190b04a935cf90075cf9f3da237", + "initial_tx": "0xb629ee7fb29df995ea9fae1d02db475f878ffea6657cea91d21fe1986779692f", + "valid_tx": "0xfa2e16ceccde2faf8a2ddcd8bedd2cbdbf0438cedb74d8e2684fea9e6ad98496", + "acceptance_harness_name": "token-action-builder-v1", + "acceptance_harness_implementation": "token-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 311, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 712, + "measured_cycles": 4918, + "measured_output_capacity_shannons": [ + 10000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 10000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 28, + "witness_count": 1 + } + }, + { + "name": "token.cell:merge", + "action": "merge", + "artifact_data_hash": "0x6c976866fb9c343bd28922b92aca893f292fed889e52b75a59a10f738b31f4e7", + "initial_tx": "0x5debff60864d562bb94f7020d6a1180c7736815ee352e48c9905b920bc0cdf31", + "valid_tx": "0x19f683cc8c1d057780fae566d09ef252abb98559730bc9c17e6bebc703240968", + "acceptance_harness_name": "token-action-builder-v1", + "acceptance_harness_implementation": "token-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 464, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 2, + "json_envelope_size_bytes": 1050, + "measured_cycles": 8734, + "measured_output_capacity_shannons": [ + 30000000000 + ], + "occupied_capacity_shannons": 9000000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 30000000000, + "output_count": 1, + "output_data_bytes": 16, + "output_occupied_capacity_shannons": [ + 9000000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 2 + } + }, + { + "name": "nft.cell:create_collection", + "action": "create_collection", + "artifact_data_hash": "0xa34564aa114e28106b02f63243b50f5135adc633ff7a74e735d27e9404bf0710", + "initial_tx": "0x0fc3217536599ec792bf62408185b2a5c32103c994bd312fa19dbb7d02a957ac", + "valid_tx": "0x8729c54e1abbda37d62f0a446976f690613c634292e5e895b063547c5caa8e70", + "acceptance_harness_name": "nft-action-builder-v1", + "acceptance_harness_implementation": "nft-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 608, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1310, + "measured_cycles": 10653, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 20800000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 134, + "output_occupied_capacity_shannons": [ + 20800000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 138, + "witness_count": 1 + } + }, + { + "name": "nft.cell:mint", + "action": "mint", + "artifact_data_hash": "0x967d150b17ee92167fda6eb3b28e453cfc084f19c7cbfc773bb3f1b93d4dea61", + "initial_tx": "0x1c5ec34e3022bdcee5b055f54102be7267a807b4baf9885b20efd665ba5ccbd9", + "valid_tx": "0xf212913e057843c248eea6ca642ad7cd8c1d930865fdea6604f4588b07f5e9f4", + "acceptance_harness_name": "nft-action-builder-v1", + "acceptance_harness_implementation": "nft-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 842, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1767, + "measured_cycles": 17699, + "measured_output_capacity_shannons": [ + 30000000000, + 30000000000 + ], + "occupied_capacity_shannons": 42000000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 60000000000, + "output_count": 2, + "output_data_bytes": 272, + "output_occupied_capacity_shannons": [ + 20800000000, + 21200000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 92, + "witness_count": 1 + } + }, + { + "name": "nft.cell:transfer", + "action": "transfer", + "artifact_data_hash": "0x2ca9877f46bc3e89b31d76e256714e075ad57732d7fcd489fdc6aa636772f93d", + "initial_tx": "0x4b79a85846e54477f3689cc8fa64e3488e1f6c7da2ad02b12b4d7a623a8093f4", + "valid_tx": "0xa0d20ba71c2ee983d8b2ce0c261dad1978f0c078122ec9cf711ece0d24d6b223", + "acceptance_harness_name": "nft-action-builder-v1", + "acceptance_harness_implementation": "nft-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 534, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1162, + "measured_cycles": 15274, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 21200000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 138, + "output_occupied_capacity_shannons": [ + 21200000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 1 + } + }, + { + "name": "nft.cell:create_listing", + "action": "create_listing", + "artifact_data_hash": "0xaaf97b20c59720fa641958ba0c26eb2bfd3ed6c23444975811f4f1c08989c368", + "initial_tx": "0xebdaef4d2108c25778301a3237fbdcf71a260c384e1d8d3f738d16b65d7a66b8", + "valid_tx": "0xd12b75240c9745693c87a94242cc383e3f4facb87b3d5a0e23a9f4e8242d9c5c", + "acceptance_harness_name": "nft-action-builder-v1", + "acceptance_harness_implementation": "nft-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 3, + "consensus_serialized_tx_size_bytes": 620, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 1378, + "measured_cycles": 10434, + "measured_output_capacity_shannons": [ + 30000000000, + 70000000000 + ], + "occupied_capacity_shannons": 20500000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 2, + "output_data_bytes": 89, + "output_occupied_capacity_shannons": [ + 16400000000, + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 36, + "witness_count": 1 + } + }, + { + "name": "nft.cell:cancel_listing", + "action": "cancel_listing", + "artifact_data_hash": "0x30999ca44c3638eceddd4d707a01242294bd06473c9e34f92f5cdea23c2bdb75", + "initial_tx": "0xb81a922893475d9f7bb43877d52d7b7c15c1bc1db63a4cbdc5aa0a5d08abf784", + "valid_tx": "0xe60611e6f8611fb019ebb0dac2c76cfa5081ef8d05ae8b57c44c3e887cd656dd", + "acceptance_harness_name": "nft-action-builder-v1", + "acceptance_harness_implementation": "nft-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 311, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 712, + "measured_cycles": 4723, + "measured_output_capacity_shannons": [ + 30000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 30000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 28, + "witness_count": 1 + } + }, + { + "name": "nft.cell:buy_from_listing", + "action": "buy_from_listing", + "artifact_data_hash": "0xd305deeaaf5715086b9ca367189279b7709450c3df4a6b73620f841578e1195f", + "initial_tx": "0x4cc2653d8451a590de947172988ec164e7bf5a4fe457fdb4e2908b77e66c9d6d", + "valid_tx": "0x536a1329df3e98119af6bc48f9b8894650d7c85a852a37ae461fc28cd59ea098", + "acceptance_harness_name": "nft-action-builder-v1", + "acceptance_harness_implementation": "nft-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 1009, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 4, + "json_envelope_size_bytes": 2183, + "measured_cycles": 31755, + "measured_output_capacity_shannons": [ + 100000000000, + 20000000000, + 20000000000 + ], + "occupied_capacity_shannons": 39500000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 140000000000, + "output_count": 3, + "output_data_bytes": 170, + "output_occupied_capacity_shannons": [ + 21300000000, + 9100000000, + 9100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 4 + } + }, + { + "name": "nft.cell:create_offer", + "action": "create_offer", + "artifact_data_hash": "0xb961548b7fb156288df5e3c472a626d1791f0753906579980f1c75f1069698a1", + "initial_tx": "0x40461a7a9e89d6574d81d1c496cd2ada9191d4a10684f6df1a632805c20fa75d", + "valid_tx": "0x1db48d9dedbc8fbf3feb809f32e63986b8e07ebe639b8dae8a2c7f9ed0134ba1", + "acceptance_harness_name": "nft-action-builder-v1", + "acceptance_harness_implementation": "nft-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 590, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 1276, + "measured_cycles": 10188, + "measured_output_capacity_shannons": [ + 30000000000 + ], + "occupied_capacity_shannons": 17200000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 30000000000, + "output_count": 1, + "output_data_bytes": 97, + "output_occupied_capacity_shannons": [ + 17200000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 124, + "witness_count": 1 + } + }, + { + "name": "nft.cell:accept_offer", + "action": "accept_offer", + "artifact_data_hash": "0x9c1a6fed0b778db0b8006116e9d9a6213d2b5dadf1ac391118790c44a9ffe275", + "initial_tx": "0x43aa34c172d01d43c427650825c520f05f0775b6691bb9448488bd542475db62", + "valid_tx": "0xe9f918cc4cd4842ac8cc6f54c0bd1c2158ceb0105d1b71b97c22524acef3e33f", + "acceptance_harness_name": "nft-action-builder-v1", + "acceptance_harness_implementation": "nft-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 1009, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 4, + "json_envelope_size_bytes": 2187, + "measured_cycles": 30706, + "measured_output_capacity_shannons": [ + 100000000000, + 20000000000, + 20000000000 + ], + "occupied_capacity_shannons": 39500000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 140000000000, + "output_count": 3, + "output_data_bytes": 170, + "output_occupied_capacity_shannons": [ + 21300000000, + 9100000000, + 9100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 28, + "witness_count": 4 + } + }, + { + "name": "nft.cell:burn", + "action": "burn", + "artifact_data_hash": "0xafc1309eef287471f5feb8e01a6bad53624851301ed7e5117b803290c2362c80", + "initial_tx": "0x264f70265a964d3719a86579311a2f1adb0b6de22fcf524908c46b500dca2770", + "valid_tx": "0x09a58ddb0bb12c02eb2ca45b0d43f56eb40ebbd1a58fe5224831bb01d8f394f1", + "acceptance_harness_name": "nft-action-builder-v1", + "acceptance_harness_implementation": "nft-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 311, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 713, + "measured_cycles": 4739, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 28, + "witness_count": 1 + } + }, + { + "name": "nft.cell:batch_mint", + "action": "batch_mint", + "artifact_data_hash": "0x0a4cb03cd7e44ca4449ded0f8c6014c0f919819072badeb07bd51e0add611f35", + "initial_tx": "0xab0f22960f33ac447c3075073190a7127930d337a090125a7279a8544f3d28d5", + "valid_tx": "0xb97f4c75e0015d8deae35de3cc121201aae47742a6e57687c1c8b5049796759c", + "acceptance_harness_name": "nft-action-builder-v1", + "acceptance_harness_implementation": "nft-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 1879, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 3816, + "measured_cycles": 42230, + "measured_output_capacity_shannons": [ + 100000000000, + 25000000000, + 25000000000, + 25000000000, + 25000000000 + ], + "occupied_capacity_shannons": 106100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 200000000000, + "output_count": 5, + "output_data_bytes": 686, + "output_occupied_capacity_shannons": [ + 20900000000, + 21300000000, + 21300000000, + 21300000000, + 21300000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 284, + "witness_count": 1 + } + }, + { + "name": "timelock.cell:create_absolute_lock", + "action": "create_absolute_lock", + "artifact_data_hash": "0xc1e7dce634480aceedc7bd5dfbb7df1e5951cd945fb0d10cb8ea70968af0443b", + "initial_tx": "0xd427ffbf8a602f10b6b1c845f50683fbc468d636c9749080b653c671827e22dd", + "valid_tx": "0x10a51089ec0c33634bb2ed3d14f89840602faf9866fea6d2d4e51b5d7cf98b07", + "acceptance_harness_name": "timelock-action-builder-v1", + "acceptance_harness_implementation": "timelock-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 549, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 1195, + "measured_cycles": 8253, + "measured_output_capacity_shannons": [ + 30000000000 + ], + "occupied_capacity_shannons": 15500000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 30000000000, + "output_count": 1, + "output_data_bytes": 81, + "output_occupied_capacity_shannons": [ + 15500000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 100, + "witness_count": 1 + } + }, + { + "name": "timelock.cell:create_relative_lock", + "action": "create_relative_lock", + "artifact_data_hash": "0x828a52262139378ce50e40ea7a24d4e8d8219cb2b9ae8f0e2a2cde2a8712a546", + "initial_tx": "0x349aa74d03c45384b56f8dbc5aef108759d23116eeba59a92774cfc08682bf67", + "valid_tx": "0x6f6ed0c878e8dd8d80724a1b65adbc3ff9509f1727f2432790be4d5aebafb7ff", + "acceptance_harness_name": "timelock-action-builder-v1", + "acceptance_harness_implementation": "timelock-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 549, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 1195, + "measured_cycles": 8279, + "measured_output_capacity_shannons": [ + 30000000000 + ], + "occupied_capacity_shannons": 15500000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 30000000000, + "output_count": 1, + "output_data_bytes": 81, + "output_occupied_capacity_shannons": [ + 15500000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 100, + "witness_count": 1 + } + }, + { + "name": "timelock.cell:lock_asset", + "action": "lock_asset", + "artifact_data_hash": "0xca92833ef77901845dd672a356fa9e568a3d46f179a1df5469dbbc05b7313427", + "initial_tx": "0x8cfc356815eccbcae35af59d0034cdda7af6094bb3a6e95e1b89b799f5d4cca5", + "valid_tx": "0xe795d5d96599dbae3f86bf88419c29ea037bd479b9339acb1fa189f5ebd5d259", + "acceptance_harness_name": "timelock-action-builder-v1", + "acceptance_harness_implementation": "timelock-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 3, + "consensus_serialized_tx_size_bytes": 539, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1212, + "measured_cycles": 8660, + "measured_output_capacity_shannons": [ + 30000000000, + 70000000000 + ], + "occupied_capacity_shannons": 16400000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 2, + "output_data_bytes": 48, + "output_occupied_capacity_shannons": [ + 12300000000, + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 28, + "witness_count": 1 + } + }, + { + "name": "timelock.cell:request_release", + "action": "request_release", + "artifact_data_hash": "0xc13b702287bf01246fd600189482d58fb1e8bdadce497124858e010750a4b4cd", + "initial_tx": "0x2b6f560e02fd1d710f9e47d3fb9771d37ae3b88362ebca48a822e3bee8e318f8", + "valid_tx": "0x352b275582f167c4a2332d05c5bab89ffb39f2053dcc899f5d42f57a9f075234", + "acceptance_harness_name": "timelock-action-builder-v1", + "acceptance_harness_implementation": "timelock-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 3, + "consensus_serialized_tx_size_bytes": 628, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 1394, + "measured_cycles": 10961, + "measured_output_capacity_shannons": [ + 30000000000, + 70000000000 + ], + "occupied_capacity_shannons": 18900000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 2, + "output_data_bytes": 73, + "output_occupied_capacity_shannons": [ + 14800000000, + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 1 + } + }, + { + "name": "timelock.cell:request_emergency_release", + "action": "request_emergency_release", + "artifact_data_hash": "0x734c3177a05355d83a3be6c68309e377cfa444fc62d5d6504b3664ea090e9b02", + "initial_tx": "0x33611509a83b78a19cf2ce0980216ef2aa22a359ffb7d07bf31fbd73c339444c", + "valid_tx": "0xb112c9cde54c7772d740ce548093c97278a1c295fd05a60d2999dbab9ef7186c", + "acceptance_harness_name": "timelock-action-builder-v1", + "acceptance_harness_implementation": "timelock-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 3, + "consensus_serialized_tx_size_bytes": 706, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 1550, + "measured_cycles": 12910, + "measured_output_capacity_shannons": [ + 30000000000, + 70000000000 + ], + "occupied_capacity_shannons": 24200000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 2, + "output_data_bytes": 126, + "output_occupied_capacity_shannons": [ + 20100000000, + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 85, + "witness_count": 1 + } + }, + { + "name": "timelock.cell:approve_emergency_release", + "action": "approve_emergency_release", + "artifact_data_hash": "0x93f05f4fb67225c694ab2cb4c4b57125136e08fcff490458f42fa63e26086bdd", + "initial_tx": "0x740f68ba912a942022541741c57d332680fb0d6d73fece3763ce2f1a1a4d0628", + "valid_tx": "0xc8a5c0e66095d60b6955962dd47327012167b91791b6f762684de515b9c1354e", + "acceptance_harness_name": "timelock-action-builder-v1", + "acceptance_harness_implementation": "timelock-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 587, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1268, + "measured_cycles": 13061, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 26500000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 190, + "output_occupied_capacity_shannons": [ + 26500000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 1 + } + }, + { + "name": "timelock.cell:extend_lock", + "action": "extend_lock", + "artifact_data_hash": "0x078a625eb933f34dee98290d89c9cd6b57ab95d8bb1a89f254f422649a972954", + "initial_tx": "0x7ebc3eee04e3daaf2d17874dcb156ce493376dfb56327782ad398567d2d154ee", + "valid_tx": "0x9c2c24f15cb3583f2f36a4bf4febc0fed09c369a71f5c3cd2148e206b8d788ee", + "acceptance_harness_name": "timelock-action-builder-v1", + "acceptance_harness_implementation": "timelock-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 517, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 1132, + "measured_cycles": 13726, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 15500000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 81, + "output_occupied_capacity_shannons": [ + 15500000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 68, + "witness_count": 1 + } + }, + { + "name": "timelock.cell:execute_release", + "action": "execute_release", + "artifact_data_hash": "0xc5eb6cb03878c77e75b8462c561205189574fc4677e5538cc9136c10ab9921da", + "initial_tx": "0x100622e71992fc7f45b3e46ff83c5f30748a75144c05ed015a39acaa2aefe84e", + "valid_tx": "0xd5fa5dcfd1dc5ac7749aac58e2ccc70953e8e86adcbbd315e7d28eac991c6bbc", + "acceptance_harness_name": "timelock-action-builder-v1", + "acceptance_harness_implementation": "timelock-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 764, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 3, + "json_envelope_size_bytes": 1676, + "measured_cycles": 23303, + "measured_output_capacity_shannons": [ + 30000000000, + 30000000000 + ], + "occupied_capacity_shannons": 23800000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 60000000000, + "output_count": 2, + "output_data_bytes": 88, + "output_occupied_capacity_shannons": [ + 9100000000, + 14700000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 3 + } + }, + { + "name": "timelock.cell:execute_emergency_release", + "action": "execute_emergency_release", + "artifact_data_hash": "0xa80dc0eca06d915541974ed828b671ef3583818828c8fd86aaf8922a8d282f64", + "initial_tx": "0x31aa013f1e5e95cef0f8a5cc0a4dc6c069ad72d6a2989a7aa02e13c7d30576c8", + "valid_tx": "0x5e9cccf3c3feeef58ad7e21e3b611b765752e45370870fbdcb2363fe645e714d", + "acceptance_harness_name": "timelock-action-builder-v1", + "acceptance_harness_implementation": "timelock-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 764, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 3, + "json_envelope_size_bytes": 1676, + "measured_cycles": 23092, + "measured_output_capacity_shannons": [ + 30000000000, + 30000000000 + ], + "occupied_capacity_shannons": 23800000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 60000000000, + "output_count": 2, + "output_data_bytes": 88, + "output_occupied_capacity_shannons": [ + 9100000000, + 14700000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 3 + } + }, + { + "name": "timelock.cell:batch_create_locks", + "action": "batch_create_locks", + "artifact_data_hash": "0x2eb610fca034a18303d192bcbf52ba0f68e6ee8b1cafa90b200d5edad55257ef", + "initial_tx": "0xd6d214048b0d486197d309dcd9317e52a42e3e20cc4f049c4e87281d1f5a6d5a", + "valid_tx": "0xdf6318fdf0dc8c8103363dc325dff2676b363c85405355debbdd24a41e424998", + "acceptance_harness_name": "timelock-action-builder-v1", + "acceptance_harness_implementation": "timelock-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 1434, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 2938, + "measured_cycles": 22840, + "measured_output_capacity_shannons": [ + 30000000000, + 30000000000, + 30000000000, + 30000000000 + ], + "occupied_capacity_shannons": 62000000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 120000000000, + "output_count": 4, + "output_data_bytes": 324, + "output_occupied_capacity_shannons": [ + 15500000000, + 15500000000, + 15500000000, + 15500000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 316, + "witness_count": 1 + } + }, + { + "name": "multisig.cell:create_wallet", + "action": "create_wallet", + "artifact_data_hash": "0x67c60bfc34958c28b1d5277be6995af9920d480f60ded40806eaf173c704d10d", + "initial_tx": "0x0cba9e700c8a662884ab05066a027cc22b03e79ac0facfcbe0d002acd391bc7f", + "valid_tx": "0xb74d691e2b3b09ba70b33cae3a78c04ab723fede031598d5ebbb30f3f79c8442", + "acceptance_harness_name": "multisig-action-builder-v1", + "acceptance_harness_implementation": "multisig-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 619, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1332, + "measured_cycles": 10500, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 21600000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 141, + "output_occupied_capacity_shannons": [ + 21600000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 141, + "witness_count": 1 + } + }, + { + "name": "multisig.cell:propose_transfer", + "action": "propose_transfer", + "artifact_data_hash": "0x540be7d1575ea9e60a6df5678ec2e4dc857edb687f1ad2f8359317be2288ff60", + "initial_tx": "0xe2caa37ca2e0591eb4662a9c263d9bbd2fb0c1717253b0e909e24658f5d5dbf9", + "valid_tx": "0x5fae038da17633b4994474ccfab8cd4769b9670ca984573a047d8e73fa1321f9", + "acceptance_harness_name": "multisig-action-builder-v1", + "acceptance_harness_implementation": "multisig-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 920, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1925, + "measured_cycles": 20931, + "measured_output_capacity_shannons": [ + 70000000000, + 30000000000 + ], + "occupied_capacity_shannons": 48200000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 2, + "output_data_bytes": 332, + "output_occupied_capacity_shannons": [ + 21600000000, + 26600000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 108, + "witness_count": 1 + } + }, + { + "name": "multisig.cell:record_approval", + "action": "record_approval", + "artifact_data_hash": "0xb5e6f61a513204507f6cf508cbc23b449a60d91ef6ebdfde79fcf886bf4cb020", + "initial_tx": "0xc10773a7ba18c8d32e4deab978518e5d1f4665d833b6361ccc306d5382387f7a", + "valid_tx": "0xc962300d035f0d3e1a401d57d0420ee6e984c4cabc0da7cf8beae28bb3b0c040", + "acceptance_harness_name": "multisig-action-builder-v1", + "acceptance_harness_implementation": "multisig-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 3, + "consensus_serialized_tx_size_bytes": 888, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1913, + "measured_cycles": 24951, + "measured_output_capacity_shannons": [ + 60000000000, + 30000000000 + ], + "occupied_capacity_shannons": 45300000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 90000000000, + "output_count": 2, + "output_data_bytes": 303, + "output_occupied_capacity_shannons": [ + 33000000000, + 12300000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 68, + "witness_count": 1 + } + }, + { + "name": "multisig.cell:propose_add_signer", + "action": "propose_add_signer", + "artifact_data_hash": "0x204920209dab2137b0f75335063856cc1e84d28b41d1e31c2160614832de50ec", + "initial_tx": "0x402005d53808fc439e58003f9ffecbc77ab2cf229dbf93bff3888221c05c37cd", + "valid_tx": "0x73908c7815c16a3a45f876d8695355d173f8d1ab68c8b7e74d2bd6d398d440ae", + "acceptance_harness_name": "multisig-action-builder-v1", + "acceptance_harness_implementation": "multisig-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 944, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1973, + "measured_cycles": 22313, + "measured_output_capacity_shannons": [ + 70000000000, + 30000000000 + ], + "occupied_capacity_shannons": 51400000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 2, + "output_data_bytes": 364, + "output_occupied_capacity_shannons": [ + 21600000000, + 29800000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 100, + "witness_count": 1 + } + }, + { + "name": "multisig.cell:propose_remove_signer", + "action": "propose_remove_signer", + "artifact_data_hash": "0x7cda3da79fb46eaed62752444d9d4f666897861039c77bf94f0532fc43162a70", + "initial_tx": "0x0495a8b89852af2a653540ebe699453d04134738225430a174119ef7db3fa047", + "valid_tx": "0xc390427394790da202c9564f872551a36bcee7747e19fcc58d0eac765d7bbaae", + "acceptance_harness_name": "multisig-action-builder-v1", + "acceptance_harness_implementation": "multisig-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 912, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1909, + "measured_cycles": 21975, + "measured_output_capacity_shannons": [ + 70000000000, + 30000000000 + ], + "occupied_capacity_shannons": 48200000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 2, + "output_data_bytes": 332, + "output_occupied_capacity_shannons": [ + 21600000000, + 26600000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 100, + "witness_count": 1 + } + }, + { + "name": "multisig.cell:propose_change_threshold", + "action": "propose_change_threshold", + "artifact_data_hash": "0x9da2791f3f0a46790e4b187a1d1dd15813f2c14883138631255c58b495a845d8", + "initial_tx": "0x1213c894962b0b93e2fd49eb38ba5121b5df709d1d78ee888b4a060534f99ab9", + "valid_tx": "0xdfb65c7699a692c39bdba73ec0647d99c56398310465d1db372c8a63369c0c93", + "acceptance_harness_name": "multisig-action-builder-v1", + "acceptance_harness_implementation": "multisig-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 882, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1849, + "measured_cycles": 20324, + "measured_output_capacity_shannons": [ + 70000000000, + 30000000000 + ], + "occupied_capacity_shannons": 48300000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 2, + "output_data_bytes": 333, + "output_occupied_capacity_shannons": [ + 21600000000, + 26700000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 69, + "witness_count": 1 + } + }, + { + "name": "multisig.cell:execute_proposal", + "action": "execute_proposal", + "artifact_data_hash": "0xee0a4d051aeb5c2c39df4eeeda6a55b8cb9ea79a964b905d3288c0d83138fabc", + "initial_tx": "0x014f954ce1b5dcd67562a3094bda3060e3807bd071a5da61a107a3c4c02716e2", + "valid_tx": "0xf18073c9dd4436dfca5146f8b7aac0e4bfe4398b8b6f91f1727873aeef202c9d", + "acceptance_harness_name": "multisig-action-builder-v1", + "acceptance_harness_implementation": "multisig-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 3, + "consensus_serialized_tx_size_bytes": 491, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1128, + "measured_cycles": 12997, + "measured_output_capacity_shannons": [ + 20000000000 + ], + "occupied_capacity_shannons": 12400000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 20000000000, + "output_count": 1, + "output_data_bytes": 49, + "output_occupied_capacity_shannons": [ + 12400000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 68, + "witness_count": 1 + } + }, + { + "name": "multisig.cell:cancel_proposal", + "action": "cancel_proposal", + "artifact_data_hash": "0xf51e1060b13ba495ab2cac42ab5102d7ff6bb2c00df50f115846b5fcc9429298", + "initial_tx": "0x96729af7693292d31397d4ebdf3624d743424a305cb8c4f7c6bdc5ad63111c9e", + "valid_tx": "0xf222cd329af79ea45c70e20dca573edbfb9d93769d15c1cf06d0c6d30f572804", + "acceptance_harness_name": "multisig-action-builder-v1", + "acceptance_harness_implementation": "multisig-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 3, + "consensus_serialized_tx_size_bytes": 380, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 903, + "measured_cycles": 9378, + "measured_output_capacity_shannons": [ + 49000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 49000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 1 + } + }, + { + "name": "vesting.cell:create_vesting_config", + "action": "create_vesting_config", + "artifact_data_hash": "0xc6c1fd22456162ae2457a61bb7fd4f0ad0ac92955adbc6a8da10a0caf1900362", + "initial_tx": "0x15da87cfff34c6bfc7a7012177b4845fbdd717f50dd145d2772678981f5a14e4", + "valid_tx": "0xba87194e3b5862bb583ac228ca3b79b0230c2667d71c095fb5c8e33f375c4006", + "acceptance_harness_name": "vesting-action-builder-v1", + "acceptance_harness_implementation": "vesting-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 479, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1050, + "measured_cycles": 7798, + "measured_output_capacity_shannons": [ + 30000000000 + ], + "occupied_capacity_shannons": 13200000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 30000000000, + "output_count": 1, + "output_data_bytes": 57, + "output_occupied_capacity_shannons": [ + 13200000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 85, + "witness_count": 1 + } + }, + { + "name": "vesting.cell:grant_vesting", + "action": "grant_vesting", + "artifact_data_hash": "0xdebeeebed4b050592aeeda4cd26e530632ce8236e4a0481c150ee8264cfcdffb", + "initial_tx": "0x403c6468185cda532c013187efc8a8037ddab784faa77ffa2f598d8aac72eb71", + "valid_tx": "0xec6798ce41a5f6e605ff145156155055fa3de7d9d3ae339a7a362f91fdc060c9", + "acceptance_harness_name": "vesting-action-builder-v1", + "acceptance_harness_implementation": "vesting-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 3, + "consensus_serialized_tx_size_bytes": 681, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 2, + "json_envelope_size_bytes": 1540, + "measured_cycles": 12327, + "measured_output_capacity_shannons": [ + 30000000000, + 118011109343 + ], + "occupied_capacity_shannons": 19800000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 148011109343, + "output_count": 2, + "output_data_bytes": 81, + "output_occupied_capacity_shannons": [ + 15700000000, + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 1 + } + }, + { + "name": "vesting.cell:claim_vested", + "action": "claim_vested", + "artifact_data_hash": "0xcc321278301291afc3c43ae43d5e3e3c10ce9cf0658c3063587b0123cdce7cef", + "initial_tx": "0xb7978d739c6d861ab1902226dbc51bfad44edf6dcfdee1dc303f50606f4c62ea", + "valid_tx": "0x328af8fa27cee70d6009d30c6b9ce494b1cd01e8f30f36e7c0e8b1031056850b", + "acceptance_harness_name": "vesting-action-builder-v1", + "acceptance_harness_implementation": "vesting-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 637, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 1362, + "measured_cycles": 18801, + "measured_output_capacity_shannons": [ + 20000000000, + 20000000000 + ], + "occupied_capacity_shannons": 24700000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 40000000000, + "output_count": 2, + "output_data_bytes": 97, + "output_occupied_capacity_shannons": [ + 9100000000, + 15600000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 28, + "witness_count": 1 + } + }, + { + "name": "vesting.cell:claim_fully_vested", + "action": "claim_fully_vested", + "artifact_data_hash": "0x318333f71531adb7109813cd89f757d76d7a1a8aebb79e93d800df8f4f0bc3c3", + "initial_tx": "0x4a2f379980234301b6755cdb05bbcf5d46f407f31a8fe7228bf2cce0c29cbc7c", + "valid_tx": "0x2ad1120afda308f8aabe45f3ace721125f268138917137a0e2681c435e47b6c6", + "acceptance_harness_name": "vesting-action-builder-v1", + "acceptance_harness_implementation": "vesting-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 637, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 1362, + "measured_cycles": 12869, + "measured_output_capacity_shannons": [ + 20000000000, + 20000000000 + ], + "occupied_capacity_shannons": 24700000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 40000000000, + "output_count": 2, + "output_data_bytes": 97, + "output_occupied_capacity_shannons": [ + 9100000000, + 15600000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 28, + "witness_count": 1 + } + }, + { + "name": "vesting.cell:revoke_grant", + "action": "revoke_grant", + "artifact_data_hash": "0x7902ead8098947fb0d9cf87ed357d821d21c0e85505621627fbd81d873290140", + "initial_tx": "0x515a67864ac1959d9cd4fa108ba421b195a4410f1878832bd01208b5d86e7fcd", + "valid_tx": "0xad8a03597cf6aeceb16aa4a16ccc2828bcbd49b1aa2861b5fa01440d6fe803e3", + "acceptance_harness_name": "vesting-action-builder-v1", + "acceptance_harness_implementation": "vesting-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 3, + "consensus_serialized_tx_size_bytes": 650, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 1428, + "measured_cycles": 15427, + "measured_output_capacity_shannons": [ + 20000000000, + 20000000000 + ], + "occupied_capacity_shannons": 18300000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 40000000000, + "output_count": 2, + "output_data_bytes": 32, + "output_occupied_capacity_shannons": [ + 9200000000, + 9100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 2 + } + }, + { + "name": "amm_pool.cell:seed_pool", + "action": "seed_pool", + "artifact_data_hash": "0x13814f1b2b6fa776385ebf4a63ee7ec94ebc806957ac34dc565d573e7f9b0f4f", + "initial_tx": "0x8b85a45b4b3c0da4633ca155697290d15ec99bb27068b1a2f611d49214869704", + "valid_tx": "0xd8e30c66d1da8a5af3a43c6e7514e691948b6aea907874ed80858eaae0201caf", + "acceptance_harness_name": "amm-action-builder-v1", + "acceptance_harness_implementation": "amm-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 773, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 2, + "json_envelope_size_bytes": 1658, + "measured_cycles": 21009, + "measured_output_capacity_shannons": [ + 20000000000, + 20000000000 + ], + "occupied_capacity_shannons": 32900000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 40000000000, + "output_count": 2, + "output_data_bytes": 178, + "output_occupied_capacity_shannons": [ + 18100000000, + 14800000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 62, + "witness_count": 2 + } + }, + { + "name": "amm_pool.cell:add_liquidity", + "action": "add_liquidity", + "artifact_data_hash": "0xee46f625f63eaccefbc7c9bf1393a21295d1d21e712fc704523d6f349a39ca26", + "initial_tx": "0x3682c9124a707c7882a88d8e30916895d83e6989ee0538f9f4e3393cbed1c523", + "valid_tx": "0x1535a036000b44931b13d0b9248ff807dcb50831b85c97aa6a6bb54e454c3d39", + "acceptance_harness_name": "amm-action-builder-v1", + "acceptance_harness_implementation": "amm-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 823, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 3, + "json_envelope_size_bytes": 1789, + "measured_cycles": 35100, + "measured_output_capacity_shannons": [ + 40000000000, + 20000000000 + ], + "occupied_capacity_shannons": 32900000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 60000000000, + "output_count": 2, + "output_data_bytes": 178, + "output_occupied_capacity_shannons": [ + 18100000000, + 14800000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 3 + } + }, + { + "name": "amm_pool.cell:swap_a_for_b", + "action": "swap_a_for_b", + "artifact_data_hash": "0xa3bdc8e44991a1790d469db884797763ce5dd7ef8631b77f45c2d925633dbbd9", + "initial_tx": "0x899bac1c9e02a9ca6cd47386e6fd5470d06ba1348a018b2acd8d3d0bbde2bade", + "valid_tx": "0xb83abaee23854733da3a985f90440de3c831022c65e128ae2b0b1c0b2ca82850", + "acceptance_harness_name": "amm-action-builder-v1", + "acceptance_harness_implementation": "amm-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 723, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 2, + "json_envelope_size_bytes": 1559, + "measured_cycles": 34234, + "measured_output_capacity_shannons": [ + 40000000000, + 20000000000 + ], + "occupied_capacity_shannons": 27300000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 60000000000, + "output_count": 2, + "output_data_bytes": 122, + "output_occupied_capacity_shannons": [ + 18100000000, + 9200000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 68, + "witness_count": 2 + } + }, + { + "name": "amm_pool.cell:remove_liquidity", + "action": "remove_liquidity", + "artifact_data_hash": "0x513c43aa3e994cbdf9bcd7903f1e220882873ad830b3eb024cdeefe2ca3afcb3", + "initial_tx": "0x95cf642ae48c517930fe6e3c737fd3981c2f910242f22d5dee7ab53f44c20d9f", + "valid_tx": "0xcaf1e3fead81946aed95a54b532f739b4c68b6fbae7165a5f1ff919c8f8b3756", + "acceptance_harness_name": "amm-action-builder-v1", + "acceptance_harness_implementation": "amm-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 875, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 2, + "json_envelope_size_bytes": 1853, + "measured_cycles": 33668, + "measured_output_capacity_shannons": [ + 40000000000, + 20000000000, + 20000000000 + ], + "occupied_capacity_shannons": 36500000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 80000000000, + "output_count": 3, + "output_data_bytes": 138, + "output_occupied_capacity_shannons": [ + 18100000000, + 9200000000, + 9200000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 2 + } + }, + { + "name": "launch.cell:launch_token", + "action": "launch_token", + "artifact_data_hash": "0x450a496151c4111710cd56ffe7558b63b2a5e22e0ad7fd33edcf52173121c440", + "initial_tx": "0x8e884dba5cfbc95c6e63d17866f4568b4fb2f28003b49d51d98ce98042d5c3b1", + "valid_tx": "0x1d50df7be4e0dc58deab8432cbcc769e2d8d00ff832dd9f6c55522f40c9360ed", + "acceptance_harness_name": "launch-action-builder-v1", + "acceptance_harness_implementation": "launch-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 1882, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 3786, + "measured_cycles": 43676, + "measured_output_capacity_shannons": [ + 40000000000, + 20000000000, + 20000000000, + 20000000000, + 20000000000, + 40000000000, + 20000000000, + 20000000000 + ], + "occupied_capacity_shannons": 89000000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 200000000000, + "output_count": 8, + "output_data_bytes": 282, + "output_occupied_capacity_shannons": [ + 10000000000, + 9200000000, + 9200000000, + 9200000000, + 9200000000, + 18200000000, + 14800000000, + 9200000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 254, + "witness_count": 1 + } + }, + { + "name": "launch.cell:bootstrap_token", + "action": "bootstrap_token", + "artifact_data_hash": "0xb6d00cb658e0732961e4c25b5322160074ac41e52182067bc326353930063479", + "initial_tx": "0x189e7a1b87b0df7c8fb2117191bb6fd6fa7f33b4aac9f4a53edc5ca5cb913a6c", + "valid_tx": "0xc1027a7241ef72189a265f99ee6df274cffd53983ff85f0fc6e51b3372bb47a7", + "acceptance_harness_name": "launch-action-builder-v1", + "acceptance_harness_implementation": "launch-action-builder-v1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 1006, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 2074, + "measured_cycles": 16332, + "measured_output_capacity_shannons": [ + 40000000000, + 20000000000, + 20000000000, + 20000000000 + ], + "occupied_capacity_shannons": 37600000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 4, + "output_data_bytes": 72, + "output_occupied_capacity_shannons": [ + 10000000000, + 9200000000, + 9200000000, + 9200000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 164, + "witness_count": 1 + } + } + ], + "lock_cases": [ + { + "name": "nft.cell:nft_ownership", + "example": "nft.cell", + "lock": "nft_ownership", + "artifact_data_hash": "0x18758b10cc53dcf2fcd775462ec3ad8052ca19f21158a315eedc926cd085d520", + "acceptance_harness_name": "cellscript-lock-spend-matrix-builder-v1", + "acceptance_harness_implementation": "builder-backed-local-ckb-lock-spend-matrix", + "valid_create_tx": "0xc0ca46dcc6d0c1c6ba5944656d1e5650b877f934911cb540e2b52a1f191f85b9", + "valid_tx": "0xb492fefbdce3c5a93e58b60643f9b3f851703401e75a5f6070e6e016bb1b6e48", + "invalid_create_tx": "0x69d4ed19143215adfaec3dd6a0030b59200a21d8931079cd8504c0726bbe866c", + "invalid_tx": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xf0f807aecb16aaf7820fdb2c70d719912dd02d8274f76343fc42eebdbc3bcc02" + } + } + ], + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x69d4ed19143215adfaec3dd6a0030b59200a21d8931079cd8504c0726bbe866c" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631003333333333333333333333333333333333333333333333333333333333333333" + ] + }, + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 343, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 776, + "measured_cycles": 5119, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 1 + } + }, + { + "name": "nft.cell:listing_seller", + "example": "nft.cell", + "lock": "listing_seller", + "artifact_data_hash": "0x6d3f4ff4a0012611cfad302ba4de186169643407d6fb5b302cbd11183f5b280e", + "acceptance_harness_name": "cellscript-lock-spend-matrix-builder-v1", + "acceptance_harness_implementation": "builder-backed-local-ckb-lock-spend-matrix", + "valid_create_tx": "0x1f7b418973fc0723338fc7f2ff45fbd774b0afe291bca5e12e501388fc0d1f8a", + "valid_tx": "0x67be331af3ce7812f3b9acc4dd3f4e6fdda26bce7daaf7e97250aa7a018214ce", + "invalid_create_tx": "0x71ebb2e3086c8a436787a22c176235cc45e7b75779ba47d34bd390f41b4b9af5", + "invalid_tx": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xac7777ae99467a32a740f9f89776f0a5c1a17808c49e29e69a1b193d9f751e1b" + } + } + ], + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x71ebb2e3086c8a436787a22c176235cc45e7b75779ba47d34bd390f41b4b9af5" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631003333333333333333333333333333333333333333333333333333333333333333" + ] + }, + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 343, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 776, + "measured_cycles": 5103, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 1 + } + }, + { + "name": "nft.cell:offer_buyer", + "example": "nft.cell", + "lock": "offer_buyer", + "artifact_data_hash": "0xe47eaf52f41e951f3eb7ae9bfd1173ca26b884ecff101bfa931b5a75ddf1be70", + "acceptance_harness_name": "cellscript-lock-spend-matrix-builder-v1", + "acceptance_harness_implementation": "builder-backed-local-ckb-lock-spend-matrix", + "valid_create_tx": "0x58389e2aa2f07b207b69c854bb471cbfa9e980b3fb0f5c5acde5d16fe4163f05", + "valid_tx": "0xd097c95c41a0970b66c253c9abe6b3f276282b2a8f6126dda3cf18494340b2c3", + "invalid_create_tx": "0x39d8620d7cdab5fe38e1f273955366ec78088bb03ca244c8e652ca01eda72ffd", + "invalid_tx": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x1e601e8f4a43db4216118fd235a8c21841e611d4d32208c6f8745d6ff5049d74" + } + } + ], + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x39d8620d7cdab5fe38e1f273955366ec78088bb03ca244c8e652ca01eda72ffd" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631003333333333333333333333333333333333333333333333333333333333333333" + ] + }, + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 343, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 776, + "measured_cycles": 5111, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 1 + } + }, + { + "name": "nft.cell:valid_royalty", + "example": "nft.cell", + "lock": "valid_royalty", + "artifact_data_hash": "0x2f95e743beef860d851bf2277fc56c036c5be0b956db68627791b404b45067e9", + "acceptance_harness_name": "cellscript-lock-spend-matrix-builder-v1", + "acceptance_harness_implementation": "builder-backed-local-ckb-lock-spend-matrix", + "valid_create_tx": "0xa0ad32ea6682bbe84f394ed740b42c4412c497273adb2955e6cbbba2d6d25137", + "valid_tx": "0xd816ab4444154f8550b94676b6195160a7a09d0ba5d9d42ccee11c4d34370f1e", + "invalid_create_tx": "0xe36126e163f21a6ca37cad04416acdee8215a45efb9627b21209c0d73f8e70aa", + "invalid_tx": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xef6eddcdb6a4500202839de5d3929cdcb7ade4274dee72021b5612314ad01235" + } + } + ], + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xe36126e163f21a6ca37cad04416acdee8215a45efb9627b21209c0d73f8e70aa" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 311, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 712, + "measured_cycles": 2557, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 28, + "witness_count": 1 + } + }, + { + "name": "nft.cell:collection_creator", + "example": "nft.cell", + "lock": "collection_creator", + "artifact_data_hash": "0x87c083fadb4ec6e5823e9bd76b000f7b98f4b374cea82ed6a528915317d8a59e", + "acceptance_harness_name": "cellscript-lock-spend-matrix-builder-v1", + "acceptance_harness_implementation": "builder-backed-local-ckb-lock-spend-matrix", + "valid_create_tx": "0xa120df0ec121ee17c99593ca6475b3874bd6fff05693e1e579f03c878e6b6303", + "valid_tx": "0xaf62b59e32e627da106edf13d40c811ce3dec551c1d67ea8831100cf3862c90f", + "invalid_create_tx": "0x303ddda76da0fffdac25e53f5e93c2bfca6617c7281d37afd216d8f64410ceb3", + "invalid_tx": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x969891936e2843de5c335b05af807ebe3ffc6d6f3e371e129c150b7944389b27" + } + } + ], + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x303ddda76da0fffdac25e53f5e93c2bfca6617c7281d37afd216d8f64410ceb3" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631003333333333333333333333333333333333333333333333333333333333333333" + ] + }, + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 343, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 776, + "measured_cycles": 4995, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 1 + } + }, + { + "name": "timelock.cell:can_unlock_lock", + "example": "timelock.cell", + "lock": "can_unlock_lock", + "artifact_data_hash": "0x582102f2b60c104220dfb5607341b748d7a0651050af5d2ee44f3a5fc540ce1d", + "acceptance_harness_name": "cellscript-lock-spend-matrix-builder-v1", + "acceptance_harness_implementation": "builder-backed-local-ckb-lock-spend-matrix", + "valid_create_tx": "0x7dbcaed57f3138633b094969af7e098cfc7e5615d0822585ef1bc3d2a0621514", + "valid_tx": "0x7e40aa9553c4f2c63d5ae3732a4d57a9e697e14b8bf8428dcd99574709a66b9e", + "invalid_create_tx": "0xabf216907540017b954863b29e925febb092f833c52f4b0c4678603a0c60cfd7", + "invalid_tx": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xd0b58fa147e5809651e0e3ec1edf1ebd5d25f78cd62529195d11c23bebab6f72" + } + } + ], + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xabf216907540017b954863b29e925febb092f833c52f4b0c4678603a0c60cfd7" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 343, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 780, + "measured_cycles": 3221, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 28, + "witness_count": 1 + } + }, + { + "name": "timelock.cell:is_owner", + "example": "timelock.cell", + "lock": "is_owner", + "artifact_data_hash": "0x67cf56e6c2bcd82506436ade5d8a220f9b51ad099c307d7cb59de6629cca279e", + "acceptance_harness_name": "cellscript-lock-spend-matrix-builder-v1", + "acceptance_harness_implementation": "builder-backed-local-ckb-lock-spend-matrix", + "valid_create_tx": "0x359f39061473e35c6dbab0c66794d75a41382022924c32e1f8adb6b7e18bc87a", + "valid_tx": "0x3c849919043c16e3e898eda183516a34bbcdc02458f05c8d208cf134cf0ed8f0", + "invalid_create_tx": "0xceaaabab7b6cb8b1b1a6332e8f0624978e76fa9931a2e5097dbb48abebb09df2", + "invalid_tx": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x59e35a159719575477e11dfd15b9b1c0c0c895495034a74e23e8f89a884cfa44" + } + } + ], + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xceaaabab7b6cb8b1b1a6332e8f0624978e76fa9931a2e5097dbb48abebb09df2" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631003333333333333333333333333333333333333333333333333333333333333333" + ] + }, + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 343, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 776, + "measured_cycles": 5097, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 1 + } + }, + { + "name": "timelock.cell:lock_id_commitment", + "example": "timelock.cell", + "lock": "lock_id_commitment", + "artifact_data_hash": "0x1ef8dbe9b2f531b18576d6ec194fae7e744ac501952706adcb6382d1deea04b4", + "acceptance_harness_name": "cellscript-lock-spend-matrix-builder-v1", + "acceptance_harness_implementation": "builder-backed-local-ckb-lock-spend-matrix", + "valid_create_tx": "0x08cbc0f0e4b4a1201e687be7ab5380399f3f971ab68bae873e7b6e5dde6dac8c", + "valid_tx": "0xf784caf50f8ff3466cfb61ca40b3fecb90bff03f1dfb1916ca749f33b54d216d", + "invalid_create_tx": "0xf7a35513fabdaa0d83307fa67eb92badabb850a2b71ec2a2f67b49229ed9dc59", + "invalid_tx": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x49ce2be6cbe254c0a78346d81c7b098e74f44601c4693a509ba36d4b3c681f69" + } + } + ], + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xf7a35513fabdaa0d83307fa67eb92badabb850a2b71ec2a2f67b49229ed9dc59" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631005555555555555555555555555555555555555555555555555555555555555555" + ] + }, + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 343, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 776, + "measured_cycles": 17895, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 1 + } + }, + { + "name": "timelock.cell:asset_matches", + "example": "timelock.cell", + "lock": "asset_matches", + "artifact_data_hash": "0x214893fb9dd10fe10e4a92ef80b06c126808256f524d308629c828a49c4327de", + "acceptance_harness_name": "cellscript-lock-spend-matrix-builder-v1", + "acceptance_harness_implementation": "builder-backed-local-ckb-lock-spend-matrix", + "valid_create_tx": "0x2746ae89bf4c9c652cebc4119bbd5a9df9f721f9eedd64182d188bbc17e5d489", + "valid_tx": "0xfe15000508fa702953f7966b7da93a3ee01952d7fbf7968c831b4d4103a1f587", + "invalid_create_tx": "0x98b4b13e3fc4f920bf1ae1626c1959f156e3d8ba0b608e4742e6e8ef998a149f", + "invalid_tx": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x1", + "tx_hash": "0x98b4b13e3fc4f920bf1ae1626c1959f156e3d8ba0b608e4742e6e8ef998a149f" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xdb30f9d5f126ed97836bb953e06415433f7367a42dd24b0a6b53f934259b70c9" + } + } + ], + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x98b4b13e3fc4f920bf1ae1626c1959f156e3d8ba0b608e4742e6e8ef998a149f" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100", + "0x" + ] + }, + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 3, + "consensus_serialized_tx_size_bytes": 356, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 844, + "measured_cycles": 4608, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 28, + "witness_count": 2 + } + }, + { + "name": "timelock.cell:not_expired", + "example": "timelock.cell", + "lock": "not_expired", + "artifact_data_hash": "0xf7a0ff2f4e06b8af72ebbd3aa6a7e6ffe61d3ada8258397c6c224d217ae9d3cd", + "acceptance_harness_name": "cellscript-lock-spend-matrix-builder-v1", + "acceptance_harness_implementation": "builder-backed-local-ckb-lock-spend-matrix", + "valid_create_tx": "0x073245d0e75464ee92f0f4a3264f989fe4ea3c11dac3e4a1abd6084ea8dc2305", + "valid_tx": "0x964480ea9d113044f45b7aa836999dc9486a95c1f3786c7ffaa6df2a1457cc0c", + "invalid_create_tx": "0xed204f9b9fa736fae8691f41c9674b625c5a831a7d6fa0d5d2b50c1d4ce5e142", + "invalid_tx": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xdff9e4e52961c196f41c52c2d211468dfa6b3af8c4f194a02d4bc59bfe39d066" + } + } + ], + "header_deps": [ + "0xb1308c5915db1ae2ed1511f83daf95224bbdef11e226d7054b5720b9efee90d5" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xed204f9b9fa736fae8691f41c9674b625c5a831a7d6fa0d5d2b50c1d4ce5e142" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 343, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 780, + "measured_cycles": 3164, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 28, + "witness_count": 1 + } + }, + { + "name": "timelock.cell:emergency_approved", + "example": "timelock.cell", + "lock": "emergency_approved", + "artifact_data_hash": "0xe383a15d1b2e326df64363cab5778e79bd5edef2bc97a2ae3d4c77ab072752e3", + "acceptance_harness_name": "cellscript-lock-spend-matrix-builder-v1", + "acceptance_harness_implementation": "builder-backed-local-ckb-lock-spend-matrix", + "valid_create_tx": "0xb9f8fd253e6da658201c7d51eb1d64c53ae37639fc94ead37dce09dc7b8a122f", + "valid_tx": "0xfc4b81ff774304b41f674c3e53c374264a3ec988118144a8d49ebb87e66e2f00", + "invalid_create_tx": "0x2119c94d3c5beaff73b1cd02bacc32f3f83a69baded172e851e65d5d8a52f3f4", + "invalid_tx": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x9572797579e20683ff0f7c6fe0152a168a9c6bade9800dcb77751ff651df8478" + } + } + ], + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x2119c94d3c5beaff73b1cd02bacc32f3f83a69baded172e851e65d5d8a52f3f4" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 311, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 712, + "measured_cycles": 2710, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 28, + "witness_count": 1 + } + }, + { + "name": "multisig.cell:is_signer_lock", + "example": "multisig.cell", + "lock": "is_signer_lock", + "artifact_data_hash": "0xaebdd9d7c1cd1a9b581bc3a42a6c5f40d5b41f2ee637f7a1b4e4eef38500c349", + "acceptance_harness_name": "cellscript-lock-spend-matrix-builder-v1", + "acceptance_harness_implementation": "builder-backed-local-ckb-lock-spend-matrix", + "valid_create_tx": "0x835773fb38ff3537ca5a342019797b8ac9f66e92beaf0308bd39cf93eec19ad0", + "valid_tx": "0x8128c4595a00a0cc220271dded5f787a8106cbefee1554c9719e586e76b9893f", + "invalid_create_tx": "0x0ea342c485118cb69e4ecbc668e9c916b479fd6be1a284ef27db92c29f0b7141", + "invalid_tx": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x2da50f9b01999a58735ab822ba3bc2b743d5d297b196e77f7cbbe84bc2766aac" + } + } + ], + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x0ea342c485118cb69e4ecbc668e9c916b479fd6be1a284ef27db92c29f0b7141" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631003333333333333333333333333333333333333333333333333333333333333333" + ] + }, + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 343, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 776, + "measured_cycles": 5078, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 1 + } + }, + { + "name": "multisig.cell:can_execute", + "example": "multisig.cell", + "lock": "can_execute", + "artifact_data_hash": "0x4e52ecbe274e6cefae5fdb1eba4f1cee15d92370a2a991bce607e86bd12c2cae", + "acceptance_harness_name": "cellscript-lock-spend-matrix-builder-v1", + "acceptance_harness_implementation": "builder-backed-local-ckb-lock-spend-matrix", + "valid_create_tx": "0x336a97dc34dcfb9fd522d94f3a0653fb50f2aaf7d4cba9278462fe43e92a70c9", + "valid_tx": "0x32a7a73a12207334bbb3966a636e22d5ea60ee3dbcf4b8f0d757c90e3cc282b6", + "invalid_create_tx": "0x2b965ae1e6b62320a3cf421dc790d3a585e91e990f098ee61a63f21f8edfb669", + "invalid_tx": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xdb690265e0a48d6b81c1b38a4af3366976ec7d73515a63128a97e320a4664175" + } + } + ], + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x2b965ae1e6b62320a3cf421dc790d3a585e91e990f098ee61a63f21f8edfb669" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x24000000100000001000000024000000100000004353415247763100c409000000000000" + ] + }, + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 319, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 728, + "measured_cycles": 4771, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 36, + "witness_count": 1 + } + }, + { + "name": "multisig.cell:can_cancel", + "example": "multisig.cell", + "lock": "can_cancel", + "artifact_data_hash": "0x0d708753715b0c50502a06fea8c0d48b890212a5217b4330b7a77805a71f3dd7", + "acceptance_harness_name": "cellscript-lock-spend-matrix-builder-v1", + "acceptance_harness_implementation": "builder-backed-local-ckb-lock-spend-matrix", + "valid_create_tx": "0x5784153f26770fa3021edb7050dc2555d2f43f08ffaed620bb1ff9a76abaa993", + "valid_tx": "0x3e1251358de881931f81bfe6f8a80a66309befa2db94fef5889a81b57334bc13", + "invalid_create_tx": "0x86a24cb3b26a8379df76a852b569c5148b51988ba34b411062d49a545fb0d876", + "invalid_tx": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x757f318b25b23b441765bcbfac6ae04d0986ffcbdbc86cc58ebdd7eef79c65fc" + } + } + ], + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x86a24cb3b26a8379df76a852b569c5148b51988ba34b411062d49a545fb0d876" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631002222222222222222222222222222222222222222222222222222222222222222" + ] + }, + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 343, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 776, + "measured_cycles": 5055, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 1 + } + }, + { + "name": "multisig.cell:has_enough_approvals", + "example": "multisig.cell", + "lock": "has_enough_approvals", + "artifact_data_hash": "0xc39605e25bec7c9fe1297e640cf6ef42128dff83cec50c6e94bcc1eeb7aa268a", + "acceptance_harness_name": "cellscript-lock-spend-matrix-builder-v1", + "acceptance_harness_implementation": "builder-backed-local-ckb-lock-spend-matrix", + "valid_create_tx": "0xeaeed3d06f30198c983882e0221720faf11c02473429adb757bd570b910363f9", + "valid_tx": "0x444ee91ba47e5385db975ecd93a4a7ddbca24280a7b63c7ff4898461e206388a", + "invalid_create_tx": "0x638614460e8f5f22cc0aff1077c4ab63559f16b230b4230bc0767abad961fd13", + "invalid_tx": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0x4549c1c05bb03fe8c884c329830b6ba4cbc4fe2f560fa5db94addb128fc14015" + } + } + ], + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x638614460e8f5f22cc0aff1077c4ab63559f16b230b4230bc0767abad961fd13" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x1c00000010000000100000001c000000080000004353415247763100" + ] + }, + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 311, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 712, + "measured_cycles": 3046, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 28, + "witness_count": 1 + } + }, + { + "name": "multisig.cell:not_expired", + "example": "multisig.cell", + "lock": "not_expired", + "artifact_data_hash": "0xa983e55b6bf70b5e24415864e1ab3640ccef502351a814d229b66d6738e0c83a", + "acceptance_harness_name": "cellscript-lock-spend-matrix-builder-v1", + "acceptance_harness_implementation": "builder-backed-local-ckb-lock-spend-matrix", + "valid_create_tx": "0x1747065ef33181c2ef81ca3e925090b9f8d10d68db884951cb0d13d62bb8761a", + "valid_tx": "0x4d6d94bf0b85a090775f7c8c7127e5b0b4d334547d299080282dac7fc94eafd9", + "invalid_create_tx": "0xf0d43d47f20b19cd70aed45f330cbb4c98a5898d4ad3096cf5b588b5dfe6f834", + "invalid_tx": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xb547f36f187c1934d41fdd9aa8a0e855842721d14c598386bdc850d6b17b5517" + } + } + ], + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0xf0d43d47f20b19cd70aed45f330cbb4c98a5898d4ad3096cf5b588b5dfe6f834" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x24000000100000001000000024000000100000004353415247763100c409000000000000" + ] + }, + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 319, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 728, + "measured_cycles": 4190, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 36, + "witness_count": 1 + } + }, + { + "name": "vesting.cell:vesting_admin", + "example": "vesting.cell", + "lock": "vesting_admin", + "artifact_data_hash": "0x9b4ed74a5469e89dd428a35929c57c901ef5dee0ea5a3e1979ac81d53dc2ea4e", + "acceptance_harness_name": "cellscript-lock-spend-matrix-builder-v1", + "acceptance_harness_implementation": "builder-backed-local-ckb-lock-spend-matrix", + "valid_create_tx": "0x6a3d32809ed36e7835e40e027dc05ee5adc36c9f04cc84866dfd3b58fe0f3043", + "valid_tx": "0xe712cd2c89aeadb85ad178be1df80fa32c72c563007d02022307da44d2b10f17", + "invalid_create_tx": "0x58e74715d125d7cbd4c7a98b8aad1518cfaaf118e9e0defca5728b9430946249", + "invalid_tx": { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x5", + "tx_hash": "0xf45444cf5da12e571a468cb521b52ba1e0b79adfb1ca7b42ab925b0c5dc6fc7e" + } + }, + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xc71e873453ffa750d9ef781214a1e015b9fe92ba751672b9592f3593750cb2fd" + } + } + ], + "header_deps": [], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x58e74715d125d7cbd4c7a98b8aad1518cfaaf118e9e0defca5728b9430946249" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "args": "0x", + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [ + "0x3c00000010000000100000003c0000002800000043534152477631003333333333333333333333333333333333333333333333333333333333333333" + ] + }, + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 343, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 776, + "measured_cycles": 5156, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 60, + "witness_count": 1 + } + } + ], + "stateful_scenarios": [ + { + "name": "token.mint-with-authority-transfer-mint-with-authority-merge-burn", + "kind": "stateful-scenario", + "action_ids": [ + "token.cell:mint_with_authority", + "token.cell:transfer_token", + "token.cell:merge", + "token.cell:burn" + ], + "steps": [ + { + "step": "mint_first_token_to_transfer", + "old_tx_hash": "0x00409256e78106d58106d68a0fc529399530b444f5e73ebf74960616d208c2a4", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 568, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1220, + "measured_cycles": 9321, + "measured_output_capacity_shannons": [ + 60000000000, + 10000000000 + ], + "occupied_capacity_shannons": 19000000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 70000000000, + "output_count": 2, + "output_data_bytes": 40, + "output_occupied_capacity_shannons": [ + 9900000000, + 9100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 48, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true + } + }, + { + "step": "transfer_first_token_to_merge", + "old_tx_hash": "0x1231896def8739036e5e85f79df05e268e5900cf8285d1ed7601157a3e0cc38e", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 393, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 879, + "measured_cycles": 6044, + "measured_output_capacity_shannons": [ + 10000000000 + ], + "occupied_capacity_shannons": 9100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 10000000000, + "output_count": 1, + "output_data_bytes": 16, + "output_occupied_capacity_shannons": [ + 9100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 40, + "witness_count": 1 + }, + "outputs_live": { + "0": true + } + }, + { + "step": "mint_second_token_to_merge", + "old_tx_hash": "0xe9680f21dbf851055f0cb2fcc4cd51a05b5e2f6846b22b08462ffa12e7dc7d2d", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 568, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1220, + "measured_cycles": 9321, + "measured_output_capacity_shannons": [ + 50000000000, + 10000000000 + ], + "occupied_capacity_shannons": 19000000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 60000000000, + "output_count": 2, + "output_data_bytes": 40, + "output_occupied_capacity_shannons": [ + 9900000000, + 9100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 48, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true + } + }, + { + "step": "merge_tokens_to_burn", + "old_tx_hash": "0x558ddcd2b7e2faf8b3e72f03235cee6ae1ab465b76732cfede9c6967ceb130ec", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 445, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 2, + "json_envelope_size_bytes": 1013, + "measured_cycles": 7877, + "measured_output_capacity_shannons": [ + 20000000000 + ], + "occupied_capacity_shannons": 9100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 20000000000, + "output_count": 1, + "output_data_bytes": 16, + "output_occupied_capacity_shannons": [ + 9100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 40, + "witness_count": 2 + }, + "outputs_live": { + "0": true + } + }, + { + "step": "burn_merged_token", + "old_tx_hash": "0xe32ba198cf261e9245eeb097056d69457006704701255e84c64181d8115d1fea", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 291, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 671, + "measured_cycles": 4918, + "measured_output_capacity_shannons": [ + 20000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 20000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 8, + "witness_count": 1 + }, + "outputs_live": { + "0": true + } + } + ] + }, + { + "name": "nft.mint-list-transfer-by-listing", + "kind": "stateful-scenario", + "action_ids": [ + "nft.cell:create_collection", + "nft.cell:mint", + "nft.cell:create_listing", + "nft.cell:buy_from_listing" + ], + "steps": [ + { + "step": "create_collection_for_live_mint", + "old_tx_hash": "0xa278863a1589ef75f641ead8c869a21b4f426a167f4814927af651f01de54cea", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 603, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1300, + "measured_cycles": 8664, + "measured_output_capacity_shannons": [ + 80000000000 + ], + "occupied_capacity_shannons": 21600000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 80000000000, + "output_count": 1, + "output_data_bytes": 141, + "output_occupied_capacity_shannons": [ + 21600000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 125, + "witness_count": 1 + }, + "outputs_live": { + "0": true + } + }, + { + "step": "mint_nft_for_listing_sale", + "old_tx_hash": "0xef62d924ff6af766acc21727b36c6e6483fac2768527599bf597348eb3c55a91", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 831, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1746, + "measured_cycles": 16983, + "measured_output_capacity_shannons": [ + 50000000000, + 30000000000 + ], + "occupied_capacity_shannons": 42900000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 80000000000, + "output_count": 2, + "output_data_bytes": 279, + "output_occupied_capacity_shannons": [ + 21600000000, + 21300000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 72, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true + } + }, + { + "step": "create_listing_from_live_nft_dep", + "old_tx_hash": "0xc67554cbd1c3973fe04e014c2271023a82d9874a4b84ec38bda8bca1f9a65b26", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 3, + "consensus_serialized_tx_size_bytes": 600, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 1337, + "measured_cycles": 9961, + "measured_output_capacity_shannons": [ + 30000000000, + 20000000000 + ], + "occupied_capacity_shannons": 20500000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 50000000000, + "output_count": 2, + "output_data_bytes": 89, + "output_occupied_capacity_shannons": [ + 16400000000, + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 16, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true + } + }, + { + "step": "buy_listing_from_live_nft_and_listing", + "old_tx_hash": "0x6643966a91792a95d2fa6dc1fa6e1cf1a1c1c677930c6d95df344e7edbbab27b", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 990, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 4, + "json_envelope_size_bytes": 2144, + "measured_cycles": 30898, + "measured_output_capacity_shannons": [ + 30000000000, + 15000000000, + 15000000000 + ], + "occupied_capacity_shannons": 39600000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 60000000000, + "output_count": 3, + "output_data_bytes": 170, + "output_occupied_capacity_shannons": [ + 21400000000, + 9100000000, + 9100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 40, + "witness_count": 4 + }, + "outputs_live": { + "0": true, + "1": true, + "2": true + } + } + ] + }, + { + "name": "timelock.create-lock-lock-asset-request-release-execute", + "kind": "stateful-scenario", + "action_ids": [ + "timelock.cell:create_absolute_lock", + "timelock.cell:lock_asset", + "timelock.cell:request_release", + "timelock.cell:execute_release" + ], + "steps": [ + { + "step": "create_absolute_lock_for_release", + "old_tx_hash": "0x2f2a53ea7336cf1d1b199a59b22148e7357d2e12846050a664cce5bf73094e80", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 530, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 1157, + "measured_cycles": 6756, + "measured_output_capacity_shannons": [ + 30000000000 + ], + "occupied_capacity_shannons": 15600000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 30000000000, + "output_count": 1, + "output_data_bytes": 81, + "output_occupied_capacity_shannons": [ + 15600000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 80, + "witness_count": 1 + }, + "outputs_live": { + "0": true + } + }, + { + "step": "lock_asset_against_live_lock", + "old_tx_hash": "0xa8e8c60bbed4ebf0747eb82243ce7c6644d925a506d822cdc080dfc26a067dd2", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 3, + "consensus_serialized_tx_size_bytes": 519, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1172, + "measured_cycles": 8660, + "measured_output_capacity_shannons": [ + 30000000000, + 70000000000 + ], + "occupied_capacity_shannons": 16400000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 2, + "output_data_bytes": 48, + "output_occupied_capacity_shannons": [ + 12300000000, + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 8, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true + } + }, + { + "step": "request_release_from_live_lock", + "old_tx_hash": "0x402f7e5dd680c1d6dc63abfc07b59a2583aa577503c506bef3d00a3a9318608b", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 3, + "consensus_serialized_tx_size_bytes": 608, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 1354, + "measured_cycles": 10104, + "measured_output_capacity_shannons": [ + 30000000000, + 70000000000 + ], + "occupied_capacity_shannons": 18900000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 2, + "output_data_bytes": 73, + "output_occupied_capacity_shannons": [ + 14800000000, + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 40, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true + } + }, + { + "step": "execute_release_from_live_cells", + "old_tx_hash": "0xf36de341cb16e3887aa7fca0f4421e35bc3bd224f9e39d215606a49f73dabee4", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 744, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 3, + "json_envelope_size_bytes": 1635, + "measured_cycles": 22446, + "measured_output_capacity_shannons": [ + 30000000000, + 30000000000 + ], + "occupied_capacity_shannons": 23800000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 60000000000, + "output_count": 2, + "output_data_bytes": 88, + "output_occupied_capacity_shannons": [ + 9100000000, + 14700000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 40, + "witness_count": 3 + }, + "outputs_live": { + "0": true, + "1": true + } + } + ] + }, + { + "name": "launch.launch-token-then-mint-with-authority", + "kind": "stateful-scenario", + "action_ids": [ + "launch.cell:launch_token", + "token.cell:mint_with_authority" + ], + "steps": [ + { + "step": "launch_token_to_live_mint_authority", + "old_tx_hash": "0x1c8c4325505326f747420de5e8560c32794f3e1ef786c38d1bcd5b186c669784", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 1858, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 3741, + "measured_cycles": 39715, + "measured_output_capacity_shannons": [ + 40000000000, + 20000000000, + 20000000000, + 20000000000, + 20000000000, + 40000000000, + 20000000000, + 20000000000 + ], + "occupied_capacity_shannons": 88600000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 200000000000, + "output_count": 8, + "output_data_bytes": 282, + "output_occupied_capacity_shannons": [ + 9900000000, + 9200000000, + 9200000000, + 9200000000, + 9200000000, + 18100000000, + 14700000000, + 9100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 234, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true, + "2": true, + "3": true, + "4": true, + "5": true, + "6": true, + "7": true + } + }, + { + "step": "mint_with_authority_again_from_launched_authority", + "old_tx_hash": "0xd44187944519beb8fb0d67544e148c80880dc5e12336bae473be6e788ff980c2", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 569, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1221, + "measured_cycles": 9858, + "measured_output_capacity_shannons": [ + 30000000000, + 10000000000 + ], + "occupied_capacity_shannons": 19100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 40000000000, + "output_count": 2, + "output_data_bytes": 40, + "output_occupied_capacity_shannons": [ + 9900000000, + 9200000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 48, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true + } + } + ] + }, + { + "name": "amm.seed-add-swap-remove", + "kind": "stateful-scenario", + "action_ids": [ + "amm_pool.cell:seed_pool", + "amm_pool.cell:add_liquidity", + "amm_pool.cell:swap_a_for_b", + "amm_pool.cell:remove_liquidity" + ], + "steps": [ + { + "step": "seed_pool_for_add_liquidity", + "old_tx_hash": "0x69a432d0677efdd81acdd9ee50ac097f3cfe6e4dc981c86cb3bf7b090d32b833", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 752, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 2, + "json_envelope_size_bytes": 1618, + "measured_cycles": 20120, + "measured_output_capacity_shannons": [ + 20000000000, + 20000000000 + ], + "occupied_capacity_shannons": 32800000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 40000000000, + "output_count": 2, + "output_data_bytes": 178, + "output_occupied_capacity_shannons": [ + 18100000000, + 14700000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 42, + "witness_count": 2 + }, + "outputs_live": { + "0": true, + "1": true + } + }, + { + "step": "add_liquidity_to_live_pool", + "old_tx_hash": "0x3318719ba10143a600ebda5a3f0b3a563c8f1d94d4c791831497295a4abcac74", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 802, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 3, + "json_envelope_size_bytes": 1748, + "measured_cycles": 34243, + "measured_output_capacity_shannons": [ + 20000000000, + 20000000000 + ], + "occupied_capacity_shannons": 32800000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 40000000000, + "output_count": 2, + "output_data_bytes": 178, + "output_occupied_capacity_shannons": [ + 18100000000, + 14700000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 40, + "witness_count": 3 + }, + "outputs_live": { + "0": true, + "1": true + } + }, + { + "step": "swap_against_live_pool", + "old_tx_hash": "0x216dde4df2ea8fe1425edc0dedca51a7e00dd08d33941db55d205a18314c34af", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 703, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 2, + "json_envelope_size_bytes": 1519, + "measured_cycles": 33249, + "measured_output_capacity_shannons": [ + 20000000000, + 20000000000 + ], + "occupied_capacity_shannons": 27300000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 40000000000, + "output_count": 2, + "output_data_bytes": 122, + "output_occupied_capacity_shannons": [ + 18100000000, + 9200000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 48, + "witness_count": 2 + }, + "outputs_live": { + "0": true, + "1": true + } + }, + { + "step": "remove_liquidity_from_live_pool", + "old_tx_hash": "0x2c4b0dd0bcfb2f67d16e2dd6d86136b2d4c67596a13e419fed44981d1181bdf1", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 994, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 3, + "json_envelope_size_bytes": 2110, + "measured_cycles": 33348, + "measured_output_capacity_shannons": [ + 20000000000, + 20000000000, + 20000000000, + 98474034418 + ], + "occupied_capacity_shannons": 40400000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 158474034418, + "output_count": 4, + "output_data_bytes": 138, + "output_occupied_capacity_shannons": [ + 18100000000, + 9100000000, + 9100000000, + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 40, + "witness_count": 3 + }, + "outputs_live": { + "0": true, + "1": true, + "2": true, + "3": true + } + } + ] + }, + { + "name": "vesting.create-config-grant-revoke", + "kind": "stateful-scenario", + "action_ids": [ + "vesting.cell:create_vesting_config", + "vesting.cell:grant_vesting", + "vesting.cell:revoke_grant" + ], + "steps": [ + { + "step": "create_config_for_grant", + "old_tx_hash": "0x0ed6bca9b7b257ea7fd5b25d4f686479d892da1349a94a24c20be92d396dcaa3", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 459, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1010, + "measured_cycles": 6541, + "measured_output_capacity_shannons": [ + 30000000000 + ], + "occupied_capacity_shannons": 13200000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 30000000000, + "output_count": 1, + "output_data_bytes": 57, + "output_occupied_capacity_shannons": [ + 13200000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 65, + "witness_count": 1 + }, + "outputs_live": { + "0": true + } + }, + { + "step": "grant_vesting_from_live_config", + "old_tx_hash": "0x115b8ecbcb808b3b25b5f9cbc4883d27337aea43395ded9325a2db79ff74e71d", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 3, + "consensus_serialized_tx_size_bytes": 668, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 2, + "json_envelope_size_bytes": 1504, + "measured_cycles": 11470, + "measured_output_capacity_shannons": [ + 30000000000, + 104694478663 + ], + "occupied_capacity_shannons": 19700000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 134694478663, + "output_count": 2, + "output_data_bytes": 81, + "output_occupied_capacity_shannons": [ + 15600000000, + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 40, + "witness_count": 2 + }, + "outputs_live": { + "0": true, + "1": true + } + }, + { + "step": "revoke_live_grant", + "old_tx_hash": "0x5a8ff906574c1edf1e5fbd1487c723f67038565705582d8ac112264d57cbfe07", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 3, + "consensus_serialized_tx_size_bytes": 621, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 1382, + "measured_cycles": 14841, + "measured_output_capacity_shannons": [ + 15000000000, + 15000000000 + ], + "occupied_capacity_shannons": 18200000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 30000000000, + "output_count": 2, + "output_data_bytes": 32, + "output_occupied_capacity_shannons": [ + 9100000000, + 9100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 40, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true + } + } + ] + }, + { + "name": "multisig.create-propose-approve-approve-execute", + "kind": "stateful-scenario", + "action_ids": [ + "multisig.cell:create_wallet", + "multisig.cell:propose_transfer", + "multisig.cell:record_approval", + "multisig.cell:execute_proposal" + ], + "steps": [ + { + "step": "create_wallet_for_proposal", + "old_tx_hash": "0x17606461a3d98871a31a1d2dc71e0e81e47c2fb246665a0c19f207255b32f70a", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 599, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1292, + "measured_cycles": 8347, + "measured_output_capacity_shannons": [ + 200000000000 + ], + "occupied_capacity_shannons": 21600000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 200000000000, + "output_count": 1, + "output_data_bytes": 141, + "output_occupied_capacity_shannons": [ + 21600000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 121, + "witness_count": 1 + }, + "outputs_live": { + "0": true + } + }, + { + "step": "propose_transfer_from_live_wallet", + "old_tx_hash": "0xc1992e679cdcbc64bb722f94b5d226099b2b9ead0529e4ccf45833055f369b2a", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 900, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1885, + "measured_cycles": 19306, + "measured_output_capacity_shannons": [ + 50000000000, + 150000000000 + ], + "occupied_capacity_shannons": 48200000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 200000000000, + "output_count": 2, + "output_data_bytes": 332, + "output_occupied_capacity_shannons": [ + 21600000000, + 26600000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 88, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true + } + }, + { + "step": "record_first_approval", + "old_tx_hash": "0x3b5f601fb0d58eec101f8758734ca3adaa979411bc16d348e7dad955ae10f23d", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 3, + "consensus_serialized_tx_size_bytes": 836, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1809, + "measured_cycles": 21716, + "measured_output_capacity_shannons": [ + 120000000000, + 30000000000 + ], + "occupied_capacity_shannons": 42100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 150000000000, + "output_count": 2, + "output_data_bytes": 271, + "output_occupied_capacity_shannons": [ + 29800000000, + 12300000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 48, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true + } + }, + { + "step": "record_second_approval", + "old_tx_hash": "0x5e784733c02e52fe1d4c6996255dcfdbf2b792d69c36414970e539e90901d2b2", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 3, + "consensus_serialized_tx_size_bytes": 868, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1873, + "measured_cycles": 23966, + "measured_output_capacity_shannons": [ + 90000000000, + 30000000000 + ], + "occupied_capacity_shannons": 45300000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 120000000000, + "output_count": 2, + "output_data_bytes": 303, + "output_occupied_capacity_shannons": [ + 33000000000, + 12300000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 48, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true + } + }, + { + "step": "execute_approved_proposal", + "old_tx_hash": "0x0538556ab99b85f0633cbb009edcc62be34efb26015f555c59d118424785c27b", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 3, + "consensus_serialized_tx_size_bytes": 471, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1087, + "measured_cycles": 12012, + "measured_output_capacity_shannons": [ + 40000000000 + ], + "occupied_capacity_shannons": 12400000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 40000000000, + "output_count": 1, + "output_data_bytes": 49, + "output_occupied_capacity_shannons": [ + 12400000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 48, + "witness_count": 1 + }, + "outputs_live": { + "0": true + } + } + ] + }, + { + "name": "launch.cell.bootstrap_token.stateful-branch", + "kind": "stateful-action-branch", + "action_ids": [ + "launch.cell:bootstrap_token" + ], + "steps": [ + { + "step": "valid_action_branch", + "old_tx_hash": "0x0b9fa823515ffce03746d1c4344db4e1e50bad3668c81088b7ca5eafc6040913", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 986, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 2034, + "measured_cycles": 13811, + "measured_output_capacity_shannons": [ + 40000000000, + 20000000000, + 20000000000, + 20000000000 + ], + "occupied_capacity_shannons": 37600000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 4, + "output_data_bytes": 72, + "output_occupied_capacity_shannons": [ + 10000000000, + 9200000000, + 9200000000, + 9200000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 144, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true, + "2": true, + "3": true + } + } + ] + }, + { + "name": "multisig.cell.cancel_proposal.stateful-branch", + "kind": "stateful-action-branch", + "action_ids": [ + "multisig.cell:cancel_proposal" + ], + "steps": [ + { + "step": "valid_action_branch", + "old_tx_hash": "0x42897b5ae6adaa91365deb19c8ce0fa269befa90f83fbb2d1aa06e7a3f64a131", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 3, + "consensus_serialized_tx_size_bytes": 360, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 863, + "measured_cycles": 8521, + "measured_output_capacity_shannons": [ + 49000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 49000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 40, + "witness_count": 1 + }, + "outputs_live": { + "0": true + } + } + ] + }, + { + "name": "multisig.cell.propose_add_signer.stateful-branch", + "kind": "stateful-action-branch", + "action_ids": [ + "multisig.cell:propose_add_signer" + ], + "steps": [ + { + "step": "valid_action_branch", + "old_tx_hash": "0x226c0a2e34cedaa363a9d4b223982d2daf4fc419d302115e05e98396b3d68c9b", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 924, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1933, + "measured_cycles": 20816, + "measured_output_capacity_shannons": [ + 70000000000, + 30000000000 + ], + "occupied_capacity_shannons": 51400000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 2, + "output_data_bytes": 364, + "output_occupied_capacity_shannons": [ + 21600000000, + 29800000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 80, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true + } + } + ] + }, + { + "name": "multisig.cell.propose_change_threshold.stateful-branch", + "kind": "stateful-action-branch", + "action_ids": [ + "multisig.cell:propose_change_threshold" + ], + "steps": [ + { + "step": "valid_action_branch", + "old_tx_hash": "0xac9d28c6d3ff7bbf0357a655c0aac471c77bb9ad97374dbc2d507eae0541a733", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 862, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1809, + "measured_cycles": 19323, + "measured_output_capacity_shannons": [ + 70000000000, + 30000000000 + ], + "occupied_capacity_shannons": 48300000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 2, + "output_data_bytes": 333, + "output_occupied_capacity_shannons": [ + 21600000000, + 26700000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 49, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true + } + } + ] + }, + { + "name": "multisig.cell.propose_remove_signer.stateful-branch", + "kind": "stateful-action-branch", + "action_ids": [ + "multisig.cell:propose_remove_signer" + ], + "steps": [ + { + "step": "valid_action_branch", + "old_tx_hash": "0x8b4922b49150481d756b6c3af4236357618d9dcbff0eee4e164ff3288640e9f5", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 892, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1869, + "measured_cycles": 20478, + "measured_output_capacity_shannons": [ + 70000000000, + 30000000000 + ], + "occupied_capacity_shannons": 48200000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 2, + "output_data_bytes": 332, + "output_occupied_capacity_shannons": [ + 21600000000, + 26600000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 80, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true + } + } + ] + }, + { + "name": "nft.cell.accept_offer.stateful-branch", + "kind": "stateful-action-branch", + "action_ids": [ + "nft.cell:accept_offer" + ], + "steps": [ + { + "step": "valid_action_branch", + "old_tx_hash": "0x7a8b320dab64745045b14d0bc21679b0d6b136775eae11033b5041b6f2912c5a", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 989, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 4, + "json_envelope_size_bytes": 2147, + "measured_cycles": 30706, + "measured_output_capacity_shannons": [ + 100000000000, + 20000000000, + 20000000000 + ], + "occupied_capacity_shannons": 39500000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 140000000000, + "output_count": 3, + "output_data_bytes": 170, + "output_occupied_capacity_shannons": [ + 21300000000, + 9100000000, + 9100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 8, + "witness_count": 4 + }, + "outputs_live": { + "0": true, + "1": true, + "2": true + } + } + ] + }, + { + "name": "nft.cell.batch_mint.stateful-branch", + "kind": "stateful-action-branch", + "action_ids": [ + "nft.cell:batch_mint" + ], + "steps": [ + { + "step": "valid_action_branch", + "old_tx_hash": "0xe5d28d78e2c97cfb5cd0fb6d236304cd6b66cbeb235ca2b5cf7468378817844a", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 1859, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 3776, + "measured_cycles": 37789, + "measured_output_capacity_shannons": [ + 100000000000, + 25000000000, + 25000000000, + 25000000000, + 25000000000 + ], + "occupied_capacity_shannons": 106100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 200000000000, + "output_count": 5, + "output_data_bytes": 686, + "output_occupied_capacity_shannons": [ + 20900000000, + 21300000000, + 21300000000, + 21300000000, + 21300000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 264, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true, + "2": true, + "3": true, + "4": true + } + } + ] + }, + { + "name": "nft.cell.burn.stateful-branch", + "kind": "stateful-action-branch", + "action_ids": [ + "nft.cell:burn" + ], + "steps": [ + { + "step": "valid_action_branch", + "old_tx_hash": "0xffe1382e9db1645da25b1602fba1f38b7df1a11b2e72bc98420e9e7353a4ae27", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 291, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 673, + "measured_cycles": 4739, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 8, + "witness_count": 1 + }, + "outputs_live": { + "0": true + } + } + ] + }, + { + "name": "nft.cell.cancel_listing.stateful-branch", + "kind": "stateful-action-branch", + "action_ids": [ + "nft.cell:cancel_listing" + ], + "steps": [ + { + "step": "valid_action_branch", + "old_tx_hash": "0xd7d1822d5820493f4a5c03812e71ecf7a2943734611dfe351598146890453059", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 291, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 672, + "measured_cycles": 4723, + "measured_output_capacity_shannons": [ + 30000000000 + ], + "occupied_capacity_shannons": 4100000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 30000000000, + "output_count": 1, + "output_data_bytes": 0, + "output_occupied_capacity_shannons": [ + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 8, + "witness_count": 1 + }, + "outputs_live": { + "0": true + } + } + ] + }, + { + "name": "nft.cell.create_offer.stateful-branch", + "kind": "stateful-action-branch", + "action_ids": [ + "nft.cell:create_offer" + ], + "steps": [ + { + "step": "valid_action_branch", + "old_tx_hash": "0xd8c0053e479d1e7c9f45e2abc8c2f082194cd47634c2d6388f4e2e7a240366a7", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 570, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 1236, + "measured_cycles": 8307, + "measured_output_capacity_shannons": [ + 30000000000 + ], + "occupied_capacity_shannons": 17200000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 30000000000, + "output_count": 1, + "output_data_bytes": 97, + "output_occupied_capacity_shannons": [ + 17200000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 104, + "witness_count": 1 + }, + "outputs_live": { + "0": true + } + } + ] + }, + { + "name": "nft.cell.transfer.stateful-branch", + "kind": "stateful-action-branch", + "action_ids": [ + "nft.cell:transfer" + ], + "steps": [ + { + "step": "valid_action_branch", + "old_tx_hash": "0x90885689172ed74eedb55cca655df84188643e6cd752f1aa350dc8cb9679dd88", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 514, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1122, + "measured_cycles": 14417, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 21200000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 138, + "output_occupied_capacity_shannons": [ + 21200000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 40, + "witness_count": 1 + }, + "outputs_live": { + "0": true + } + } + ] + }, + { + "name": "timelock.cell.approve_emergency_release.stateful-branch", + "kind": "stateful-action-branch", + "action_ids": [ + "timelock.cell:approve_emergency_release" + ], + "steps": [ + { + "step": "valid_action_branch", + "old_tx_hash": "0x715c3e373c2d4cc35c03c86a41031d7f8be2bc768e644461eac57e5eab004d28", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 567, + "cycles_status": "dry-run-measured", + "header_dep_count": 0, + "input_count": 1, + "json_envelope_size_bytes": 1228, + "measured_cycles": 12204, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 26500000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 190, + "output_occupied_capacity_shannons": [ + 26500000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 40, + "witness_count": 1 + }, + "outputs_live": { + "0": true + } + } + ] + }, + { + "name": "timelock.cell.batch_create_locks.stateful-branch", + "kind": "stateful-action-branch", + "action_ids": [ + "timelock.cell:batch_create_locks" + ], + "steps": [ + { + "step": "valid_action_branch", + "old_tx_hash": "0x1028526cfa99595cebeff3b2745d0bd5a2ef4003cb96a974c3766829f80594d5", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 1414, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 2898, + "measured_cycles": 17887, + "measured_output_capacity_shannons": [ + 30000000000, + 30000000000, + 30000000000, + 30000000000 + ], + "occupied_capacity_shannons": 62000000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 120000000000, + "output_count": 4, + "output_data_bytes": 324, + "output_occupied_capacity_shannons": [ + 15500000000, + 15500000000, + 15500000000, + 15500000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 296, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true, + "2": true, + "3": true + } + } + ] + }, + { + "name": "timelock.cell.create_relative_lock.stateful-branch", + "kind": "stateful-action-branch", + "action_ids": [ + "timelock.cell:create_relative_lock" + ], + "steps": [ + { + "step": "valid_action_branch", + "old_tx_hash": "0xdb5b770c97e55457e5b576a9612fa4a5ba8867c9c11899060f2193129e51d923", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 529, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 1155, + "measured_cycles": 6782, + "measured_output_capacity_shannons": [ + 30000000000 + ], + "occupied_capacity_shannons": 15500000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 30000000000, + "output_count": 1, + "output_data_bytes": 81, + "output_occupied_capacity_shannons": [ + 15500000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 80, + "witness_count": 1 + }, + "outputs_live": { + "0": true + } + } + ] + }, + { + "name": "timelock.cell.execute_emergency_release.stateful-branch", + "kind": "stateful-action-branch", + "action_ids": [ + "timelock.cell:execute_emergency_release" + ], + "steps": [ + { + "step": "valid_action_branch", + "old_tx_hash": "0x330bd555021ad2b154510d81eeccf8cbd8e6e62ebae7c456e5fabc2831e5eb26", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 744, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 3, + "json_envelope_size_bytes": 1636, + "measured_cycles": 22235, + "measured_output_capacity_shannons": [ + 30000000000, + 30000000000 + ], + "occupied_capacity_shannons": 23800000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 60000000000, + "output_count": 2, + "output_data_bytes": 88, + "output_occupied_capacity_shannons": [ + 9100000000, + 14700000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 40, + "witness_count": 3 + }, + "outputs_live": { + "0": true, + "1": true + } + } + ] + }, + { + "name": "timelock.cell.extend_lock.stateful-branch", + "kind": "stateful-action-branch", + "action_ids": [ + "timelock.cell:extend_lock" + ], + "steps": [ + { + "step": "valid_action_branch", + "old_tx_hash": "0xa74c6e001ecc03a1e0432afe27307efcfb85090f1ad2734deca603240a2da157", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 497, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 1092, + "measured_cycles": 12741, + "measured_output_capacity_shannons": [ + 100000000000 + ], + "occupied_capacity_shannons": 15500000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 1, + "output_data_bytes": 81, + "output_occupied_capacity_shannons": [ + 15500000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 48, + "witness_count": 1 + }, + "outputs_live": { + "0": true + } + } + ] + }, + { + "name": "timelock.cell.request_emergency_release.stateful-branch", + "kind": "stateful-action-branch", + "action_ids": [ + "timelock.cell:request_emergency_release" + ], + "steps": [ + { + "step": "valid_action_branch", + "old_tx_hash": "0x563bdf20a03aa85513c4c052b7e8c1489f5c47d97ad0377084d898aabb32be0c", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 3, + "consensus_serialized_tx_size_bytes": 686, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 1510, + "measured_cycles": 11653, + "measured_output_capacity_shannons": [ + 30000000000, + 70000000000 + ], + "occupied_capacity_shannons": 24200000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 100000000000, + "output_count": 2, + "output_data_bytes": 126, + "output_occupied_capacity_shannons": [ + 20100000000, + 4100000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 65, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true + } + } + ] + }, + { + "name": "vesting.cell.claim_fully_vested.stateful-branch", + "kind": "stateful-action-branch", + "action_ids": [ + "vesting.cell:claim_fully_vested" + ], + "steps": [ + { + "step": "valid_action_branch", + "old_tx_hash": "0xc5f4a0ba516ae824a4b48b2c604120abd7d5140c18b0f27ac2b13dba0aec548a", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 617, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 1322, + "measured_cycles": 12869, + "measured_output_capacity_shannons": [ + 20000000000, + 20000000000 + ], + "occupied_capacity_shannons": 24700000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 40000000000, + "output_count": 2, + "output_data_bytes": 97, + "output_occupied_capacity_shannons": [ + 9100000000, + 15600000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 8, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true + } + } + ] + }, + { + "name": "vesting.cell.claim_vested.stateful-branch", + "kind": "stateful-action-branch", + "action_ids": [ + "vesting.cell:claim_vested" + ], + "steps": [ + { + "step": "valid_action_branch", + "old_tx_hash": "0x7c08591b593710f6481af4afcbbdb671fa46332b64a890850192409e7a7242c2", + "measured_constraints": { + "capacity_is_sufficient": true, + "cell_dep_count": 2, + "consensus_serialized_tx_size_bytes": 617, + "cycles_status": "dry-run-measured", + "header_dep_count": 1, + "input_count": 1, + "json_envelope_size_bytes": 1322, + "measured_cycles": 18801, + "measured_output_capacity_shannons": [ + 20000000000, + 20000000000 + ], + "occupied_capacity_shannons": 24700000000, + "occupied_capacity_status": "derived-by-cellscript-ckb-tx-measure", + "output_capacity_shannons": 40000000000, + "output_count": 2, + "output_data_bytes": 97, + "output_occupied_capacity_shannons": [ + 9100000000, + 15600000000 + ], + "tx_measure_error": null, + "tx_size_status": "measured-by-cellscript-ckb-tx-measure", + "under_capacity_output_indexes": [], + "witness_bytes": 8, + "witness_count": 1 + }, + "outputs_live": { + "0": true, + "1": true + } + } + ] + } + ] +} diff --git a/crates/cellscript-tools/src/acceptance_helpers.rs b/crates/cellscript-tools/src/acceptance_helpers.rs new file mode 100644 index 00000000..8797e5bc --- /dev/null +++ b/crates/cellscript-tools/src/acceptance_helpers.rs @@ -0,0 +1,344 @@ +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; + +use anyhow::{bail, Context, Result}; +use serde_json::{json, Value}; + +use crate::shared::stable_json_pretty; + +fn read_json(path: &Path) -> Result { + serde_json::from_slice(&fs::read(path).with_context(|| format!("failed to read {}", path.display()))?) + .with_context(|| format!("failed to parse {} as JSON", path.display())) +} + +fn scalar(value: Option<&Value>) -> String { + match value { + Some(Value::Bool(value)) => value.to_string(), + Some(Value::String(value)) => value.clone(), + Some(Value::Number(value)) => value.to_string(), + Some(Value::Null) | None => "unknown".into(), + Some(value) => value.to_string(), + } +} + +fn require(condition: bool, message: impl Into) -> Result<()> { + if !condition { + bail!(message.into()); + } + Ok(()) +} + +pub fn novaseal_summary(report_path: &Path) -> Result<()> { + let report = read_json(report_path)?; + println!( + "{}\t{}\t{}\t{}\t{}\t{}", + scalar(report.get("status")), + scalar(report.get("live_devnet_rpc_executed")), + scalar(report.get("local_blocker_count")), + scalar(report.get("acceptance_blocker_count")), + scalar(report.get("blocker_count")), + scalar(report.pointer("/external_endpoint_coverage/status")), + ); + Ok(()) +} + +pub fn fiber_report_binding(compatibility_path: &Path, acceptance_path: &Path, expected_revision: &str) -> Result<()> { + let compatibility = read_json(compatibility_path)?; + let acceptance = read_json(acceptance_path)?; + require( + compatibility.pointer("/binding/fiber_revision").and_then(Value::as_str) == Some(expected_revision), + "compatibility report Fiber revision does not match the pinned checkout", + )?; + require( + compatibility.get("binding_fingerprint") == acceptance.get("binding_fingerprint"), + "acceptance report is not bound to compatibility.json", + )?; + require( + matches!( + compatibility.get("status").and_then(Value::as_str), + Some("LocalNodeAdvertised" | "ChannelReady" | "TopologyCertified") + ), + "full acceptance requires at least LocalNodeAdvertised compatibility evidence", + ) +} + +pub fn ecosystem_reuse_contracts(compatibility_path: &Path, action_path: &Path) -> Result<()> { + let compatibility = read_json(compatibility_path)?; + let action = read_json(action_path)?; + require(compatibility["status"] == "ok", "CKB compatibility status must be ok")?; + require(compatibility["schema"] == "cellscript-ckb-std-compat-report-v0.19", "CKB compatibility schema drift")?; + require( + compatibility.pointer("/inline_abi/syscalls/load_cell_by_field") == Some(&json!(2081)), + "load_cell_by_field syscall drift", + )?; + require(compatibility.pointer("/inline_abi/syscalls/load_witness") == Some(&json!(2074)), "load_witness syscall drift")?; + require(compatibility.pointer("/inline_abi/sources/group_input") == Some(&json!((1_u64 << 56) | 1)), "group_input source drift")?; + require( + compatibility.pointer("/inline_abi/sources/group_output") == Some(&json!((1_u64 << 56) | 2)), + "group_output source drift", + )?; + require( + compatibility.pointer("/witness_args_policy/entry_payload_abi") == Some(&json!("cellscript-entry-witness-v1")), + "entry witness ABI drift", + )?; + require( + compatibility.pointer("/witness_args_policy/final_witness_args_owner") == Some(&json!("adapter")), + "WitnessArgs ownership drift", + )?; + require( + compatibility.pointer("/adapter_boundary/compiler_core_uses_ckb_sdk_rust") == Some(&json!(false)), + "compiler core SDK boundary drift", + )?; + require( + compatibility.pointer("/test_evidence/script_construction_api") == Some(&json!(true)), + "script construction evidence missing", + )?; + require( + compatibility.pointer("/adapter_boundary/script_construction/packed_type") == Some(&json!("ckb_types::packed::Script")), + "packed Script type drift", + )?; + require( + compatibility.pointer("/adapter_boundary/script_construction/evidence_schema") + == Some(&json!("cellscript-ckb-script-evidence-v0.19")), + "script evidence schema drift", + )?; + let supports = compatibility + .pointer("/adapter_boundary/script_construction/supports") + .and_then(Value::as_array) + .context("compatibility supports must be an array")?; + for required in ["args_exact_prefix_suffix", "script_ref_readback", "explicit_cell_dep_binding"] { + require(supports.iter().any(|value| value == required), format!("missing adapter support {required}"))?; + } + + require(action["status"] == "ok", "action build status must be ok")?; + require(action["policy"] == "cellscript-action-builder-plan-v1", "action build policy drift")?; + require(action["headless"] == true, "action build must remain headless")?; + require(action["ui_scope"] == "none", "action build UI scope drift")?; + require(action.pointer("/transaction_draft/state") == Some(&json!("ActionPlan")), "transaction draft state drift")?; + require(action.pointer("/transaction_draft/can_submit") == Some(&json!(false)), "unmaterialized action must not submit")?; + require( + action.pointer("/transaction_draft/requires_packed_materialization") == Some(&json!(true)), + "packed materialization must remain required", + )?; + for (field, expected) in [ + ("transaction", "ckb_types::packed::Transaction"), + ("script", "ckb_types::packed::Script"), + ("out_point", "ckb_types::packed::OutPoint"), + ] { + require( + action.pointer(&format!("/transaction_draft/packed_materialization/{field}")) == Some(&json!(expected)), + format!("packed materialization {field} drift"), + )?; + } + require( + action.pointer("/adapter_contract/schema") == Some(&json!("cellscript-ckb-adapter-contract-v0.19")), + "adapter contract schema drift", + )?; + require( + action.pointer("/adapter_contract/witness_policy/default_action_payload_field") == Some(&json!("input_type")), + "default action payload field drift", + )?; + require( + action.pointer("/adapter_contract/witness_policy/lock_signature_policy") + == Some(&json!("explicit-adapter-owned-do-not-overwrite")), + "lock signature policy drift", + )?; + let required_fields = action + .pointer("/adapter_contract/resolved_tx_required_fields") + .and_then(Value::as_array) + .context("resolved_tx_required_fields must be an array")?; + for required in ["outputs_data", "cell_deps", "lineage"] { + require(required_fields.iter().any(|value| value == required), format!("resolved transaction field missing: {required}"))?; + } + require( + action.pointer("/adapter_contract/acceptance_report_template/schema") + == Some(&json!("cellscript-ckb-action-acceptance-report-v0.19")), + "adapter acceptance template schema drift", + ) +} + +fn collect_entries<'a>(metadata: &'a Value, group: &str, field: &str) -> impl Iterator { + metadata[group].as_array().into_iter().flatten().flat_map(move |entry| entry[field].as_array().into_iter().flatten()) +} + +pub fn scope_014(out_dir: &Path, metadata_paths: &[PathBuf]) -> Result<()> { + require( + metadata_paths.len() == 7, + format!("0.14 scope metadata oracle failed: expected 7 v0.14 language metadata files, got {}", metadata_paths.len()), + )?; + let mut features = BTreeSet::new(); + let mut operations = BTreeSet::new(); + let mut purposes = BTreeSet::new(); + let mut capacity_types = BTreeSet::new(); + let mut has_type_id_plan = false; + let mut has_output_data_binding = false; + let mut names = Vec::new(); + for path in metadata_paths { + let metadata = read_json(path).map_err(|error| anyhow::anyhow!("0.14 scope metadata oracle failed: {error:#}"))?; + names.push(path.file_name().context("metadata path has no file name")?.to_string_lossy().into_owned()); + let profile = &metadata["target_profile"]; + for (field, expected) in [ + ("name", "ckb"), + ("source_encoding", "ckb-source-group-high-bit"), + ("witness_abi", "ckb-molecule-witness-args-input-type-v2+cellscript-entry-witness-v1"), + ("spawn_ipc_abi", "ckb-vm-v2-spawn-ipc-syscalls-2601-2608"), + ("output_data_abi", "ckb-outputs-and-outputs-data-index-aligned"), + ("type_id_abi", "ckb-type-id-v1"), + ] { + require( + profile[field] == expected, + format!("0.14 scope metadata oracle failed: {} target profile {field} drift", path.display()), + )?; + } + require( + metadata["artifact_hash"].as_str().is_some_and(|value| !value.is_empty()), + format!("{} missing artifact hash", path.display()), + )?; + require(metadata["artifact_size_bytes"].as_u64().unwrap_or(0) > 0, format!("{} missing artifact size", path.display()))?; + let ckb = metadata.pointer("/constraints/ckb").and_then(Value::as_object).context("metadata missing constraints.ckb")?; + let abi = ckb.get("profile_abi_contract").context("metadata missing profile_abi_contract")?; + require(abi["witness_abi"] == profile["witness_abi"], format!("{} profile ABI witness drift", path.display()))?; + require(abi["output_data_abi"] == profile["output_data_abi"], format!("{} profile ABI output_data drift", path.display()))?; + for value in metadata.pointer("/runtime/ckb_runtime_features").and_then(Value::as_array).into_iter().flatten() { + if let Some(value) = value.as_str() { + features.insert(value.to_owned()); + } + } + let runtime_accesses = metadata.pointer("/runtime/ckb_runtime_accesses").and_then(Value::as_array).into_iter().flatten(); + for access in runtime_accesses.chain(collect_entries(&metadata, "actions", "ckb_runtime_accesses")).chain(collect_entries( + &metadata, + "locks", + "ckb_runtime_accesses", + )) { + if let Some(value) = access["operation"].as_str() { + operations.insert(value.to_owned()); + } + } + for reference in ckb.get("script_references").and_then(Value::as_array).into_iter().flatten() { + if let Some(purpose) = reference["purpose"].as_str() { + purposes.insert(purpose.to_owned()); + if purpose == "spawn-target" { + require( + reference["dep_source"] == "CellDep-or-DepGroup", + format!("{} spawn target dep_source overclaimed", path.display()), + )?; + require( + reference["status"] == "runtime-required-builder-resolved", + format!("{} spawn target status drift", path.display()), + )?; + require( + reference["code_hash"].is_null() && reference["hash_type"].is_null() && reference["args"].is_null(), + format!("{} spawn target must remain builder-resolved", path.display()), + )?; + } + } + } + for floor in ckb.get("declared_capacity_floors").and_then(Value::as_array).into_iter().flatten() { + if let Some(kind) = floor["type_name"].as_str() { + capacity_types.insert(kind.to_owned()); + } + require(floor["source"] == "dsl-with_capacity_floor", format!("{} capacity floor source drift", path.display()))?; + require(floor["shannons"].as_u64().unwrap_or(0) > 0, format!("{} non-positive capacity floor", path.display()))?; + } + for create in collect_entries(&metadata, "actions", "create_set").chain(collect_entries(&metadata, "locks", "create_set")) { + has_type_id_plan |= !create["ckb_type_id"].is_null(); + has_output_data_binding |= !create["ckb_output_data"].is_null(); + } + } + for required in [ + "ckb-spawn-ipc", + "ckb-source-view", + "ckb-witness-args", + "ckb-lock-args", + "ckb-sighash-all", + "ckb-declarative-since", + "ckb-declarative-capacity", + "ckb-blake2b", + ] { + require(features.contains(required), format!("0.14 scope metadata oracle failed: missing runtime feature {required}"))?; + } + for required in [ + "spawn", + "wait", + "pipe", + "pipe-write", + "pipe-read", + "close-fd", + "source-group-input", + "witness-lock", + "lock-args", + "sighash-all", + "require-maturity", + "require-time", + "require-epoch-after", + "require-epoch-relative", + "occupied-capacity", + "hash-blake2b", + ] { + require(operations.contains(required), format!("0.14 scope metadata oracle failed: missing runtime operation {required}"))?; + } + require(purposes.contains("spawn-target"), "0.14 scope metadata oracle failed: missing spawn target script-reference obligation")?; + require( + purposes.contains("type-id-create-output"), + "0.14 scope metadata oracle failed: missing TYPE_ID create script-reference obligation", + )?; + require(capacity_types.contains("TimedToken"), "0.14 scope metadata oracle failed: missing TimedToken capacity floor")?; + require(has_type_id_plan, "0.14 scope metadata oracle failed: missing TYPE_ID output plan in language examples")?; + require(has_output_data_binding, "0.14 scope metadata oracle failed: missing outputs_data binding in language examples")?; + let report = json!({ + "status": "passed", + "metadata_files": names, + "features": features, + "operations": operations, + "script_reference_purposes": purposes, + "capacity_floor_types": capacity_types, + }); + let report_path = out_dir.join("cellscript-0-14-scope-audit-report.json"); + fs::write(&report_path, format!("{}\n", stable_json_pretty(&report)?))?; + println!("valid CellScript 0.14 scope audit: {}", report_path.display()); + Ok(()) +} + +pub fn cellfabric_bridge(envelope_path: &Path, summary_path: &Path) -> Result<()> { + let envelope = read_json(envelope_path)?; + let summary = read_json(summary_path)?; + let source = &envelope["source"]; + for (condition, message) in [ + (envelope["schema"] == "cellscript-cellfabric-intent-envelope-v0.20", "envelope schema mismatch"), + (envelope["status"] == "requires-runtime-binding", "envelope status mismatch"), + (summary["schema"] == "cellscript-cellfabric-intent-envelope-v0.20", "summary schema mismatch"), + (summary["import_status"] == "requires-runtime-binding", "import status mismatch"), + (summary["status"] == "submitted-and-soft-confirmed-non-final", "flow status mismatch"), + (summary["action_plan_hash_hex"] == source["action_plan_hash"], "action_plan_hash mismatch"), + (summary["chain_id"] == source["target_profile"], "chain_id mismatch"), + (summary["app_namespace"] == source["module"], "app_namespace mismatch"), + (summary["action"] == source["action"], "action mismatch"), + (summary["payload_format"] == "cellscript-action-plan-json-v1", "payload format mismatch"), + (summary["requires_signature"] == true, "summary must require signature"), + (summary["submitted"] == true, "summary must claim gateway submission"), + (summary["soft_confirmed"] == true, "summary must claim soft confirmation"), + (summary["l1_final"] == false, "summary must not claim L1 finality"), + (summary["gateway_status"] == "Indexed", "gateway status mismatch"), + (summary.pointer("/ledger_status/status/SoftConfirmed/non_final") == Some(&json!(true)), "ledger status mismatch"), + (summary["bundle_intent_count"] == 1, "bundle must contain one intent"), + (summary["excluded_conflict_count"] == 0, "unexpected excluded conflicts"), + (summary["receipt_non_final"] == true, "receipt must remain non-final"), + (summary["soft_confirmation_confidence"] == "unsigned-non-final-receipt", "unexpected soft confirmation confidence label"), + (summary["settlement_requires_external_builder"] == true, "CellScript settlement must require external runtime builder"), + ] { + require(condition, message)?; + } + for field in ["intent_id", "bundle_id"] { + let value = summary[field].as_str().unwrap_or_default(); + require(value.starts_with("0x") && value.len() == 66, format!("{field} must be 0x-prefixed 32-byte hash"))?; + } + println!("valid CellScript -> CellFabric bridge flow summary"); + Ok(()) +} + +pub fn rust_toolchain_channel(root: &Path) -> Result<()> { + let manifest: toml::Value = toml::from_str(&fs::read_to_string(root.join("rust-toolchain.toml"))?)?; + println!("{}", manifest["toolchain"]["channel"].as_str().context("rust-toolchain.toml is missing toolchain.channel")?); + Ok(()) +} diff --git a/crates/cellscript-tools/src/bip340_tcb.rs b/crates/cellscript-tools/src/bip340_tcb.rs new file mode 100644 index 00000000..23ea849b --- /dev/null +++ b/crates/cellscript-tools/src/bip340_tcb.rs @@ -0,0 +1,273 @@ +//! Local NovaSeal BIP340 runtime-verifier TCB review bundle. + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{Context, Result}; +use serde_json::{json, Value}; +use sha2::{Digest, Sha256}; + +use crate::crypto::sha256_hex; +use crate::shared::{lexical_path, stable_json_pretty}; + +fn load(root: &Path, path: &Path) -> Result { + if !path.exists() { + return Ok(json!({ "missing": true, "path": path.strip_prefix(root).unwrap_or(path).to_string_lossy().replace('\\', "/") })); + } + serde_json::from_slice(&fs::read(path)?).with_context(|| format!("failed to decode {}", path.display())) +} + +fn collect_source(root: &Path, directory: &Path, files: &mut Vec, invalid: &mut Vec) -> Result<()> { + let mut entries = fs::read_dir(directory)?.collect::, _>>()?; + entries.sort_by_key(std::fs::DirEntry::path); + for entry in entries { + let path = entry.path(); + let metadata = fs::symlink_metadata(&path)?; + if metadata.file_type().is_symlink() { + invalid.push(path.strip_prefix(root).unwrap_or(&path).to_string_lossy().replace('\\', "/")); + continue; + } + if metadata.is_dir() { + let name = entry.file_name(); + if ["target", "build", ".git"].iter().any(|skip| name == *skip) { + continue; + } + collect_source(root, &path, files, invalid)?; + } else if metadata.is_file() { + let name = entry.file_name(); + if path.extension().and_then(|value| value.to_str()) == Some("rs") + || ["Cargo.toml", "Cargo.lock", "README.md"].iter().any(|allowed| name == *allowed) + { + files.push(path); + } + } + } + Ok(()) +} + +fn source_inventory(root: &Path, verifier_dirs: &[PathBuf]) -> Result { + let mut files = Vec::new(); + let mut invalid = Vec::new(); + for directory in verifier_dirs { + collect_source(root, directory, &mut files, &mut invalid)?; + } + files.sort(); + invalid.sort(); + let mut rows = Vec::new(); + let mut tree = Sha256::new(); + let mut unsafe_hits = Vec::new(); + let mut review_hits = Vec::new(); + let mut total_lines = 0_usize; + for path in files { + let relative = path.strip_prefix(root).unwrap_or(&path).to_string_lossy().replace('\\', "/"); + let bytes = fs::read(&path)?; + let digest = sha256_hex(&bytes); + let text = String::from_utf8_lossy(&bytes); + let lines = text.matches('\n').count() + usize::from(!text.ends_with('\n')); + total_lines += lines; + rows.push(json!({ "path": relative, "sha256": format!("0x{digest}"), "lines": lines })); + tree.update(relative.as_bytes()); + tree.update([0]); + tree.update(hex::decode(&digest)?); + for (index, line) in text.lines().enumerate() { + let stripped = line.trim(); + if stripped.contains("unsafe") { + unsafe_hits.push(json!({ "path": relative, "line": index + 1, "text": stripped })); + } + if ["TODO", "todo!", "unimplemented!", "panic!"].iter().any(|token| stripped.contains(token)) { + review_hits.push(json!({ "path": relative, "line": index + 1, "text": stripped })); + } + } + } + Ok(json!({ + "source_tree_sha256": format!("0x{}", hex::encode(tree.finalize())), + "files": rows, + "total_files": rows.len(), + "total_lines": total_lines, + "valid": invalid.is_empty(), + "invalid_paths": invalid, + "unsafe_hits": unsafe_hits, + "review_hits": review_hits + })) +} + +fn gate(name: &str, passed: bool, evidence: &str, detail: Value) -> Value { + json!({ "name": name, "status": if passed { "passed" } else { "failed" }, "evidence": evidence, "detail": detail }) +} + +fn bool_at(value: &Value, pointer: &str) -> bool { + value.pointer(pointer).and_then(Value::as_bool) == Some(true) +} + +fn equal_at(value: &Value, left: &str, right: &str) -> bool { + value.pointer(left) == value.pointer(right) +} + +fn git_commit(root: &Path) -> Option { + let output = Command::new("git").args(["rev-parse", "HEAD"]).current_dir(root).output().ok()?; + output.status.success().then(|| String::from_utf8_lossy(&output.stdout).trim().to_owned()) +} + +pub fn run(root: &Path, output: Option<&Path>, pretty: bool) -> Result { + let core = root.join("proposals/novaseal/v0-mvp-skeleton"); + let target = root.join("target"); + let report_paths = [ + ("reference_vectors", core.join("target/novaseal-btc-verifier-vectors.json")), + ("ipc_vectors", core.join("target/novaseal-btc-verifier-ipc-vectors.json")), + ("shell_report", core.join("target/novaseal-btc-verifier-shell-report.json")), + ("riscv_artifact", core.join("target/novaseal-riscv-shell-artifact.json")), + ("child_verifier_ckb_vm", core.join("target/novaseal-ckb-vm-child-verifier-report.json")), + ("parent_lock_ckb_vm", core.join("target/novaseal-parent-lock-ckb-vm-report.json")), + ("combined_tx_ckb_vm", core.join("target/novaseal-combined-tx-report.json")), + ("core_live_devnet", target.join("novaseal-devnet-stateful-live.json")), + ("agreement_live_devnet", target.join("novaseal-agreement-devnet-stateful-live.json")), + ]; + let mut reports = serde_json::Map::new(); + for (name, path) in report_paths { + reports.insert(name.to_owned(), load(root, &path)?); + } + let reports = Value::Object(reports); + let vectors = reports.pointer("/reference_vectors/summary").cloned().unwrap_or_else(|| json!({})); + let ipc = reports.pointer("/ipc_vectors/summary").cloned().unwrap_or_else(|| json!({})); + let shell = reports.pointer("/shell_report/summary").cloned().unwrap_or_else(|| json!({})); + let artifact = reports.get("riscv_artifact").cloned().unwrap_or_else(|| json!({})); + let child = reports.pointer("/child_verifier_ckb_vm/summary").cloned().unwrap_or_else(|| json!({})); + let parent = reports.pointer("/parent_lock_ckb_vm/summary").cloned().unwrap_or_else(|| json!({})); + let combined = reports.pointer("/combined_tx_ckb_vm/summary").cloned().unwrap_or_else(|| json!({})); + let core_live = reports.get("core_live_devnet").cloned().unwrap_or_else(|| json!({})); + let agreement_live = reports.get("agreement_live_devnet").cloned().unwrap_or_else(|| json!({})); + let artifact_hash = artifact + .pointer("/staged_release_elf/sha256") + .and_then(Value::as_str) + .map(|value| if value.starts_with("0x") { value.to_owned() } else { format!("0x{value}") }) + .map(Value::String) + .unwrap_or(Value::Null); + let gates = vec![ + gate( + "reference_bip340_vectors", + vectors.get("positive_self_verified").and_then(Value::as_u64).unwrap_or(0) > 0 + && equal_at(&vectors, "/positive_self_verified", "/positive_vectors") + && equal_at(&vectors, "/negative_self_rejected", "/negative_vectors"), + "target/novaseal-btc-verifier-vectors.json", + vectors, + ), + gate( + "fixed_ipc_vectors", + ipc.get("expected_accept").and_then(Value::as_u64).unwrap_or(0) > 0 + && ipc.get("expected_reject").and_then(Value::as_u64).unwrap_or(0) > 0 + && ipc.get("total_vectors").and_then(Value::as_u64).unwrap_or(0) + == ipc.get("expected_accept").and_then(Value::as_u64).unwrap_or(0) + + ipc.get("expected_reject").and_then(Value::as_u64).unwrap_or(0), + "target/novaseal-btc-verifier-ipc-vectors.json", + ipc, + ), + gate( + "riscv_shell_spawn_word_report", + bool_at(&shell, "/all_expected_matched") && equal_at(&shell, "/matched_expected", "/total_vectors"), + "target/novaseal-btc-verifier-shell-report.json", + shell, + ), + gate( + "riscv_artifact_preflight", + bool_at(&artifact, "/staged_matches_release") + && bool_at(&artifact, "/status/preflight_passed") + && bool_at(&artifact, "/status/ready_for_ckb_vm_dry_run"), + "target/novaseal-riscv-shell-artifact.json", + json!({ + "artifact_hash": artifact_hash, + "size_bytes": artifact.pointer("/staged_release_elf/size_bytes").cloned().unwrap_or(Value::Null), + "production_ready_claim": artifact.pointer("/status/production_ready").cloned().unwrap_or(Value::Null) + }), + ), + gate( + "child_verifier_ckb_vm", + bool_at(&child, "/child_verifier_ckb_vm_executed") + && equal_at(&child, "/matched_expected", "/total_cases") + && child.get("mismatched").and_then(Value::as_u64) == Some(0), + "target/novaseal-ckb-vm-child-verifier-report.json", + child, + ), + gate( + "parent_lock_spawn_ckb_vm", + bool_at(&parent, "/parent_spawn_executed") + && bool_at(&parent, "/child_verifier_ckb_vm_executed") + && bool_at(&parent, "/full_transaction_verifier_matched_expected") + && equal_at(&parent, "/matched_expected", "/total_cases"), + "target/novaseal-parent-lock-ckb-vm-report.json", + parent, + ), + gate( + "combined_lock_type_node_stack", + ((bool_at(&combined, "/ckb_node_verification_stack_executed") + && equal_at(&combined, "/node_stack_matched_expected", "/total_cases")) + || (bool_at(&combined, "/combined_full_transaction_executed") + && equal_at(&combined, "/matched_expected", "/total_cases") + && bool_at(&combined, "/lock_and_type_script_groups_present"))) + && bool_at(&combined, "/child_spawn_target_cell_dep0_modelled"), + "target/novaseal-combined-tx-report.json", + combined, + ), + gate( + "live_local_devnet_core_and_agreement", + core_live.get("status").and_then(Value::as_str) == Some("passed") + && bool_at(&core_live, "/live_devnet_rpc_executed") + && agreement_live.get("status").and_then(Value::as_str) == Some("passed") + && bool_at(&agreement_live, "/live_devnet_rpc_executed"), + "target/novaseal-devnet-stateful-live.json + target/novaseal-agreement-devnet-stateful-live.json", + json!({ + "core_status": core_live.get("status").cloned().unwrap_or(Value::Null), + "agreement_status": agreement_live.get("status").cloned().unwrap_or(Value::Null), + "core_verifier_data_hash": core_live.pointer("/artifacts/verifier/data_hash").cloned().unwrap_or(Value::Null), + "agreement_verifier_data_hash": agreement_live.pointer("/artifacts/verifier/data_hash").cloned().unwrap_or(Value::Null) + }), + ), + ]; + let verifier_dirs = [ + core.join("verifier/novaseal_btc_verifier_core"), + core.join("verifier/novaseal_btc_verifier_riscv"), + core.join("verifier/novaseal_btc_verifier"), + ]; + let inventory = source_inventory(root, &verifier_dirs)?; + let passed = gates.iter().all(|gate| gate["status"] == "passed") && inventory["valid"] == true; + let report = json!({ + "schema": "novaseal-bip340-tcb-review-v0.1", + "status": if passed { "passed_local_review_external_attestation_required" } else { "failed" }, + "repo_commit": git_commit(root), + "verifier_id": "btc.bip340.v0", + "ipc_abi": "cellscript-btc-bip340-ipc-v0", + "runtime_artifact": { + "name": "cellscript_btc_bip340_verifier_riscv", + "role": "runtime_verifier", + "artifact_hash": artifact_hash, + "artifact_hash_algorithm": "sha256", + "size_bytes": artifact.pointer("/staged_release_elf/size_bytes").cloned().unwrap_or(Value::Null) + }, + "local_review_gates": gates, + "source_inventory": inventory, + "tcb_boundary": { + "included": ["BIP340 verifier core", "RISC-V spawn/pipe/wait shell", "IPC envelope parser", "artifact hash used by NovaSeal manifests"], + "excluded": ["NovaSeal .cell protocol code", "CKB node implementation", "test harness Rust used only to construct evidence", "wallet UI implementation"] + }, + "external_review": { + "required_for_production": true, + "attestation_file": "proposals/novaseal/v0-mvp-skeleton/proofs/bip340_external_tcb_review_attestation.json", + "template": "proposals/novaseal/v0-mvp-skeleton/proofs/bip340_external_tcb_review_attestation.template.json", + "status": "missing_attestation" + } + }); + let default_output = target.join("novaseal-bip340-tcb-review.json"); + let output = lexical_path(output.unwrap_or(&default_output)); + fs::create_dir_all(output.parent().context("output path has no parent")?)?; + fs::write(&output, format!("{}\n", stable_json_pretty(&report)?))?; + if pretty { + println!( + "wrote {} status={} artifact={} local_gates={}", + output.display(), + report["status"].as_str().unwrap_or("failed"), + report.pointer("/runtime_artifact/artifact_hash").and_then(Value::as_str).unwrap_or("None"), + report["local_review_gates"].as_array().map_or(0, Vec::len) + ); + } + Ok(if passed { 0 } else { 1 }) +} diff --git a/crates/cellscript-tools/src/btc_anchor.rs b/crates/cellscript-tools/src/btc_anchor.rs new file mode 100644 index 00000000..e1a53259 --- /dev/null +++ b/crates/cellscript-tools/src/btc_anchor.rs @@ -0,0 +1,67 @@ +//! Shared NovaSeal BTC public-anchor shape checks. + +use std::collections::BTreeSet; + +use serde_json::{Map, Value}; + +use crate::crypto::nonzero_hex32; + +fn exact_keys(value: &Map, keys: &[&str]) -> bool { + value.keys().map(String::as_str).collect::>() == keys.iter().copied().collect::>() +} + +fn non_negative_integer(value: Option<&Value>) -> bool { + value.and_then(Value::as_i64).is_some_and(|number| number >= 0) || value.and_then(Value::as_u64).is_some() +} + +fn positive_integer(value: Option<&Value>) -> bool { + value.and_then(Value::as_i64).is_some_and(|number| number > 0) || value.and_then(Value::as_u64).is_some_and(|number| number > 0) +} + +pub fn public_btc_anchor_shape_matches_profile(profile: &str, anchor: Option<&Value>) -> bool { + let Some(anchor) = anchor.and_then(Value::as_object) else { + return false; + }; + if profile == "btc-transaction-commitment-profile-v0" { + return exact_keys( + anchor, + &["kind", "anchor_source", "btc_txid", "btc_wtxid", "btc_output_index", "btc_amount_sats", "ckb_btc_commitment_hash"], + ) && anchor.get("kind").and_then(Value::as_str) == Some("btc_transaction_commitment") + && anchor.get("anchor_source").and_then(Value::as_str).is_some_and(|source| !source.is_empty()) + && anchor.get("btc_txid").is_some_and(nonzero_hex32) + && anchor.get("btc_wtxid").is_some_and(nonzero_hex32) + && non_negative_integer(anchor.get("btc_output_index")) + && positive_integer(anchor.get("btc_amount_sats")) + && anchor.get("ckb_btc_commitment_hash").is_some_and(nonzero_hex32); + } + if matches!(profile, "btc-utxo-seal-profile-v0" | "dual-seal-profile-v0") { + let expected_kind = if profile == "btc-utxo-seal-profile-v0" { "btc_utxo_spend" } else { "dual_seal_btc_closure" }; + return exact_keys( + anchor, + &[ + "kind", + "anchor_source", + "sealed_btc_txid", + "sealed_btc_vout_index", + "sealed_btc_amount_sats", + "script_pubkey_hash", + "btc_txid", + "btc_wtxid", + "spend_input_index", + "ckb_btc_commitment_hash", + "sealed_utxo_commitment_hash", + ], + ) && anchor.get("kind").and_then(Value::as_str) == Some(expected_kind) + && anchor.get("anchor_source").and_then(Value::as_str).is_some_and(|source| !source.is_empty()) + && anchor.get("sealed_btc_txid").is_some_and(nonzero_hex32) + && non_negative_integer(anchor.get("sealed_btc_vout_index")) + && positive_integer(anchor.get("sealed_btc_amount_sats")) + && anchor.get("script_pubkey_hash").is_some_and(nonzero_hex32) + && anchor.get("btc_txid").is_some_and(nonzero_hex32) + && anchor.get("btc_wtxid").is_some_and(nonzero_hex32) + && non_negative_integer(anchor.get("spend_input_index")) + && anchor.get("ckb_btc_commitment_hash").is_some_and(nonzero_hex32) + && anchor.get("sealed_utxo_commitment_hash").is_some_and(nonzero_hex32); + } + false +} diff --git a/crates/cellscript-tools/src/btc_spv_adapter.rs b/crates/cellscript-tools/src/btc_spv_adapter.rs new file mode 100644 index 00000000..03d71586 --- /dev/null +++ b/crates/cellscript-tools/src/btc_spv_adapter.rs @@ -0,0 +1,276 @@ +//! Rust port of the NovaSeal public BTC SPV evidence adapter request. + +use std::fs; +use std::path::Path; + +use anyhow::{Context, Result}; +use serde_json::{json, Value}; + +use crate::crypto::canonical_report_hash; +use crate::shared::{lexical_path, stable_json_pretty}; + +const PERSON: &[u8] = b"NovaBtcSpvReqV0"; +const PROFILES: [&str; 3] = ["btc-transaction-commitment-profile-v0", "btc-utxo-seal-profile-v0", "dual-seal-profile-v0"]; + +fn scenario(profile: &str) -> &'static str { + match profile { + "btc-transaction-commitment-profile-v0" => "btc-transaction-commitment-transition", + "btc-utxo-seal-profile-v0" => "btc-utxo-seal-closure", + _ => "dual-seal-finality", + } +} + +fn production_anchor(profile: &str) -> &'static str { + if profile == "btc-transaction-commitment-profile-v0" { + "external_public_btc_transaction" + } else { + "external_public_btc_spend" + } +} + +fn hash(label: &str, value: &Value) -> Result { + canonical_report_hash(PERSON, label, value) +} + +fn hex32(value: &Value) -> bool { + value.as_str().is_some_and(|text| { + text.len() == 66 && text.starts_with("0x") && text[2..].chars().all(|character| character.is_ascii_hexdigit()) + }) +} + +fn non_negative(value: &Value) -> bool { + value.as_i64().is_some_and(|number| number >= 0) || value.as_u64().is_some() +} + +fn positive(value: &Value) -> bool { + value.as_i64().is_some_and(|number| number > 0) || value.as_u64().is_some_and(|number| number > 0) +} + +fn truthy(value: &Value) -> bool { + match value { + Value::Null | Value::Bool(false) => false, + Value::String(text) => !text.is_empty(), + Value::Array(values) => !values.is_empty(), + Value::Object(values) => !values.is_empty(), + Value::Number(number) => number.as_f64().is_some_and(|number| number != 0.0), + Value::Bool(true) => true, + } +} + +pub(crate) fn required_fields() -> Value { + json!([ + "network", + "generated_at", + "evidence_provider", + "required_profiles", + "profile", + "scenario", + "ckb_live_tx_hash", + "live_report_hash", + "service_builder_case_hash", + "service_builder_tx_skeleton_hash", + "service_builder_receipt_binding_hash", + "ckb_btc_commitment_hash", + "btc_txid", + "btc_wtxid", + "btc_tx_hex", + "btc_block_hash", + "btc_block_header", + "btc_merkle_proof.tx_index", + "btc_merkle_proof.merkle_branch", + "btc_merkle_proof.merkle_root", + "btc_merkle_proof.block_height", + "btc_merkle_proof.observed_tip_height", + "btc_transaction_binding.kind", + "btc_transaction_binding.btc_output_index", + "btc_transaction_binding.btc_amount_sats", + "btc_transaction_binding.spend_input_index", + "btc_transaction_binding.sealed_btc_txid", + "btc_transaction_binding.sealed_btc_vout_index", + "btc_transaction_binding.sealed_btc_amount_sats", + "btc_transaction_binding.script_pubkey_hash", + "btc_transaction_binding.sealed_btc_tx_hex", + "btc_transaction_binding.sealed_utxo_commitment_hash", + "spv_proof_hash", + "minimum_confirmations", + "confirmations", + "spv_client_cell_dep.out_point", + "spv_client_cell_dep.data_hash", + "spv_client_cell_dep.dep_type", + "spv_client_cell_dep.hash_type", + "source_service.name", + "source_service.commit", + "source_service.report_hash", + "request_handoff.bundle", + "request_handoff.bundle_hash", + "request_handoff.bundle_hash_algorithm", + "request_handoff.group" + ]) +} + +pub(crate) fn field_constraints() -> Value { + json!({ + "network": "explicit public mainnet/testnet name; placeholders and local/devnet/regtest/simnet/private/fake labels are rejected", + "generated_at": "UTC timestamp in YYYY-MM-DDTHH:MM:SSZ form; future timestamps are rejected", + "evidence_provider": "real external provider identity; placeholder, first-party NovaSeal/CellScript/a19q3, local/devnet/fake/internal, example, and unknown tokens are rejected", + "ckb_live_tx_hash": "0x-prefixed 32-byte CKB live transaction hash matching the current NovaSeal service-builder case", + "live_report_hash": "0x-prefixed 32-byte hash of the current NovaSeal live devnet report for this profile", + "service_builder_case_hash": "0x-prefixed 32-byte hash of the current NovaSeal service-builder case for this profile", + "service_builder_tx_skeleton_hash": "0x-prefixed 32-byte service-builder transaction skeleton hash for this profile", + "service_builder_receipt_binding_hash": "0x-prefixed 32-byte service-builder receipt binding hash for this profile", + "ckb_btc_commitment_hash": "0x-prefixed 32-byte CKB-side BTC commitment hash from the current live profile report", + "btc_txid": "0x-prefixed 32-byte non-placeholder Bitcoin transaction id", + "btc_wtxid": "0x-prefixed 32-byte Bitcoin witness transaction id derived from btc_tx_hex", + "btc_tx_hex": "0x-prefixed raw Bitcoin transaction bytes whose txid/wtxid match the public evidence case", + "btc_block_hash": "0x-prefixed 32-byte non-placeholder Bitcoin block hash anchoring the SPV proof", + "btc_block_header": "0x-prefixed 80-byte Bitcoin block header whose double-SHA256 hash matches btc_block_hash", + "btc_merkle_proof.tx_index": "zero-based transaction index used to orient the Merkle branch", + "btc_merkle_proof.merkle_branch": "array of 0x-prefixed 32-byte Bitcoin sibling hashes in display order; empty only for tx_index 0 in a single-transaction block", + "btc_merkle_proof.merkle_root": "0x-prefixed 32-byte Bitcoin Merkle root matching the block header", + "btc_merkle_proof.block_height": "public Bitcoin block height containing btc_txid", + "btc_merkle_proof.observed_tip_height": "public Bitcoin tip height used to compute confirmations", + "btc_transaction_binding.kind": "profile-specific binding kind: btc_transaction_output, btc_utxo_spend, or dual_seal_btc_closure", + "btc_transaction_binding.btc_output_index": "BTC transaction commitment output index; required for btc-transaction-commitment-profile-v0", + "btc_transaction_binding.btc_amount_sats": "BTC transaction commitment output amount in sats; required for btc-transaction-commitment-profile-v0", + "btc_transaction_binding.spend_input_index": "Bitcoin spend input index; required for UTXO and dual-seal closure profiles", + "btc_transaction_binding.sealed_btc_txid": "sealed Bitcoin transaction id whose output is spent; required for btc-utxo-seal-profile-v0 and dual-seal-profile-v0", + "btc_transaction_binding.sealed_btc_vout_index": "sealed Bitcoin output index; required for btc-utxo-seal-profile-v0 and dual-seal-profile-v0", + "btc_transaction_binding.sealed_btc_amount_sats": "sealed Bitcoin output amount in sats; required for btc-utxo-seal-profile-v0 and dual-seal-profile-v0", + "btc_transaction_binding.script_pubkey_hash": "0x-prefixed CKB Blake2b-256 hash of the sealed output scriptPubKey bytes; required for btc-utxo-seal-profile-v0 and dual-seal-profile-v0", + "btc_transaction_binding.sealed_btc_tx_hex": "0x-prefixed raw sealed Bitcoin transaction bytes; required for btc-utxo-seal-profile-v0 and dual-seal-profile-v0", + "btc_transaction_binding.sealed_utxo_commitment_hash": "0x-prefixed 32-byte CKB-side sealed UTXO commitment hash; required for btc-utxo-seal-profile-v0 and dual-seal-profile-v0", + "spv_proof_hash": "0x-prefixed SHA-256 hash of the canonical BTC SPV proof material carried in this case", + "minimum_confirmations": "integer confirmation floor; at least 6", + "confirmations": "integer observed confirmations meeting minimum_confirmations", + "spv_client_cell_dep.out_point": "0x-prefixed 32-byte CKB transaction hash plus numeric output index", + "spv_client_cell_dep.data_hash": "0x-prefixed 32-byte non-placeholder SPV client data hash", + "spv_client_cell_dep.dep_type": "code", + "spv_client_cell_dep.hash_type": "data, data1, or type CKB script hash type", + "source_service.name": "real external SPV service identity; placeholder, first-party NovaSeal/CellScript/a19q3, local/devnet/fake/internal, example, and unknown tokens are rejected", + "source_service.commit": "40-character hex service source commit", + "source_service.report_hash": "0x-prefixed 32-byte non-placeholder SPV service report hash", + "request_handoff.bundle": "target/novaseal-external-evidence-handoff-bundle.json", + "request_handoff.bundle_hash": "0x-prefixed 32-byte hash of the NovaSeal external evidence handoff bundle", + "request_handoff.bundle_hash_algorithm": "blake2b-256(person=NovaExtHandoff)", + "request_handoff.group": "public_btc_spv_evidence" + }) +} + +fn find_profile<'a>(cases: Option<&'a Vec>, profile: &str) -> Option<&'a Value> { + cases?.iter().find(|case| case.get("profile").and_then(Value::as_str) == Some(profile)) +} + +fn profile_cases(service: &Value, template: &Value) -> Result> { + let builder_cases = service.get("cases").and_then(Value::as_array); + let template_cases = template.get("cases").and_then(Value::as_array); + let mut cases = Vec::new(); + for profile in PROFILES { + let builder = find_profile(builder_cases, profile); + let template_case = find_profile(template_cases, profile); + let external_inputs = builder + .and_then(|case| case.pointer("/request/production_external_inputs")) + .and_then(Value::as_array) + .cloned() + .unwrap_or_default(); + let live_inputs = builder.and_then(|case| case.pointer("/request/required_live_inputs")).cloned().unwrap_or_else(|| json!({})); + let anchor = live_inputs.get("public_btc_anchor").filter(|value| value.is_object()).cloned().unwrap_or_else(|| json!({})); + let builder_value = builder.cloned().unwrap_or(Value::Null); + let template_value = template_case.cloned().unwrap_or(Value::Null); + let request = json!({ + "profile": profile, + "scenario": template_case.and_then(|case| case.get("scenario")).cloned().unwrap_or(Value::Null), + "minimum_confirmations": template_case.and_then(|case| case.get("minimum_confirmations")).cloned().unwrap_or(Value::from(6)), + "required_public_fields": required_fields(), + "field_constraints": field_constraints(), + "required_external_inputs": external_inputs, + "ckb_live_tx_hash": live_inputs.get("live_devnet_tx_hash").cloned().unwrap_or(Value::Null), + "live_report_hash": live_inputs.get("live_report_hash").cloned().unwrap_or(Value::Null), + "service_builder_case_hash": hash("service_builder_case", &builder_value)?, + "service_builder_tx_skeleton_hash": builder.and_then(|case| case.pointer("/response/tx_skeleton_hash")).cloned().unwrap_or(Value::Null), + "service_builder_receipt_binding_hash": builder.and_then(|case| case.pointer("/response/receipt_binding_hash")).cloned().unwrap_or(Value::Null), + "local_anchor_source": anchor.get("anchor_source").cloned().unwrap_or(Value::Null), + "expected_anchor_source": production_anchor(profile), + "ckb_btc_commitment_hash": anchor.get("ckb_btc_commitment_hash").cloned().unwrap_or(Value::Null), + "expected_btc_txid": anchor.get("btc_txid").cloned().unwrap_or(Value::Null), + "expected_btc_wtxid": anchor.get("btc_wtxid").cloned().unwrap_or(Value::Null), + "expected_btc_output_index": anchor.get("btc_output_index").cloned().unwrap_or(Value::Null), + "expected_btc_amount_sats": anchor.get("btc_amount_sats").cloned().unwrap_or(Value::Null), + "expected_sealed_btc_txid": anchor.get("sealed_btc_txid").cloned().unwrap_or(Value::Null), + "expected_sealed_btc_vout_index": anchor.get("sealed_btc_vout_index").cloned().unwrap_or(Value::Null), + "expected_sealed_btc_amount_sats": anchor.get("sealed_btc_amount_sats").cloned().unwrap_or(Value::Null), + "expected_script_pubkey_hash": anchor.get("script_pubkey_hash").cloned().unwrap_or(Value::Null), + "expected_spend_input_index": anchor.get("spend_input_index").cloned().unwrap_or(Value::Null), + "expected_sealed_utxo_commitment_hash": anchor.get("sealed_utxo_commitment_hash").cloned().unwrap_or(Value::Null), + "template_case_hash": hash("template_case", &template_value)? + }); + let transaction = profile == PROFILES[0]; + let utxo = profile == PROFILES[1]; + let dual = profile == PROFILES[2]; + let utxo_fields = hex32(&request["expected_sealed_btc_txid"]) + && non_negative(&request["expected_sealed_btc_vout_index"]) + && positive(&request["expected_sealed_btc_amount_sats"]) + && hex32(&request["expected_script_pubkey_hash"]) + && non_negative(&request["expected_spend_input_index"]) + && hex32(&request["expected_sealed_utxo_commitment_hash"]); + let checks = json!({ + "service_builder_case_present": builder.is_some(), + "template_case_present": template_case.is_some(), + "scenario_matches_required_profile": request["scenario"] == scenario(profile), + "public_btc_spv_external_input_named": request["required_external_inputs"].as_array().is_some_and(|items| items.iter().any(|item| item == "public_btc_spv_evidence")), + "minimum_confirmations_at_least_six": non_negative(&request["minimum_confirmations"]) && request["minimum_confirmations"].as_u64().unwrap_or(0) >= 6, + "live_binding_hashes_present": hex32(&request["ckb_live_tx_hash"]) && hex32(&request["live_report_hash"]), + "service_builder_hashes_present": hex32(&request["service_builder_tx_skeleton_hash"]) && hex32(&request["service_builder_receipt_binding_hash"]), + "expected_anchor_source_production_eligible": request["expected_anchor_source"] == production_anchor(profile), + "local_anchor_source_present": truthy(&request["local_anchor_source"]), + "ckb_btc_commitment_hash_present": hex32(&request["ckb_btc_commitment_hash"]), + "expected_btc_txid_present": hex32(&request["expected_btc_txid"]), + "expected_btc_wtxid_present": hex32(&request["expected_btc_wtxid"]), + "expected_output_fields_present": !transaction || (non_negative(&request["expected_btc_output_index"]) && positive(&request["expected_btc_amount_sats"])), + "expected_utxo_fields_present": !utxo || utxo_fields, + "expected_dual_sealed_utxo_fields_present": !dual || utxo_fields, + "required_public_fields_complete": request["required_public_fields"].as_array().is_some_and(|fields| fields.len() == 46) + }); + let passed = checks.as_object().is_some_and(|map| map.values().all(|value| value == &Value::Bool(true))); + cases.push( + json!({ "profile": profile, "status": if passed { "passed" } else { "failed" }, "checks": checks, "request": request }), + ); + } + Ok(cases) +} + +pub fn run(root: &Path, service_builder: Option<&Path>, template: Option<&Path>, output: Option<&Path>, pretty: bool) -> Result { + let default_service = root.join("target/novaseal-service-builder-fixtures.json"); + let default_template = root.join("proposals/novaseal/v0-mvp-skeleton/proofs/public_btc_spv_evidence.template.json"); + let default_output = root.join("target/novaseal-btc-spv-evidence-adapter.json"); + let service = serde_json::from_slice::(&fs::read(lexical_path(service_builder.unwrap_or(&default_service)))?)?; + let template = serde_json::from_slice::(&fs::read(lexical_path(template.unwrap_or(&default_template)))?)?; + let cases = profile_cases(&service, &template)?; + let matched = cases.iter().filter(|case| case["status"] == "passed").count(); + let passed = matched == cases.len(); + let report = json!({ + "schema": "novaseal-btc-spv-evidence-adapter-v0.1", + "status": if passed { "passed" } else { "failed" }, + "adapter_status": "request_ready_external_evidence_required", + "source_service_builder_report": "target/novaseal-service-builder-fixtures.json", + "source_service_builder_report_hash": hash("service_builder_report", &service)?, + "source_public_btc_spv_template": "proposals/novaseal/v0-mvp-skeleton/proofs/public_btc_spv_evidence.template.json", + "source_public_btc_spv_template_hash": hash("public_btc_spv_template", &template)?, + "production_output": "proposals/novaseal/v0-mvp-skeleton/proofs/public_btc_spv_evidence.json", + "production_boundary": "This adapter proves the request contract is complete; it does not prove BTC inclusion, spend validity, confirmation depth, or public SPV client deployment.", + "summary": { "total": cases.len(), "matched": matched, "required_profiles": PROFILES }, + "cases": cases + }); + let output = lexical_path(output.unwrap_or(&default_output)); + fs::create_dir_all(output.parent().context("output path has no parent")?)?; + fs::write(&output, format!("{}\n", stable_json_pretty(&report)?))?; + if pretty { + println!( + "wrote {} status={} profiles={}/{}", + output.display(), + report["status"].as_str().unwrap_or("failed"), + matched, + report["summary"]["total"] + ); + } + Ok(if passed { 0 } else { 1 }) +} diff --git a/crates/cellscript-tools/src/ckb_acceptance.rs b/crates/cellscript-tools/src/ckb_acceptance.rs new file mode 100644 index 00000000..6205aeaf --- /dev/null +++ b/crates/cellscript-tools/src/ckb_acceptance.rs @@ -0,0 +1,634 @@ +use std::collections::BTreeSet; +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +use anyhow::{bail, Context, Result}; +use serde_json::{json, Map, Value}; +use sha2::{Digest, Sha256}; +use time::format_description::well_known::Rfc3339; +use time::OffsetDateTime; + +use crate::ckb_devnet::{ckb_hash_hex, sha256_hex}; +use crate::production_evidence::{ + self, ACTION_RUNS, BUILD_REPORT_SCHEMA, EXPECTED_CRITICAL_ELF_ABI_EXAMPLES, EXPECTED_EXAMPLES, EXPECTED_LANGUAGE_EXAMPLES, + EXPECTED_NON_PRODUCTION_EXAMPLES, LOCKS, PUBLIC_TIMELOCK_ACTIONS, SOURCE_PROVENANCE_SCHEMA, +}; + +const PROFILE_TRAILER: &[u8] = b"SPORABI\0"; +const TRAMPOLINE: [u8; 20] = hex_literal::hex!("97000000e7804001b70800009388d80573000000"); + +#[derive(Clone)] +pub(crate) struct ArtifactRecord { + pub name: String, + pub kind: String, + pub example: Option, + pub entry: Option, + pub entry_flag: Option, + pub source: PathBuf, + pub path: PathBuf, + pub bytes: Vec, + pub data_hash: String, + pub sha256: String, + pub abi: Value, +} + +pub(crate) struct CompileEvidence { + pub report: Value, + pub artifacts: Vec, + pub report_path: PathBuf, + pub run_dir: PathBuf, +} + +fn command_output(command: &mut Command, label: &str) -> Result { + let output = command.output().with_context(|| format!("failed to run {label}"))?; + if !output.status.success() { + bail!( + "{label} failed with {}\nstdout:\n{}\nstderr:\n{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + Ok(output) +} + +fn git_stdout(root: &Path, args: &[&str]) -> Result { + let output = command_output(Command::new("git").args(args).current_dir(root), "git source query")?; + Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned()) +} + +fn read_u16(bytes: &[u8], offset: usize) -> Result { + Ok(u16::from_le_bytes(bytes.get(offset..offset + 2).context("truncated ELF u16")?.try_into()?)) +} + +fn read_u32(bytes: &[u8], offset: usize) -> Result { + Ok(u32::from_le_bytes(bytes.get(offset..offset + 4).context("truncated ELF u32")?.try_into()?)) +} + +fn read_u64(bytes: &[u8], offset: usize) -> Result { + Ok(u64::from_le_bytes(bytes.get(offset..offset + 8).context("truncated ELF u64")?.try_into()?)) +} + +fn audit_elf(name: &str, bytes: &[u8]) -> Result { + if bytes.len() < 64 || &bytes[..4] != b"\x7fELF" || bytes[4] != 2 || bytes[5] != 1 { + bail!("{name} is not a little-endian ELF64 artifact"); + } + if read_u16(bytes, 18)? != 243 { + bail!("{name} is not an ELF RISC-V artifact"); + } + if bytes[bytes.len().saturating_sub(64)..].windows(PROFILE_TRAILER.len()).any(|window| window == PROFILE_TRAILER) { + bail!("{name} contains the forbidden profile trailer"); + } + let entry = read_u64(bytes, 24)?; + let program_offset = read_u64(bytes, 32)? as usize; + let program_size = read_u16(bytes, 54)? as usize; + let program_count = read_u16(bytes, 56)? as usize; + let mut executable = None; + for index in 0..program_count { + let offset = program_offset + index * program_size; + if read_u32(bytes, offset)? != 1 { + continue; + } + let flags = read_u32(bytes, offset + 4)?; + let file_offset = read_u64(bytes, offset + 8)?; + let virtual_address = read_u64(bytes, offset + 16)?; + let file_size = read_u64(bytes, offset + 32)?; + let memory_size = read_u64(bytes, offset + 40)?; + if flags & 1 != 0 && entry >= virtual_address && entry < virtual_address + memory_size { + executable = Some((index, flags, file_offset, virtual_address, file_size, memory_size)); + break; + } + } + let (index, flags, file_offset, virtual_address, file_size, memory_size) = + executable.with_context(|| format!("{name} has no executable load segment containing its entry point"))?; + if flags != 5 || file_size != memory_size { + bail!("{name} executable segment must be RX-only with equal file/memory size"); + } + let entry_offset = (file_offset + entry - virtual_address) as usize; + let trampoline = bytes.get(entry_offset..entry_offset + TRAMPOLINE.len()).context("truncated ELF entry trampoline")?; + if trampoline != TRAMPOLINE { + bail!("{name} has an unexpected CKB entry trampoline: 0x{}", hex::encode(trampoline)); + } + Ok(json!({ + "schema": "cellscript-ckb-elf-entry-abi-v0.22", + "status": "passed", + "entry_point": format!("0x{entry:x}"), + "executable_load_segment": { + "index": index, "flags": flags, "flags_symbolic": "R|X", "writable": false, + "file_offset": file_offset, "virtual_address": format!("0x{virtual_address:x}"), + "file_size": file_size, "memory_size": memory_size, "file_size_equals_memory_size": true + }, + "trampoline": { + "size_bytes": TRAMPOLINE.len(), "entry_file_offset": entry_offset, + "bytes_hex": hex::encode(trampoline), + "instructions_le_hex": ["0x00000097", "0x014080e7", "0x000008b7", "0x05d88893", "0x00000073"], + "first_instruction_le_hex": "0x00000097", "first_instruction_opcode": "auipc", "first_instruction_rd": "ra", + "call_instruction_opcode": "jalr", "call_target": format!("0x{:x}", entry + 20), + "expected_call_target": format!("0x{:x}", entry + 20), "exit_syscall_number": 93, + "exit_sequence_exact": true, "calls_entry_with_ra": true, + "preserves_ckb_vm_stack_pointer": true, "forbidden_sp_initialisation": false + } + })) +} + +fn example_build_path(root: &Path, example: &str) -> PathBuf { + let package = root.join("examples").join(example.trim_end_matches(".cell")); + if package.join("Cell.toml").is_file() { + package + } else { + root.join("examples").join(example) + } +} + +#[allow(clippy::too_many_arguments)] +fn compile_artifact( + cellc: &Path, + source: &Path, + output: &Path, + name: &str, + kind: &str, + example: Option<&str>, + entry_flag: Option<&str>, + entry: Option<&str>, +) -> Result { + let mut command = Command::new(cellc); + command.arg(source).args(["--target-profile", "ckb", "--target", "riscv64-elf", "--primitive-strict", "0.16"]); + if let (Some(flag), Some(value)) = (entry_flag, entry) { + command.args([flag, value]); + } + command.arg("-o").arg(output); + for key in ["CELLSCRIPT_RISCV_CC", "CELLSCRIPT_RISCV_AS", "CELLSCRIPT_RISCV_LD"] { + command.env_remove(key); + } + command_output(&mut command, &format!("compile {name}"))?; + let metadata = PathBuf::from(format!("{}.meta.json", output.display())); + if !metadata.is_file() { + bail!("compile {name} did not emit {}", metadata.display()); + } + let verify = command_output( + Command::new(cellc).arg("verify-artifact").arg(output).args(["--expect-target-profile", "ckb", "--json"]), + &format!("verify {name}"), + )?; + let verify: Value = serde_json::from_slice(&verify.stdout).with_context(|| format!("invalid verify JSON for {name}"))?; + if verify["target_profile"] != "ckb" { + bail!("verify-artifact did not bind {name} to target_profile=ckb"); + } + let bytes = fs::read(output)?; + let abi = audit_elf(name, &bytes)?; + Ok(ArtifactRecord { + name: name.to_owned(), + kind: kind.to_owned(), + example: example.map(str::to_owned), + entry: entry.map(str::to_owned), + entry_flag: entry_flag.map(str::to_owned), + source: source.to_path_buf(), + path: output.to_path_buf(), + data_hash: ckb_hash_hex(&bytes), + sha256: sha256_hex(&bytes), + bytes, + abi, + }) +} + +fn build_cellc(root: &Path) -> Result { + let target = env::var_os("CELLSCRIPT_CELLC_TARGET_DIR").map(PathBuf::from).unwrap_or_else(|| root.join("target/cellscript-cellc")); + command_output( + Command::new("cargo") + .args(["build", "--locked", "--manifest-path"]) + .arg(root.join("Cargo.toml")) + .args(["--bin", "cellc", "--target-dir"]) + .arg(&target), + "build cellc", + )?; + let binary = target.join("debug/cellc"); + if !binary.is_file() { + bail!("cellc build succeeded but {} is missing", binary.display()); + } + Ok(binary) +} + +fn recursive_files(root: &Path) -> Result> { + fn visit(path: &Path, out: &mut Vec) -> Result<()> { + let mut entries = fs::read_dir(path)?.collect::, _>>()?; + entries.sort_by_key(std::fs::DirEntry::file_name); + for entry in entries { + let path = entry.path(); + if path.is_dir() { + visit(&path, out)?; + } else if path.is_file() { + out.push(path); + } + } + Ok(()) + } + let mut files = Vec::new(); + visit(root, &mut files)?; + files.sort(); + Ok(files) +} + +fn builder_contracts(root: &Path, cellc: &Path, run_dir: &Path) -> Result { + let builder_root = run_dir.join("public-builders"); + let mut contracts = Vec::new(); + for example in EXPECTED_EXAMPLES { + let matrix_actions = ACTION_RUNS + .iter() + .find(|(_, candidate, _)| candidate == example) + .map(|(_, _, actions)| *actions) + .with_context(|| format!("missing production action matrix for {example}"))?; + let actions = if *example == "timelock.cell" { PUBLIC_TIMELOCK_ACTIONS } else { matrix_actions }; + let source = root.join("examples").join(example); + let output = builder_root.join(example.trim_end_matches(".cell")); + let package_name = format!("@cellscript-acceptance/{}", example.trim_end_matches(".cell")); + let generated = command_output( + Command::new(cellc) + .arg("gen-builder") + .arg(&source) + .args(["--target", "typescript", "--target-profile", "ckb", "--output"]) + .arg(&output) + .args(["--package-name", &package_name, "--json"]), + &format!("gen-builder {example}"), + )?; + let summary: Value = serde_json::from_slice(&generated.stdout)?; + let manifest_path = output.join("cellscript-builder-manifest.json"); + let manifest: Value = serde_json::from_slice(&fs::read(&manifest_path)?)?; + let manifest_actions = manifest["actions"] + .as_array() + .context("builder manifest actions missing")? + .iter() + .map(|row| row["name"].as_str().unwrap_or_default()) + .collect::>(); + if manifest_actions != *actions || summary["actions"] != json!(actions) { + bail!("generated builder actions for {example} do not match the production matrix"); + } + let plan_dir = output.join("action-plans"); + fs::create_dir_all(&plan_dir)?; + let mut action_plans = Vec::new(); + for action in actions { + let plan_path = plan_dir.join(format!("{action}.json")); + command_output( + Command::new(cellc) + .args(["action", "build"]) + .arg(&source) + .args(["--action", action, "--target-profile", "ckb", "--output"]) + .arg(&plan_path), + &format!("action build {example}:{action}"), + )?; + let plan: Value = serde_json::from_slice(&fs::read(&plan_path)?)?; + if plan["status"] != "ok" || plan["policy"] != "cellscript-action-builder-plan-v1" || plan["action"] != *action { + bail!("invalid action plan for {example}:{action}"); + } + action_plans.push(json!({ + "action": action, "contract_id": format!("{example}:{action}"), + "policy": "cellscript-action-builder-plan-v1", "artifact_hash": plan["artifact_hash"], + "plan_path": plan_path, "plan_sha256": sha256_hex(&fs::read(&plan_path)?), "status": "passed" + })); + } + let files = recursive_files(&output)?; + let mut digest = Sha256::new(); + for path in &files { + let relative = path.strip_prefix(&output)?.to_string_lossy().replace('\\', "/"); + digest.update(relative.as_bytes()); + digest.update([0]); + digest.update(Sha256::digest(fs::read(path)?)); + } + contracts.push(json!({ + "example": example, "source": source, "status": "passed", + "generator_schema": summary["schema"], "builder_manifest_schema": manifest["schema"], + "target": summary["target"], "target_profile": manifest["target_profile"], + "actions": actions, "action_count": actions.len(), "manifest_path": manifest_path, + "manifest_sha256": sha256_hex(&fs::read(output.join("cellscript-builder-manifest.json"))?), + "generated_tree_sha256": format!("0x{}", hex::encode(digest.finalize())), + "generated_file_count": files.len(), "action_plans": action_plans, + "runtime_adapter_execution": "not-proven-by-this-contract-gate" + })); + } + Ok(json!({ + "schema": "cellscript-public-builder-contract-gate-v0.22", "status": "passed", + "example_count": contracts.len(), "action_count": 43, + "requires_gen_builder": true, "requires_action_build": true, + "transaction_origin_claim": "acceptance-rust-harness-not-generated-builder", "contracts": contracts + })) +} + +fn source_provenance(root: &Path) -> Result { + let mut current = production_evidence::current_source_provenance(root)?; + current.insert("schema".into(), json!(SOURCE_PROVENANCE_SCHEMA)); + current.insert("generated_at_utc".into(), json!(OffsetDateTime::now_utc().format(&Rfc3339)?)); + Ok(Value::Object(current)) +} + +fn elf_gate(artifacts: &[ArtifactRecord]) -> Value { + let rows = artifacts + .iter() + .map(|artifact| { + let trampoline = &artifact.abi["trampoline"]; + json!({ + "name": artifact.name, "kind": artifact.kind, "source": artifact.source, + "example": artifact.example, "artifact": artifact.path, "status": "passed", + "preserves_ckb_vm_stack_pointer": true, "entry_trampoline_calls_with_ra": true, + "executable_segment_rx_only": true, "executable_segment_file_size_equals_memory_size": true, + "first_instruction_le_hex": trampoline["first_instruction_le_hex"], + "trampoline_bytes_hex": trampoline["bytes_hex"], + "trampoline_instructions_le_hex": trampoline["instructions_le_hex"], + "call_target": trampoline["call_target"], "expected_call_target": trampoline["expected_call_target"], + "exit_syscall_number": 93, "exit_sequence_exact": true, "entry_point": artifact.abi["entry_point"] + }) + }) + .collect::>(); + let mut critical = Map::new(); + for example in EXPECTED_CRITICAL_ELF_ABI_EXAMPLES { + let names = + artifacts.iter().filter(|row| row.example.as_deref() == Some(*example)).map(|row| row.name.clone()).collect::>(); + critical.insert( + (*example).into(), + json!({"status":"passed", "artifact_count":names.len(), "audited_artifacts":names, "missing":false, "failures":[]}), + ); + } + json!({ + "schema":"cellscript-ckb-elf-entry-abi-gate-v0.22", "status":"passed", + "requires_ckb_vm_stack_pointer_preserved":true, "requires_entry_trampoline_call_sequence":true, + "requires_rx_only_executable_segment":true, "requires_no_fake_stack_load_segment":true, + "critical_examples":EXPECTED_CRITICAL_ELF_ABI_EXAMPLES, "critical_example_gate":critical, + "audited_artifact_count":rows.len(), "failures":[], "rows":rows + }) +} + +fn build_reports(artifacts: &[ArtifactRecord]) -> Value { + let rows = artifacts + .iter() + .map(|artifact| { + json!({ + "schema": BUILD_REPORT_SCHEMA, "name":artifact.name, "kind":artifact.kind, + "source":artifact.source, "original_source":artifact.example.as_ref().map(|name| format!("examples/{name}")), + "example":artifact.example, "entry_flag":artifact.entry_flag, "entry":artifact.entry, + "target_profile":"ckb", "vm_profile":"ckb-vm", "artifact_format":"riscv64-elf", + "artifact_path":artifact.path, "metadata_sidecar":format!("{}.meta.json", artifact.path.display()), + "artifact_packaging":"ckb-elf", "artifact_size_bytes":artifact.bytes.len(), + "artifact_hash_algorithm":"ckb-blake2b256", "deployable_elf_hash":artifact.data_hash, + "artifact_sha256":artifact.sha256, "deployment_hash_type_used_by_gate":"data1", + "verify_artifact_status":"passed", "verify_target_profile":"ckb", "elf_entry_abi_status":"passed", + "abi_trailer_stripped":true, "onchain_deployments":[] + }) + }) + .collect::>(); + json!({ + "schema":"cellscript-ckb-build-report-index-v0.20", "status":"passed", "artifact_count":rows.len(), + "artifact_hash_algorithm":"ckb-blake2b256", "artifact_format":"riscv64-elf", "target_profile":"ckb", + "vm_profile":"ckb-vm", "requires_exact_artifact_hash":true, "requires_elf_entry_abi_gate":true, + "requires_live_code_cell_data_hash_match":true, "reports":rows + }) +} + +fn expected_lock_scope() -> Value { + let mut result = Map::new(); + for (example, locks) in LOCKS { + result.insert((*example).to_owned(), json!(locks)); + } + Value::Object(result) +} + +pub(crate) fn business_coverage(full: bool) -> Value { + let rows = ACTION_RUNS + .iter() + .map(|(_, example, actions)| { + let locks = LOCKS.iter().find(|(candidate, _)| candidate == example).map(|(_, locks)| *locks).unwrap_or(&[]); + json!({ + "example":example, "source_actions":actions, "source_locks":locks, + "strict_ckb_actions":actions, "strict_ckb_locks":locks, + "expected_fail_closed_actions":[], "expected_fail_closed_locks":[], + "ckb_onchain_actions":if full { json!(actions) } else { json!([]) }, + "missing_strict_ckb_actions":[], "missing_strict_ckb_locks":[], + "missing_ckb_onchain_actions":if full { json!([]) } else { json!(actions) }, + "strict_action_coverage_complete":true, "strict_lock_coverage_complete":true, + "ckb_onchain_action_coverage_complete":full + }) + }) + .collect::>(); + json!({ + "status":if full {"complete"} else {"incomplete"}, "strict_compile_coverage_complete":true, + "onchain_action_coverage_complete":full, "source_action_count":43, "source_lock_count":17, + "strict_ckb_action_count":43, "strict_ckb_lock_count":17, + "expected_fail_closed_action_count":0, "expected_fail_closed_lock_count":0, + "ckb_onchain_action_count":if full {43} else {0}, + "missing_strict_ckb_actions":{}, "missing_strict_ckb_locks":{}, + "missing_ckb_onchain_actions":if full { json!({}) } else { json!(ACTION_RUNS.iter().map(|(_, example, actions)| ((*example).to_owned(), json!(actions))).collect::>()) }, + "rows":rows + }) +} + +fn compile_matrix(root: &Path, cellc: &Path, run_dir: &Path) -> Result> { + let artifact_root = run_dir.join("artifacts"); + fs::create_dir_all(&artifact_root)?; + let mut artifacts = Vec::new(); + for example in EXPECTED_EXAMPLES { + let source = example_build_path(root, example); + artifacts.push(compile_artifact( + cellc, + &source, + &artifact_root.join(format!("{}.strict.elf", example)), + example, + "bundled-example-strict-original", + Some(example), + None, + None, + )?); + } + for (_, example, actions) in ACTION_RUNS { + let source = example_build_path(root, example); + for action in *actions { + artifacts.push(compile_artifact( + cellc, + &source, + &artifact_root.join(format!("original_{}_{}.elf", example.trim_end_matches(".cell"), action)), + &format!("{example}:{action}"), + "original-scoped-action-strict", + Some(example), + Some("--entry-action"), + Some(action), + )?); + } + } + for (example, locks) in LOCKS { + let source = example_build_path(root, example); + for lock in *locks { + artifacts.push(compile_artifact( + cellc, + &source, + &artifact_root.join(format!("original_{}_{}.elf", example.trim_end_matches(".cell"), lock)), + &format!("{example}:{lock}"), + "original-scoped-lock-strict", + Some(example), + Some("--entry-lock"), + Some(lock), + )?); + } + } + Ok(artifacts) +} + +fn validate_example_layout(root: &Path) -> Result<()> { + let examples = root.join("examples"); + let production = fs::read_dir(&examples)? + .filter_map(std::result::Result::ok) + .map(|entry| entry.path()) + .filter(|path| path.extension().is_some_and(|ext| ext == "cell")) + .filter_map(|path| path.file_name().and_then(|name| name.to_str()).map(str::to_owned)) + .filter(|name| !EXPECTED_NON_PRODUCTION_EXAMPLES.contains(&name.as_str())) + .collect::>(); + if production != EXPECTED_EXAMPLES.iter().map(|value| (*value).to_owned()).collect() { + bail!("canonical bundled example set changed: {production:?}"); + } + let language = fs::read_dir(examples.join("language"))? + .filter_map(std::result::Result::ok) + .map(|entry| entry.path()) + .filter(|path| path.extension().is_some_and(|ext| ext == "cell")) + .filter_map(|path| path.file_name().and_then(|name| name.to_str()).map(str::to_owned)) + .collect::>(); + if language != EXPECTED_LANGUAGE_EXAMPLES.iter().map(|value| (*value).to_owned()).collect() { + bail!("language example set changed: {language:?}"); + } + for stale in ["business", "acceptance"] { + if examples.join(stale).exists() { + bail!("stale checked-in example mirror exists: examples/{stale}"); + } + } + Ok(()) +} + +pub(crate) fn prepare(root: &Path, run_dir: &Path, mode: &str) -> Result { + validate_example_layout(root)?; + fs::create_dir_all(run_dir)?; + let cellc = build_cellc(root)?; + let artifacts = compile_matrix(root, &cellc, run_dir)?; + let builder_contracts = builder_contracts(root, &cellc, run_dir)?; + let report_path = run_dir.join("ckb-cellscript-acceptance-report.json"); + let report = json!({ + "status":"passed", "acceptance_mode":mode, + "ckb_acceptance_scope":"Production mode is a hard gate and must not depend on synthetic harnesses, expected fail-closed entries, or non-original artifacts. Bounded mode is a development coverage matrix only.", + "cellc":cellc, "source_provenance":source_provenance(root)?, + "bundled_examples_exact_order":EXPECTED_EXAMPLES, "bundled_examples_count":EXPECTED_EXAMPLES.len(), + "non_production_examples":EXPECTED_NON_PRODUCTION_EXAMPLES, + "language_examples_exact_order":EXPECTED_LANGUAGE_EXAMPLES, "language_examples_count":EXPECTED_LANGUAGE_EXAMPLES.len(), + "example_scope":{ + "production_bundled_examples":EXPECTED_EXAMPLES, + "non_production_top_level_examples":EXPECTED_NON_PRODUCTION_EXAMPLES, + "non_production_language_examples":EXPECTED_LANGUAGE_EXAMPLES, + "production_scope_note":"Only production_bundled_examples are deployed and action-exercised by this CKB production acceptance report. non_production_top_level_examples and non_production_language_examples are covered by compiler/tooling tests unless promoted." + }, + "example_source_layout":{ + "canonical_bundled_examples":root.join("examples"), "language_examples":root.join("examples/language"), + "canonical_examples_note":"Production acceptance compiles the checked-in top-level examples/*.cell directly. examples/business and examples/acceptance are intentionally absent." + }, + "lock_acceptance_scope":{ + "strict_compile_only":true, "onchain_lock_spend_matrix":false, + "pending_onchain_lock_spend_matrix":expected_lock_scope(), + "required_cases_per_lock_when_promoted":["valid_spend","invalid_spend"], + "scope_note":"Scoped lock entries are strict-compiled under the CKB profile before live promotion." + }, + "ckb_elf_entry_abi_gate":elf_gate(&artifacts), "cellscript_build_reports":build_reports(&artifacts), + "public_builder_contracts":builder_contracts, + "bundled_examples_strict_admitted":EXPECTED_EXAMPLES, + "strict_original_ckb_compile_policy_fail_closed":[], "strict_original_ckb_compile_unexpected_failures":[], + "original_scoped_action_count":43, "original_scoped_lock_count":17, + "original_scoped_action_fail_closed_count":0, "original_scoped_lock_fail_closed_count":0, + "original_scoped_action_fail_closed":[], "original_scoped_lock_fail_closed":[], + "ckb_business_coverage":business_coverage(false), "production_ready":false, + "production_gate":{ + "status":"passed", "failures":[], "requires_original_scoped_harnesses":true, + "requires_no_expected_fail_closed_entries":true, "requires_all_bundled_examples_strict_original_ckb":true, + "requires_ckb_elf_entry_abi_gate":true, "requires_cellscript_build_reports":true, + "requires_public_builder_contracts":true + }, + "onchain":{"status":"skipped","reason":"compile-only"} + }); + write_report(&report_path, &report)?; + Ok(CompileEvidence { report, artifacts, report_path, run_dir: run_dir.to_path_buf() }) +} + +pub(crate) fn write_report(path: &Path, report: &Value) -> Result<()> { + let mut bytes = serde_json::to_vec_pretty(report)?; + bytes.push(b'\n'); + fs::write(path, bytes)?; + Ok(()) +} + +fn default_ckb_repo(root: &Path) -> PathBuf { + let parent = root.parent().unwrap_or(root); + if parent.join("ckb").is_dir() { + parent.join("ckb") + } else { + parent.parent().unwrap_or(parent).join("ckb") + } +} + +#[allow(clippy::too_many_arguments)] +pub fn run( + root: &Path, + ckb_repo: Option<&Path>, + ckb_bin: Option<&Path>, + compile_only: bool, + stateful_scenarios: bool, + mode: &str, + explicit_run_dir: Option<&Path>, + keep_node: bool, +) -> Result { + if mode == "production" { + let dirty = git_stdout(root, &["status", "--porcelain", "--untracked-files=all"])?; + if !dirty.is_empty() { + bail!("production acceptance requires a clean CellScript source tree\n{dirty}"); + } + } + let stamp = OffsetDateTime::now_utc().unix_timestamp(); + let run_dir = explicit_run_dir + .map(Path::to_path_buf) + .unwrap_or_else(|| root.join(format!("target/ckb-cellscript-acceptance/{stamp}-{}", std::process::id()))); + let mut evidence = prepare(root, &run_dir, mode)?; + if compile_only { + if mode == "production" { + production_evidence::run(root, &evidence.report_path, Some(root), true)?; + eprintln!("CKB compile-only production evidence is not sufficient for external release; run without --compile-only for final hardening."); + } + println!("CKB CellScript {mode} compile-only acceptance passed: {}", evidence.report_path.display()); + return Ok(0); + } + let repo = fs::canonicalize(ckb_repo.map(Path::to_path_buf).unwrap_or_else(|| default_ckb_repo(root)))?; + crate::ckb_acceptance_live::run(root, &repo, ckb_bin, stateful_scenarios || mode == "production", mode, keep_node, &mut evidence)?; + if mode == "production" { + production_evidence::run(root, &evidence.report_path, Some(root), false)?; + } + println!("CKB CellScript {mode} acceptance passed: {}", evidence.report_path.display()); + Ok(0) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn production_matrix_counts_are_stable() { + assert_eq!(ACTION_RUNS.iter().map(|(_, _, actions)| actions.len()).sum::(), 43); + assert_eq!(LOCKS.iter().map(|(_, locks)| locks.len()).sum::(), 17); + } + + #[test] + fn transaction_recipe_fixture_is_native_v023() { + let fixture: Value = serde_json::from_str(include_str!("../fixtures/ckb_acceptance/transactions-v0.23.json")).unwrap(); + assert_eq!(fixture["schema"], "cellscript-ckb-acceptance-transaction-recipes-v0.23"); + assert_eq!(fixture["action_cases"].as_array().unwrap().len(), 43); + assert_eq!(fixture["lock_cases"].as_array().unwrap().len(), 17); + assert_eq!(fixture["stateful_scenarios"].as_array().unwrap().len(), 26); + + let action_cases = fixture["action_cases"].as_array().unwrap(); + for (name, expected_hash) in [ + ("timelock.cell:create_absolute_lock", "0xc1e7dce634480aceedc7bd5dfbb7df1e5951cd945fb0d10cb8ea70968af0443b"), + ("timelock.cell:extend_lock", "0x078a625eb933f34dee98290d89c9cd6b57ab95d8bb1a89f254f422649a972954"), + ("timelock.cell:batch_create_locks", "0x2eb610fca034a18303d192bcbf52ba0f68e6ee8b1cafa90b200d5edad55257ef"), + ] { + let case = action_cases.iter().find(|case| case["name"] == name).unwrap(); + assert_eq!(case["artifact_data_hash"], expected_hash, "stale audited artifact identity for {name}"); + } + } +} diff --git a/crates/cellscript-tools/src/ckb_acceptance_live.rs b/crates/cellscript-tools/src/ckb_acceptance_live.rs new file mode 100644 index 00000000..2f50a3b8 --- /dev/null +++ b/crates/cellscript-tools/src/ckb_acceptance_live.rs @@ -0,0 +1,824 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{bail, Context, Result}; +use ckb_jsonrpc_types::Transaction as JsonTransaction; +use ckb_types::{packed, prelude::Entity}; +use serde_json::{json, Map, Value}; + +use crate::ckb_acceptance::{self, ArtifactRecord, CompileEvidence}; +use crate::ckb_devnet::{ + always_success_dep, decode_hex, deploy_code, funding_cells, out_point, resolve_ckb_bin, sha256_hex, CkbDevnet, + ALWAYS_SUCCESS_CODE_HASH, +}; +use crate::production_evidence::{ACTION_RUNS, EXPECTED_END_TO_END_STATEFUL_SCENARIOS, EXPECTED_EXAMPLES, LOCKS}; + +const RECIPES: &str = include_str!("../fixtures/ckb_acceptance/transactions-v0.23.json"); +const PINNED_CKB_CXXFLAGS: &str = "-include cstdint"; +const PINNED_CKB_CXX_COMPATIBILITY: &str = "ckb-librocksdb-sys-8.5.4-explicit-cstdint-v1"; + +fn production_ckb_build_command(ckb_repo: &Path, target: &Path) -> Command { + let mut command = Command::new("cargo"); + command + .args(["build", "--locked", "--bin", "ckb", "--target-dir"]) + .arg(target) + .current_dir(ckb_repo) + // The CKB 0.207.0 pin resolves ckb-librocksdb-sys 8.5.4. Its + // trace_record.h uses fixed-width integers without including + // ; current C++ toolchains no longer provide that header + // transitively. Inject the missing standard header without patching + // the clean, pinned CKB checkout. + .env("CXXFLAGS", PINNED_CKB_CXXFLAGS); + command +} + +fn command_stdout(root: &Path, program: &str, args: &[&str]) -> Result { + let output = Command::new(program).args(args).current_dir(root).output()?; + if !output.status.success() { + bail!("{program} {} failed: {}", args.join(" "), String::from_utf8_lossy(&output.stderr).trim()); + } + Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned()) +} + +fn parse_hex_u64(value: &Value) -> Result { + let text = value.as_str().context("expected hex quantity")?; + Ok(u64::from_str_radix(text.trim_start_matches("0x"), 16)?) +} + +fn file_sha256(path: &Path) -> Result { + Ok(sha256_hex(&fs::read(path)?)) +} + +fn build_ckb(root: &Path, ckb_repo: &Path, ckb_bin: Option<&Path>, mode: &str, run_dir: &Path) -> Result { + if mode != "production" { + return resolve_ckb_bin(ckb_repo, ckb_bin); + } + if ckb_bin.is_some() { + bail!("production acceptance does not accept --ckb-bin; the pinned source must be rebuilt"); + } + let target = run_dir.join(".ckb-build-target"); + let output = production_ckb_build_command(ckb_repo, &target).output()?; + if !output.status.success() { + bail!( + "fresh pinned CKB build failed:\n{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + let built = target.join("debug/ckb"); + let archived = run_dir.join("ckb-runtime/ckb"); + fs::create_dir_all(archived.parent().unwrap())?; + fs::copy(&built, &archived).with_context(|| format!("archive {}", built.display()))?; + let _ = root; + Ok(fs::canonicalize(archived)?) +} + +fn verify_pin(root: &Path, ckb_repo: &Path, mode: &str) -> Result { + let pin_path = root.join("scripts/ckb_acceptance_pin.json"); + let pin: Value = serde_json::from_slice(&fs::read(&pin_path)?)?; + if mode == "production" { + let head = command_stdout(ckb_repo, "git", &["rev-parse", "HEAD"])?; + if pin["revision"] != head { + bail!("CKB revision mismatch: checkout={head}, pin={}", pin["revision"]); + } + let dirty = command_stdout(ckb_repo, "git", &["status", "--porcelain", "--untracked-files=all"])?; + if !dirty.is_empty() { + bail!("CKB acceptance requires a clean pinned checkout: {}\n{dirty}", ckb_repo.display()); + } + } + for template in pin["template_paths"].as_array().context("pin template_paths missing")? { + let path = ckb_repo.join(template.as_str().context("pin template path must be a string")?); + if !path.is_file() { + bail!("pinned CKB template is missing: {}", path.display()); + } + } + Ok(pin) +} + +fn deployment_evidence(artifact: &ArtifactRecord, deployment: &Value) -> Value { + json!({ + "run_name":artifact.name, "run_kind":artifact.kind, + "tx_hash":deployment["commit"]["tx_hash"], "output_index":"0x0", + "out_point":deployment["cell_dep"]["out_point"], "code_cell_live":true, + "artifact_ckb_data_hash_blake2b":artifact.data_hash, + "live_code_cell_data_hash":artifact.data_hash, "live_code_cell_data_hash_matches_artifact":true + }) +} + +struct Replayer<'a> { + devnet: &'a mut CkbDevnet, + fixture: &'a Value, + deployments: &'a BTreeMap, + always_dep: Value, + old_to_new: BTreeMap, +} + +impl Replayer<'_> { + fn transaction(&self, old_hash: &str) -> Result { + self.fixture["transactions"][old_hash] + .as_object() + .map(|object| Value::Object(object.clone())) + .with_context(|| format!("transaction recipe missing for {old_hash}")) + } + + fn replay_recursive(&mut self, old_hash: &str, label: &str) -> Result { + if let Some(new_hash) = self.old_to_new.get(old_hash) { + return Ok(json!({"tx_hash":new_hash,"status":{"status":"committed"},"generated_blocks_after_submit":0})); + } + let tx = self.rebind(old_hash)?; + self.devnet.dry_run(&tx).with_context(|| format!("dry-run replay {label}"))?; + let commit = self.devnet.submit_and_commit(&tx, label)?; + self.old_to_new.insert(old_hash.to_owned(), commit["tx_hash"].as_str().unwrap().to_owned()); + Ok(commit) + } + + fn rebind(&mut self, old_hash: &str) -> Result { + let mut tx = self.transaction(old_hash)?; + tx.as_object_mut().unwrap().remove("hash"); + let inputs = tx["inputs"].as_array_mut().context("recipe inputs missing")?; + for input in inputs { + let previous = input["previous_output"]["tx_hash"].as_str().context("recipe input hash missing")?.to_owned(); + let replacement = if let Some(hash) = self.old_to_new.get(&previous) { + json!({"tx_hash":hash,"index":input["previous_output"]["index"]}) + } else if self.fixture["transactions"].get(&previous).is_some() { + let commit = self.replay_recursive(&previous, &format!("ancestor {previous}"))?; + json!({"tx_hash":commit["tx_hash"],"index":input["previous_output"]["index"]}) + } else { + let funding = self.devnet.find_spendable()?; + out_point(funding["tx_hash"].as_str().unwrap(), funding["index"].as_u64().unwrap()) + }; + input["previous_output"] = replacement; + } + let deps = tx["cell_deps"].as_array_mut().context("recipe cell_deps missing")?; + for dep in deps { + let old_tx = dep["out_point"]["tx_hash"].as_str().context("cell dep hash missing")?.to_owned(); + let index = dep["out_point"]["index"].as_str().context("cell dep index missing")?.to_owned(); + if let Some(mapped) = self.old_to_new.get(&old_tx) { + dep["out_point"]["tx_hash"] = json!(mapped); + continue; + } + if self.fixture["transactions"].get(&old_tx).is_some() { + let commit = self.replay_recursive(&old_tx, &format!("cell-dep ancestor {old_tx}"))?; + dep["out_point"]["tx_hash"] = commit["tx_hash"].clone(); + continue; + } + let key = format!("{old_tx}:{index}"); + let data_hash = self.fixture["cell_deps"][&key]["data_hash"] + .as_str() + .with_context(|| format!("cell-dep identity missing for {key}"))?; + let replacement = if data_hash == ALWAYS_SUCCESS_CODE_HASH { + self.always_dep.clone() + } else { + self.deployments + .get(data_hash) + .with_context(|| format!("no current artifact deployment matches recipe dependency {data_hash} ({key})"))? + ["cell_dep"] + .clone() + }; + *dep = replacement; + } + let old_headers = tx["header_deps"].as_array().context("recipe header_deps missing")?.clone(); + let mut headers = Vec::new(); + for old_header in old_headers { + let old_header = old_header.as_str().context("header dep must be a string")?; + let number = parse_hex_u64(&self.fixture["headers"][old_header]["number"])?; + loop { + let tip = self.devnet.rpc("get_tip_header", vec![])?; + if parse_hex_u64(&tip["number"])? >= number { + break; + } + self.devnet.rpc("generate_block", vec![])?; + } + let block = self.devnet.get_block_by_number(number)?; + headers.push(block["header"]["hash"].clone()); + } + tx["header_deps"] = Value::Array(headers); + self.balance_change_capacity(&mut tx).with_context(|| format!("balance rebound transaction {old_hash}"))?; + Ok(tx) + } + + fn balance_change_capacity(&mut self, tx: &mut Value) -> Result<()> { + let mut input_capacity = 0_u64; + for input in tx["inputs"].as_array().context("transaction inputs missing")? { + let live = self.devnet.rpc("get_live_cell", vec![input["previous_output"].clone(), json!(false)])?; + if live["status"] != "live" { + bail!("rebound input is not live: {}", input["previous_output"]); + } + input_capacity = + input_capacity.checked_add(parse_hex_u64(&live["cell"]["output"]["capacity"])?).context("input capacity overflow")?; + } + let outputs = tx["outputs"].as_array().context("transaction outputs missing")?; + let output_capacity = + outputs.iter().try_fold(0_u64, |total, output| Ok::<_, anyhow::Error>(total + parse_hex_u64(&output["capacity"])?))?; + if input_capacity >= output_capacity { + return Ok(()); + } + let outputs_data = tx["outputs_data"].as_array().context("transaction outputs_data missing")?; + let candidate = outputs + .iter() + .zip(outputs_data) + .enumerate() + .rev() + .find(|(_, (output, data))| { + output["lock"]["code_hash"] == ALWAYS_SUCCESS_CODE_HASH + && output["type"].is_null() + && data.as_str().is_some_and(|value| value == "0x") + }) + .map(|(index, _)| index); + const ALWAYS_SUCCESS_EMPTY_OCCUPIED: u64 = 4_100_000_000; + if let Some(candidate) = candidate { + let old_change = parse_hex_u64(&outputs[candidate]["capacity"])?; + let fixed = output_capacity - old_change; + if let Some(new_change) = input_capacity.checked_sub(fixed) + && new_change >= ALWAYS_SUCCESS_EMPTY_OCCUPIED + { + tx["outputs"][candidate]["capacity"] = json!(format!("0x{new_change:x}")); + return Ok(()); + } + } + + // Some recipe transactions intentionally have no disposable change + // output: every output is a typed scenario cell. A replacement + // cellbase input can be smaller than the original fixture input, so + // add fresh always-success funding instead of mutating scenario state. + let deficit = output_capacity - input_capacity; + let funding = self.devnet.collect_spendable(deficit)?; + let inputs = tx["inputs"].as_array_mut().context("transaction inputs missing")?; + for cell in funding_cells(&funding) { + inputs.push(json!({ + "previous_output": out_point( + cell["tx_hash"].as_str().context("funding transaction hash missing")?, + cell["index"].as_u64().context("funding output index missing")?, + ), + "since": "0x0", + })); + } + Ok(()) + } +} + +fn rejection(devnet: &CkbDevnet, tx: &Value, label: &str, data_hash: &str, error_code: Option) -> Result { + let value = devnet.dry_run_rejects(tx, label, Some("Inputs[0].Lock"), Some(data_hash), error_code)?; + Ok(json!({ + "status":"rejected", "check":"dry_run_transaction", "reason":value["reason"], + "expected_reason_matched":value["matched_expected"], "policy_or_capacity_reason":false + })) +} + +fn invalidate_action(tx: &Value, fixture: &Value, old_hash: &str) -> Result { + let mut invalid = tx.clone(); + let witnesses = invalid["witnesses"].as_array_mut().context("transaction witnesses missing")?; + if witnesses.is_empty() { + witnesses.push(json!("0x00")); + } else { + let raw = witnesses[0].as_str().unwrap_or("0x"); + let mut bytes = decode_hex(raw)?; + if bytes.is_empty() { + bytes.push(0); + } else { + bytes[0] ^= 0xff; + } + witnesses[0] = json!(format!("0x{}", hex::encode(bytes))); + } + let old_tx = &fixture["transactions"][old_hash]; + let fallback_cell = old_tx["inputs"].as_array().and_then(|inputs| inputs.first()).and_then(|input| { + let hash = input["previous_output"]["tx_hash"].as_str()?; + let index = parse_hex_u64(&input["previous_output"]["index"]).ok()? as usize; + Some((fixture["transactions"][hash]["outputs"][index].clone(), fixture["transactions"][hash]["outputs_data"][index].clone())) + }); + let fallback_data = fallback_cell.as_ref().and_then(|(_, data)| data.as_str()).unwrap_or("0x00").to_owned(); + let all_output_data_empty = invalid["outputs_data"] + .as_array() + .is_some_and(|values| values.iter().all(|value| value.as_str().is_none_or(|value| value == "0x"))); + if all_output_data_empty + && let Some((cell, _)) = &fallback_cell + && let Some(output) = invalid["outputs"].as_array_mut().and_then(|values| values.first_mut()) + { + output["type"] = cell["type"].clone(); + } + for output_data in invalid["outputs_data"].as_array_mut().context("transaction outputs_data missing")? { + let mut bytes = decode_hex(output_data.as_str().unwrap_or("0x"))?; + if bytes.is_empty() { + *output_data = json!(if fallback_data == "0x" { "0x00" } else { &fallback_data }); + } else { + let last = bytes.len() - 1; + bytes[last] ^= 1; + *output_data = json!(format!("0x{}", hex::encode(bytes))); + } + } + Ok(invalid) +} + +fn measured_constraints(template: &Value, tx: &Value, dry_run: &Value) -> Result { + let mut measured = template.clone(); + let cycles = parse_hex_u64(&dry_run["cycles"])?; + let json_tx: JsonTransaction = + serde_json::from_value(tx.clone()).context("transaction recipe is not valid CKB transaction JSON")?; + let packed_tx: packed::Transaction = json_tx.into(); + let outputs = tx["outputs"].as_array().context("transaction outputs missing")?; + let outputs_data = tx["outputs_data"].as_array().context("transaction outputs_data missing")?; + if outputs.len() != outputs_data.len() { + bail!("transaction output/data length mismatch: {} != {}", outputs.len(), outputs_data.len()); + } + let output_capacities = outputs.iter().map(|output| parse_hex_u64(&output["capacity"])).collect::>>()?; + let occupied_capacities = outputs + .iter() + .zip(outputs_data) + .map(|(output, data)| { + let script_bytes = |script: &Value| -> Result { + if script.is_null() { + return Ok(0); + } + Ok(33 + u64::try_from(decode_hex(script["args"].as_str().context("script args missing")?)?.len())?) + }; + let data_bytes = u64::try_from(decode_hex(data.as_str().context("output data must be hex")?)?.len())?; + Ok((8 + script_bytes(&output["lock"])? + script_bytes(&output["type"])? + data_bytes) * 100_000_000) + }) + .collect::>>()?; + let under_capacity = output_capacities + .iter() + .zip(&occupied_capacities) + .enumerate() + .filter_map(|(index, (capacity, occupied))| (capacity < occupied).then_some(index)) + .collect::>(); + let capacity_is_sufficient = under_capacity.is_empty(); + let output_data_bytes = outputs_data + .iter() + .map(|data| decode_hex(data.as_str().unwrap_or("0x")).map(|bytes| bytes.len())) + .collect::>>()? + .into_iter() + .sum::(); + let witness_bytes = tx["witnesses"] + .as_array() + .context("transaction witnesses missing")? + .iter() + .map(|witness| decode_hex(witness.as_str().unwrap_or("0x")).map(|bytes| bytes.len())) + .collect::>>()? + .into_iter() + .sum::(); + measured["measured_cycles"] = json!(cycles); + measured["cycles_status"] = json!("dry-run-measured"); + measured["consensus_serialized_tx_size_bytes"] = json!(packed_tx.as_bytes().len()); + measured["json_envelope_size_bytes"] = json!(serde_json::to_vec(tx)?.len()); + measured["input_count"] = json!(tx["inputs"].as_array().map_or(0, Vec::len)); + measured["output_count"] = json!(outputs.len()); + measured["cell_dep_count"] = json!(tx["cell_deps"].as_array().map_or(0, Vec::len)); + measured["header_dep_count"] = json!(tx["header_deps"].as_array().map_or(0, Vec::len)); + measured["witness_count"] = json!(tx["witnesses"].as_array().map_or(0, Vec::len)); + measured["witness_bytes"] = json!(witness_bytes); + measured["output_data_bytes"] = json!(output_data_bytes); + measured["measured_output_capacity_shannons"] = json!(output_capacities); + measured["output_capacity_shannons"] = json!(output_capacities.iter().sum::()); + measured["output_occupied_capacity_shannons"] = json!(occupied_capacities); + measured["occupied_capacity_shannons"] = json!(occupied_capacities.iter().sum::()); + measured["under_capacity_output_indexes"] = json!(under_capacity); + measured["capacity_is_sufficient"] = json!(capacity_is_sufficient); + Ok(measured) +} + +fn code_report(artifact: &ArtifactRecord, deployment: &Value) -> Value { + json!({ + "artifact":artifact.path, "artifact_size_bytes":artifact.bytes.len(), + "artifact_ckb_data_hash_blake2b":artifact.data_hash, + "code_cell_dep":deployment["cell_dep"], "code_cell_deploy":deployment["commit"], + "code_cell_live":true, "live_code_cell_data_hash":artifact.data_hash, + "live_code_cell_data_hash_matches_artifact":true, "deploy_attempts":1 + }) +} + +fn action_group_key(example: &str) -> Result<&'static str> { + ACTION_RUNS + .iter() + .find(|(_, candidate, _)| *candidate == example) + .map(|(key, _, _)| *key) + .with_context(|| format!("unknown action example {example}")) +} + +fn replay_actions( + replayer: &mut Replayer<'_>, + fixture: &Value, + artifacts: &BTreeMap, +) -> Result>> { + let mut groups = BTreeMap::>::new(); + for case in fixture["action_cases"].as_array().context("action_cases missing")? { + let name = case["name"].as_str().context("action case name missing")?; + let (example, _) = name.split_once(':').context("invalid action case name")?; + let artifact = artifacts.get(name).with_context(|| format!("compiled action artifact missing for {name}"))?; + let expected_hash = case["artifact_data_hash"].as_str().context("action fixture artifact hash missing")?; + if artifact.data_hash != expected_hash { + bail!("{name} artifact changed from audited transaction recipe: {} != {expected_hash}", artifact.data_hash); + } + let deployment = replayer.deployments.get(&artifact.data_hash).unwrap(); + let initial_old = case["initial_tx"].as_str().unwrap(); + replayer.replay_recursive(initial_old, &format!("{name} initial cells"))?; + let valid_old = case["valid_tx"].as_str().unwrap(); + let valid_tx = replayer.rebind(valid_old)?; + let invalid_tx = invalidate_action(&valid_tx, fixture, valid_old)?; + let malformed = rejection(replayer.devnet, &invalid_tx, &format!("{name} malformed action"), &artifact.data_hash, None)?; + let dry_run = replayer.devnet.dry_run(&valid_tx)?; + let commit = replayer.devnet.submit_and_commit(&valid_tx, &format!("{name} valid action"))?; + replayer.old_to_new.insert(valid_old.to_owned(), commit["tx_hash"].as_str().unwrap().to_owned()); + let mut output_live = Vec::new(); + for index in 0..valid_tx["outputs"].as_array().unwrap().len() { + replayer.devnet.wait_live_cell(commit["tx_hash"].as_str().unwrap(), index as u64)?; + output_live.push(true); + } + let row = json!({ + "name":name, "action":case["action"], "status":"passed", "builder_backed":false, + "transaction_origin":"acceptance-rust-harness", "harness_origin":"rust-transaction-recipe-replay", + "acceptance_harness_name":case["acceptance_harness_name"], + "acceptance_harness_implementation":case["acceptance_harness_implementation"], + "public_builder_contract_id":name, "public_builder_contract_verified":true, + "artifact":artifact.path, "code":code_report(artifact, deployment), + "malformed_transaction":malformed, "valid_dry_run":dry_run, + "valid_commit":commit, "valid_outputs_live":output_live, + "measured_constraints":measured_constraints(&case["measured_constraints"], &valid_tx, &dry_run)? + }); + groups.entry(action_group_key(example)?.to_owned()).or_default().push(row); + } + Ok(groups) +} + +fn replay_locks(replayer: &mut Replayer<'_>, fixture: &Value, artifacts: &BTreeMap) -> Result> { + let mut rows = Vec::new(); + for case in fixture["lock_cases"].as_array().context("lock_cases missing")? { + let name = case["name"].as_str().context("lock case name missing")?; + let artifact = artifacts.get(name).with_context(|| format!("compiled lock artifact missing for {name}"))?; + let expected_hash = case["artifact_data_hash"].as_str().unwrap(); + if artifact.data_hash != expected_hash { + bail!("{name} artifact changed from audited transaction recipe: {} != {expected_hash}", artifact.data_hash); + } + let deployment = replayer.deployments.get(&artifact.data_hash).unwrap(); + + let invalid_create = case["invalid_create_tx"].as_str().unwrap(); + replayer.replay_recursive(invalid_create, &format!("{name} invalid input create"))?; + let mut invalid_tx = case["invalid_tx"].clone(); + invalid_tx.as_object_mut().unwrap().remove("hash"); + // The stored invalid transaction is rebound through a temporary recipe entry. + let synthetic = format!("invalid:{name}"); + let mut fixture_with_invalid = replayer.fixture.clone(); + fixture_with_invalid["transactions"][&synthetic] = invalid_tx; + let rebound_invalid = { + let mut nested = Replayer { + devnet: replayer.devnet, + fixture: &fixture_with_invalid, + deployments: replayer.deployments, + always_dep: replayer.always_dep.clone(), + old_to_new: replayer.old_to_new.clone(), + }; + let tx = nested.rebind(&synthetic)?; + replayer.old_to_new = nested.old_to_new; + tx + }; + let invalid_rejection = + rejection(replayer.devnet, &rebound_invalid, &format!("{name} invalid lock spend"), &artifact.data_hash, Some(5))?; + let invalid_input_hash = rebound_invalid["inputs"][0]["previous_output"]["tx_hash"].as_str().unwrap(); + let invalid_input_index = parse_hex_u64(&rebound_invalid["inputs"][0]["previous_output"]["index"])?; + let live = replayer.devnet.wait_live_cell(invalid_input_hash, invalid_input_index)?; + + let valid_create = case["valid_create_tx"].as_str().unwrap(); + replayer.replay_recursive(valid_create, &format!("{name} valid input create"))?; + let valid_old = case["valid_tx"].as_str().unwrap(); + let valid_tx = replayer.rebind(valid_old)?; + let dry_run = replayer.devnet.dry_run(&valid_tx)?; + let commit = replayer.devnet.submit_and_commit(&valid_tx, &format!("{name} valid lock spend"))?; + replayer.old_to_new.insert(valid_old.to_owned(), commit["tx_hash"].as_str().unwrap().to_owned()); + replayer.devnet.wait_live_cell(commit["tx_hash"].as_str().unwrap(), 0)?; + rows.push(json!({ + "name":name, "example":case["example"], "lock":case["lock"], "status":"passed", + "kind":"original-scoped-lock-strict", "builder_backed":false, + "transaction_origin":"acceptance-rust-harness", "harness_origin":"rust-transaction-recipe-replay", + "acceptance_harness_name":case["acceptance_harness_name"], + "acceptance_harness_implementation":case["acceptance_harness_implementation"], + "artifact":artifact.path, "code":code_report(artifact, deployment), + "valid_spend":{"status":"passed","dry_run":dry_run,"commit":commit,"output_live":true}, + "invalid_spend":{"status":"rejected","rejection":invalid_rejection,"input_cells_live_after_rejection":[live["status"] == "live"]}, + "measured_constraints":measured_constraints(&case["measured_constraints"], &valid_tx, &dry_run)? + })); + } + Ok(rows) +} + +fn replay_scenarios(replayer: &mut Replayer<'_>, fixture: &Value) -> Result { + let mut runs = Vec::new(); + let mut covered = BTreeSet::new(); + let mut step_count = 0_usize; + for scenario in fixture["stateful_scenarios"].as_array().context("stateful_scenarios missing")? { + let name = scenario["name"].as_str().unwrap(); + let mut steps = Vec::new(); + for step in scenario["steps"].as_array().unwrap() { + let old_hash = step["old_tx_hash"].as_str().unwrap(); + let tx = replayer.rebind(old_hash)?; + let dry_run = replayer.devnet.dry_run(&tx)?; + let consumed = tx["inputs"] + .as_array() + .unwrap() + .iter() + .map(|input| json!({"tx_hash":input["previous_output"]["tx_hash"],"index":input["previous_output"]["index"]})) + .collect::>(); + let commit = replayer.devnet.submit_and_commit(&tx, &format!("{name}:{}", step["step"].as_str().unwrap()))?; + replayer.old_to_new.insert(old_hash.to_owned(), commit["tx_hash"].as_str().unwrap().to_owned()); + let mut consumed_status = Vec::new(); + for input in consumed { + let status = replayer + .devnet + .rpc("get_live_cell", vec![json!({"tx_hash":input["tx_hash"],"index":input["index"]}), json!(false)])?; + consumed_status.push(status); + } + let mut outputs_live = Map::new(); + for index in 0..tx["outputs"].as_array().unwrap().len() { + replayer.devnet.wait_live_cell(commit["tx_hash"].as_str().unwrap(), index as u64)?; + outputs_live.insert(index.to_string(), json!(true)); + } + steps.push(json!({ + "step":step["step"], "status":"passed", "dry_run":dry_run, "commit":commit, + "measured_constraints":measured_constraints(&step["measured_constraints"], &tx, &dry_run)?, + "consumed_inputs":consumed_status, "outputs_live":outputs_live + })); + step_count += 1; + } + for action in scenario["action_ids"].as_array().unwrap() { + covered.insert(action.as_str().unwrap().to_owned()); + } + runs.push(json!({ + "name":name, "kind":scenario["kind"], "status":"passed", "builder_backed":false, + "transaction_origin":"acceptance-rust-harness", "harness_origin":"rust-transaction-recipe-replay", + "acceptance_harness_name":"rust-transaction-recipe-replayer-v0.23", + "action_ids":scenario["action_ids"], "steps":steps + })); + } + let mut required = ACTION_RUNS + .iter() + .flat_map(|(_, example, actions)| actions.iter().map(move |action| format!("{example}:{action}"))) + .collect::>(); + required.sort(); + let covered = covered.into_iter().collect::>(); + if covered != required { + bail!("stateful recipe action coverage mismatch"); + } + let leading = + runs.iter().take(EXPECTED_END_TO_END_STATEFUL_SCENARIOS.len()).map(|row| row["name"].as_str().unwrap()).collect::>(); + if leading != EXPECTED_END_TO_END_STATEFUL_SCENARIOS { + bail!("stateful end-to-end scenario order changed: {leading:?}"); + } + Ok(json!({ + "status":"passed", "scenario_count":runs.len(), + "end_to_end_scenario_count":EXPECTED_END_TO_END_STATEFUL_SCENARIOS.len(), + "action_branch_scenario_count":runs.len()-EXPECTED_END_TO_END_STATEFUL_SCENARIOS.len(), + "step_count":step_count, "runs":runs, + "stateful_action_coverage":{ + "status":"passed", "required_action_count":required.len(), "covered_action_count":covered.len(), + "required_action_ids":required, "covered_action_ids":covered, + "missing_action_ids":[], "missing_artifact_ids":[], "unexpected_artifact_ids":[] + } + })) +} + +fn runtime_provenance( + root: &Path, + ckb_repo: &Path, + ckb_bin: &Path, + devnet: &CkbDevnet, + pin: &Value, + genesis_hash: &str, + mode: &str, +) -> Result { + let pin_path = root.join("scripts/ckb_acceptance_pin.json"); + let templates = pin["template_paths"].as_array().unwrap(); + let source_config = ckb_repo.join(templates[0].as_str().unwrap()); + let source_spec = ckb_repo.join(templates[1].as_str().unwrap()); + let effective_config = devnet.ckb_dir.join("ckb.toml"); + let effective_spec = devnet.ckb_dir.join("specs/integration.toml"); + let version = command_stdout(ckb_repo, ckb_bin.to_str().unwrap(), &["--version"])?; + if mode == "production" + && (!version.contains(pin["version"].as_str().unwrap()) || !version.contains(&pin["revision"].as_str().unwrap()[..7])) + { + bail!("CKB executable provenance mismatch: {version}"); + } + Ok(json!({ + "schema":"cellscript-ckb-runtime-provenance-v0.22", "pin_schema":pin["schema"], + "pin_file_sha256":file_sha256(&pin_path)?, "repository":pin["repository"], + "revision":pin["revision"], "repo_head":command_stdout(ckb_repo,"git",&["rev-parse","HEAD"])? , + "repo_dirty":!command_stdout(ckb_repo,"git",&["status","--porcelain","--untracked-files=all"])?.is_empty(), + "version":pin["version"], "version_output":version, + "build_mode":if mode=="production" {"fresh-dedicated-cargo-target"} else {"bounded-existing-binary"}, + "cxxflags":if mode=="production" {PINNED_CKB_CXXFLAGS} else {"not-applied-bounded-existing-binary"}, + "cxx_compatibility_contract":if mode=="production" {PINNED_CKB_CXX_COMPATIBILITY} else {"not-applied-bounded-existing-binary"}, + "binary_archived_with_report":mode=="production", "binary_path":ckb_bin, + "binary_sha256":file_sha256(ckb_bin)?, "source_template_path":source_config, + "source_template_sha256":file_sha256(&source_config)?, "source_spec_path":source_spec, + "source_spec_sha256":file_sha256(&source_spec)?, "effective_config_path":effective_config, + "effective_config_sha256":file_sha256(&effective_config)?, "effective_spec_path":effective_spec, + "effective_spec_sha256":file_sha256(&effective_spec)?, "genesis_hash":genesis_hash + })) +} + +fn group_actions(groups: &BTreeMap>, key: &str) -> Vec { + groups.get(key).cloned().unwrap_or_default() +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn run( + root: &Path, + ckb_repo: &Path, + configured_ckb_bin: Option<&Path>, + stateful: bool, + mode: &str, + keep_node: bool, + evidence: &mut CompileEvidence, +) -> Result<()> { + let fixture: Value = serde_json::from_str(RECIPES)?; + if fixture["schema"] != "cellscript-ckb-acceptance-transaction-recipes-v0.23" { + bail!("unexpected CKB acceptance transaction recipe schema"); + } + let pin = verify_pin(root, ckb_repo, mode)?; + let ckb_bin = build_ckb(root, ckb_repo, configured_ckb_bin, mode, &evidence.run_dir)?; + let mut devnet = CkbDevnet::new(ckb_repo.to_path_buf(), ckb_bin.clone(), evidence.run_dir.clone())?; + devnet.start()?; + let genesis = devnet.get_block_by_number(0)?; + let genesis_hash = genesis["header"]["hash"].as_str().context("genesis hash missing")?.to_owned(); + let genesis_cellbase = genesis["transactions"][0]["hash"].as_str().context("genesis cellbase missing")?.to_owned(); + let always_dep = always_success_dep(&genesis_cellbase); + + let mut deployments = BTreeMap::::new(); + let mut artifact_deployments = BTreeMap::::new(); + for artifact in &evidence.artifacts { + let deployment = if let Some(existing) = deployments.get(&artifact.data_hash) { + existing.clone() + } else { + let created = deploy_code(&mut devnet, &artifact.name, &artifact.bytes, &always_dep)?; + deployments.insert(artifact.data_hash.clone(), created.clone()); + created + }; + artifact_deployments.insert(artifact.path.to_string_lossy().into_owned(), deployment); + } + let artifact_by_name = evidence + .artifacts + .iter() + .filter(|artifact| artifact.entry.is_some()) + .map(|artifact| (artifact.name.clone(), artifact.clone())) + .collect::>(); + let mut replayer = Replayer { + devnet: &mut devnet, + fixture: &fixture, + deployments: &deployments, + always_dep: always_dep.clone(), + old_to_new: BTreeMap::new(), + }; + let action_groups = replay_actions(&mut replayer, &fixture, &artifact_by_name)?; + let lock_runs = replay_locks(&mut replayer, &fixture, &artifact_by_name)?; + let stateful_report = if stateful { + replay_scenarios(&mut replayer, &fixture)? + } else { + json!({"status":"skipped","reason":"stateful scenarios not requested","runs":[]}) + }; + + let mut deployment_runs = Vec::new(); + for example in EXPECTED_EXAMPLES { + let artifact = evidence + .artifacts + .iter() + .find(|artifact| artifact.kind == "bundled-example-strict-original" && artifact.example.as_deref() == Some(*example)) + .unwrap(); + let deployment = artifact_deployments.get(&artifact.path.to_string_lossy().into_owned()).unwrap(); + deployment_runs.push(json!({ + "name":example, "kind":"bundled-example-strict-original", "status":"passed", + "artifact":artifact.path, "artifact_size_bytes":artifact.bytes.len(), "code_cell_live":true, + "artifact_ckb_data_hash_blake2b":artifact.data_hash, "live_code_cell_data_hash":artifact.data_hash, + "live_code_cell_data_hash_matches_artifact":true, + "valid_deploy_dry_run":deployment["valid_deploy_dry_run"], "code_cell_dep":deployment["cell_dep"] + })); + } + + let build_index = evidence.report["cellscript_build_reports"].as_object_mut().unwrap(); + for row in build_index["reports"].as_array_mut().unwrap() { + let path = row["artifact_path"].as_str().unwrap(); + let artifact = evidence.artifacts.iter().find(|artifact| artifact.path == Path::new(path)).unwrap(); + let deployment = artifact_deployments.get(path).unwrap(); + row["onchain_deployments"] = json!([deployment_evidence(artifact, deployment)]); + } + let report_count = build_index["reports"].as_array().unwrap().len(); + build_index.insert("onchain_deployed_artifact_count".into(), json!(report_count)); + build_index.insert("live_code_cell_data_hash_match_count".into(), json!(report_count)); + build_index.insert("missing_onchain_deployments".into(), json!([])); + build_index.insert("live_code_cell_data_hash_mismatches".into(), json!([])); + build_index.insert("unexpected_onchain_artifacts".into(), json!([])); + + let action_count = action_groups.values().map(Vec::len).sum::(); + let lock_count = lock_runs.len(); + let mut onchain = json!({ + "status":"passed", "tip_before":genesis["header"], "tip_after":replayer.devnet.rpc("get_tip_header",vec![])?, + "genesis_hash":genesis_hash, "genesis_cellbase_hash":genesis_cellbase, + "chain_template":replayer.devnet.ckb_dir, "always_success_system_cell_index":"0x5", + "bundled_example_deployment_runs":deployment_runs, "bundled_examples_deployed":EXPECTED_EXAMPLES, + "all_bundled_examples_deployed":true, "all_artifacts_deployed_and_spent":true, + "resource_identity_evidence_scope":{ + "status":"fixture-only", "always_success_resource_types":true, "production_resource_identity_proven":false, + "scope_note":"Acceptance resource Type Scripts are always-success fixtures; action and lock verifier behavior remains real CKB-VM evidence." + }, + "token_action_runs":group_actions(&action_groups,"token_action_runs"), + "nft_action_runs":group_actions(&action_groups,"nft_action_runs"), + "timelock_action_runs":group_actions(&action_groups,"timelock_action_runs"), + "multisig_action_runs":group_actions(&action_groups,"multisig_action_runs"), + "vesting_action_runs":group_actions(&action_groups,"vesting_action_runs"), + "amm_action_runs":group_actions(&action_groups,"amm_action_runs"), + "launch_action_runs":group_actions(&action_groups,"launch_action_runs"), + "lock_spend_matrix_runs":lock_runs, "stateful_scenarios":stateful_report, + "all_token_actions_exercised":true, "all_nft_actions_exercised":true, + "all_timelock_actions_exercised":true, "all_multisig_actions_exercised":true, + "all_vesting_actions_exercised":true, "all_amm_actions_exercised":true, + "all_launch_actions_exercised":true, "builder_backed_action_count":0, + "acceptance_harness_action_count":action_count, "public_builder_contract_action_count":action_count, + "measured_cycles_action_count":action_count, "tx_size_measured_action_count":action_count, + "occupied_capacity_measured_action_count":action_count, "lock_spend_matrix_count":lock_count, + "builder_backed_lock_spend_matrix_count":0, "acceptance_harness_lock_spend_matrix_count":lock_count, + "lock_valid_spend_count":lock_count, "lock_invalid_spend_count":lock_count, + "measured_cycles_lock_count":lock_count, "tx_size_measured_lock_count":lock_count, + "occupied_capacity_measured_lock_count":lock_count, "all_locks_behavior_exercised":true + }); + for (key, _, actions) in ACTION_RUNS { + let prefix = key.trim_end_matches("_action_runs"); + onchain[format!("{prefix}_actions_exercised")] = json!(actions); + } + evidence.report["onchain"] = onchain; + evidence.report["ckb_repo"] = json!(ckb_repo); + evidence.report["ckb_bin"] = json!(ckb_bin); + evidence.report["rpc_url"] = json!(replayer.devnet.rpc_url); + evidence.report["ckb_log"] = json!(replayer.devnet.log_path); + evidence.report["ckb_runtime_provenance"] = + runtime_provenance(root, ckb_repo, &ckb_bin, replayer.devnet, &pin, &genesis_hash, mode)?; + evidence.report["lock_acceptance_scope"] = json!({ + "strict_compile_only":false, "onchain_lock_spend_matrix":true, + "onchain_lock_spend_matrix_scope":LOCKS.iter().map(|(example,locks)|((*example).to_owned(),json!(locks))).collect::>(), + "required_cases_per_lock":["valid_spend","invalid_spend"], + "scope_note":"Scoped lock entries are strict-compiled under the CKB profile and each lock is exercised through Rust transaction-recipe valid-spend and invalid-spend transactions." + }); + evidence.report["ckb_business_coverage"] = ckb_acceptance::business_coverage(true); + let production_ready = mode == "production"; + evidence.report["production_ready"] = json!(production_ready); + evidence.report["status"] = json!("passed"); + evidence.report["final_production_hardening_gate"] = json!({ + "status":if production_ready { "passed" } else { "not-evaluated-in-bounded-mode" }, + "ready":production_ready, "requires_builder_generated_transactions":false, + "requires_public_builder_contracts":true, "requires_acceptance_harness_transactions":true, + "requires_measured_cycles":true, "requires_consensus_serialized_tx_size":true, + "requires_exact_occupied_capacity":true, "requires_stateful_action_coverage":true, + "production_resource_identity_claim":false, "resource_identity_evidence_scope":"always-success-fixture-only", + "requires_build_report_live_artifact_linkage":true, "failures":[] + }); + ckb_acceptance::write_report(&evidence.report_path, &evidence.report)?; + if !keep_node { + replayer.devnet.stop(); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::ffi::OsStr; + + use ckb_types::{packed::WitnessArgs, prelude::*}; + + use super::*; + + #[test] + fn production_ckb_build_injects_the_pinned_cstdint_compatibility_flag() { + let command = production_ckb_build_command(Path::new("/tmp/pinned-ckb"), Path::new("/tmp/pinned-ckb-target")); + let cxxflags = command.get_envs().find_map(|(key, value)| (key == OsStr::new("CXXFLAGS")).then_some(value)).flatten(); + + assert_eq!(cxxflags, Some(OsStr::new(PINNED_CKB_CXXFLAGS))); + assert_eq!(PINNED_CKB_CXX_COMPATIBILITY, "ckb-librocksdb-sys-8.5.4-explicit-cstdint-v1"); + } + + fn assert_entry_witnesses(transaction: &Value, label: &str, count: &mut usize) { + for witness in transaction["witnesses"].as_array().expect("transaction witnesses") { + let encoded = decode_hex(witness.as_str().expect("hex witness")).expect("valid witness hex"); + if encoded.is_empty() { + continue; + } + let args = WitnessArgs::from_slice(&encoded) + .unwrap_or_else(|error| panic!("{label} witness must be Molecule WitnessArgs: {error}")); + assert!(args.lock().to_opt().is_none(), "{label} entry witness must not occupy lock"); + assert!(args.output_type().to_opt().is_none(), "{label} entry witness must not occupy output_type"); + let payload = + args.input_type().to_opt().unwrap_or_else(|| panic!("{label} entry witness must occupy input_type")).raw_data(); + assert!(payload.starts_with(b"CSARGv1\0"), "{label} input_type must contain a CSARG payload"); + *count += 1; + } + } + + #[test] + fn acceptance_recipes_use_canonical_witness_args_input_type() { + let fixture: Value = serde_json::from_str(RECIPES).expect("valid acceptance fixture"); + let mut count = 0; + for (hash, transaction) in fixture["transactions"].as_object().expect("transactions") { + assert_entry_witnesses(transaction, hash, &mut count); + } + for case in fixture["lock_cases"].as_array().expect("lock cases") { + assert_entry_witnesses(&case["invalid_tx"], case["name"].as_str().expect("lock case name"), &mut count); + } + assert_eq!(count, 123, "the complete Edition 2026 acceptance witness matrix must be covered"); + } +} diff --git a/crates/cellscript-tools/src/ckb_adapter_live.rs b/crates/cellscript-tools/src/ckb_adapter_live.rs new file mode 100644 index 00000000..5307a810 --- /dev/null +++ b/crates/cellscript-tools/src/ckb_adapter_live.rs @@ -0,0 +1,132 @@ +use std::fs; +use std::path::Path; + +use anyhow::{bail, Context, Result}; +use serde_json::{json, Value}; + +use crate::ckb_devnet::{ + always_success_dep, always_success_lock, ckb_hash_hex, decode_hex, hex0x, out_point, resolve_ckb_bin, transaction, CkbDevnet, +}; +use crate::shared::{stable_json_compact, stable_json_pretty}; + +const FEE: u64 = 1_000; + +fn capacity(cell: &Value) -> Result { + cell["capacity"].as_u64().context("funding cell capacity missing") +} + +pub fn run(ckb_repo: &Path, ckb_bin: Option<&Path>, run_dir: &Path, action_plan_path: &Path, report_path: &Path) -> Result { + fs::create_dir_all(run_dir)?; + let ckb_repo = fs::canonicalize(ckb_repo).with_context(|| format!("failed to resolve CKB repo {}", ckb_repo.display()))?; + let ckb_bin = resolve_ckb_bin(&ckb_repo, ckb_bin)?; + let action_plan: Value = serde_json::from_slice(&fs::read(action_plan_path)?)?; + let mut devnet = CkbDevnet::new(ckb_repo.clone(), ckb_bin.clone(), run_dir.to_path_buf())?; + devnet.start()?; + + let genesis = devnet.get_block_by_number(0)?; + let genesis_hash = genesis.pointer("/transactions/0/hash").and_then(Value::as_str).context("genesis cellbase hash missing")?; + let always_dep = always_success_dep(genesis_hash); + + let funding = devnet.find_spendable()?; + let funding_capacity = capacity(&funding)?; + if funding_capacity <= FEE { + bail!("funding capacity is too small for adapter smoke transaction"); + } + let smoke_tx = transaction( + std::slice::from_ref(&funding), + vec![json!({"capacity": format!("0x{:x}", funding_capacity - FEE), "lock": always_success_lock("0x"), "type": Value::Null})], + vec!["0x".into()], + vec![always_dep.clone()], + vec![], + vec![], + ); + let estimate = devnet.rpc("estimate_cycles", vec![smoke_tx.clone()])?; + let pool_accept = devnet.rpc("test_tx_pool_accept", vec![smoke_tx.clone(), json!("passthrough")])?; + + let deploy_funding = devnet.find_spendable()?; + let deploy_capacity = capacity(&deploy_funding)?; + let artifact: Vec = (0_u8..32).collect(); + let mut type_id_preimage = decode_hex(deploy_funding["tx_hash"].as_str().context("deploy funding hash missing")?)?; + type_id_preimage.extend_from_slice(&deploy_funding["index"].as_u64().unwrap_or(0).to_le_bytes()); + type_id_preimage.extend_from_slice(&0_u64.to_le_bytes()); + let type_id_args = ckb_hash_hex(&type_id_preimage); + let type_script = json!({"code_hash": crate::ckb_devnet::ALWAYS_SUCCESS_CODE_HASH, "hash_type": "data", "args": type_id_args}); + let code_capacity = 200_000_000_000_u64; + if deploy_capacity < code_capacity + FEE { + bail!("deploy funding {deploy_capacity} insufficient for code output {code_capacity} + fee {FEE}"); + } + let change_capacity = deploy_capacity - code_capacity - FEE; + let deploy_tx = transaction( + std::slice::from_ref(&deploy_funding), + vec![ + json!({"capacity": format!("0x{code_capacity:x}"), "lock": always_success_lock("0x"), "type": type_script}), + json!({"capacity": format!("0x{change_capacity:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + ], + vec![hex0x(&artifact), "0x".into()], + vec![always_dep.clone()], + vec!["0x0000000000000000".into()], + vec![], + ); + let deploy_estimate = devnet.rpc("estimate_cycles", vec![deploy_tx.clone()])?; + let deploy_pool_accept = devnet.rpc("test_tx_pool_accept", vec![deploy_tx.clone(), json!("passthrough")])?; + let commit = devnet.submit_and_commit(&deploy_tx, "adapter deploy probe")?; + let deploy_hash = commit["tx_hash"].as_str().context("deploy commit hash missing")?; + let live = devnet.assert_live_cell( + deploy_hash, + 0, + "adapter deploy probe", + Some(code_capacity), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&artifact), + )?; + + let smoke_text = stable_json_compact(&smoke_tx)?; + let deploy_text = stable_json_compact(&deploy_tx)?; + let report = json!({ + "schema": "cellscript-ckb-adapter-local-node-acceptance-v0.19", + "status": "passed", + "rpc_url": devnet.rpc_url, + "ckb_repo": ckb_repo, + "ckb_bin": ckb_bin, + "ckb_log": devnet.log_path, + "action_plan": { + "policy": action_plan.get("policy"), "action": action_plan.get("action"), + "adapter_contract_schema": action_plan.pointer("/adapter_contract/schema"), + "can_submit": action_plan.pointer("/transaction_draft/can_submit"), + "requires_packed_materialization": action_plan.pointer("/transaction_draft/requires_packed_materialization"), + }, + "adapter_materialization": {"crate": "crates/cellscript-ckb-adapter", "test": "materializes_resolved_action_with_ckb_sdk_transaction_builder", "status": "passed"}, + "adapter_deploy_probe": {"crate": "crates/cellscript-ckb-adapter", "test": "builds_deploy_transaction_with_type_id_code_cell", "status": "passed"}, + "local_node": { + "estimate_cycles": estimate, "test_tx_pool_accept": pool_accept, "tx_size_json_bytes": smoke_text.len(), + "output_capacity_shannons": funding_capacity - FEE, "fee_shannons": FEE, + "cell_deps": smoke_tx["cell_deps"], "header_deps": smoke_tx["header_deps"], "witnesses": smoke_tx["witnesses"], + "outputs_data_count": smoke_tx["outputs_data"].as_array().map_or(0, Vec::len), + "outputs_count": smoke_tx["outputs"].as_array().map_or(0, Vec::len), + "lineage": [{"from": out_point(funding["tx_hash"].as_str().unwrap(), funding["index"].as_u64().unwrap()), "to_output_index": 0, "relation": "adapter-local-node-smoke"}], + "tx_shape_hash": ckb_hash_hex(smoke_text.as_bytes()), + }, + "deploy_probe": { + "status": "passed", "type_id_args": type_id_args, "artifact_data_hash": ckb_hash_hex(&artifact), + "code_output_capacity_shannons": code_capacity, "change_output_capacity_shannons": change_capacity, "fee_shannons": FEE, + "estimate_cycles": deploy_estimate, "test_tx_pool_accept": deploy_pool_accept, "tx_size_json_bytes": deploy_text.len(), + "outputs_count": deploy_tx["outputs"].as_array().map_or(0, Vec::len), + "outputs_data_count": deploy_tx["outputs_data"].as_array().map_or(0, Vec::len), + "cell_deps_count": deploy_tx["cell_deps"].as_array().map_or(0, Vec::len), + }, + "commit_evidence": {"status": "committed", "deploy_tx_hash": deploy_hash, "commit_block_hash": "0x", + "code_cell_live": live["status"] == "live", "code_cell_has_type_script": !live.pointer("/cell/output/type").unwrap_or(&Value::Null).is_null()}, + "known_limitations": [ + "This focused adapter acceptance proves CKB SDK/RPC materialization boundary evidence, not full CellScript business-flow semantics.", + "Stateful business-flow semantics remain covered by ckb_cellscript_acceptance.sh and release gates.", + "No wallet UI, CellFabric intent DAG, external audit, or mainnet-value certification is claimed.", + "The deploy probe uses always_success with hash_type=data as the type script for devnet acceptance; production TYPE_ID uses hash_type=type with the actual TYPE_ID script code_hash." + ], + "implementation": {"language": "rust", "tool": "cellscript-tools", "source": "crates/cellscript-tools/src/ckb_adapter_live.rs"}, + }); + fs::write(report_path, format!("{}\n", stable_json_pretty(&report)?))?; + println!("{}", report_path.display()); + devnet.stop(); + Ok(0) +} diff --git a/crates/cellscript-tools/src/ckb_devnet.rs b/crates/cellscript-tools/src/ckb_devnet.rs new file mode 100644 index 00000000..65a5690d --- /dev/null +++ b/crates/cellscript-tools/src/ckb_devnet.rs @@ -0,0 +1,639 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::error::Error; +use std::fmt::{Display, Formatter}; +use std::fs::{self, File}; +use std::net::TcpListener; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Stdio}; +use std::thread; +use std::time::Duration; + +use anyhow::{bail, Context, Result}; +use blake2b_ref::Blake2bBuilder; +use ckb_types::{bytes::Bytes, packed::WitnessArgs, prelude::*}; +use k256::schnorr::SigningKey; +use regex::Regex; +use reqwest::blocking::{Client, ClientBuilder}; +use serde_json::{json, Map, Value}; +use sha2::{Digest, Sha256}; +use wait_timeout::ChildExt; + +pub const CKB_PERSONAL: &[u8] = b"ckb-default-hash"; +pub const PACKED_HASH_DOMAIN: &[u8] = b"CellScriptPackedHashV0\0"; +pub const ALWAYS_SUCCESS_CODE_HASH: &str = "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5"; +pub const ALWAYS_SUCCESS_INDEX: u64 = 5; +pub const SHANNONS: u64 = 100_000_000; +pub const STATE_CAPACITY: u64 = 1_000 * SHANNONS; +pub const RECEIPT_CAPACITY: u64 = 1_000 * SHANNONS; +pub const ZERO_HASH: [u8; 32] = [0; 32]; +pub const TEST_SECRET_KEY: [u8; 32] = hex_literal::hex!("3e7490680639a2f7bbe8361dd3f34eb6429a9c924d8b342c015e555e628f94e5"); +pub const TEST_AUX_RAND: [u8; 32] = [0x42; 32]; + +#[derive(Debug)] +pub struct RpcFailure { + message: String, + pub error: Value, +} + +impl Display for RpcFailure { + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + formatter.write_str(&self.message) + } +} + +impl Error for RpcFailure {} + +pub fn ckb_hash(data: &[u8]) -> [u8; 32] { + let mut state = Blake2bBuilder::new(32).personal(CKB_PERSONAL).build(); + state.update(data); + let mut result = [0_u8; 32]; + state.finalize(&mut result); + result +} + +pub fn ckb_hash_hex(data: &[u8]) -> String { + hex0x(&ckb_hash(data)) +} + +pub fn sha256_hex(data: &[u8]) -> String { + format!("0x{}", hex::encode(Sha256::digest(data))) +} + +pub fn hex0x(data: &[u8]) -> String { + format!("0x{}", hex::encode(data)) +} + +pub fn entry_witness_input_type_hex(payload: &[u8]) -> String { + let witness = WitnessArgs::new_builder().input_type(Some(Bytes::copy_from_slice(payload)).pack()).build(); + hex0x(witness.as_slice()) +} + +pub fn decode_hex(value: &str) -> Result> { + Ok(hex::decode(value.strip_prefix("0x").unwrap_or(value))?) +} + +pub fn u8_bytes(value: u64) -> Vec { + vec![value as u8] +} + +pub fn u16_bytes(value: u64) -> Vec { + (value as u16).to_le_bytes().to_vec() +} + +pub fn u32_bytes(value: usize) -> Vec { + (value as u32).to_le_bytes().to_vec() +} + +pub fn u64_bytes(value: u64) -> Vec { + value.to_le_bytes().to_vec() +} + +pub fn packed_hash(type_name: &str, packed: &[u8]) -> [u8; 32] { + let mut preimage = Vec::with_capacity(PACKED_HASH_DOMAIN.len() + type_name.len() + 5 + packed.len()); + preimage.extend_from_slice(PACKED_HASH_DOMAIN); + preimage.extend_from_slice(type_name.as_bytes()); + preimage.push(0); + preimage.extend_from_slice(&(packed.len() as u32).to_le_bytes()); + preimage.extend_from_slice(packed); + ckb_hash(&preimage) +} + +pub fn xonly_pubkey(secret: &[u8; 32]) -> Result<[u8; 32]> { + let key = SigningKey::from_bytes(secret).map_err(|error| anyhow::anyhow!("invalid BIP340 secret key: {error}"))?; + Ok(key.verifying_key().to_bytes().into()) +} + +pub fn schnorr_sign(message: &[u8; 32], secret: &[u8; 32], aux: &[u8; 32]) -> Result<([u8; 32], [u8; 64])> { + let key = SigningKey::from_bytes(secret).map_err(|error| anyhow::anyhow!("invalid BIP340 secret key: {error}"))?; + let signature = key.sign_prehash_with_aux_rand(message, aux).map_err(|error| anyhow::anyhow!("BIP340 signing failed: {error}"))?; + Ok((key.verifying_key().to_bytes().into(), signature.to_bytes())) +} + +fn display_path(path: &Path, root: &Path) -> String { + path.strip_prefix(root).unwrap_or(path).to_string_lossy().replace('\\', "/") +} + +fn collect_source_files(root: &Path, path: &Path, files: &mut BTreeSet, invalid: &mut BTreeSet) -> Result<()> { + let metadata = match fs::symlink_metadata(path) { + Ok(value) => value, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(error) => return Err(error.into()), + }; + if metadata.file_type().is_symlink() { + invalid.insert(display_path(path, root)); + return Ok(()); + } + if metadata.is_file() { + files.insert(path.to_path_buf()); + return Ok(()); + } + if !metadata.is_dir() { + return Ok(()); + } + for entry in fs::read_dir(path)? { + let entry = entry?; + let child = entry.path(); + let relative = child.strip_prefix(path).unwrap_or(&child); + if relative.components().any(|component| matches!(component.as_os_str().to_str(), Some("target" | "build" | ".git"))) { + continue; + } + let metadata = fs::symlink_metadata(&child)?; + if metadata.file_type().is_symlink() { + invalid.insert(display_path(&child, root)); + continue; + } + if metadata.is_dir() { + collect_source_files(root, &child, files, invalid)?; + continue; + } + if !metadata.is_file() { + continue; + } + let extension = child.extension().and_then(|value| value.to_str()); + if matches!(extension, Some("cell" | "schema" | "toml" | "json" | "rs")) + || child.file_name().is_some_and(|value| value == "Cargo.lock") + { + files.insert(child); + } + } + Ok(()) +} + +pub fn source_tree_hash(root: &Path, paths: &[PathBuf]) -> Result { + let mut files = BTreeSet::new(); + let mut invalid = BTreeSet::new(); + for raw in paths { + let path = if raw.is_absolute() { raw.clone() } else { root.join(raw) }; + collect_source_files(root, &path, &mut files, &mut invalid)?; + } + let mut hasher = Sha256::new(); + let mut rows = Vec::new(); + for path in files { + let relative = display_path(&path, root); + let digest = Sha256::digest(fs::read(&path)?); + hasher.update(relative.as_bytes()); + hasher.update([0]); + hasher.update(digest); + rows.push(relative); + } + Ok(json!({ + "sha256": if invalid.is_empty() { Value::String(format!("0x{}", hex::encode(hasher.finalize()))) } else { Value::Null }, + "files": rows, "file_count": rows.len(), "valid": invalid.is_empty(), "invalid_paths": invalid + })) +} + +pub fn provenance(root: &Path, source_paths: &[PathBuf], artifacts: &BTreeMap) -> Result { + let commit = Command::new("git") + .args(["rev-parse", "HEAD"]) + .current_dir(root) + .output() + .ok() + .filter(|output| output.status.success()) + .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_owned()); + let mut artifact_rows = Map::new(); + for (name, path) in artifacts { + let bytes = fs::read(path)?; + artifact_rows.insert(name.clone(), json!({ + "path": display_path(path, root), "sha256": sha256_hex(&bytes), "ckb_data_hash": ckb_hash_hex(&bytes), "size_bytes": bytes.len() + })); + } + Ok(json!({"repo_commit": commit, "source_tree": source_tree_hash(root, source_paths)?, "artifacts": artifact_rows})) +} + +fn copy_tree(source: &Path, destination: &Path) -> Result<()> { + fs::create_dir_all(destination)?; + for entry in fs::read_dir(source)? { + let entry = entry?; + let target = destination.join(entry.file_name()); + if entry.file_type()?.is_dir() { + copy_tree(&entry.path(), &target)?; + } else { + fs::copy(entry.path(), target)?; + } + } + Ok(()) +} + +fn pick_port() -> Result { + Ok(TcpListener::bind(("127.0.0.1", 0))?.local_addr()?.port()) +} + +pub fn resolve_ckb_bin(repo: &Path, configured: Option<&Path>) -> Result { + if let Some(path) = configured { + if !path.is_file() { + bail!("CKB binary is not executable: {}", path.display()); + } + return Ok(fs::canonicalize(path)?); + } + for path in [repo.join("target/debug/ckb"), repo.join("target/release/ckb")] { + if path.is_file() { + return Ok(fs::canonicalize(path)?); + } + } + bail!("no CKB binary found under {}; pass --ckb-bin", repo.display()) +} + +fn patch_config(path: &Path, rpc: u16, p2p: u16) -> Result<()> { + let text = fs::read_to_string(path)?; + let rpc_pattern = Regex::new(r#"listen_address = "127\.0\.0\.1:\d+""#)?; + let p2p_pattern = Regex::new(r#"listen_addresses = \["/ip4/0\.0\.0\.0/tcp/\d+"\]"#)?; + let text = rpc_pattern.replacen(&text, 1, format!("listen_address = \"127.0.0.1:{rpc}\"").as_str()); + let text = p2p_pattern.replacen(&text, 1, format!("listen_addresses = [\"/ip4/127.0.0.1/tcp/{p2p}\"]").as_str()); + fs::write(path, text.as_bytes())?; + Ok(()) +} + +pub struct CkbDevnet { + pub ckb_repo: PathBuf, + pub ckb_bin: PathBuf, + pub ckb_dir: PathBuf, + pub log_path: PathBuf, + pub rpc_url: String, + client: Client, + process: Option, + reserved: BTreeSet<(String, u64)>, +} + +impl CkbDevnet { + pub fn new(ckb_repo: PathBuf, ckb_bin: PathBuf, run_dir: PathBuf) -> Result { + let rpc = pick_port()?; + let p2p = pick_port()?; + let ckb_dir = run_dir.join("ckb-node"); + let log_path = run_dir.join("ckb.log"); + let client = ClientBuilder::new().no_proxy().timeout(Duration::from_secs(20)).build()?; + let mut devnet = Self { + ckb_repo, + ckb_bin, + ckb_dir, + log_path, + rpc_url: format!("http://127.0.0.1:{rpc}"), + client, + process: None, + reserved: BTreeSet::new(), + }; + devnet.prepare(rpc, p2p)?; + Ok(devnet) + } + + fn prepare(&mut self, rpc: u16, p2p: u16) -> Result<()> { + let template = self.ckb_repo.join("test/template"); + if !template.is_dir() { + bail!("CKB test template not found: {}", template.display()); + } + fs::create_dir_all(self.ckb_dir.parent().context("CKB directory has no parent")?)?; + if self.ckb_dir.exists() { + bail!("CKB run directory already exists: {}", self.ckb_dir.display()); + } + copy_tree(&template, &self.ckb_dir)?; + patch_config(&self.ckb_dir.join("ckb.toml"), rpc, p2p) + } + + pub fn start(&mut self) -> Result<()> { + let log = File::create(&self.log_path)?; + self.process = Some( + Command::new(&self.ckb_bin) + .args(["-C", self.ckb_dir.to_str().unwrap(), "run", "--ba-advanced"]) + .stdout(Stdio::from(log.try_clone()?)) + .stderr(Stdio::from(log)) + .spawn()?, + ); + for _ in 0..80 { + if self.rpc("get_tip_header", vec![]).is_ok() { + return Ok(()); + } + if self.process.as_mut().and_then(|process| process.try_wait().ok()).flatten().is_some() { + bail!("CKB process exited early; see {}", self.log_path.display()); + } + thread::sleep(Duration::from_millis(250)); + } + bail!("CKB RPC did not become ready at {}; see {}", self.rpc_url, self.log_path.display()) + } + + pub fn stop(&mut self) { + let Some(process) = self.process.as_mut() else { return }; + if process.try_wait().ok().flatten().is_none() { + let _ = Command::new("kill").args(["-TERM", &process.id().to_string()]).status(); + if process.wait_timeout(Duration::from_secs(5)).ok().flatten().is_none() { + let _ = process.kill(); + let _ = process.wait(); + } + } + } + + pub fn rpc(&self, method: &str, params: Vec) -> Result { + let mut last = String::new(); + for attempt in 0..6 { + match self.client.post(&self.rpc_url).json(&json!({"id": 42, "jsonrpc": "2.0", "method": method, "params": params})).send() + { + Ok(response) => { + let payload: Value = response.json()?; + if !payload["error"].is_null() { + return Err(RpcFailure { + message: format!("RPC {method} returned error: {}", payload["error"]), + error: payload["error"].clone(), + } + .into()); + } + return Ok(payload.get("result").cloned().unwrap_or(Value::Null)); + } + Err(error) => last = error.to_string(), + } + thread::sleep(Duration::from_millis(250 * (attempt + 1))); + } + bail!("RPC {method} failed after retries: {last}") + } + + pub fn get_block(&self, hash: &str) -> Result { + for _ in 0..20 { + let block = self.rpc("get_block", vec![json!(hash)])?; + if !block.is_null() { + return Ok(block); + } + thread::sleep(Duration::from_millis(50)); + } + bail!("block not found: {hash}") + } + + pub fn get_block_by_number(&self, number: u64) -> Result { + let block = self.rpc("get_block_by_number", vec![json!(format!("0x{number:x}"))])?; + if block.is_null() { + bail!("block number not found: {number}"); + } + Ok(block) + } + + pub fn wait_live_cell(&self, hash: &str, index: u64) -> Result { + let mut last = Value::Null; + for _ in 0..40 { + last = self.rpc("get_live_cell", vec![out_point(hash, index), json!(true)])?; + if last["status"] == "live" { + return Ok(last); + } + thread::sleep(Duration::from_millis(50)); + } + bail!("cell is not live: {hash}:{index}; last={last}") + } + + #[allow(clippy::too_many_arguments)] + pub fn assert_live_cell( + &self, + hash: &str, + index: u64, + label: &str, + capacity: Option, + lock: Option<&Value>, + type_script: Option<&Value>, + data: Option<&[u8]>, + ) -> Result { + let live = self.wait_live_cell(hash, index)?; + let output = &live["cell"]["output"]; + let actual_data = &live["cell"]["data"]; + if let Some(expected) = capacity { + let actual = output["capacity"] + .as_str() + .and_then(|value| u64::from_str_radix(value.trim_start_matches("0x"), 16).ok()) + .unwrap_or(0); + if actual != expected { + bail!("{label} capacity mismatch: {} != 0x{expected:x}", output["capacity"]); + } + } + if let Some(expected) = lock + && &output["lock"] != expected + { + bail!("{label} lock mismatch: {} != {expected}", output["lock"]); + } + if let Some(expected) = type_script + && &output["type"] != expected + { + bail!("{label} type mismatch: {} != {expected}", output["type"]); + } + if let Some(expected) = data { + if actual_data["content"] != hex0x(expected) { + bail!("{label} data content mismatch"); + } + let expected_hash = ckb_hash_hex(expected); + if actual_data["hash"] != expected_hash { + bail!("{label} data hash mismatch: {} != {expected_hash}", actual_data["hash"]); + } + } + Ok(live) + } + + pub fn wait_dead_cell(&self, hash: &str, index: u64) -> Result { + let mut last = Value::Null; + for _ in 0..40 { + last = self.rpc("get_live_cell", vec![out_point(hash, index), json!(false)])?; + if !last.is_null() && last["status"] != "live" { + return Ok(last); + } + thread::sleep(Duration::from_millis(50)); + } + bail!("cell is still live: {hash}:{index}; last={last}") + } + + pub fn find_spendable(&mut self) -> Result { + for _ in 0..80 { + let hash = self.rpc("generate_block", vec![])?.as_str().context("generate_block returned no hash")?.to_owned(); + let block = self.get_block(&hash)?; + let cellbase = &block["transactions"][0]; + let tx_hash = cellbase["hash"].as_str().context("cellbase hash missing")?; + for (index, output) in cellbase["outputs"].as_array().map(Vec::as_slice).unwrap_or(&[]).iter().enumerate() { + let capacity = output["capacity"] + .as_str() + .and_then(|value| u64::from_str_radix(value.trim_start_matches("0x"), 16).ok()) + .unwrap_or(0); + if capacity > 0 && self.reserved.insert((tx_hash.into(), index as u64)) { + self.wait_live_cell(tx_hash, index as u64)?; + return Ok(json!({"tx_hash": tx_hash, "index": index, "capacity": capacity})); + } + } + } + bail!("no spendable cellbase found") + } + + pub fn collect_spendable(&mut self, minimum: u64) -> Result { + let mut cells = Vec::new(); + let mut total = 0; + while total < minimum { + let cell = self.find_spendable()?; + total += cell["capacity"].as_u64().unwrap(); + cells.push(cell); + } + Ok(json!({"cells": cells, "total_capacity": total})) + } + + pub fn submit_and_commit(&self, tx: &Value, label: &str) -> Result { + let hash = self + .rpc("send_test_transaction", vec![tx.clone(), json!("passthrough")])? + .as_str() + .context("send_test_transaction returned no hash")? + .to_owned(); + let mut last = Value::Null; + for generated in 0..80 { + let status = self.rpc("get_transaction", vec![json!(hash)])?; + last = status.get("tx_status").cloned().unwrap_or_else(|| json!({})); + if last["status"] == "committed" { + return Ok(json!({"tx_hash": hash, "generated_blocks_after_submit": generated, "status": last})); + } + if last["status"] == "rejected" { + bail!("{label} rejected: {hash}; status={last}"); + } + self.rpc("generate_block", vec![])?; + thread::sleep(Duration::from_millis(50)); + } + bail!("{label} not committed: {hash}; last_status={last}") + } + + pub fn dry_run(&self, tx: &Value) -> Result { + self.rpc("dry_run_transaction", vec![tx.clone()]) + } + + pub fn dry_run_rejects( + &self, + tx: &Value, + label: &str, + source: Option<&str>, + data_hash: Option<&str>, + error_code: Option, + ) -> Result { + match self.rpc("dry_run_transaction", vec![tx.clone()]) { + Ok(value) => bail!("{label} unexpectedly passed dry-run: {value}"), + Err(error) => { + let reason = error.to_string(); + let rpc = error.downcast_ref::(); + let mut checks = Map::new(); + if let Some(expected) = source { + checks.insert("source".into(), json!(reason.contains(expected))); + } + if let Some(expected) = data_hash { + checks.insert( + "data_hash".into(), + json!(reason.to_lowercase().contains(expected.trim_start_matches("0x").to_lowercase().as_str())), + ); + } + if let Some(expected) = error_code { + checks.insert("error_code".into(), json!(script_error_matches(&reason, rpc.map(|value| &value.error), expected))); + } + let matched = checks.values().all(|value| value == true); + if !matched { + bail!("{label} rejected for unexpected reason: checks={} reason={reason}", Value::Object(checks)); + } + Ok(json!({"status": "rejected", "label": label, "reason": reason, + "expected": {"source": source, "data_hash": data_hash, "error_code": error_code}, "matched_expected": matched})) + } + } + } +} + +impl Drop for CkbDevnet { + fn drop(&mut self) { + self.stop(); + } +} + +fn script_error_value(value: &Value, keys: &[&str]) -> Option { + match value { + Value::Object(object) => { + for (key, value) in object { + if keys.contains(&key.as_str()) + && let Some(number) = value.as_i64().or_else(|| value.as_str().and_then(|value| value.parse().ok())) + { + return Some(number); + } + if let Some(found) = script_error_value(value, keys) { + return Some(found); + } + } + None + } + Value::Array(values) => values.iter().find_map(|value| script_error_value(value, keys)), + _ => None, + } +} + +fn script_error_matches(reason: &str, error: Option<&Value>, expected: i64) -> bool { + let keys = ["error_code", "errorCode", "exit_code", "exitCode", "script_error_code", "scriptErrorCode"]; + if error.and_then(|value| script_error_value(value, &keys)) == Some(expected) { + return true; + } + [ + format!(r"\berror code\s*[:#]?\s*{expected}\b"), + format!(r"\berror_code\s*[:=]\s*{expected}\b"), + format!(r"\bexit[_ ]?code\s*[:=]\s*{expected}\b"), + format!(r"\bExitCode\(\s*{expected}\s*\)"), + format!(r"#{expected}\b"), + ] + .iter() + .any(|pattern| Regex::new(pattern).is_ok_and(|regex| regex.is_match(reason))) +} + +pub fn out_point(hash: &str, index: u64) -> Value { + json!({"tx_hash": hash, "index": format!("0x{index:x}")}) +} +pub fn always_success_dep(genesis: &str) -> Value { + json!({"out_point": out_point(genesis, ALWAYS_SUCCESS_INDEX), "dep_type": "code"}) +} +pub fn always_success_lock(args: &str) -> Value { + json!({"code_hash": ALWAYS_SUCCESS_CODE_HASH, "hash_type": "data", "args": args}) +} + +pub fn transaction( + inputs: &[Value], + outputs: Vec, + outputs_data: Vec, + deps: Vec, + witnesses: Vec, + headers: Vec, +) -> Value { + json!({"version": "0x0", "cell_deps": deps, "header_deps": headers, + "inputs": inputs.iter().map(|cell| json!({"previous_output": out_point(cell["tx_hash"].as_str().unwrap(), cell["index"].as_u64().unwrap()), "since": "0x0"})).collect::>(), + "outputs": outputs, "outputs_data": outputs_data, "witnesses": witnesses}) +} + +pub fn funding_cells(funding: &Value) -> &[Value] { + funding["cells"].as_array().map(Vec::as_slice).unwrap_or(&[]) +} + +pub fn deploy_code(devnet: &mut CkbDevnet, name: &str, artifact: &[u8], always_dep: &Value) -> Result { + let funding = devnet.collect_spendable((artifact.len() as u64 + 1_000) * SHANNONS)?; + let cells = funding_cells(&funding); + let total = funding["total_capacity"].as_u64().unwrap(); + let tx = transaction( + cells, + vec![json!({"capacity": format!("0x{total:x}"), "lock": always_success_lock("0x"), "type": Value::Null})], + vec![hex0x(artifact)], + vec![always_dep.clone()], + vec!["0x".into(); cells.len()], + vec![], + ); + let dry_run = devnet.dry_run(&tx)?; + let commit = devnet.submit_and_commit(&tx, &format!("deploy {name}"))?; + devnet.assert_live_cell( + commit["tx_hash"].as_str().unwrap(), + 0, + &format!("deploy {name}"), + Some(total), + Some(&always_success_lock("0x")), + Some(&Value::Null), + Some(artifact), + )?; + Ok(json!({"name": name, "artifact_size_bytes": artifact.len(), "data_hash": ckb_hash_hex(artifact), + "cell_dep": {"out_point": out_point(commit["tx_hash"].as_str().unwrap(), 0), "dep_type": "code"}, + "valid_deploy_dry_run": dry_run, "commit": commit})) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn entry_witness_helper_places_payload_in_input_type() { + let payload = b"CSARGv1\0payload"; + let encoded = decode_hex(&entry_witness_input_type_hex(payload)).unwrap(); + let witness = WitnessArgs::from_slice(&encoded).unwrap(); + + assert!(witness.lock().to_opt().is_none()); + assert_eq!(witness.input_type().to_opt().unwrap().raw_data(), Bytes::from_static(payload)); + assert!(witness.output_type().to_opt().is_none()); + } +} diff --git a/crates/cellscript-tools/src/crypto.rs b/crates/cellscript-tools/src/crypto.rs new file mode 100644 index 00000000..18930b47 --- /dev/null +++ b/crates/cellscript-tools/src/crypto.rs @@ -0,0 +1,61 @@ +//! Hashing helpers shared by migrated evidence generators. + +use anyhow::{bail, Context, Result}; +use blake2b_ref::Blake2bBuilder; +use serde_json::Value; +use sha2::{Digest, Sha256}; + +use crate::shared::stable_json_compact; + +pub fn hex0x(bytes: &[u8]) -> String { + format!("0x{}", hex::encode(bytes)) +} + +pub fn decode_hex0x(value: &str) -> Result> { + hex::decode(value.strip_prefix("0x").unwrap_or(value)).with_context(|| format!("invalid hexadecimal value: {value}")) +} + +pub fn bytes32(value: &str) -> Result<[u8; 32]> { + let bytes = decode_hex0x(value)?; + bytes.try_into().map_err(|bytes: Vec| anyhow::anyhow!("expected Byte32, got {} bytes", bytes.len())) +} + +pub fn personalized_blake2b256(personalization: &[u8], chunks: &[&[u8]]) -> Result<[u8; 32]> { + if personalization.len() > 16 { + bail!("BLAKE2b personalization exceeds 16 bytes"); + } + let mut state = Blake2bBuilder::new(32).personal(personalization).build(); + for chunk in chunks { + state.update(chunk); + } + let mut digest = [0_u8; 32]; + state.finalize(&mut digest); + Ok(digest) +} + +pub fn ckb_blake2b256(bytes: &[u8]) -> Result<[u8; 32]> { + personalized_blake2b256(b"ckb-default-hash", &[bytes]) +} + +pub fn canonical_report_hash(personalization: &[u8], label: &str, value: &Value) -> Result { + let canonical = stable_json_compact(value)?; + let digest = personalized_blake2b256(personalization, &[label.as_bytes(), b"\0", canonical.as_bytes()])?; + Ok(hex0x(&digest)) +} + +pub fn sha256_hex(bytes: &[u8]) -> String { + hex::encode(Sha256::digest(bytes)) +} + +pub fn nonzero_hex32(value: &Value) -> bool { + let Some(value) = value.as_str() else { + return false; + }; + let Some(raw) = value.strip_prefix("0x") else { + return false; + }; + if raw.len() != 64 { + return false; + } + hex::decode(raw).is_ok_and(|bytes| bytes.iter().any(|byte| *byte != 0)) +} diff --git a/crates/cellscript-tools/src/external_attestation.rs b/crates/cellscript-tools/src/external_attestation.rs new file mode 100644 index 00000000..79d12199 --- /dev/null +++ b/crates/cellscript-tools/src/external_attestation.rs @@ -0,0 +1,217 @@ +//! Rust port of the NovaSeal external attestation request adapter. + +use std::collections::BTreeSet; +use std::fs; +use std::path::Path; + +use anyhow::{Context, Result}; +use serde_json::{json, Value}; + +use crate::crypto::canonical_report_hash; +use crate::shared::{lexical_path, stable_json_pretty}; + +const PERSON: &[u8] = b"NovaExtAttReqV0"; + +fn read_json(path: &Path) -> Result { + serde_json::from_slice(&fs::read(path).with_context(|| format!("failed to read {}", path.display()))?) + .with_context(|| format!("failed to decode {}", path.display())) +} + +fn hash(label: &str, value: &Value) -> Result { + canonical_report_hash(PERSON, label, value) +} + +fn present(value: &Value) -> bool { + !value.is_null() + && value.as_str() != Some("") + && !value.as_array().is_some_and(Vec::is_empty) + && !value.as_object().is_some_and(serde_json::Map::is_empty) +} + +fn public_case(template: &Value, tcb: &Value) -> Result { + let verifier = template.get("runtime_verifier").cloned().unwrap_or_else(|| json!({})); + let release = template.get("release").cloned().unwrap_or_else(|| json!({})); + let runtime = tcb.get("runtime_artifact").cloned().unwrap_or_else(|| json!({})); + let required_fields = json!([ + "network", + "attested_at", + "attestor", + "release.package", + "release.version", + "release.manifest_commit", + "runtime_verifier.verifier_id", + "runtime_verifier.ipc_abi", + "runtime_verifier.out_point", + "runtime_verifier.data_hash", + "runtime_verifier.dep_type", + "runtime_verifier.hash_type", + "runtime_verifier.artifact_hash", + "request_handoff.bundle", + "request_handoff.bundle_hash", + "request_handoff.bundle_hash_algorithm", + "request_handoff.group" + ]); + let request = json!({ + "attestation_type": "public_shared_cell_dep_attestation", + "production_output": "proposals/novaseal/v0-mvp-skeleton/proofs/public_shared_cell_dep_attestation.json", + "template_schema": template.get("schema").cloned().unwrap_or(Value::Null), + "template_hash": hash("public_celldep_template", template)?, + "required_public_fields": required_fields, + "field_constraints": { + "network": "explicit public CKB mainnet/testnet name; placeholders and local/devnet/regtest/simnet/private/fake labels are rejected", + "attested_at": "UTC timestamp in YYYY-MM-DDTHH:MM:SSZ form; future timestamps are rejected", + "attestor": "real independent release signer or deployer identity; placeholder, first-party NovaSeal/CellScript/a19q3, local/devnet/fake/internal, example, and unknown tokens are rejected", + "release.package": "novaseal", + "release.version": "exact NovaSeal release version 0.0.1-v0-mvp", + "release.manifest_commit": "40-character hex source commit matching the reviewed TCB repo_commit", + "runtime_verifier.verifier_id": "btc.bip340.v0", + "runtime_verifier.ipc_abi": "cellscript-btc-bip340-ipc-v0", + "runtime_verifier.out_point": "0x-prefixed 32-byte CKB transaction hash plus numeric output index", + "runtime_verifier.data_hash": "0x-prefixed 32-byte non-placeholder CellDep data hash", + "runtime_verifier.dep_type": "code", + "runtime_verifier.hash_type": "data1", + "runtime_verifier.artifact_hash": "0x-prefixed 32-byte non-placeholder BIP340 runtime verifier artifact hash", + "request_handoff.bundle": "target/novaseal-external-evidence-handoff-bundle.json", + "request_handoff.bundle_hash": "0x-prefixed 32-byte hash of the NovaSeal external evidence handoff bundle", + "request_handoff.bundle_hash_algorithm": "blake2b-256(person=NovaExtHandoff)", + "request_handoff.group": "public_shared_cell_dep_attestation" + }, + "verifier_id": verifier.get("verifier_id").cloned().unwrap_or(Value::Null), + "ipc_abi": verifier.get("ipc_abi").cloned().unwrap_or(Value::Null), + "expected_artifact_hash": runtime.get("artifact_hash").filter(|value| value.as_str().is_some_and(|text| !text.is_empty())).or_else(|| verifier.get("artifact_hash")).cloned().unwrap_or(Value::Null), + "expected_release_package": release.get("package").cloned().unwrap_or(Value::Null), + "expected_release_version": release.get("version").cloned().unwrap_or(Value::Null), + "expected_release_manifest_commit": tcb.get("repo_commit").cloned().unwrap_or(Value::Null), + "expected_dep_type": verifier.get("dep_type").cloned().unwrap_or(Value::Null), + "expected_hash_type": verifier.get("hash_type").cloned().unwrap_or(Value::Null), + "template_artifact_hash": verifier.get("artifact_hash").cloned().unwrap_or(Value::Null), + "required_status": "attested", + "network_must_not_equal": "local-devnet" + }); + let release_keys = release.as_object().map(|map| map.keys().map(String::as_str).collect::>()).unwrap_or_default(); + let checks = json!({ + "template_schema_current": request["template_schema"] == "novaseal-public-shared-cell-dep-attestation-v0.1", + "template_status_attested": template.get("status").and_then(Value::as_str) == Some("attested"), + "release_fields_current": release_keys == BTreeSet::from(["package", "version", "manifest_commit"]), + "release_package_current": release.get("package").and_then(Value::as_str) == Some("novaseal"), + "release_version_current": release.get("version").and_then(Value::as_str) == Some("0.0.1-v0-mvp"), + "release_manifest_commit_present": release.get("manifest_commit").is_some_and(present), + "expected_release_manifest_commit_present": present(&request["expected_release_manifest_commit"]), + "verifier_id_current": request["verifier_id"] == "btc.bip340.v0", + "ipc_abi_current": request["ipc_abi"] == "cellscript-btc-bip340-ipc-v0", + "dep_type_current": request["expected_dep_type"] == "code", + "hash_type_current": request["expected_hash_type"] == "data1", + "artifact_hash_matches_tcb": request["template_artifact_hash"] == request["expected_artifact_hash"], + "required_fields_complete": request["required_public_fields"].as_array().is_some_and(|fields| fields.len() == 17) + }); + let passed = checks.as_object().is_some_and(|map| map.values().all(|value| value == &Value::Bool(true))); + Ok( + json!({ "name": "public_shared_cell_dep_attestation", "status": if passed { "passed" } else { "failed" }, "checks": checks, "request": request }), + ) +} + +fn external_case(template: &Value, tcb: &Value) -> Result { + let runtime = tcb.get("runtime_artifact").cloned().unwrap_or_else(|| json!({})); + let source = tcb.get("source_inventory").cloned().unwrap_or_else(|| json!({})); + let request = json!({ + "attestation_type": "external_bip340_tcb_review_attestation", + "production_output": "proposals/novaseal/v0-mvp-skeleton/proofs/bip340_external_tcb_review_attestation.json", + "template_schema": template.get("schema").cloned().unwrap_or(Value::Null), + "template_hash": hash("external_tcb_template", template)?, + "required_public_fields": ["reviewer", "review_date", "review_scope", "verifier_id", "ipc_abi", "artifact_hash", "artifact_hash_algorithm", "source_tree_sha256", "report_uri", "request_handoff.bundle", "request_handoff.bundle_hash", "request_handoff.bundle_hash_algorithm", "request_handoff.group"], + "field_constraints": { + "reviewer": "real external reviewer identity; placeholder, first-party NovaSeal/CellScript/a19q3, local/devnet/fake/internal, example, and unknown tokens are rejected", + "review_date": "UTC date in YYYY-MM-DD form; future dates are rejected", + "review_scope": "exact BIP340 verifier, RISC-V shell, IPC envelope, and artifact/CellDep pinning scope", + "verifier_id": "btc.bip340.v0", + "ipc_abi": "cellscript-btc-bip340-ipc-v0", + "artifact_hash": "0x-prefixed 32-byte non-placeholder BIP340 runtime verifier artifact hash", + "artifact_hash_algorithm": "sha256", + "source_tree_sha256": "0x-prefixed 32-byte non-placeholder SHA-256 source tree hash", + "report_uri": "HTTPS URI for the public review report or source-controlled review commit; example, loopback, private, and reserved hosts are rejected", + "request_handoff.bundle": "target/novaseal-external-evidence-handoff-bundle.json", + "request_handoff.bundle_hash": "0x-prefixed 32-byte hash of the NovaSeal external evidence handoff bundle", + "request_handoff.bundle_hash_algorithm": "blake2b-256(person=NovaExtHandoff)", + "request_handoff.group": "external_bip340_tcb_review_attestation" + }, + "verifier_id": template.get("verifier_id").cloned().unwrap_or(Value::Null), + "ipc_abi": template.get("ipc_abi").cloned().unwrap_or(Value::Null), + "expected_artifact_hash": runtime.get("artifact_hash").cloned().unwrap_or(Value::Null), + "template_artifact_hash": template.get("artifact_hash").cloned().unwrap_or(Value::Null), + "expected_artifact_hash_algorithm": runtime.get("artifact_hash_algorithm").cloned().unwrap_or(Value::Null), + "template_artifact_hash_algorithm": template.get("artifact_hash_algorithm").cloned().unwrap_or(Value::Null), + "expected_source_tree_sha256": source.get("source_tree_sha256").cloned().unwrap_or(Value::Null), + "template_source_tree_sha256": template.get("source_tree_sha256").cloned().unwrap_or(Value::Null), + "expected_review_scope": template.get("review_scope").cloned().unwrap_or(Value::Null), + "required_status": "accepted" + }); + let expected_scope = json!([ + "BIP340 verifier core", + "RISC-V runtime verifier shell", + "CellScript BIP340 IPC envelope", + "artifact hash and CellDep pinning requirements" + ]); + let checks = json!({ + "template_schema_current": request["template_schema"] == "novaseal-bip340-external-tcb-review-attestation-v0.1", + "template_status_accepted": template.get("status").and_then(Value::as_str) == Some("accepted"), + "verifier_id_current": request["verifier_id"] == "btc.bip340.v0", + "ipc_abi_current": request["ipc_abi"] == "cellscript-btc-bip340-ipc-v0", + "artifact_hash_matches_tcb": present(&request["expected_artifact_hash"]) && request["template_artifact_hash"] == request["expected_artifact_hash"], + "artifact_hash_algorithm_current": template.get("artifact_hash_algorithm").and_then(Value::as_str) == Some("sha256"), + "artifact_hash_algorithm_matches_tcb": present(&request["expected_artifact_hash_algorithm"]) && request["template_artifact_hash_algorithm"] == request["expected_artifact_hash_algorithm"], + "source_tree_hash_matches_tcb": present(&request["expected_source_tree_sha256"]) && request["template_source_tree_sha256"] == request["expected_source_tree_sha256"], + "review_scope_exact": template.get("review_scope") == Some(&expected_scope), + "required_fields_complete": request["required_public_fields"].as_array().is_some_and(|fields| fields.len() == 13) + }); + let passed = checks.as_object().is_some_and(|map| map.values().all(|value| value == &Value::Bool(true))); + Ok( + json!({ "name": "external_bip340_tcb_review_attestation", "status": if passed { "passed" } else { "failed" }, "checks": checks, "request": request }), + ) +} + +pub fn run( + root: &Path, + tcb_review: Option<&Path>, + public_template: Option<&Path>, + external_template: Option<&Path>, + output: Option<&Path>, + pretty: bool, +) -> Result { + let default_tcb = root.join("target/novaseal-bip340-tcb-review.json"); + let default_public = root.join("proposals/novaseal/v0-mvp-skeleton/proofs/public_shared_cell_dep_attestation.template.json"); + let default_external = root.join("proposals/novaseal/v0-mvp-skeleton/proofs/bip340_external_tcb_review_attestation.template.json"); + let default_output = root.join("target/novaseal-external-attestation-adapter.json"); + let tcb = read_json(&lexical_path(tcb_review.unwrap_or(&default_tcb)))?; + let public = read_json(&lexical_path(public_template.unwrap_or(&default_public)))?; + let external = read_json(&lexical_path(external_template.unwrap_or(&default_external)))?; + let cases = vec![public_case(&public, &tcb)?, external_case(&external, &tcb)?]; + let matched = cases.iter().filter(|case| case["status"] == "passed").count(); + let passed = matched == cases.len(); + let report = json!({ + "schema": "novaseal-external-attestation-adapter-v0.1", + "status": if passed { "passed" } else { "failed" }, + "adapter_status": "request_ready_external_attestations_required", + "source_tcb_review": "target/novaseal-bip340-tcb-review.json", + "source_tcb_review_hash": hash("tcb_review", &tcb)?, + "source_public_cell_dep_template": "proposals/novaseal/v0-mvp-skeleton/proofs/public_shared_cell_dep_attestation.template.json", + "source_public_cell_dep_template_hash": hash("public_celldep_template", &public)?, + "source_external_tcb_template": "proposals/novaseal/v0-mvp-skeleton/proofs/bip340_external_tcb_review_attestation.template.json", + "source_external_tcb_template_hash": hash("external_tcb_template", &external)?, + "production_boundary": "This adapter proves the attestation request package is complete; it does not prove public CellDep deployment or independent external TCB review.", + "summary": { "total": cases.len(), "matched": matched, "required_attestations": cases.iter().map(|case| case["name"].clone()).collect::>() }, + "cases": cases + }); + let output = lexical_path(output.unwrap_or(&default_output)); + fs::create_dir_all(output.parent().context("output path has no parent")?)?; + fs::write(&output, format!("{}\n", stable_json_pretty(&report)?))?; + if pretty { + println!( + "wrote {} status={} attestations={}/{}", + output.display(), + report["status"].as_str().unwrap_or("failed"), + matched, + report["summary"]["total"] + ); + } + Ok(if passed { 0 } else { 1 }) +} diff --git a/crates/cellscript-tools/src/external_handoff.rs b/crates/cellscript-tools/src/external_handoff.rs new file mode 100644 index 00000000..a5e6f67d --- /dev/null +++ b/crates/cellscript-tools/src/external_handoff.rs @@ -0,0 +1,472 @@ +//! NovaSeal external production-evidence handoff bundle. + +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::{Path, PathBuf}; + +use anyhow::{bail, Context, Result}; +use serde_json::{json, Map, Value}; +use sha2::{Digest, Sha256}; + +use crate::btc_spv_adapter::{field_constraints as btc_field_constraints, required_fields as btc_required_fields}; +use crate::crypto::{canonical_report_hash, sha256_hex}; +use crate::shared::{lexical_path, stable_json_pretty}; + +const PERSON: &[u8] = b"NovaExtHandoff"; +const HASH_ALGORITHM: &str = "blake2b-256(person=NovaExtHandoff)"; +const BTC_OUTPUT: &str = "proposals/novaseal/v0-mvp-skeleton/proofs/public_btc_spv_evidence.json"; +const CELLDEP_OUTPUT: &str = "proposals/novaseal/v0-mvp-skeleton/proofs/public_shared_cell_dep_attestation.json"; +const TCB_OUTPUT: &str = "proposals/novaseal/v0-mvp-skeleton/proofs/bip340_external_tcb_review_attestation.json"; +const RWA_OUTPUT: &str = "proposals/novaseal/rwa-receipt-profile-v0/proofs/legal_registry_review_evidence.json"; +const PROFILES: [&str; 3] = ["btc-transaction-commitment-profile-v0", "btc-utxo-seal-profile-v0", "dual-seal-profile-v0"]; + +fn hash(label: &str, value: &Value) -> Result { + canonical_report_hash(PERSON, label, value) +} + +fn hex32(value: &Value) -> bool { + value.as_str().is_some_and(|text| { + text.len() == 66 && text.starts_with("0x") && text[2..].chars().all(|character| character.is_ascii_hexdigit()) + }) +} + +fn non_placeholder(value: &Value) -> bool { + hex32(value) && value.as_str().is_some_and(|text| text[2..].chars().any(|character| character != '0')) +} + +fn non_negative(value: &Value) -> bool { + value.as_i64().is_some_and(|number| number >= 0) || value.as_u64().is_some() +} + +fn positive(value: &Value) -> bool { + value.as_i64().is_some_and(|number| number > 0) || value.as_u64().is_some_and(|number| number > 0) +} + +fn anchor_source(profile: &str) -> &'static str { + if profile == PROFILES[0] { + "external_public_btc_transaction" + } else { + "external_public_btc_spend" + } +} + +fn profile_mapping(profile: &str) -> BTreeMap<&'static str, &'static str> { + let mut fields = BTreeMap::from([ + ("anchor_source", "expected_anchor_source"), + ("btc_txid", "expected_btc_txid"), + ("btc_wtxid", "expected_btc_wtxid"), + ]); + if profile == PROFILES[0] { + fields.extend([("btc_output_index", "expected_btc_output_index"), ("btc_amount_sats", "expected_btc_amount_sats")]); + } else { + fields.extend([ + ("spend_input_index", "expected_spend_input_index"), + ("sealed_btc_txid", "expected_sealed_btc_txid"), + ("sealed_btc_vout_index", "expected_sealed_btc_vout_index"), + ("sealed_btc_amount_sats", "expected_sealed_btc_amount_sats"), + ("script_pubkey_hash", "expected_script_pubkey_hash"), + ("sealed_utxo_commitment_hash", "expected_sealed_utxo_commitment_hash"), + ]); + } + fields +} + +fn expected_binding_fields(profile: &str) -> BTreeSet { + let mut fields = [ + "ckb_live_tx_hash", + "live_report_hash", + "service_builder_case_hash", + "service_builder_tx_skeleton_hash", + "service_builder_receipt_binding_hash", + "ckb_btc_commitment_hash", + ] + .into_iter() + .map(ToOwned::to_owned) + .collect::>(); + fields.extend(profile_mapping(profile).keys().map(|value| (*value).to_owned())); + fields +} + +fn binding_valid(profile: &str, field: &str, value: &Value) -> bool { + match field { + "ckb_live_tx_hash" + | "live_report_hash" + | "service_builder_case_hash" + | "service_builder_tx_skeleton_hash" + | "service_builder_receipt_binding_hash" + | "ckb_btc_commitment_hash" + | "btc_txid" + | "btc_wtxid" + | "sealed_btc_txid" + | "script_pubkey_hash" + | "sealed_utxo_commitment_hash" => non_placeholder(value), + "anchor_source" => value.as_str() == Some(anchor_source(profile)), + "spend_input_index" | "sealed_btc_vout_index" | "btc_output_index" => non_negative(value), + "btc_amount_sats" | "sealed_btc_amount_sats" => positive(value), + _ => false, + } +} + +fn btc_case(adapter: &Value) -> Result { + let cases = adapter.get("cases").and_then(Value::as_array).cloned().unwrap_or_default(); + let profiles = cases.iter().filter_map(|case| case.get("profile").and_then(Value::as_str)).collect::>(); + let mut scenarios = Map::new(); + let mut bindings = Map::new(); + for case in &cases { + let Some(profile) = case.get("profile").and_then(Value::as_str) else { + continue; + }; + if let Some(scenario) = case.pointer("/request/scenario").and_then(Value::as_str) { + scenarios.insert(profile.to_owned(), Value::String(scenario.to_owned())); + } + let request = case.get("request").cloned().unwrap_or_else(|| json!({})); + let mut binding = Map::new(); + for field in [ + "ckb_live_tx_hash", + "live_report_hash", + "service_builder_case_hash", + "service_builder_tx_skeleton_hash", + "service_builder_receipt_binding_hash", + "ckb_btc_commitment_hash", + ] { + binding.insert(field.to_owned(), request.get(field).cloned().unwrap_or(Value::Null)); + } + for (output_field, request_field) in profile_mapping(profile) { + if let Some(value) = request.get(request_field) + && !value.is_null() + { + binding.insert(output_field.to_owned(), value.clone()); + } + } + bindings.insert(profile.to_owned(), Value::Object(binding)); + } + let required_profiles = PROFILES.into_iter().collect::>(); + let binding_complete = bindings.keys().map(String::as_str).collect::>() == required_profiles + && bindings.iter().all(|(profile, value)| { + let Some(values) = value.as_object() else { + return false; + }; + values.keys().cloned().collect::>() == expected_binding_fields(profile) + && values.iter().all(|(field, value)| binding_valid(profile, field, value)) + }); + let checks = json!({ + "source_adapter_passed": adapter.get("status").and_then(Value::as_str) == Some("passed"), + "source_adapter_status_request_ready": adapter.get("adapter_status").and_then(Value::as_str) == Some("request_ready_external_evidence_required"), + "production_output_matches": adapter.get("production_output").and_then(Value::as_str) == Some(BTC_OUTPUT), + "summary_counts_match": adapter.pointer("/summary/total").and_then(Value::as_u64) == Some(3) && adapter.pointer("/summary/matched") == adapter.pointer("/summary/total"), + "required_profiles_complete": profiles == required_profiles, + "expected_scenarios_complete": scenarios.keys().map(String::as_str).collect::>() == required_profiles && scenarios.values().all(|value| value.as_str().is_some_and(|text| !text.is_empty())), + "expected_case_bindings_complete": binding_complete, + "source_cases_passed": cases.iter().all(|case| case.get("status").and_then(Value::as_str) == Some("passed")) + }); + let passed = checks.as_object().is_some_and(|map| map.values().all(|value| value == &Value::Bool(true))); + Ok(json!({ + "group": "public_btc_spv_evidence", + "status": if passed { "passed" } else { "failed" }, + "checks": checks, + "source_adapter": "target/novaseal-btc-spv-evidence-adapter.json", + "source_adapter_hash": hash("btc_spv_adapter", adapter)?, + "production_output": BTC_OUTPUT, + "required_profiles": PROFILES, + "expected_scenarios": scenarios, + "expected_case_bindings": bindings, + "required_external_fields": btc_required_fields(), + "field_constraints": btc_field_constraints() + })) +} + +fn field_set(case: &Value) -> BTreeSet<&str> { + case.pointer("/request/required_public_fields").and_then(Value::as_array).into_iter().flatten().filter_map(Value::as_str).collect() +} + +fn truthy(value: Option<&Value>) -> bool { + value.is_some_and(|value| match value { + Value::Null | Value::Bool(false) => false, + Value::String(text) => !text.is_empty(), + Value::Array(values) => !values.is_empty(), + Value::Object(values) => !values.is_empty(), + Value::Number(number) => number.as_f64().is_some_and(|number| number != 0.0), + Value::Bool(true) => true, + }) +} + +fn attestation_case(adapter: &Value, name: &str, group: &str, output: &str, required: &[&str]) -> Result { + let empty = json!({}); + let source = adapter + .get("cases") + .and_then(Value::as_array) + .into_iter() + .flatten() + .find(|case| case.get("name").and_then(Value::as_str) == Some(name)) + .unwrap_or(&empty); + let request = source.get("request").cloned().unwrap_or_else(|| json!({})); + let fields = field_set(source); + let checks = json!({ + "source_adapter_passed": adapter.get("status").and_then(Value::as_str) == Some("passed"), + "source_adapter_status_request_ready": adapter.get("adapter_status").and_then(Value::as_str) == Some("request_ready_external_attestations_required"), + "source_case_passed": source.get("status").and_then(Value::as_str) == Some("passed"), + "production_output_matches": request.get("production_output").and_then(Value::as_str) == Some(output), + "required_fields_complete": required.iter().all(|field| fields.contains(field)) + }); + let passed = checks.as_object().is_some_and(|map| map.values().all(|value| value == &Value::Bool(true))); + let mut expected = Map::new(); + let mappings = [ + ("expected_release_package", "release.package"), + ("expected_release_version", "release.version"), + ("expected_release_manifest_commit", "release.manifest_commit"), + ("expected_dep_type", "runtime_verifier.dep_type"), + ("expected_hash_type", "runtime_verifier.hash_type"), + ]; + for (input, output) in mappings { + if truthy(request.get(input)) { + expected.insert(output.to_owned(), request[input].clone()); + } + } + if name == "public_shared_cell_dep_attestation" { + for (input, output) in [("ipc_abi", "runtime_verifier.ipc_abi"), ("verifier_id", "runtime_verifier.verifier_id")] { + if truthy(request.get(input)) { + expected.insert(output.to_owned(), request[input].clone()); + } + } + } else { + for input in ["ipc_abi", "verifier_id"] { + if truthy(request.get(input)) { + expected.insert(input.to_owned(), request[input].clone()); + } + } + } + for (input, output) in [ + ("expected_artifact_hash", "artifact_hash"), + ("expected_artifact_hash_algorithm", "artifact_hash_algorithm"), + ("expected_review_scope", "review_scope"), + ("expected_source_tree_sha256", "source_tree_sha256"), + ] { + if truthy(request.get(input)) { + expected.insert(output.to_owned(), request[input].clone()); + } + } + let mut result = json!({ + "group": group, + "status": if passed { "passed" } else { "failed" }, + "checks": checks, + "source_adapter": "target/novaseal-external-attestation-adapter.json", + "source_adapter_hash": hash("external_attestation_adapter", adapter)?, + "source_case": name, + "production_output": output, + "required_external_fields": required, + "field_constraints": request.get("field_constraints").cloned().unwrap_or_else(|| json!({})) + }); + if !expected.is_empty() { + result["expected_values"] = Value::Object(expected); + } + Ok(result) +} + +fn collect_hash_files(root: &Path, path: &Path, files: &mut BTreeSet) -> Result<()> { + let metadata = fs::symlink_metadata(path)?; + if metadata.file_type().is_symlink() { + bail!("source tree path must not be a symlink: {}", path.strip_prefix(root).unwrap_or(path).display()); + } + if metadata.is_file() { + files.insert(path.to_owned()); + return Ok(()); + } + if !metadata.is_dir() { + return Ok(()); + } + for entry in fs::read_dir(path)? { + let entry = entry?; + let child = entry.path(); + let name = entry.file_name(); + if child.is_dir() && ["target", "build", ".git"].iter().any(|skip| name == *skip) { + continue; + } + let child_meta = fs::symlink_metadata(&child)?; + if child_meta.file_type().is_symlink() { + bail!("source tree path must not be a symlink: {}", child.strip_prefix(root).unwrap_or(&child).display()); + } + if child_meta.is_dir() { + collect_hash_files(root, &child, files)?; + } else if child_meta.is_file() + && (child.file_name().and_then(|value| value.to_str()) == Some("Cargo.lock") + || ["cell", "schema", "toml", "py", "json", "rs"] + .contains(&child.extension().and_then(|value| value.to_str()).unwrap_or(""))) + { + files.insert(child); + } + } + Ok(()) +} + +fn source_tree_hash(root: &Path) -> Result { + let paths = [ + "proposals/novaseal/rwa-receipt-profile-v0/Cell.toml", + "proposals/novaseal/rwa-receipt-profile-v0/src/nova_rwa_receipt_type.cell", + "proposals/novaseal/rwa-receipt-profile-v0/src/nova_rwa_receipt_lifecycle_type.cell", + "proposals/novaseal/rwa-receipt-profile-v0/schemas", + "proposals/novaseal/rwa-receipt-profile-v0/fixtures", + "proposals/novaseal/rwa-receipt-profile-v0/proofs/invariant_matrix.json", + ]; + let mut files = BTreeSet::new(); + for path in paths { + collect_hash_files(root, &root.join(path), &mut files)?; + } + let mut state = Sha256::new(); + for path in files { + let relative = path.strip_prefix(root).unwrap_or(&path).to_string_lossy().replace('\\', "/"); + state.update(relative.as_bytes()); + state.update([0]); + state.update(hex::decode(sha256_hex(&fs::read(path)?))?); + } + Ok(format!("0x{}", hex::encode(state.finalize()))) +} + +fn rwa_constraints() -> Value { + json!({ + "profile": "rwa-receipt-profile-v0", + "reviewer": "real external legal or registry reviewer identity; placeholder, first-party NovaSeal/CellScript/a19q3, local/devnet/fake/internal, example, and unknown tokens are rejected", + "review_date": "UTC date in YYYY-MM-DD form; future dates are rejected", + "review_scope": "exact RWA receipt legal-title, custody, registry-state, oracle-fact, and enforceability review scope", + "registry.authority": "real registry or custodian authority identity; placeholder, first-party NovaSeal/CellScript/a19q3, local/devnet/fake/internal, example, and unknown tokens are rejected", + "registry.jurisdiction": "explicit real-world jurisdiction; placeholder, local/devnet/fake/internal, example, and unknown tokens are rejected", + "registry.registry_report_hash": "0x-prefixed 32-byte non-placeholder hash of the external registry/legal review report", + "profile_source_tree_sha256": "0x-prefixed 32-byte non-placeholder SHA-256 hash of the RWA profile source tree", + "report_uri": "HTTPS URI for the public legal/registry review report or source-controlled review commit; example, loopback, private, and reserved hosts are rejected", + "request_handoff.bundle": "target/novaseal-external-evidence-handoff-bundle.json", + "request_handoff.bundle_hash": "0x-prefixed 32-byte hash of the NovaSeal external evidence handoff bundle", + "request_handoff.bundle_hash_algorithm": HASH_ALGORITHM, + "request_handoff.group": "rwa_legal_registry_review_evidence" + }) +} + +fn rwa_case(root: &Path, adapter: &Value) -> Result { + let source_hash = source_tree_hash(root)?; + let checks = json!({ + "source_external_attestation_adapter_passed": adapter.get("status").and_then(Value::as_str) == Some("passed"), + "source_external_attestation_adapter_status_request_ready": adapter.get("adapter_status").and_then(Value::as_str) == Some("request_ready_external_attestations_required"), + "production_output_matches": RWA_OUTPUT.ends_with("legal_registry_review_evidence.json"), + "profile_source_tree_hash_current": source_hash.len() == 66 && source_hash.starts_with("0x") + }); + let passed = checks.as_object().is_some_and(|map| map.values().all(|value| value == &Value::Bool(true))); + Ok(json!({ + "group": "rwa_legal_registry_review_evidence", + "status": if passed { "passed" } else { "failed" }, + "checks": checks, + "source_adapter": "target/novaseal-external-attestation-adapter.json", + "source_adapter_hash": hash("external_attestation_adapter", adapter)?, + "production_output": RWA_OUTPUT, + "required_external_fields": ["profile", "reviewer", "review_date", "review_scope", "registry.authority", "registry.jurisdiction", "registry.registry_report_hash", "profile_source_tree_sha256", "report_uri", "request_handoff.bundle", "request_handoff.bundle_hash", "request_handoff.bundle_hash_algorithm", "request_handoff.group"], + "field_constraints": rwa_constraints(), + "expected_values": { + "profile": "rwa-receipt-profile-v0", + "profile_source_tree_sha256": source_hash, + "review_scope": ["RWA receipt legal title boundary", "RWA receipt custody and registry-state provenance", "RWA receipt oracle-fact exclusion boundary", "RWA receipt enforceability and jurisdiction boundary"] + } + })) +} + +pub fn run( + root: &Path, + btc_adapter: Option<&Path>, + attestation_adapter: Option<&Path>, + output: Option<&Path>, + pretty: bool, +) -> Result { + let default_btc = root.join("target/novaseal-btc-spv-evidence-adapter.json"); + let default_attestation = root.join("target/novaseal-external-attestation-adapter.json"); + let default_output = root.join("target/novaseal-external-evidence-handoff-bundle.json"); + let btc: Value = serde_json::from_slice(&fs::read(lexical_path(btc_adapter.unwrap_or(&default_btc)))?)?; + let attestation: Value = serde_json::from_slice(&fs::read(lexical_path(attestation_adapter.unwrap_or(&default_attestation)))?)?; + let celldep_fields = [ + "network", + "attested_at", + "attestor", + "release.package", + "release.version", + "release.manifest_commit", + "runtime_verifier.verifier_id", + "runtime_verifier.ipc_abi", + "runtime_verifier.out_point", + "runtime_verifier.data_hash", + "runtime_verifier.dep_type", + "runtime_verifier.hash_type", + "runtime_verifier.artifact_hash", + "request_handoff.bundle", + "request_handoff.bundle_hash", + "request_handoff.bundle_hash_algorithm", + "request_handoff.group", + ]; + let tcb_fields = [ + "reviewer", + "review_date", + "review_scope", + "verifier_id", + "ipc_abi", + "artifact_hash", + "artifact_hash_algorithm", + "source_tree_sha256", + "report_uri", + "request_handoff.bundle", + "request_handoff.bundle_hash", + "request_handoff.bundle_hash_algorithm", + "request_handoff.group", + ]; + let cases = vec![ + btc_case(&btc)?, + attestation_case( + &attestation, + "public_shared_cell_dep_attestation", + "public_shared_cell_dep_attestation", + CELLDEP_OUTPUT, + &celldep_fields, + )?, + attestation_case( + &attestation, + "external_bip340_tcb_review_attestation", + "external_bip340_tcb_review_attestation", + TCB_OUTPUT, + &tcb_fields, + )?, + rwa_case(root, &attestation)?, + ]; + let matched = cases.iter().filter(|case| case["status"] == "passed").count(); + let passed = matched == cases.len(); + let mut report = json!({ + "schema": "novaseal-external-evidence-handoff-bundle-v0.1", + "status": if passed { "passed" } else { "failed" }, + "handoff_status": "request_bundle_ready_external_evidence_required", + "source_btc_spv_adapter": "target/novaseal-btc-spv-evidence-adapter.json", + "source_btc_spv_adapter_hash": hash("btc_spv_adapter", &btc)?, + "source_external_attestation_adapter": "target/novaseal-external-attestation-adapter.json", + "source_external_attestation_adapter_hash": hash("external_attestation_adapter", &attestation)?, + "production_outputs": cases.iter().map(|case| case["production_output"].clone()).collect::>(), + "production_boundary": "This handoff proves external request completeness; it does not satisfy external production evidence.", + "summary": { "total": cases.len(), "matched": matched, "groups": cases.iter().map(|case| case["group"].clone()).collect::>() }, + "cases": cases + }); + report["bundle_hash_algorithm"] = Value::String(HASH_ALGORITHM.to_owned()); + report["bundle_hash"] = Value::String(hash( + "external_evidence_handoff_bundle", + &report + .as_object() + .context("report must be an object")? + .iter() + .filter(|(key, _)| !matches!(key.as_str(), "bundle_hash" | "bundle_hash_algorithm")) + .map(|(key, value)| (key.clone(), value.clone())) + .collect::>() + .into(), + )?); + let output = lexical_path(output.unwrap_or(&default_output)); + fs::create_dir_all(output.parent().context("output path has no parent")?)?; + fs::write(&output, format!("{}\n", stable_json_pretty(&report)?))?; + if pretty { + println!( + "wrote {} status={} groups={}/{}", + output.display(), + report["status"].as_str().unwrap_or("failed"), + matched, + report["summary"]["total"] + ); + } + Ok(if passed { 0 } else { 1 }) +} diff --git a/crates/cellscript-tools/src/fiber_experiments.rs b/crates/cellscript-tools/src/fiber_experiments.rs new file mode 100644 index 00000000..d8854a63 --- /dev/null +++ b/crates/cellscript-tools/src/fiber_experiments.rs @@ -0,0 +1,506 @@ +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::env; +use std::fs::{self, File}; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Output, Stdio}; +use std::thread; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use anyhow::{bail, Context, Result}; +use regex::Regex; +use serde_json::{json, Map, Value}; +use wait_timeout::ChildExt; + +use crate::shared::stable_json_pretty; + +const SCHEMA: &str = "novaseal-fiber-node-execution-v0.4"; +const PREVIOUS_SCHEMAS: &[&str] = + &["novaseal-fiber-node-execution-v0.1", "novaseal-fiber-node-execution-v0.2", "novaseal-fiber-node-execution-v0.3", SCHEMA]; + +struct Workflow { + suite: &'static str, + category: &'static str, + description: &'static str, + profiles: &'static [&'static str], + terms: &'static [&'static str], + requires_lnd: bool, +} + +const WORKFLOWS: &[Workflow] = &[ + Workflow { + suite: "open-use-close-a-channel", + category: "channel-lifecycle", + description: "single-channel open, TLC add/remove, cooperative shutdown, and closed-state checks", + profiles: &["fiber-candidate-profile-v0"], + terms: &["open-channel", "add-tlc", "remove-tlc", "shutdown", "list-channel"], + requires_lnd: false, + }, + Workflow { + suite: "3-nodes-transfer", + category: "multi-hop-transfer", + description: "three-node channel graph with routed TLC transfer and shutdown", + profiles: &["fiber-candidate-profile-v0"], + terms: &["connect", "open-channel", "add-tlc", "remove-tlc", "shutdown"], + requires_lnd: false, + }, + Workflow { + suite: "router-pay", + category: "multi-hop-payment", + description: "router payment workflow with invoice, keysend, graph, duplicate, and failure paths", + profiles: &["fiber-candidate-profile-v0"], + terms: &["send-payment", "gen-invoice", "get-payment-status", "list-graph", "will-fail"], + requires_lnd: false, + }, + Workflow { + suite: "invoice-ops", + category: "invoice", + description: "invoice generation, duplicate rejection, decode, lookup, and cancellation", + profiles: &["fiber-candidate-profile-v0"], + terms: &["gen-invoice", "duplicate", "decode", "get-invoice", "cancel"], + requires_lnd: false, + }, + Workflow { + suite: "shutdown-force", + category: "force-close", + description: "force shutdown after peer disconnect and closed-channel assertions", + profiles: &["fiber-candidate-profile-v0"], + terms: &["shutdown-force", "disconnect", "closed-channel", "trigger-check"], + requires_lnd: false, + }, + Workflow { + suite: "reestablish", + category: "reconnect", + description: "channel reestablishment after disconnect before TLC removal and shutdown", + profiles: &["fiber-candidate-profile-v0"], + terms: &["disconnect", "reconnect", "remove-tlc", "shutdown"], + requires_lnd: false, + }, + Workflow { + suite: "external-funding-open", + category: "external-funding", + description: "external funding script, signing, submission, channel ready, shutdown, and balance checks", + profiles: &["fiber-candidate-profile-v0", "btc-transaction-commitment-profile-v0"], + terms: &["funding-script", "external-funding", "sign", "submit", "balance-after"], + requires_lnd: false, + }, + Workflow { + suite: "funding-tx-verification", + category: "funding-verification", + description: "funding transaction verification with a shell builder and auto-accepted channel check", + profiles: &["fiber-candidate-profile-v0", "btc-transaction-commitment-profile-v0"], + terms: &["funding-tx", "verification", "open-channel", "auto-accepted"], + requires_lnd: false, + }, + Workflow { + suite: "udt", + category: "udt-channel", + description: "UDT channel open, invoice/TLC flow, invalid open, manual accept, and shutdown", + profiles: &["fiber-candidate-profile-v0", "fungible-xudt-profile-v0"], + terms: &["udt", "open-channel", "add-tlc", "remove-tlc", "invalid", "shutdown"], + requires_lnd: false, + }, + Workflow { + suite: "udt-router-pay", + category: "udt-routing", + description: "multi-hop routed UDT payment including invoice and keysend paths", + profiles: &["fiber-candidate-profile-v0", "fungible-xudt-profile-v0"], + terms: &["udt", "router", "send-payment", "gen-invoice", "keysend"], + requires_lnd: false, + }, + Workflow { + suite: "watchtower/force-close-after-open-channel", + category: "watchtower", + description: "watchtower force-close settlement after opening a channel", + profiles: &["fiber-candidate-profile-v0"], + terms: &["force-close", "commitment-tx", "settlement", "check-balance"], + requires_lnd: false, + }, + Workflow { + suite: "watchtower/force-close-with-pending-tlcs", + category: "watchtower", + description: "force-close with pending TLCs, settlement transaction generation, and balance checks", + profiles: &["fiber-candidate-profile-v0"], + terms: &["pending-tlcs", "force-close", "settlement", "commitment-tx", "check-balance"], + requires_lnd: false, + }, + Workflow { + suite: "watchtower/force-close-with-pending-tlcs-and-udt", + category: "watchtower-udt", + description: "force-close with pending UDT TLCs and CKB/UDT balance checks", + profiles: &["fiber-candidate-profile-v0", "fungible-xudt-profile-v0"], + terms: &["pending-tlcs", "udt", "force-close", "settlement", "check-balance"], + requires_lnd: false, + }, + Workflow { + suite: "watchtower/force-close-preimage-multiple", + category: "watchtower-preimage", + description: "multiple preimage settlement path after force-close", + profiles: &["fiber-candidate-profile-v0"], + terms: &["preimage", "force-close", "settlement", "check-balance"], + requires_lnd: false, + }, + Workflow { + suite: "cross-chain-hub", + category: "cross-chain", + description: "Fiber plus Lightning/BTC hub send and receive order workflow", + profiles: &["fiber-candidate-profile-v0", "btc-transaction-commitment-profile-v0", "btc-utxo-seal-profile-v0"], + terms: &["btc", "lnd", "send-payment", "order", "wrapped-btc", "shutdown"], + requires_lnd: true, + }, + Workflow { + suite: "cross-chain-hub-separate", + category: "cross-chain", + description: "Fiber plus Lightning/BTC hub workflow with CCH running as a separate service", + profiles: &["fiber-candidate-profile-v0", "btc-transaction-commitment-profile-v0", "btc-utxo-seal-profile-v0"], + terms: &["btc", "lnd", "send-payment", "order", "wrapped-btc", "shutdown"], + requires_lnd: true, + }, +]; + +fn git_value(repo: &Path, args: &[&str]) -> Option { + let output = Command::new("git").args(args).current_dir(repo).output().ok()?; + output.status.success().then(|| String::from_utf8_lossy(&output.stdout).trim().to_owned()) +} + +fn provenance(repo: &Path) -> Value { + json!({ + "path": repo.to_string_lossy().replace('\\', "/"), + "origin": git_value(repo, &["remote", "get-url", "origin"]), + "branch": git_value(repo, &["branch", "--show-current"]), + "commit": git_value(repo, &["rev-parse", "HEAD"]), + "dirty": git_value(repo, &["status", "--short"]).is_some_and(|value| !value.is_empty()), + }) +} + +fn same_provenance(left: Option<&Value>, right: &Value) -> bool { + left.and_then(Value::as_object) + .is_some_and(|left| ["path", "origin", "branch", "commit", "dirty"].iter().all(|key| left.get(*key) == right.get(*key))) +} + +fn relative(path: &Path, root: &Path) -> String { + path.strip_prefix(root).unwrap_or(path).to_string_lossy().replace('\\', "/") +} + +fn suite_files(repo: &Path, suite: &str) -> Vec { + let directory = repo.join("tests/bruno/e2e").join(suite); + let mut files = fs::read_dir(directory) + .ok() + .into_iter() + .flatten() + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| path.extension().is_some_and(|value| value == "bru")) + .collect::>(); + files.sort(); + files +} + +fn rpc_methods(files: &[PathBuf]) -> Vec { + let mut methods = BTreeSet::new(); + for path in files { + let Ok(text) = fs::read_to_string(path) else { continue }; + for line in text.lines().filter(|line| line.contains("\"method\"")) { + let after = line.split_once(':').map_or("", |(_, value)| value).trim().trim_end_matches(',').trim(); + if after.starts_with('"') && after.ends_with('"') { + methods.insert(after.trim_matches('"').to_owned()); + } + } + } + methods.into_iter().collect() +} + +fn workflow_report(repo: &Path, workflow: &Workflow, execution: Option<&Value>) -> Value { + let files = suite_files(repo, workflow.suite); + let names = files.iter().map(|path| path.to_string_lossy().to_lowercase()).collect::>().join(" "); + let terms = + workflow.terms.iter().map(|term| ((*term).to_owned(), json!(names.contains(&term.to_lowercase())))).collect::>(); + let present = !files.is_empty() && terms.values().all(|value| value == true); + json!({ + "suite": workflow.suite, "category": workflow.category, "description": workflow.description, + "mapped_profiles": workflow.profiles, "requires_lnd": workflow.requires_lnd, + "status": execution.and_then(|value| value["status"].as_str()).unwrap_or(if present { "present" } else { "missing" }), + "present": present, "step_count": files.len(), "expected_terms": terms, "rpc_methods": rpc_methods(&files), + "evidence_files": files.iter().map(|path| relative(path, repo)).collect::>(), + "execution": execution.cloned().unwrap_or(Value::Null), + }) +} + +fn previous(output: &Path, current: &Value) -> BTreeMap { + let Ok(bytes) = fs::read(output) else { return BTreeMap::new() }; + let Ok(report) = serde_json::from_slice::(&bytes) else { return BTreeMap::new() }; + if !report["schema"].as_str().is_some_and(|schema| PREVIOUS_SCHEMAS.contains(&schema)) + || !same_provenance(report.get("fiber_repo"), current) + { + return BTreeMap::new(); + } + report["workflows"] + .as_array() + .into_iter() + .flatten() + .filter_map(|row| { + let suite = row["suite"].as_str()?; + let execution = row.get("execution")?; + (execution.is_object() && same_provenance(execution.get("fiber_repo"), current)) + .then(|| (suite.to_owned(), execution.clone())) + }) + .collect() +} + +fn which(name: &str) -> Option { + env::var_os("PATH") + .and_then(|paths| env::split_paths(&paths).map(|path| path.join(name)).find(|path| path.is_file())) + .map(|path| path.to_string_lossy().into_owned()) +} + +fn command_with_timeout(mut command: Command, timeout: Duration) -> Result { + command.stdout(Stdio::piped()).stderr(Stdio::piped()); + let mut child = command.spawn()?; + if child.wait_timeout(timeout)?.is_none() { + let _ = child.kill(); + } + Ok(child.wait_with_output()?) +} + +fn cleanup(repo: &Path, all: bool) { + let escaped = regex::escape(&repo.to_string_lossy()); + let mut patterns = vec![ + Regex::new(r"\.\./\.\./target/[^ ]*/fnn -d (?:[123]|cch)(?:\s|$)").unwrap(), + Regex::new(&format!(r"ckb run -C {escaped}/tests/deploy/node-data")).unwrap(), + Regex::new(&format!(r"bitcoind -conf={escaped}/tests/deploy/lnd-init/bitcoind/bitcoin\.conf")).unwrap(), + Regex::new(&format!(r"lnd --lnddir={escaped}/tests/deploy/lnd-init/lnd-(?:bob|ingrid)")).unwrap(), + ]; + if all { + patterns.push(Regex::new(r"bash \./tests/nodes/start\.sh e2e/").unwrap()); + } + let Ok(output) = Command::new("ps").args(["-axo", "pid=,command="]).output() else { return }; + let mut pids = Vec::new(); + for line in String::from_utf8_lossy(&output.stdout).lines() { + let Some((pid, command)) = line.trim().split_once(char::is_whitespace) else { continue }; + let Ok(pid) = pid.parse::() else { continue }; + if pid != std::process::id() && patterns.iter().any(|pattern| pattern.is_match(command.trim())) { + let _ = Command::new("kill").args(["-TERM", &pid.to_string()]).status(); + pids.push(pid); + } + } + thread::sleep(Duration::from_secs(2)); + for pid in pids { + let _ = Command::new("kill").args(["-KILL", &pid.to_string()]).status(); + } +} + +fn copy_tree(source: &Path, destination: &Path) -> Result<()> { + fs::create_dir_all(destination)?; + for entry in fs::read_dir(source)? { + let entry = entry?; + if entry.file_name() == "node_modules" { + continue; + } + let target = destination.join(entry.file_name()); + if entry.file_type()?.is_dir() { + copy_tree(&entry.path(), &target)?; + } else { + fs::copy(entry.path(), target)?; + } + } + Ok(()) +} + +fn bruno_workspace(repo: &Path, suite: &str, log: &Path) -> Result<(PathBuf, Vec)> { + if !matches!(suite, "watchtower/force-close-with-pending-tlcs-and-udt" | "cross-chain-hub" | "cross-chain-hub-separate") { + return Ok((repo.join("tests/bruno"), vec![])); + } + let workspace = log.join("bruno-worktree"); + if workspace.exists() { + fs::remove_dir_all(&workspace)?; + } + copy_tree(&repo.join("tests/bruno"), &workspace)?; + let mut replacements = Vec::new(); + if suite == "watchtower/force-close-with-pending-tlcs-and-udt" { + for name in ["NODE1_BALANCE", "NODE2_BALANCE", "NODE1_NEW_BALANCE", "NODE2_NEW_BALANCE"] { + replacements.push((format!("bru.setVar(\"{name}\", capacity);"), format!("bru.setVar(\"{name}\", capacity.toString());"))); + } + } + if matches!(suite, "cross-chain-hub" | "cross-chain-hub-separate") { + replacements.extend([ + ("bru.setVar(\"FIBER_PAY_REQ\", res.body.result.invoice_address);\n bru.setVar(\"PAYMENT_HASH\", res.body.result.invoice.data.payment_hash);".into(), "bru.setVar(\"FIBER_PAY_REQ\", res.body.result.invoice_address);\n bru.setVar(\"PAYMENT_HASH\", res.body.result.invoice.data.payment_hash);\n console.log(\"receive_fiber_pay_req\", res.body.result.invoice_address);\n console.log(\"receive_payment_hash\", res.body.result.invoice.data.payment_hash);".into()), + ("if (resp.data !== undefined) {\n resp.data.destroy();\n }".into(), "if (resp.data !== undefined && typeof resp.data.destroy === \"function\") {\n resp.data.destroy();\n }".into()), + ]); + } + let mut patched = Vec::new(); + for path in suite_files(&workspace.parent().unwrap().join("bruno-worktree/.."), suite) { + let _ = path; + } + let suite_dir = workspace.join("e2e").join(suite); + for entry in fs::read_dir(suite_dir).ok().into_iter().flatten().filter_map(Result::ok) { + let path = entry.path(); + if path.extension().is_none_or(|value| value != "bru") { + continue; + } + let text = fs::read_to_string(&path)?; + let updated = replacements.iter().fold(text.clone(), |text, (old, new)| text.replace(old, new)); + if updated != text { + fs::write(&path, updated)?; + patched.push(relative(&path, &workspace)); + } + } + patched.sort(); + Ok((workspace, patched)) +} + +fn stop(child: &mut Child) { + let _ = Command::new("kill").args(["-TERM", &child.id().to_string()]).status(); + if child.wait_timeout(Duration::from_secs(20)).ok().flatten().is_none() { + let _ = child.kill(); + let _ = child.wait(); + } +} + +#[allow(clippy::too_many_arguments)] +fn execute_workflow(repo_root: &Path, repo: &Path, output: &Path, workflow: &Workflow, assume: bool, timeout: u64) -> Result { + let info = provenance(repo); + let suite_arg = format!("e2e/{}", workflow.suite); + let log = output.parent().unwrap().join("novaseal-fiber-node-experiments").join(workflow.suite.replace('/', "__")); + fs::create_dir_all(&log)?; + let environment = env::vars().collect::>(); + let clean = environment.contains_key("REMOVE_OLD_STATE") || environment.contains_key("NOVASEAL_CLEAN_FIBER_DEVNET_PROCESSES"); + let started = Instant::now(); + let mut node = None; + if !assume { + cleanup(repo, clean); + let file = File::create(log.join("start-node.log"))?; + node = Some( + Command::new("./tests/nodes/start.sh") + .arg(&suite_arg) + .current_dir(repo) + .stdout(Stdio::from(file.try_clone()?)) + .stderr(Stdio::from(file)) + .envs(&environment) + .spawn()?, + ); + let wait = command_with_timeout( + { + let mut command = Command::new("./tests/nodes/wait.sh"); + command.current_dir(repo).envs(&environment); + command + }, + Duration::from_secs(timeout), + )?; + fs::write(log.join("wait.stdout"), &wait.stdout)?; + fs::write(log.join("wait.stderr"), &wait.stderr)?; + if !wait.status.success() || node.as_mut().is_some_and(|child| child.try_wait().ok().flatten().is_some()) { + if let Some(child) = node.as_mut() { + stop(child); + } + return Ok(json!({"status": "failed", "started_node": true, "command": ["./tests/nodes/start.sh", suite_arg], + "duration_seconds": ((started.elapsed().as_secs_f64() * 1000.0).round() / 1000.0), "fiber_repo": info, + "failure": "fiber node wait failed", "wait_returncode": wait.status.code()})); + } + } + let (bruno, patches) = bruno_workspace(repo, workflow.suite, &log)?; + let command = ["npm", "exec", "--", "@usebruno/cli", "run", &suite_arg, "-r", "--env", "test"]; + let completed = command_with_timeout( + { + let mut value = Command::new(command[0]); + value.args(&command[1..]).current_dir(&bruno).envs(&environment); + value + }, + Duration::from_secs(timeout), + )?; + fs::write(log.join("bruno.stdout"), &completed.stdout)?; + fs::write(log.join("bruno.stderr"), &completed.stderr)?; + let mut execution = json!({ + "status": if completed.status.success() { "passed" } else { "failed" }, "started_node": !assume, + "command": command, "returncode": completed.status.code().unwrap_or(-1), + "noninteractive_ckb_cli_account_import_wrapper": log.join("tool-bin/ckb-cli").is_file(), + "stdout_log": relative(&log.join("bruno.stdout"), repo_root), "stderr_log": relative(&log.join("bruno.stderr"), repo_root), + "duration_seconds": ((started.elapsed().as_secs_f64() * 1000.0).round() / 1000.0), "fiber_repo": info, + }); + if !patches.is_empty() { + execution["bruno_cwd"] = json!(relative(&bruno, repo_root)); + execution["bruno_compatibility_patches"] = json!(patches); + } + if let Some(child) = node.as_mut() { + stop(child); + cleanup(repo, clean); + } + Ok(execution) +} + +#[allow(clippy::too_many_arguments)] +pub fn run( + repo_root: &Path, + fiber_repo: Option<&Path>, + output: Option<&Path>, + pretty: bool, + suites: &[String], + run_all: bool, + assume: bool, + timeout: u64, +) -> Result { + let repo_root = fs::canonicalize(repo_root)?; + let fiber_repo = fiber_repo.map(Path::to_path_buf).unwrap_or_else(|| repo_root.parent().unwrap().join("fiber")); + let fiber_repo = fs::canonicalize(&fiber_repo).unwrap_or(fiber_repo); + let output = output.map(Path::to_path_buf).unwrap_or_else(|| repo_root.join("target/novaseal-fiber-node-experiments.json")); + let allowed = WORKFLOWS.iter().map(|workflow| workflow.suite).collect::>(); + if let Some(invalid) = suites.iter().find(|suite| !allowed.contains(suite.as_str())) { + bail!("unknown Fiber suite: {invalid}"); + } + let selected = if run_all { + allowed.iter().map(|value| (*value).to_owned()).collect::>() + } else { + suites.iter().cloned().collect() + }; + let info = provenance(&fiber_repo); + let mut executions = previous(&output, &info); + for workflow in WORKFLOWS.iter().filter(|workflow| selected.contains(workflow.suite)) { + executions.insert(workflow.suite.into(), execute_workflow(&repo_root, &fiber_repo, &output, workflow, assume, timeout)?); + } + let workflows = + WORKFLOWS.iter().map(|workflow| workflow_report(&fiber_repo, workflow, executions.get(workflow.suite))).collect::>(); + let present = workflows.iter().filter(|row| row["present"] == true).count(); + let executed = workflows.iter().filter(|row| row["execution"].is_object()).count(); + let passed = workflows.iter().filter(|row| row["execution"]["status"] == "passed").count(); + let all_present = present == WORKFLOWS.len(); + let all_executed = executed == WORKFLOWS.len(); + let all_passed = all_executed && passed == WORKFLOWS.len(); + let partial = executed > 0 && executed < WORKFLOWS.len() && executed == passed; + let runnable = + ["tests/nodes/start.sh", "tests/nodes/wait.sh", "package.json", "tests/bruno/bruno.json", "docs/dev/README.md", "Cargo.lock"] + .iter() + .all(|path| fiber_repo.join(path).is_file()); + let status = if !fiber_repo.is_dir() { + "missing_fiber_clone" + } else if all_passed { + "passed" + } else if executed > 0 && passed != executed { + "failed" + } else if partial { + "partial_execution_passed" + } else if all_present && runnable { + "discovery_ready_live_not_run" + } else { + "incomplete" + }; + let profiles = WORKFLOWS.iter().flat_map(|workflow| workflow.profiles).copied().collect::>(); + let generated = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let report = json!({ + "schema": SCHEMA, "status": status, "generated_at_unix": generated, "classification": "fiber_node_execution_v0", "fiber_repo": info, + "devnet_contract": {"runnable_devnet_contract_present": runnable, "start_command": "./tests/nodes/start.sh e2e/", + "wait_command": "./tests/nodes/wait.sh", "bruno_command": "cd tests/bruno && npm exec -- @usebruno/cli run e2e/ -r --env test", "source_docs": "docs/dev/README.md"}, + "workflow_coverage": {"required_count": WORKFLOWS.len(), "present_count": present, "executed_count": executed, + "passed_execution_count": passed, "all_required_workflows_present": all_present, "all_required_workflows_executed": all_executed, + "all_required_workflows_executed_passed": all_passed, "partial_execution_passed": partial}, + "profiles_covered": profiles, "workflows": workflows, + "acceptance_boundary": {"discovery_ready_live_not_run": "the Fiber clone exposes the expected devnet/e2e workflow surface, but no live Fiber node execution is claimed", + "passed": "all required Fiber workflow suites were executed through Fiber's devnet node runner and Bruno e2e harness", + "partial_execution_passed": "at least one selected Fiber workflow suite was executed and passed, but complete Fiber coverage is not claimed", + "novaseal_mapping": "NovaSeal consumes this as external Fiber-node evidence; it does not replace NovaSeal's own CKB stateful profile reports"}, + "generated_by": {"module": "crates/cellscript-tools/src/fiber_experiments.rs", "implementation": "cellscript_tools::fiber_experiments"}, + "tooling": {"npm": which("npm"), "cargo": which("cargo"), "ckb": which("ckb"), "ckb_cli": which("ckb-cli")} + }); + fs::create_dir_all(output.parent().context("output path has no parent")?)?; + let text = if pretty { stable_json_pretty(&report)? } else { serde_json::to_string(&report)? }; + fs::write(&output, format!("{}\n", text.trim_end_matches('\n')))?; + println!("{}", output.display()); + Ok(if matches!(status, "missing_fiber_clone" | "incomplete" | "failed") { 1 } else { 0 }) +} diff --git a/crates/cellscript-tools/src/main.rs b/crates/cellscript-tools/src/main.rs new file mode 100644 index 00000000..41725921 --- /dev/null +++ b/crates/cellscript-tools/src/main.rs @@ -0,0 +1,559 @@ +//! Native Rust release, audit, fixture, and acceptance tooling for CellScript. + +#![recursion_limit = "256"] + +use std::path::PathBuf; +use std::process::ExitCode; + +use clap::{Parser, Subcommand}; + +mod acceptance_helpers; +mod bip340_tcb; +mod btc_anchor; +mod btc_spv_adapter; +mod ckb_acceptance; +mod ckb_acceptance_live; +mod ckb_adapter_live; +mod ckb_devnet; +mod crypto; +mod external_attestation; +mod external_handoff; +mod fiber_experiments; +mod novaseal_agreement_live; +mod novaseal_core_live; +mod novaseal_planned_btc_tx; +mod novaseal_planned_btc_utxo; +mod novaseal_planned_dual; +mod novaseal_planned_fiber; +mod novaseal_planned_fungible; +mod novaseal_planned_live; +mod novaseal_planned_rwa; +mod production_evidence; +mod profile_operator; +mod repository_checks; +mod service_builder; +mod shared; +mod skill_pack; +mod strict_backend; +mod syntax_combo; +mod tooling_release; +mod verifier_pinning; +mod wallet_vectors; + +#[derive(Debug, Parser)] +#[command(name = "cellscript-tools", version, about = "CellScript repository tooling")] +struct Cli { + /// Override repository-root autodetection. + #[arg(long, global = true, value_name = "PATH")] + root: Option, + + #[command(subcommand)] + command: Command, +} + +#[derive(Debug, Subcommand)] +enum Command { + /// Print the pinned Rust toolchain channel. + RustToolchainChannel, + /// Print the tab-separated fields consumed by the NovaSeal acceptance wrapper. + NovasealAcceptanceSummary { report: PathBuf }, + /// Verify that Fiber compatibility and acceptance reports share one binding. + FiberReportBinding { compatibility_report: PathBuf, acceptance_report: PathBuf, fiber_revision: String }, + /// Validate CKB compatibility and action-builder CLI contracts. + EcosystemReuseContracts { compatibility_report: PathBuf, action_report: PathBuf }, + /// Validate the CellScript 0.14 metadata scope. + Scope014 { + out_dir: PathBuf, + #[arg(required = true)] + metadata: Vec, + }, + /// Validate the CellScript-to-CellFabric bridge summary. + CellfabricBridge { envelope: PathBuf, summary: PathBuf }, + /// Run the focused CKB adapter local-node acceptance scenario. + CkbAdapterLive { + #[arg(long)] + ckb_repo: PathBuf, + #[arg(long)] + ckb_bin: Option, + #[arg(long)] + run_dir: PathBuf, + #[arg(long)] + action_plan: PathBuf, + #[arg(long)] + report: PathBuf, + }, + /// Compile and, when requested, execute the production CKB acceptance matrix. + CkbAcceptance { + #[arg(long)] + ckb_repo: Option, + #[arg(long)] + ckb_bin: Option, + #[arg(long)] + compile_only: bool, + #[arg(long)] + stateful_scenarios: bool, + #[arg(long, default_value = "production", value_parser = ["production", "bounded"])] + mode: String, + #[arg(long)] + run_dir: Option, + #[arg(long)] + keep_node: bool, + }, + /// Validate the tooling release boundary. + ValidateToolingRelease, + /// Validate the CellScript skill pack. + CheckSkillPack, + /// Run the strict backend audit. + StrictBackend { + #[arg(default_value = "quick")] + mode: String, + #[arg(trailing_var_arg = true, allow_hyphen_values = true, hide = true)] + extra: Vec, + }, + /// Generate NovaSeal service-builder fixtures. + ServiceBuilderFixtures { + #[arg(long)] + operator_fixtures: Option, + #[arg(long)] + output: Option, + #[arg(long)] + pretty: bool, + }, + /// Generate NovaSeal profile-operator fixtures. + ProfileOperatorFixtures { + /// Read live and external evidence below this root instead of the repository root. + #[arg(long)] + evidence_root: Option, + #[arg(long)] + output: Option, + #[arg(long)] + pretty: bool, + }, + /// Generate NovaSeal wallet-signing vectors. + WalletSigningVectors { + #[arg(long)] + core_vectors: Option, + #[arg(long)] + output: Option, + #[arg(long)] + pretty: bool, + }, + /// Run the syntax-combination audit. + SyntaxComboAudit { + #[arg(default_value = "quick", value_parser = ["quick", "ci", "deep", "repro"])] + mode: String, + #[arg(long, default_value_t = 20_260_503)] + seed: u64, + #[arg(long)] + budget: Option, + #[arg(long = "case")] + case_name: Option, + }, + /// Validate freshness markers in CellScript documentation headers. + CheckDocStatus, + /// Validate repository-local Markdown link targets. + CheckMarkdownLinks, + /// Reject retired runtime sources, artifacts, and active-tooling residue. + CheckSourcePolicy, + /// Validate the file list emitted by `cargo package --list`. + CheckPackageContents { package_files: PathBuf }, + /// Print the root package version from Cargo.toml. + WorkspaceVersion, + /// Build the NovaSeal external-attestation adapter report. + ExternalAttestationAdapter { + #[arg(long)] + tcb_review: Option, + #[arg(long)] + public_template: Option, + #[arg(long)] + external_template: Option, + #[arg(long)] + output: Option, + #[arg(long)] + pretty: bool, + }, + /// Build the NovaSeal BTC SPV evidence adapter report. + BtcSpvEvidenceAdapter { + #[arg(long)] + service_builder_fixtures: Option, + #[arg(long)] + template: Option, + #[arg(long)] + output: Option, + #[arg(long)] + pretty: bool, + }, + /// Run the NovaSeal BIP340 TCB review. + Bip340TcbReview { + #[arg(long)] + output: Option, + #[arg(long)] + pretty: bool, + }, + /// Build the NovaSeal external-evidence handoff bundle. + ExternalEvidenceHandoff { + #[arg(long)] + btc_spv_adapter: Option, + #[arg(long)] + external_attestation_adapter: Option, + #[arg(long)] + output: Option, + #[arg(long)] + pretty: bool, + }, + /// Validate release-critical CKB production acceptance evidence. + ValidateProductionEvidence { + report: PathBuf, + #[arg(long)] + repo_root: Option, + #[arg(long)] + compile_only: bool, + }, + /// Recompute and verify the pinned NovaSeal RISC-V verifier identity. + CheckNovasealVerifierPinning, + /// Discover or execute the required external Fiber node workflow suites. + FiberNodeExperiments { + #[arg(long)] + repo_root: Option, + #[arg(long)] + fiber_repo: Option, + #[arg(long)] + output: Option, + #[arg(long)] + pretty: bool, + #[arg(long = "run-suite")] + run_suite: Vec, + #[arg(long)] + run_all: bool, + #[arg(long)] + assume_nodes_running: bool, + #[arg(long, default_value_t = 1800)] + timeout_seconds: u64, + }, + /// Run the live NovaSeal core bootstrap/transition CKB devnet scenario. + NovasealCoreDevnet { + #[arg(long)] + repo_root: Option, + #[arg(long)] + ckb_repo: Option, + #[arg(long)] + ckb_bin: Option, + #[arg(long)] + output: Option, + #[arg(long)] + run_dir: Option, + #[arg(long)] + pretty: bool, + #[arg(long)] + keep_node: bool, + }, + /// Run the live NovaSeal Agreement originate/repay/claim CKB devnet scenario. + NovasealAgreementDevnet { + #[arg(long)] + repo_root: Option, + #[arg(long)] + ckb_repo: Option, + #[arg(long)] + ckb_bin: Option, + #[arg(long)] + output: Option, + #[arg(long)] + run_dir: Option, + #[arg(long)] + pretty: bool, + #[arg(long)] + keep_node: bool, + }, + /// Run or describe a planned NovaSeal profile devnet evidence contract. + NovasealPlannedDevnet { + #[arg(long)] + repo_root: Option, + #[arg(long)] + ckb_repo: Option, + #[arg(long)] + ckb_bin: Option, + #[arg(long)] + profile: String, + #[arg(long)] + output: Option, + #[arg(long)] + run_dir: Option, + #[arg(long)] + pretty: bool, + #[arg(long)] + keep_node: bool, + #[arg(long)] + list_contract: bool, + #[arg(long)] + prepare_artifacts: bool, + #[arg(long)] + live: bool, + }, +} + +fn failure(error: anyhow::Error) -> ExitCode { + eprintln!("{error:#}"); + ExitCode::FAILURE +} + +fn main() -> ExitCode { + let cli = Cli::parse(); + let root = match shared::resolve_repo_root(cli.root.as_deref()) { + Ok(root) => root, + Err(error) => return failure(error), + }; + + match cli.command { + Command::RustToolchainChannel => match acceptance_helpers::rust_toolchain_channel(&root) { + Ok(()) => ExitCode::SUCCESS, + Err(error) => failure(error), + }, + Command::NovasealAcceptanceSummary { report } => match acceptance_helpers::novaseal_summary(&report) { + Ok(()) => ExitCode::SUCCESS, + Err(error) => failure(error), + }, + Command::FiberReportBinding { compatibility_report, acceptance_report, fiber_revision } => { + match acceptance_helpers::fiber_report_binding(&compatibility_report, &acceptance_report, &fiber_revision) { + Ok(()) => ExitCode::SUCCESS, + Err(error) => failure(error), + } + } + Command::EcosystemReuseContracts { compatibility_report, action_report } => { + match acceptance_helpers::ecosystem_reuse_contracts(&compatibility_report, &action_report) { + Ok(()) => ExitCode::SUCCESS, + Err(error) => failure(error), + } + } + Command::Scope014 { out_dir, metadata } => match acceptance_helpers::scope_014(&out_dir, &metadata) { + Ok(()) => ExitCode::SUCCESS, + Err(error) => failure(error), + }, + Command::CellfabricBridge { envelope, summary } => match acceptance_helpers::cellfabric_bridge(&envelope, &summary) { + Ok(()) => ExitCode::SUCCESS, + Err(error) => failure(error), + }, + Command::CkbAdapterLive { ckb_repo, ckb_bin, run_dir, action_plan, report } => { + match ckb_adapter_live::run(&ckb_repo, ckb_bin.as_deref(), &run_dir, &action_plan, &report) { + Ok(0) => ExitCode::SUCCESS, + Ok(_) => ExitCode::FAILURE, + Err(error) => failure(error), + } + } + Command::CkbAcceptance { ckb_repo, ckb_bin, compile_only, stateful_scenarios, mode, run_dir, keep_node } => { + match ckb_acceptance::run( + &root, + ckb_repo.as_deref(), + ckb_bin.as_deref(), + compile_only, + stateful_scenarios, + &mode, + run_dir.as_deref(), + keep_node, + ) { + Ok(0) => ExitCode::SUCCESS, + Ok(_) => ExitCode::FAILURE, + Err(error) => failure(error), + } + } + Command::ValidateToolingRelease => match tooling_release::run(&root) { + Ok(()) => ExitCode::SUCCESS, + Err(error) => failure(error), + }, + Command::CheckSkillPack => match skill_pack::run(&root) { + Ok(0) => ExitCode::SUCCESS, + Ok(_) => ExitCode::FAILURE, + Err(error) => failure(error), + }, + Command::StrictBackend { mode, extra: _ } => match strict_backend::run(&root, &mode) { + Ok(0) => ExitCode::SUCCESS, + Ok(2) => ExitCode::from(2), + Ok(_) => ExitCode::FAILURE, + Err(error) => failure(error), + }, + Command::ServiceBuilderFixtures { operator_fixtures, output, pretty } => { + match service_builder::run(&root, operator_fixtures.as_deref(), output.as_deref(), pretty) { + Ok(0) => ExitCode::SUCCESS, + Ok(_) => ExitCode::FAILURE, + Err(error) => failure(error), + } + } + Command::ProfileOperatorFixtures { evidence_root, output, pretty } => { + match profile_operator::run(&root, evidence_root.as_deref(), output.as_deref(), pretty) { + Ok(0) => ExitCode::SUCCESS, + Ok(_) => ExitCode::FAILURE, + Err(error) => failure(error), + } + } + Command::WalletSigningVectors { core_vectors, output, pretty } => { + match wallet_vectors::run(&root, core_vectors.as_deref(), output.as_deref(), pretty) { + Ok(0) => ExitCode::SUCCESS, + Ok(_) => ExitCode::FAILURE, + Err(error) => failure(error), + } + } + Command::SyntaxComboAudit { mode, seed, budget, case_name } => { + match syntax_combo::run(&root, &mode, seed, budget, case_name.as_deref()) { + Ok(0) => ExitCode::SUCCESS, + Ok(2) => ExitCode::from(2), + Ok(_) => ExitCode::FAILURE, + Err(error) => failure(error), + } + } + Command::CheckDocStatus => match repository_checks::check_doc_status(&root) { + Ok(()) => ExitCode::SUCCESS, + Err(error) => failure(error), + }, + Command::CheckMarkdownLinks => match repository_checks::check_markdown_links(&root) { + Ok(()) => ExitCode::SUCCESS, + Err(error) => failure(error), + }, + Command::CheckSourcePolicy => match repository_checks::check_source_policy(&root) { + Ok(()) => ExitCode::SUCCESS, + Err(error) => failure(error), + }, + Command::CheckPackageContents { package_files } => match repository_checks::check_package_contents(&package_files) { + Ok(()) => ExitCode::SUCCESS, + Err(error) => failure(error), + }, + Command::WorkspaceVersion => match repository_checks::workspace_version(&root) { + Ok(version) => { + println!("{version}"); + ExitCode::SUCCESS + } + Err(error) => failure(error), + }, + Command::ExternalAttestationAdapter { tcb_review, public_template, external_template, output, pretty } => { + match external_attestation::run( + &root, + tcb_review.as_deref(), + public_template.as_deref(), + external_template.as_deref(), + output.as_deref(), + pretty, + ) { + Ok(0) => ExitCode::SUCCESS, + Ok(_) => ExitCode::FAILURE, + Err(error) => failure(error), + } + } + Command::BtcSpvEvidenceAdapter { service_builder_fixtures, template, output, pretty } => { + match btc_spv_adapter::run(&root, service_builder_fixtures.as_deref(), template.as_deref(), output.as_deref(), pretty) { + Ok(0) => ExitCode::SUCCESS, + Ok(_) => ExitCode::FAILURE, + Err(error) => failure(error), + } + } + Command::Bip340TcbReview { output, pretty } => match bip340_tcb::run(&root, output.as_deref(), pretty) { + Ok(0) => ExitCode::SUCCESS, + Ok(_) => ExitCode::FAILURE, + Err(error) => failure(error), + }, + Command::ExternalEvidenceHandoff { btc_spv_adapter, external_attestation_adapter, output, pretty } => { + match external_handoff::run( + &root, + btc_spv_adapter.as_deref(), + external_attestation_adapter.as_deref(), + output.as_deref(), + pretty, + ) { + Ok(0) => ExitCode::SUCCESS, + Ok(_) => ExitCode::FAILURE, + Err(error) => failure(error), + } + } + Command::ValidateProductionEvidence { report, repo_root, compile_only } => { + match production_evidence::run(&root, &report, repo_root.as_deref(), compile_only) { + Ok(0) => ExitCode::SUCCESS, + Ok(_) => ExitCode::FAILURE, + Err(error) => failure(error), + } + } + Command::CheckNovasealVerifierPinning => match verifier_pinning::run(&root) { + Ok(0) => ExitCode::SUCCESS, + Ok(_) => ExitCode::FAILURE, + Err(error) => failure(error), + }, + Command::FiberNodeExperiments { + repo_root, + fiber_repo, + output, + pretty, + run_suite, + run_all, + assume_nodes_running, + timeout_seconds, + } => match fiber_experiments::run( + repo_root.as_deref().unwrap_or(&root), + fiber_repo.as_deref(), + output.as_deref(), + pretty, + &run_suite, + run_all, + assume_nodes_running, + timeout_seconds, + ) { + Ok(0) => ExitCode::SUCCESS, + Ok(_) => ExitCode::FAILURE, + Err(error) => failure(error), + }, + Command::NovasealCoreDevnet { repo_root, ckb_repo, ckb_bin, output, run_dir, pretty, keep_node } => { + match novaseal_core_live::run( + repo_root.as_deref().unwrap_or(&root), + ckb_repo.as_deref(), + ckb_bin.as_deref(), + output.as_deref(), + run_dir.as_deref(), + pretty, + keep_node, + ) { + Ok(0) => ExitCode::SUCCESS, + Ok(_) => ExitCode::FAILURE, + Err(error) => failure(error), + } + } + Command::NovasealAgreementDevnet { repo_root, ckb_repo, ckb_bin, output, run_dir, pretty, keep_node } => { + match novaseal_agreement_live::run( + repo_root.as_deref().unwrap_or(&root), + ckb_repo.as_deref(), + ckb_bin.as_deref(), + output.as_deref(), + run_dir.as_deref(), + pretty, + keep_node, + ) { + Ok(0) => ExitCode::SUCCESS, + Ok(_) => ExitCode::FAILURE, + Err(error) => failure(error), + } + } + Command::NovasealPlannedDevnet { + repo_root, + ckb_repo, + ckb_bin, + profile, + output, + run_dir, + pretty, + keep_node, + list_contract, + prepare_artifacts, + live, + } => match novaseal_planned_live::run( + repo_root.as_deref().unwrap_or(&root), + &profile, + output.as_deref(), + ckb_repo.as_deref(), + ckb_bin.as_deref(), + run_dir.as_deref(), + pretty, + keep_node, + list_contract, + prepare_artifacts, + live, + ) { + Ok(0) => ExitCode::SUCCESS, + Ok(_) => ExitCode::FAILURE, + Err(error) => failure(error), + }, + } +} diff --git a/crates/cellscript-tools/src/novaseal_agreement_live.rs b/crates/cellscript-tools/src/novaseal_agreement_live.rs new file mode 100644 index 00000000..a1b48599 --- /dev/null +++ b/crates/cellscript-tools/src/novaseal_agreement_live.rs @@ -0,0 +1,1420 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::{bail, Context, Result}; +use serde_json::{json, Value}; + +use crate::ckb_devnet::{ + always_success_dep, always_success_lock, ckb_hash, ckb_hash_hex, deploy_code, entry_witness_input_type_hex, funding_cells, hex0x, + provenance, resolve_ckb_bin, schnorr_sign, transaction, u16_bytes, u32_bytes, u64_bytes, u8_bytes, xonly_pubkey, CkbDevnet, + RECEIPT_CAPACITY, SHANNONS, STATE_CAPACITY, TEST_AUX_RAND, TEST_SECRET_KEY, ZERO_HASH, +}; +use crate::shared::{stable_json_pretty, stable_json_spaced}; + +const VERSION: u64 = 0; +const ASSET_KIND_CKB: u64 = 0; +const EARLY_CLOSE_FIXED_FEE: u64 = 0; +const STATUS_OFFERED: u64 = 0; +const STATUS_ACTIVE: u64 = 1; +const STATUS_REPAID: u64 = 2; +const STATUS_DEFAULTED: u64 = 3; +const PATH_ORIGINATE: u64 = 0; +const PATH_REPAY: u64 = 1; +const PATH_CLAIM: u64 = 2; +const PAYOUT_BORROWER_PRINCIPAL: u64 = 0; +const PAYOUT_LENDER_REPAYMENT: u64 = 1; +const PAYOUT_BORROWER_COLLATERAL_RETURN: u64 = 2; +const PAYOUT_LENDER_DEFAULT_CLAIM: u64 = 3; +const PAYOUT_CAPACITY_BASE: u64 = 300 * SHANNONS; +const LENDER_SECRET: [u8; 32] = [0x11; 32]; +const LENDER_AUX: [u8; 32] = [0x24; 32]; + +type Hash = [u8; 32]; + +#[derive(Clone)] +struct Terms { + agreement_id: Hash, + terms_hash: Hash, + borrower: Hash, + lender: Hash, + collateral_kind: u64, + collateral_hash: Hash, + collateral_amount: u64, + principal_kind: u64, + principal_hash: Hash, + principal_amount: u64, + fixed_fee: u64, + start: u64, + expiry: u64, + early_close: u64, +} + +#[derive(Clone)] +struct Active { + agreement_id: Hash, + terms_hash: Hash, + borrower: Hash, + lender: Hash, + collateral_kind: u64, + collateral_hash: Hash, + collateral_amount: u64, + principal_kind: u64, + principal_hash: Hash, + principal_amount: u64, + fixed_fee: u64, + expiry: u64, + status: u64, + latest_receipt: Hash, + nonce: u64, +} + +#[derive(Clone)] +struct Payout { + action: u64, + agreement_id: Hash, + role: u64, + recipient: Hash, + asset_kind: u64, + asset_hash: Hash, + amount: u64, + terms_hash: Hash, + nonce: u64, +} + +struct OriginMaterial { + terms_data: Vec, + active: Active, + active_data: Vec, + payout_data: Vec, + receipt_data: Vec, + signed_intent: Vec, + signed_intent_hash: Hash, + latest_receipt_hash: Hash, + borrower_sig: Vec, + lender_sig: Vec, +} + +struct RepayMaterial { + terms_data: Vec, + active_data: Vec, + closed_data: Vec, + lender_payout: Payout, + lender_payout_data: Vec, + borrower_payout_data: Vec, + receipt_data: Vec, + signed_intent: Vec, + signed_intent_hash: Hash, + latest_receipt_hash: Hash, + borrower_sig: Vec, + lender_sig: Vec, + repayment_amount: u64, +} + +struct ClaimMaterial { + terms_data: Vec, + active_data: Vec, + closed_data: Vec, + claim_payout_data: Vec, + receipt_data: Vec, + signed_intent: Vec, + signed_intent_hash: Hash, + latest_receipt_hash: Hash, + borrower_sig: Vec, + lender_sig: Vec, + claim_amount: u64, +} + +fn append(target: &mut Vec, chunks: &[&[u8]]) { + for chunk in chunks { + target.extend_from_slice(chunk); + } +} + +fn pack_terms(value: &Terms) -> Vec { + let mut out = Vec::new(); + append( + &mut out, + &[ + &u16_bytes(VERSION), + &value.agreement_id, + &value.terms_hash, + &value.borrower, + &value.lender, + &u8_bytes(value.collateral_kind), + &value.collateral_hash, + &u64_bytes(value.collateral_amount), + &u8_bytes(value.principal_kind), + &value.principal_hash, + &u64_bytes(value.principal_amount), + &u64_bytes(value.fixed_fee), + &u64_bytes(value.start), + &u64_bytes(value.expiry), + &u8_bytes(value.early_close), + ], + ); + out +} + +fn pack_active(value: &Active) -> Vec { + let mut out = Vec::new(); + append( + &mut out, + &[ + &u16_bytes(VERSION), + &value.agreement_id, + &value.terms_hash, + &value.borrower, + &value.lender, + &u8_bytes(value.collateral_kind), + &value.collateral_hash, + &u64_bytes(value.collateral_amount), + &u8_bytes(value.principal_kind), + &value.principal_hash, + &u64_bytes(value.principal_amount), + &u64_bytes(value.fixed_fee), + &u64_bytes(value.expiry), + &u8_bytes(value.status), + &value.latest_receipt, + &u64_bytes(value.nonce), + ], + ); + out +} + +#[allow(clippy::too_many_arguments)] +fn pack_intent( + action: u64, + terms: &Terms, + old_status: u64, + new_status: u64, + old_nonce: u64, + new_nonce: u64, + terminal_amount: u64, + payout_hash: &Hash, +) -> Vec { + let mut out = Vec::new(); + append( + &mut out, + &[ + &u8_bytes(action), + &terms.agreement_id, + &terms.terms_hash, + &terms.borrower, + &terms.lender, + &u8_bytes(old_status), + &u8_bytes(new_status), + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + &u64_bytes(terminal_amount), + payout_hash, + &u64_bytes(terms.expiry), + ], + ); + out +} + +#[allow(clippy::too_many_arguments)] +fn canonical_hash( + action: u64, + terms: &Terms, + old_state: &Hash, + new_state: &Hash, + old_nonce: u64, + new_nonce: u64, + authority: &Hash, + body_hash: &Hash, + payout_hash: &Hash, +) -> Hash { + let mut packed = Vec::new(); + append( + &mut packed, + &[ + &terms.agreement_id, + &terms.terms_hash, + &u8_bytes(action), + &u8_bytes(action), + &terms.agreement_id, + old_state, + new_state, + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + &u64_bytes(terms.expiry), + authority, + body_hash, + payout_hash, + ], + ); + ckb_hash(&packed) +} + +#[allow(clippy::too_many_arguments)] +fn receipt_commitment( + action: u64, + terms: &Terms, + old_status: u64, + new_status: u64, + terminal_amount: u64, + old_nonce: u64, + new_nonce: u64, + intent_hash: &Hash, + payout_hash: &Hash, +) -> Hash { + let mut packed = Vec::new(); + append( + &mut packed, + &[ + &u8_bytes(action), + &terms.agreement_id, + &u8_bytes(old_status), + &u8_bytes(new_status), + &terms.terms_hash, + &terms.borrower, + &terms.lender, + &u64_bytes(terminal_amount), + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + intent_hash, + payout_hash, + ], + ); + ckb_hash(&packed) +} + +fn pack_payout(value: &Payout) -> Vec { + let mut out = Vec::new(); + append( + &mut out, + &[ + &u8_bytes(value.action), + &value.agreement_id, + &u8_bytes(value.role), + &value.recipient, + &u8_bytes(value.asset_kind), + &value.asset_hash, + &u64_bytes(value.amount), + &value.terms_hash, + &u64_bytes(value.nonce), + ], + ); + out +} + +#[allow(clippy::too_many_arguments)] +fn pack_receipt( + action: u64, + terms: &Terms, + old_status: u64, + new_status: u64, + terminal_amount: u64, + previous: &Hash, + latest: &Hash, + intent_core: &Hash, + signed_intent: &Hash, + payout: &Hash, + nonce: u64, + timepoint: u64, +) -> Vec { + let mut out = Vec::new(); + append( + &mut out, + &[ + &u8_bytes(action), + &terms.agreement_id, + &u8_bytes(old_status), + &u8_bytes(new_status), + &terms.terms_hash, + &terms.borrower, + &terms.lender, + &u64_bytes(terms.collateral_amount), + &u64_bytes(terms.principal_amount), + &u64_bytes(terms.fixed_fee), + &u64_bytes(terminal_amount), + previous, + latest, + intent_core, + signed_intent, + payout, + &u64_bytes(nonce), + &u64_bytes(timepoint), + ], + ); + out +} + +fn signature(secret: &[u8; 32], message: &Hash, aux: &[u8; 32], mutate: bool) -> Result> { + let (public, signed) = schnorr_sign(message, secret, aux)?; + let mut payload = Vec::with_capacity(96); + payload.extend_from_slice(&public); + payload.extend_from_slice(&signed); + if mutate { + *payload.last_mut().unwrap() ^= 1; + } + Ok(payload) +} + +fn witness(op: u64, terms: &[u8], active: &[u8], intent: &[u8], borrower: &[u8], lender: &[u8]) -> String { + let mut payload = b"CSARGv1\0".to_vec(); + payload.extend_from_slice(&u8_bytes(op)); + for value in [terms, active, intent, borrower, lender] { + payload.extend_from_slice(&u32_bytes(value.len())); + payload.extend_from_slice(value); + } + entry_witness_input_type_hex(&payload) +} + +fn make_terms(now: u64, label: &str, expiry: Option) -> Result { + Ok(Terms { + agreement_id: ckb_hash(format!("NovaSeal Agreement live devnet v0 {label}").as_bytes()), + terms_hash: ckb_hash(format!("NovaSeal Agreement live devnet terms v0 {label}").as_bytes()), + borrower: xonly_pubkey(&TEST_SECRET_KEY)?, + lender: xonly_pubkey(&LENDER_SECRET)?, + collateral_kind: ASSET_KIND_CKB, + collateral_hash: ZERO_HASH, + collateral_amount: 50 * SHANNONS, + principal_kind: ASSET_KIND_CKB, + principal_hash: ZERO_HASH, + principal_amount: 20 * SHANNONS, + fixed_fee: 2 * SHANNONS, + start: 0, + expiry: expiry.unwrap_or(now + 1_000_000), + early_close: EARLY_CLOSE_FIXED_FEE, + }) +} + +fn origin_material(terms: &Terms, now: u64, mutate_borrower: bool, mutate_lender: bool) -> Result { + let payout = Payout { + action: PATH_ORIGINATE, + agreement_id: terms.agreement_id, + role: PAYOUT_BORROWER_PRINCIPAL, + recipient: terms.borrower, + asset_kind: terms.principal_kind, + asset_hash: terms.principal_hash, + amount: terms.principal_amount, + terms_hash: terms.terms_hash, + nonce: 0, + }; + let payout_data = pack_payout(&payout); + let payout_hash = ckb_hash(&payout_data); + let core = pack_intent(PATH_ORIGINATE, terms, STATUS_OFFERED, STATUS_ACTIVE, 0, 0, terms.principal_amount, &payout_hash); + let core_hash = ckb_hash(&core); + let latest = receipt_commitment( + PATH_ORIGINATE, + terms, + STATUS_OFFERED, + STATUS_ACTIVE, + terms.principal_amount, + 0, + 0, + &core_hash, + &payout_hash, + ); + let canonical = canonical_hash(PATH_ORIGINATE, terms, &ZERO_HASH, &latest, 0, 0, &terms.borrower, &core_hash, &payout_hash); + let mut signed_intent = core; + signed_intent.extend_from_slice(&canonical); + signed_intent.extend_from_slice(&latest); + let signed_hash = ckb_hash(&signed_intent); + let active = Active { + agreement_id: terms.agreement_id, + terms_hash: terms.terms_hash, + borrower: terms.borrower, + lender: terms.lender, + collateral_kind: terms.collateral_kind, + collateral_hash: terms.collateral_hash, + collateral_amount: terms.collateral_amount, + principal_kind: terms.principal_kind, + principal_hash: terms.principal_hash, + principal_amount: terms.principal_amount, + fixed_fee: terms.fixed_fee, + expiry: terms.expiry, + status: STATUS_ACTIVE, + latest_receipt: latest, + nonce: 0, + }; + let active_data = pack_active(&active); + let receipt_data = pack_receipt( + PATH_ORIGINATE, + terms, + STATUS_OFFERED, + STATUS_ACTIVE, + terms.principal_amount, + &ZERO_HASH, + &latest, + &core_hash, + &signed_hash, + &payout_hash, + 0, + now, + ); + Ok(OriginMaterial { + terms_data: pack_terms(terms), + active, + active_data, + payout_data, + receipt_data, + signed_intent, + signed_intent_hash: signed_hash, + latest_receipt_hash: latest, + borrower_sig: signature(&TEST_SECRET_KEY, &signed_hash, &TEST_AUX_RAND, mutate_borrower)?, + lender_sig: signature(&LENDER_SECRET, &signed_hash, &LENDER_AUX, mutate_lender)?, + }) +} + +fn repay_material(terms: &Terms, active: &Active, previous: &Hash, now: u64, mutate_borrower: bool) -> Result { + let amount = active.principal_amount + active.fixed_fee; + let nonce = active.nonce + 1; + let lender_payout = Payout { + action: PATH_REPAY, + agreement_id: active.agreement_id, + role: PAYOUT_LENDER_REPAYMENT, + recipient: active.lender, + asset_kind: active.principal_kind, + asset_hash: active.principal_hash, + amount, + terms_hash: active.terms_hash, + nonce, + }; + let borrower_payout = Payout { + action: PATH_REPAY, + agreement_id: active.agreement_id, + role: PAYOUT_BORROWER_COLLATERAL_RETURN, + recipient: active.borrower, + asset_kind: active.collateral_kind, + asset_hash: active.collateral_hash, + amount: active.collateral_amount, + terms_hash: active.terms_hash, + nonce, + }; + let lender_data = pack_payout(&lender_payout); + let borrower_data = pack_payout(&borrower_payout); + let mut payout_commitment = Vec::new(); + payout_commitment.extend_from_slice(&ckb_hash(&lender_data)); + payout_commitment.extend_from_slice(&ckb_hash(&borrower_data)); + let payout_hash = ckb_hash(&payout_commitment); + terminal_material( + terms, + active, + previous, + now, + PATH_REPAY, + STATUS_REPAID, + amount, + payout_hash, + lender_payout, + lender_data, + Some(borrower_data), + mutate_borrower, + false, + ) + .map(|value| RepayMaterial { + terms_data: value.terms_data, + active_data: value.active_data, + closed_data: value.closed_data, + lender_payout: value.payout, + lender_payout_data: value.payout_data, + borrower_payout_data: value.second_payout_data.unwrap(), + receipt_data: value.receipt_data, + signed_intent: value.signed_intent, + signed_intent_hash: value.signed_intent_hash, + latest_receipt_hash: value.latest_receipt_hash, + borrower_sig: value.borrower_sig, + lender_sig: value.lender_sig, + repayment_amount: amount, + }) +} + +fn claim_material(terms: &Terms, active: &Active, previous: &Hash, now: u64, mutate_lender: bool) -> Result { + let amount = active.collateral_amount; + let payout = Payout { + action: PATH_CLAIM, + agreement_id: active.agreement_id, + role: PAYOUT_LENDER_DEFAULT_CLAIM, + recipient: active.lender, + asset_kind: active.collateral_kind, + asset_hash: active.collateral_hash, + amount, + terms_hash: active.terms_hash, + nonce: active.nonce + 1, + }; + let payout_data = pack_payout(&payout); + let payout_hash = ckb_hash(&payout_data); + terminal_material( + terms, + active, + previous, + now, + PATH_CLAIM, + STATUS_DEFAULTED, + amount, + payout_hash, + payout, + payout_data, + None, + false, + mutate_lender, + ) + .map(|value| ClaimMaterial { + terms_data: value.terms_data, + active_data: value.active_data, + closed_data: value.closed_data, + claim_payout_data: value.payout_data, + receipt_data: value.receipt_data, + signed_intent: value.signed_intent, + signed_intent_hash: value.signed_intent_hash, + latest_receipt_hash: value.latest_receipt_hash, + borrower_sig: value.borrower_sig, + lender_sig: value.lender_sig, + claim_amount: amount, + }) +} + +struct TerminalMaterial { + terms_data: Vec, + active_data: Vec, + closed_data: Vec, + payout: Payout, + payout_data: Vec, + second_payout_data: Option>, + receipt_data: Vec, + signed_intent: Vec, + signed_intent_hash: Hash, + latest_receipt_hash: Hash, + borrower_sig: Vec, + lender_sig: Vec, +} + +#[allow(clippy::too_many_arguments)] +fn terminal_material( + terms: &Terms, + active: &Active, + previous: &Hash, + now: u64, + action: u64, + new_status: u64, + amount: u64, + payout_hash: Hash, + payout: Payout, + payout_data: Vec, + second_payout_data: Option>, + mutate_borrower: bool, + mutate_lender: bool, +) -> Result { + let nonce = active.nonce + 1; + let core = pack_intent(action, terms, STATUS_ACTIVE, new_status, active.nonce, nonce, amount, &payout_hash); + let core_hash = ckb_hash(&core); + let latest = receipt_commitment(action, terms, STATUS_ACTIVE, new_status, amount, active.nonce, nonce, &core_hash, &payout_hash); + let authority = if action == PATH_REPAY { &active.borrower } else { &active.lender }; + let canonical = canonical_hash(action, terms, previous, &latest, active.nonce, nonce, authority, &core_hash, &payout_hash); + let mut signed_intent = core; + signed_intent.extend_from_slice(&canonical); + signed_intent.extend_from_slice(&latest); + let signed_hash = ckb_hash(&signed_intent); + let mut closed = active.clone(); + closed.status = new_status; + closed.latest_receipt = latest; + closed.nonce = nonce; + let receipt_data = pack_receipt( + action, + terms, + STATUS_ACTIVE, + new_status, + amount, + previous, + &latest, + &core_hash, + &signed_hash, + &payout_hash, + nonce, + now, + ); + Ok(TerminalMaterial { + terms_data: pack_terms(terms), + active_data: pack_active(active), + closed_data: pack_active(&closed), + payout, + payout_data, + second_payout_data, + receipt_data, + signed_intent, + signed_intent_hash: signed_hash, + latest_receipt_hash: latest, + borrower_sig: signature(&TEST_SECRET_KEY, &signed_hash, &TEST_AUX_RAND, mutate_borrower)?, + lender_sig: signature(&LENDER_SECRET, &signed_hash, &LENDER_AUX, mutate_lender)?, + }) +} + +fn lifecycle_type(data_hash: &str) -> Value { + json!({"code_hash": data_hash, "hash_type": "data2", "args": "0x"}) +} + +fn build_origin_tx( + funding: &Value, + lifecycle_hash: &str, + deps: Vec, + header: &str, + terms: &Terms, + material: &OriginMaterial, +) -> Result { + let payout_capacity = PAYOUT_CAPACITY_BASE + terms.principal_amount; + let total = funding["total_capacity"].as_u64().context("originate funding total is missing")?; + let change = + total.checked_sub(STATE_CAPACITY + payout_capacity + RECEIPT_CAPACITY).context("originate funding capacity is too small")?; + if change == 0 { + bail!("originate funding capacity is too small"); + } + let cells = funding_cells(funding); + let mut witnesses = vec![witness( + PATH_ORIGINATE, + &material.terms_data, + &material.active_data, + &material.signed_intent, + &material.borrower_sig, + &material.lender_sig, + )]; + witnesses.extend(vec!["0x".into(); cells.len().saturating_sub(1)]); + Ok(transaction( + cells, + vec![ + json!({"capacity": format!("0x{STATE_CAPACITY:x}"), "lock": always_success_lock("0x"), "type": lifecycle_type(lifecycle_hash)}), + json!({"capacity": format!("0x{payout_capacity:x}"), "lock": always_success_lock(&hex0x(&terms.borrower)), "type": Value::Null}), + json!({"capacity": format!("0x{RECEIPT_CAPACITY:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + json!({"capacity": format!("0x{change:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + ], + vec![hex0x(&material.active_data), hex0x(&material.payout_data), hex0x(&material.receipt_data), "0x".into()], + deps, + witnesses, + vec![header.into()], + )) +} + +#[allow(clippy::too_many_arguments)] +fn build_repay_tx( + active_ref: &Value, + funding: &Value, + lifecycle_hash: &str, + deps: Vec, + header: &str, + terms: &Terms, + material: &RepayMaterial, + capacity_delta: i64, + lock_override: Option<&Hash>, + payout_override: Option<&[u8]>, +) -> Result { + let base = PAYOUT_CAPACITY_BASE + material.repayment_amount; + let repayment_capacity = + if capacity_delta < 0 { base.checked_sub(capacity_delta.unsigned_abs()) } else { base.checked_add(capacity_delta as u64) } + .context("repay payout capacity overflow")?; + let collateral_capacity = PAYOUT_CAPACITY_BASE + terms.collateral_amount; + let total = funding["total_capacity"].as_u64().context("repay funding total is missing")?; + let change = total + .checked_sub(repayment_capacity + collateral_capacity + RECEIPT_CAPACITY) + .context("repay funding capacity is too small")?; + if change == 0 { + bail!("repay funding capacity is too small"); + } + let mut inputs = vec![active_ref.clone()]; + inputs.extend_from_slice(funding_cells(funding)); + let lock_args = lock_override.unwrap_or(&terms.lender); + let payout_data = payout_override.unwrap_or(&material.lender_payout_data); + let mut witnesses = vec![witness( + PATH_REPAY, + &material.terms_data, + &material.active_data, + &material.signed_intent, + &material.borrower_sig, + &material.lender_sig, + )]; + witnesses.extend(vec!["0x".into(); funding_cells(funding).len()]); + Ok(transaction( + &inputs, + vec![ + json!({"capacity": format!("0x{:x}", active_ref["capacity"].as_u64().unwrap()), "lock": always_success_lock("0x"), "type": lifecycle_type(lifecycle_hash)}), + json!({"capacity": format!("0x{repayment_capacity:x}"), "lock": always_success_lock(&hex0x(lock_args)), "type": Value::Null}), + json!({"capacity": format!("0x{collateral_capacity:x}"), "lock": always_success_lock(&hex0x(&terms.borrower)), "type": Value::Null}), + json!({"capacity": format!("0x{RECEIPT_CAPACITY:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + json!({"capacity": format!("0x{change:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + ], + vec![ + hex0x(&material.closed_data), + hex0x(payout_data), + hex0x(&material.borrower_payout_data), + hex0x(&material.receipt_data), + "0x".into(), + ], + deps, + witnesses, + vec![header.into()], + )) +} + +#[allow(clippy::too_many_arguments)] +fn build_claim_tx( + active_ref: &Value, + funding: &Value, + lifecycle_hash: &str, + deps: Vec, + header: &str, + terms: &Terms, + material: &ClaimMaterial, + capacity_delta: i64, + lock_override: Option<&Hash>, + payout_override: Option<&[u8]>, +) -> Result { + let base = PAYOUT_CAPACITY_BASE + material.claim_amount; + let claim_capacity = + if capacity_delta < 0 { base.checked_sub(capacity_delta.unsigned_abs()) } else { base.checked_add(capacity_delta as u64) } + .context("claim payout capacity overflow")?; + let total = funding["total_capacity"].as_u64().context("claim funding total is missing")?; + let change = total.checked_sub(claim_capacity + RECEIPT_CAPACITY).context("claim funding capacity is too small")?; + if change == 0 { + bail!("claim funding capacity is too small"); + } + let mut inputs = vec![active_ref.clone()]; + inputs.extend_from_slice(funding_cells(funding)); + let lock_args = lock_override.unwrap_or(&terms.lender); + let payout_data = payout_override.unwrap_or(&material.claim_payout_data); + let mut witnesses = vec![witness( + PATH_CLAIM, + &material.terms_data, + &material.active_data, + &material.signed_intent, + &material.borrower_sig, + &material.lender_sig, + )]; + witnesses.extend(vec!["0x".into(); funding_cells(funding).len()]); + Ok(transaction( + &inputs, + vec![ + json!({"capacity": format!("0x{:x}", active_ref["capacity"].as_u64().unwrap()), "lock": always_success_lock("0x"), "type": lifecycle_type(lifecycle_hash)}), + json!({"capacity": format!("0x{claim_capacity:x}"), "lock": always_success_lock(&hex0x(lock_args)), "type": Value::Null}), + json!({"capacity": format!("0x{RECEIPT_CAPACITY:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + json!({"capacity": format!("0x{change:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + ], + vec![hex0x(&material.closed_data), hex0x(payout_data), hex0x(&material.receipt_data), "0x".into()], + deps, + witnesses, + vec![header.into()], + )) +} + +fn epoch_number(header: &Value) -> Result { + let encoded = header["epoch"].as_str().context("tip header has no epoch")?; + Ok(u64::from_str_radix(encoded.trim_start_matches("0x"), 16)? & ((1 << 24) - 1)) +} + +fn wait_epoch_after(devnet: &CkbDevnet, expiry: u64) -> Result { + let mut last = Value::Null; + for _ in 0..5_000 { + last = devnet.rpc("get_tip_header", vec![])?; + if epoch_number(&last)? > expiry { + return Ok(last); + } + devnet.rpc("generate_block", vec![])?; + } + bail!("devnet epoch did not advance past expiry {expiry}; last epoch={}", last["epoch"]) +} + +struct OriginRun { + material: OriginMaterial, + active_ref: Value, + dry_run: Value, + commit: Value, + active_live: Value, + payout_live: Value, + receipt_live: Value, +} + +fn submit_origin(devnet: &mut CkbDevnet, lifecycle_hash: &str, deps: &[Value], terms: &Terms, label: &str) -> Result { + let header = devnet.rpc("get_tip_header", vec![])?; + let now = epoch_number(&header)?; + let material = origin_material(terms, now, false, false)?; + let required = STATE_CAPACITY + RECEIPT_CAPACITY + PAYOUT_CAPACITY_BASE + terms.principal_amount; + let funding = devnet.collect_spendable(required + 100 * SHANNONS)?; + let tx = build_origin_tx( + &funding, + lifecycle_hash, + deps.to_vec(), + header["hash"].as_str().context("tip header has no hash")?, + terms, + &material, + )?; + let dry_run = devnet.rpc("dry_run_transaction", vec![tx.clone()])?; + let commit = devnet.submit_and_commit(&tx, label)?; + let hash = commit["tx_hash"].as_str().context("origin commit has no transaction hash")?; + let type_script = lifecycle_type(lifecycle_hash); + let active_live = devnet.assert_live_cell( + hash, + 0, + &format!("{label} active"), + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&material.active_data), + )?; + let payout_live = devnet.assert_live_cell( + hash, + 1, + &format!("{label} principal payout"), + Some(PAYOUT_CAPACITY_BASE + terms.principal_amount), + Some(&always_success_lock(&hex0x(&terms.borrower))), + Some(&Value::Null), + Some(&material.payout_data), + )?; + let receipt_live = devnet.assert_live_cell( + hash, + 2, + &format!("{label} receipt"), + Some(RECEIPT_CAPACITY), + Some(&always_success_lock("0x")), + Some(&Value::Null), + Some(&material.receipt_data), + )?; + Ok(OriginRun { + active_ref: json!({"tx_hash": hash, "index": 0, "capacity": STATE_CAPACITY}), + material, + dry_run, + commit, + active_live, + payout_live, + receipt_live, + }) +} + +fn compile(root: &Path, output: &Path) -> Result<()> { + let status = Command::new("cargo") + .args([ + "run", + "--quiet", + "--locked", + "--bin", + "cellc", + "--", + "proposals/novaseal/agreement-profile-v0/src/nova_agreement_lifecycle_type.cell", + "--target-profile", + "ckb", + "--target", + "riscv64-elf", + "--entry-action", + "nova_agreement_lifecycle", + "-o", + output.to_str().context("agreement lifecycle output path is not UTF-8")?, + ]) + .current_dir(root) + .status()?; + if !status.success() { + bail!("failed to compile NovaSeal Agreement lifecycle"); + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +pub fn run( + root: &Path, + ckb_repo: Option<&Path>, + ckb_bin: Option<&Path>, + output: Option<&Path>, + run_dir: Option<&Path>, + pretty: bool, + keep_node: bool, +) -> Result { + let root = fs::canonicalize(root)?; + let ckb_repo = fs::canonicalize(ckb_repo.map(Path::to_path_buf).unwrap_or_else(|| root.parent().unwrap().join("ckb")))?; + let ckb_bin = resolve_ckb_bin(&ckb_repo, ckb_bin)?; + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let run_dir = run_dir + .map(Path::to_path_buf) + .unwrap_or_else(|| root.join(format!("target/novaseal-agreement-devnet-stateful-live/{timestamp}"))); + fs::create_dir_all(&run_dir)?; + let run_dir = fs::canonicalize(run_dir)?; + let lifecycle_path = run_dir.join("nova-agreement-lifecycle-type.elf"); + compile(&root, &lifecycle_path)?; + let verifier_path = root.join("proposals/novaseal/v0-mvp-skeleton/target/novaseal-btc-verifier-riscv-shell-release.elf"); + if !verifier_path.is_file() { + bail!("missing verifier ELF: {}", verifier_path.display()); + } + let mut devnet = CkbDevnet::new(ckb_repo.clone(), ckb_bin.clone(), run_dir.clone())?; + let mut report = json!({ + "schema": "novaseal-agreement-devnet-stateful-live-v0.1", + "status": "running", + "scenario": "agreement_profile_originate_repay_and_claim", + "repo_root": root.display().to_string(), + "ckb_repo": ckb_repo.display().to_string(), + "ckb_bin": ckb_bin.display().to_string(), + "run_dir": run_dir.display().to_string(), + }); + let mut stage = "initializing"; + let scenario = (|| -> Result<()> { + stage = "start devnet"; + devnet.start()?; + stage = "deploy artifacts"; + let genesis = devnet.get_block_by_number(0)?; + let always = always_success_dep(genesis["transactions"][0]["hash"].as_str().context("genesis cellbase hash is missing")?); + let verifier = deploy_code(&mut devnet, "cellscript_btc_bip340_verifier_riscv", &fs::read(&verifier_path)?, &always)?; + let lifecycle = deploy_code(&mut devnet, "nova_agreement_lifecycle_type", &fs::read(&lifecycle_path)?, &always)?; + let lifecycle_hash = lifecycle["data_hash"].as_str().context("lifecycle data hash is missing")?.to_owned(); + let deps = vec![verifier["cell_dep"].clone(), lifecycle["cell_dep"].clone(), always]; + let source_paths = [ + "proposals/novaseal/agreement-profile-v0/Cell.toml", + "proposals/novaseal/agreement-profile-v0/src", + "proposals/novaseal/agreement-profile-v0/schemas", + "proposals/novaseal/v0-mvp-skeleton/verifier/novaseal_btc_verifier", + "crates/cellscript-tools/src/novaseal_agreement_live.rs", + "crates/cellscript-tools/src/ckb_devnet.rs", + ] + .into_iter() + .map(PathBuf::from) + .collect::>(); + let artifacts = BTreeMap::from([("verifier".into(), verifier_path.clone()), ("lifecycle".into(), lifecycle_path.clone())]); + let source_provenance = provenance(&root, &source_paths, &artifacts)?; + + stage = "negative originate wrong lender signature"; + let negative_origin_header = devnet.rpc("get_tip_header", vec![])?; + let negative_origin_now = epoch_number(&negative_origin_header)?; + let wrong_lender_terms = make_terms(negative_origin_now, "wrong-lender-signature", None)?; + let wrong_lender_material = origin_material(&wrong_lender_terms, negative_origin_now, false, true)?; + let origin_required = STATE_CAPACITY + RECEIPT_CAPACITY + PAYOUT_CAPACITY_BASE + wrong_lender_terms.principal_amount; + let funding = devnet.collect_spendable(origin_required + 100 * SHANNONS)?; + let tx = build_origin_tx( + &funding, + &lifecycle_hash, + deps.clone(), + negative_origin_header["hash"].as_str().context("tip header has no hash")?, + &wrong_lender_terms, + &wrong_lender_material, + )?; + let wrong_lender_origin_reject = devnet.dry_run_rejects( + &tx, + "wrong lender signature originate", + Some("Outputs[0].Type"), + Some(&lifecycle_hash), + Some(56), + )?; + + stage = "negative originate non-CKB asset kind"; + let mut non_ckb_terms = make_terms(negative_origin_now, "non-ckb-asset-kind", None)?; + non_ckb_terms.principal_kind = 1; + let non_ckb_material = origin_material(&non_ckb_terms, negative_origin_now, false, false)?; + let funding = devnet.collect_spendable(origin_required + 100 * SHANNONS)?; + let tx = build_origin_tx( + &funding, + &lifecycle_hash, + deps.clone(), + negative_origin_header["hash"].as_str().context("tip header has no hash")?, + &non_ckb_terms, + &non_ckb_material, + )?; + let non_ckb_reject = + devnet.dry_run_rejects(&tx, "non-CKB asset kind originate", Some("Outputs[0].Type"), Some(&lifecycle_hash), Some(5))?; + + stage = "valid repay-path originate"; + let repay_seed = devnet.rpc("get_tip_header", vec![])?; + let repay_terms = make_terms(epoch_number(&repay_seed)?, "repay", None)?; + let repay_origin = submit_origin(&mut devnet, &lifecycle_hash, &deps, &repay_terms, "agreement repay-path originate")?; + + stage = "negative repay wrong borrower signature"; + let negative_header = devnet.rpc("get_tip_header", vec![])?; + let negative_now = epoch_number(&negative_header)?; + let negative_material = repay_material( + &repay_terms, + &repay_origin.material.active, + &repay_origin.material.latest_receipt_hash, + negative_now, + true, + )?; + let repay_required = RECEIPT_CAPACITY + + PAYOUT_CAPACITY_BASE + + negative_material.repayment_amount + + PAYOUT_CAPACITY_BASE + + repay_terms.collateral_amount; + let funding = devnet.collect_spendable(repay_required + 100 * SHANNONS)?; + let tx = build_repay_tx( + &repay_origin.active_ref, + &funding, + &lifecycle_hash, + deps.clone(), + negative_header["hash"].as_str().context("tip header has no hash")?, + &repay_terms, + &negative_material, + 0, + None, + None, + )?; + let wrong_borrower_reject = + devnet.dry_run_rejects(&tx, "wrong borrower signature repay", Some("Inputs[0].Type"), Some(&lifecycle_hash), Some(56))?; + + stage = "negative repay payout capacity short"; + let capacity_material = repay_material( + &repay_terms, + &repay_origin.material.active, + &repay_origin.material.latest_receipt_hash, + negative_now, + false, + )?; + let funding = devnet.collect_spendable(repay_required + 100 * SHANNONS)?; + let tx = build_repay_tx( + &repay_origin.active_ref, + &funding, + &lifecycle_hash, + deps.clone(), + negative_header["hash"].as_str().context("tip header has no hash")?, + &repay_terms, + &capacity_material, + -1, + None, + None, + )?; + let capacity_reject = + devnet.dry_run_rejects(&tx, "repay payout capacity short", Some("Inputs[0].Type"), Some(&lifecycle_hash), Some(5))?; + + stage = "negative repay payout lock args mismatch"; + let wrong_lock = ckb_hash(b"wrong lender payout lock args"); + let funding = devnet.collect_spendable(repay_required + 100 * SHANNONS)?; + let tx = build_repay_tx( + &repay_origin.active_ref, + &funding, + &lifecycle_hash, + deps.clone(), + negative_header["hash"].as_str().context("tip header has no hash")?, + &repay_terms, + &capacity_material, + 0, + Some(&wrong_lock), + None, + )?; + let lock_reject = + devnet.dry_run_rejects(&tx, "repay payout lock args mismatch", Some("Inputs[0].Type"), Some(&lifecycle_hash), Some(5))?; + + stage = "negative repay wrong payout amount"; + let mut wrong_payout = capacity_material.lender_payout.clone(); + wrong_payout.amount += 1; + let wrong_payout_data = pack_payout(&wrong_payout); + let funding = devnet.collect_spendable(repay_required + 100 * SHANNONS)?; + let tx = build_repay_tx( + &repay_origin.active_ref, + &funding, + &lifecycle_hash, + deps.clone(), + negative_header["hash"].as_str().context("tip header has no hash")?, + &repay_terms, + &capacity_material, + 0, + None, + Some(&wrong_payout_data), + )?; + let wrong_payout_reject = + devnet.dry_run_rejects(&tx, "repay wrong payout amount", Some("Inputs[0].Type"), Some(&lifecycle_hash), Some(5))?; + let agreement_type = lifecycle_type(&lifecycle_hash); + let active_still_live = devnet.assert_live_cell( + repay_origin.active_ref["tx_hash"].as_str().unwrap(), + 0, + "post-negative repay active", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&agreement_type), + Some(&repay_origin.material.active_data), + )?; + + stage = "valid repay"; + let repay_header = devnet.rpc("get_tip_header", vec![])?; + let repay_material = repay_material( + &repay_terms, + &repay_origin.material.active, + &repay_origin.material.latest_receipt_hash, + epoch_number(&repay_header)?, + false, + )?; + let funding = devnet.collect_spendable(repay_required + 100 * SHANNONS)?; + let repay_tx = build_repay_tx( + &repay_origin.active_ref, + &funding, + &lifecycle_hash, + deps.clone(), + repay_header["hash"].as_str().context("tip header has no hash")?, + &repay_terms, + &repay_material, + 0, + None, + None, + )?; + let repay_dry = devnet.rpc("dry_run_transaction", vec![repay_tx.clone()])?; + let repay_commit = devnet.submit_and_commit(&repay_tx, "agreement repay before expiry")?; + let active_dead = devnet.wait_dead_cell(repay_origin.active_ref["tx_hash"].as_str().unwrap(), 0)?; + let repay_hash = repay_commit["tx_hash"].as_str().unwrap(); + let closed_live = devnet.assert_live_cell( + repay_hash, + 0, + "repay closed agreement", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&agreement_type), + Some(&repay_material.closed_data), + )?; + let lender_live = devnet.assert_live_cell( + repay_hash, + 1, + "repay lender repayment", + Some(PAYOUT_CAPACITY_BASE + repay_material.repayment_amount), + Some(&always_success_lock(&hex0x(&repay_terms.lender))), + Some(&Value::Null), + Some(&repay_material.lender_payout_data), + )?; + let borrower_live = devnet.assert_live_cell( + repay_hash, + 2, + "repay borrower collateral return", + Some(PAYOUT_CAPACITY_BASE + repay_terms.collateral_amount), + Some(&always_success_lock(&hex0x(&repay_terms.borrower))), + Some(&Value::Null), + Some(&repay_material.borrower_payout_data), + )?; + let repay_receipt_live = devnet.assert_live_cell( + repay_hash, + 3, + "repay receipt", + Some(RECEIPT_CAPACITY), + Some(&always_success_lock("0x")), + Some(&Value::Null), + Some(&repay_material.receipt_data), + )?; + + stage = "valid claim-path originate"; + let claim_seed = devnet.rpc("get_tip_header", vec![])?; + let claim_seed_now = epoch_number(&claim_seed)?; + let claim_terms = make_terms(claim_seed_now, "claim", Some(claim_seed_now + 1))?; + let claim_origin = submit_origin(&mut devnet, &lifecycle_hash, &deps, &claim_terms, "agreement claim-path originate")?; + + stage = "negative early claim"; + let early_header = devnet.rpc("get_tip_header", vec![])?; + let early_material = claim_material( + &claim_terms, + &claim_origin.material.active, + &claim_origin.material.latest_receipt_hash, + epoch_number(&early_header)?, + false, + )?; + let claim_required = RECEIPT_CAPACITY + PAYOUT_CAPACITY_BASE + early_material.claim_amount; + let funding = devnet.collect_spendable(claim_required + 100 * SHANNONS)?; + let tx = build_claim_tx( + &claim_origin.active_ref, + &funding, + &lifecycle_hash, + deps.clone(), + early_header["hash"].as_str().context("tip header has no hash")?, + &claim_terms, + &early_material, + 0, + None, + None, + )?; + let early_reject = + devnet.dry_run_rejects(&tx, "early claim before expiry", Some("Inputs[0].Type"), Some(&lifecycle_hash), Some(5))?; + + stage = "wait claim expiry"; + let claim_header = wait_epoch_after(&devnet, claim_terms.expiry)?; + let claim_now = epoch_number(&claim_header)?; + stage = "negative claim wrong lender signature"; + let wrong_claim_material = + claim_material(&claim_terms, &claim_origin.material.active, &claim_origin.material.latest_receipt_hash, claim_now, true)?; + let funding = devnet.collect_spendable(claim_required + 100 * SHANNONS)?; + let tx = build_claim_tx( + &claim_origin.active_ref, + &funding, + &lifecycle_hash, + deps.clone(), + claim_header["hash"].as_str().context("tip header has no hash")?, + &claim_terms, + &wrong_claim_material, + 0, + None, + None, + )?; + let wrong_claim_reject = + devnet.dry_run_rejects(&tx, "wrong lender signature claim", Some("Inputs[0].Type"), Some(&lifecycle_hash), Some(56))?; + let claim_active_still_live = devnet.assert_live_cell( + claim_origin.active_ref["tx_hash"].as_str().unwrap(), + 0, + "post-negative claim active", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&agreement_type), + Some(&claim_origin.material.active_data), + )?; + + stage = "valid claim"; + let claim_material = + claim_material(&claim_terms, &claim_origin.material.active, &claim_origin.material.latest_receipt_hash, claim_now, false)?; + let funding = devnet.collect_spendable(claim_required + 100 * SHANNONS)?; + let claim_tx = build_claim_tx( + &claim_origin.active_ref, + &funding, + &lifecycle_hash, + deps.clone(), + claim_header["hash"].as_str().context("tip header has no hash")?, + &claim_terms, + &claim_material, + 0, + None, + None, + )?; + let claim_dry = devnet.rpc("dry_run_transaction", vec![claim_tx.clone()])?; + let claim_commit = devnet.submit_and_commit(&claim_tx, "agreement claim after expiry")?; + let claim_dead = devnet.wait_dead_cell(claim_origin.active_ref["tx_hash"].as_str().unwrap(), 0)?; + let claim_hash = claim_commit["tx_hash"].as_str().unwrap(); + let claim_closed_live = devnet.assert_live_cell( + claim_hash, + 0, + "claim closed agreement", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&agreement_type), + Some(&claim_material.closed_data), + )?; + let claim_payout_live = devnet.assert_live_cell( + claim_hash, + 1, + "claim lender default claim", + Some(PAYOUT_CAPACITY_BASE + claim_material.claim_amount), + Some(&always_success_lock(&hex0x(&claim_terms.lender))), + Some(&Value::Null), + Some(&claim_material.claim_payout_data), + )?; + let claim_receipt_live = devnet.assert_live_cell( + claim_hash, + 2, + "claim receipt", + Some(RECEIPT_CAPACITY), + Some(&always_success_lock("0x")), + Some(&Value::Null), + Some(&claim_material.receipt_data), + )?; + + report.as_object_mut().unwrap().extend( + json!({ + "status": "passed", + "live_devnet_rpc_executed": true, + "stateful_lifecycle_executed": true, + "ckb_log": devnet.log_path.display().to_string(), + "rpc_url": devnet.rpc_url, + "artifacts": {"verifier": verifier, "lifecycle": lifecycle}, + "provenance": source_provenance, + "repay_terms": terms_json(&repay_terms), + "claim_terms": terms_json(&claim_terms), + "originate": { + "dry_run_cycles": repay_origin.dry_run["cycles"], + "commit": repay_origin.commit, + "active_live": repay_origin.active_live["status"] == "live", + "principal_payout_live": repay_origin.payout_live["status"] == "live", + "receipt_live": repay_origin.receipt_live["status"] == "live", + "active_data_hash": hex0x(&ckb_hash(&repay_origin.material.active_data)), + "principal_payout_data_hash": ckb_hash_hex(&repay_origin.material.payout_data), + "signed_intent_hash": hex0x(&repay_origin.material.signed_intent_hash), + "latest_receipt_hash": hex0x(&repay_origin.material.latest_receipt_hash), + }, + "repay": { + "dry_run_cycles": repay_dry["cycles"], "commit": repay_commit, + "old_active_not_live": active_dead["status"] != "live", "closed_live": closed_live["status"] == "live", + "lender_repayment_live": lender_live["status"] == "live", "borrower_collateral_return_live": borrower_live["status"] == "live", + "receipt_live": repay_receipt_live["status"] == "live", "closed_data_hash": hex0x(&ckb_hash(&repay_material.closed_data)), + "lender_payout_data_hash": ckb_hash_hex(&repay_material.lender_payout_data), + "borrower_payout_data_hash": ckb_hash_hex(&repay_material.borrower_payout_data), + "signed_intent_hash": hex0x(&repay_material.signed_intent_hash), "latest_receipt_hash": hex0x(&repay_material.latest_receipt_hash), + }, + "claim_originate": { + "dry_run_cycles": claim_origin.dry_run["cycles"], "commit": claim_origin.commit, + "active_live": claim_origin.active_live["status"] == "live", "principal_payout_live": claim_origin.payout_live["status"] == "live", + "receipt_live": claim_origin.receipt_live["status"] == "live", "latest_receipt_hash": hex0x(&claim_origin.material.latest_receipt_hash), + }, + "claim": { + "dry_run_cycles": claim_dry["cycles"], "commit": claim_commit, "old_active_not_live": claim_dead["status"] != "live", + "closed_live": claim_closed_live["status"] == "live", "lender_default_claim_live": claim_payout_live["status"] == "live", + "receipt_live": claim_receipt_live["status"] == "live", "closed_data_hash": hex0x(&ckb_hash(&claim_material.closed_data)), + "claim_payout_data_hash": ckb_hash_hex(&claim_material.claim_payout_data), + "signed_intent_hash": hex0x(&claim_material.signed_intent_hash), "latest_receipt_hash": hex0x(&claim_material.latest_receipt_hash), + "timepoint": claim_now, + }, + "negative_cases": { + "wrong_lender_signature_dry_run": wrong_lender_origin_reject, + "non_ckb_asset_kind_dry_run": non_ckb_reject, + "wrong_borrower_signature_dry_run": wrong_borrower_reject, + "repay_payout_capacity_short_dry_run": capacity_reject, + "repay_payout_lock_args_mismatch_dry_run": lock_reject, + "repay_wrong_payout_amount_dry_run": wrong_payout_reject, + "early_claim_dry_run": early_reject, + "wrong_lender_claim_signature_dry_run": wrong_claim_reject, + "post_negative_active_still_live": active_still_live["status"] == "live", + "post_claim_negative_active_still_live": claim_active_still_live["status"] == "live", + }, + }) + .as_object() + .unwrap() + .clone(), + ); + Ok(()) + })(); + if let Err(error) = scenario { + report["status"] = json!("failed"); + report["stage"] = json!(stage); + report["error"] = json!(error.to_string()); + report["ckb_log"] = json!(devnet.log_path.display().to_string()); + report["rpc_url"] = json!(devnet.rpc_url); + } + if !keep_node { + devnet.stop(); + } + let output = match output { + Some(path) if path.is_absolute() => path.to_path_buf(), + Some(path) => root.join(path), + None => root.join("target/novaseal-agreement-devnet-stateful-live.json"), + }; + fs::create_dir_all(output.parent().context("output path has no parent")?)?; + let text = if pretty { stable_json_pretty(&report)? } else { stable_json_spaced(&report)? }; + fs::write(&output, format!("{text}\n"))?; + println!( + "wrote {} status={} live_devnet_rpc_executed={}", + output.display(), + report["status"].as_str().unwrap_or("failed"), + report["live_devnet_rpc_executed"].as_bool().unwrap_or(false) + ); + Ok(if report["status"] == "passed" { 0 } else { 1 }) +} + +fn terms_json(terms: &Terms) -> Value { + json!({ + "agreement_id": hex0x(&terms.agreement_id), + "terms_hash": hex0x(&terms.terms_hash), + "borrower_authority_hash": hex0x(&terms.borrower), + "lender_authority_hash": hex0x(&terms.lender), + "principal_amount": terms.principal_amount, + "collateral_amount": terms.collateral_amount, + "fixed_fee_amount": terms.fixed_fee, + "expiry_timepoint": terms.expiry, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deterministic_lender_key_matches_reference_contract() { + assert_eq!( + hex0x(&xonly_pubkey(&LENDER_SECRET).unwrap()), + "0x4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa" + ); + } + + #[test] + fn origin_material_is_deterministic() { + let terms = make_terms(42, "parity", None).unwrap(); + let first = origin_material(&terms, 42, false, false).unwrap(); + let second = origin_material(&terms, 42, false, false).unwrap(); + assert_eq!(first.active_data, second.active_data); + assert_eq!(first.signed_intent_hash, second.signed_intent_hash); + assert_eq!(hex0x(&ckb_hash(&first.active_data)), "0xba0a5845b3b3915c3852980d89277fd1ee0a98cb0d511a578599cdbd08847359"); + assert_eq!(hex0x(&first.signed_intent_hash), "0x32596edbe701807be5ab8835ee9381d3ad31ed73569800a8834b4fc7686ff201"); + assert_eq!(hex0x(&first.latest_receipt_hash), "0xf13b028a01060cd4af902360a32024e608f189638c98e84769f2e480b42e2241"); + assert_eq!(ckb_hash_hex(&first.payout_data), "0x716280b50ce2b3c50d94be67ca79726e02a83c2bcf671d7061921783dded9c80"); + } +} diff --git a/crates/cellscript-tools/src/novaseal_core_live.rs b/crates/cellscript-tools/src/novaseal_core_live.rs new file mode 100644 index 00000000..cf44c161 --- /dev/null +++ b/crates/cellscript-tools/src/novaseal_core_live.rs @@ -0,0 +1,575 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::{bail, Context, Result}; +use serde_json::{json, Value}; + +use crate::ckb_devnet::{ + always_success_dep, always_success_lock, ckb_hash, deploy_code, entry_witness_input_type_hex, funding_cells, hex0x, packed_hash, + provenance, resolve_ckb_bin, schnorr_sign, transaction, u16_bytes, u32_bytes, u64_bytes, u8_bytes, xonly_pubkey, CkbDevnet, + RECEIPT_CAPACITY, STATE_CAPACITY, TEST_AUX_RAND, TEST_SECRET_KEY, ZERO_HASH, +}; +use crate::shared::{stable_json_pretty, stable_json_spaced}; + +const VERSION: u64 = 0; +const OP_BOOTSTRAP: u64 = 0; +const OP_TRANSITION: u64 = 1; + +#[derive(Clone)] +struct CoreState { + authority: [u8; 32], + state: [u8; 32], + policy: [u8; 32], + receipt: [u8; 32], + nonce: u64, + expiry: u64, +} + +struct TransitionMaterial { + flat_header: Vec, + signed_intent: Vec, + signed_intent_hash: [u8; 32], + state_hash_commitment: [u8; 32], + signature_payload: Vec, + new_cell_data: Vec, + receipt_data: Vec, + receipt_hash: [u8; 32], + new_state_hash: [u8; 32], +} + +fn append(target: &mut Vec, chunks: &[&[u8]]) { + for chunk in chunks { + target.extend_from_slice(chunk); + } +} + +fn pack_cell(state: &CoreState) -> Vec { + let mut value = u16_bytes(VERSION); + append( + &mut value, + &[&state.authority, &state.state, &state.policy, &state.receipt, &u64_bytes(state.nonce), &u64_bytes(state.expiry)], + ); + value +} + +fn pack_outpoint(hash: &str, index: u64) -> Result> { + let mut bytes = crate::ckb_devnet::decode_hex(hash)?; + if bytes.len() != 32 { + bail!("tx hash must be 32 bytes: {hash}"); + } + bytes.extend_from_slice(&(index as u32).to_le_bytes()); + Ok(bytes) +} + +#[allow(clippy::too_many_arguments)] +fn intent_core( + protocol: &[u8; 32], + package: &[u8; 32], + policy: &[u8; 32], + hash: &str, + index: u64, + old: &[u8; 32], + new: &[u8; 32], + old_nonce: u64, + new_nonce: u64, + expiry: u64, +) -> Result> { + let mut value = Vec::new(); + append( + &mut value, + &[ + protocol, + package, + policy, + &u8_bytes(OP_TRANSITION), + &u8_bytes(OP_TRANSITION), + &pack_outpoint(hash, index)?, + old, + new, + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + &u64_bytes(expiry), + ], + ); + Ok(value) +} + +fn cell_commitment(state: &CoreState, new_hash: &[u8; 32]) -> Vec { + let mut value = u16_bytes(VERSION); + append(&mut value, &[&state.authority, new_hash, &state.policy, &u64_bytes(state.nonce + 1), &u64_bytes(state.expiry)]); + value +} + +#[allow(clippy::too_many_arguments)] +fn receipt_commitment( + protocol: &[u8; 32], + package: &[u8; 32], + state: &CoreState, + hash: &str, + index: u64, + new_cell: &[u8; 32], + new_state: &[u8; 32], + intent_hash: &[u8; 32], +) -> Result> { + let mut value = Vec::new(); + append( + &mut value, + &[ + protocol, + package, + &state.policy, + &u8_bytes(OP_TRANSITION), + &u8_bytes(OP_TRANSITION), + &pack_outpoint(hash, index)?, + new_cell, + &state.state, + new_state, + &u64_bytes(state.nonce), + &u64_bytes(state.nonce + 1), + intent_hash, + &ZERO_HASH, + ], + ); + Ok(value) +} + +#[allow(clippy::too_many_arguments)] +fn receipt( + protocol: &[u8; 32], + package: &[u8; 32], + state: &CoreState, + hash: &str, + index: u64, + new_cell: &[u8; 32], + new_state: &[u8; 32], + intent_hash: &[u8; 32], + signed_hash: &[u8; 32], +) -> Result> { + let mut value = Vec::new(); + append( + &mut value, + &[ + protocol, + package, + &state.policy, + &u8_bytes(OP_TRANSITION), + &u8_bytes(OP_TRANSITION), + &pack_outpoint(hash, index)?, + new_cell, + &state.state, + new_state, + &u64_bytes(state.nonce), + &u64_bytes(state.nonce + 1), + intent_hash, + signed_hash, + &ZERO_HASH, + &state.authority, + &u64_bytes(state.expiry), + ], + ); + Ok(value) +} + +fn material(old_hash: &str, old_index: u64, old: &CoreState, new_state: [u8; 32]) -> Result { + let protocol = ckb_hash(b"NovaSeal/core/v0"); + let package = ckb_hash(b"NovaSeal/devnet/stateful/live"); + let new_cell = packed_hash("NovaSealCellCommitmentV0", &cell_commitment(old, &new_state)); + let core = intent_core( + &protocol, + &package, + &old.policy, + old_hash, + old_index, + &old.state, + &new_state, + old.nonce, + old.nonce + 1, + old.expiry, + )?; + let intent_hash = packed_hash("NovaSealIntentCoreV0", &core); + let commitment = receipt_commitment(&protocol, &package, old, old_hash, old_index, &new_cell, &new_state, &intent_hash)?; + let receipt_hash = packed_hash("ProofReceiptCommitmentV0", &commitment); + let mut signed_intent = core.clone(); + signed_intent.extend_from_slice(&receipt_hash); + let signed_intent_hash = packed_hash("NovaSealSignedIntentV0", &signed_intent); + let state_hash_commitment = ckb_hash(&new_state); + let (pubkey, signature) = schnorr_sign(&state_hash_commitment, &TEST_SECRET_KEY, &TEST_AUX_RAND)?; + if pubkey != old.authority { + bail!("derived pubkey does not match old cell authority hash"); + } + let next = CoreState { + authority: old.authority, + state: new_state, + policy: old.policy, + receipt: receipt_hash, + nonce: old.nonce + 1, + expiry: old.expiry, + }; + let receipt_data = + receipt(&protocol, &package, old, old_hash, old_index, &new_cell, &new_state, &intent_hash, &signed_intent_hash)?; + let old_hash_bytes: [u8; 32] = crate::ckb_devnet::decode_hex(old_hash)? + .try_into() + .map_err(|bytes: Vec| anyhow::anyhow!("old hash has {} bytes", bytes.len()))?; + let mut flat = Vec::new(); + append( + &mut flat, + &[ + &protocol, + &package, + &old.policy, + &old_hash_bytes, + &old.state, + &new_state, + &u64_bytes(old.nonce), + &u64_bytes(old.nonce + 1), + &u64_bytes(old.expiry), + ], + ); + let mut payload = Vec::with_capacity(96); + payload.extend_from_slice(&pubkey); + payload.extend_from_slice(&signature); + Ok(TransitionMaterial { + flat_header: flat, + signed_intent, + signed_intent_hash, + state_hash_commitment, + signature_payload: payload, + new_cell_data: pack_cell(&next), + receipt_data, + receipt_hash, + new_state_hash: new_state, + }) +} + +fn witness( + op: u64, + old_cell: &[u8], + signed: &[u8], + state_commitment: &[u8; 32], + signature: &[u8], + flat: Option<&[u8]>, +) -> Result { + if signature.len() != 96 { + bail!("entry witness expects 32-byte pubkey plus 64-byte signature"); + } + let fallback = vec![0_u8; 216]; + let flat = flat.unwrap_or(&fallback); + let mut payload = b"CSARGv1\0".to_vec(); + append( + &mut payload, + &[ + &u8_bytes(op), + state_commitment, + signature, + &u32_bytes(flat.len()), + flat, + &u32_bytes(old_cell.len()), + old_cell, + &u32_bytes(signed.len()), + signed, + ], + ); + Ok(entry_witness_input_type_hex(&payload)) +} + +fn compile(root: &Path, output: &Path) -> Result<()> { + let status = Command::new("cargo") + .args([ + "run", + "--quiet", + "--locked", + "--bin", + "cellc", + "--", + "proposals/novaseal/v0-mvp-skeleton/src/nova_state_lifecycle_type.cell", + "--target-profile", + "ckb", + "--target", + "riscv64-elf", + "--entry-action", + "novaseal_lifecycle", + "-o", + output.to_str().unwrap(), + ]) + .current_dir(root) + .status()?; + if !status.success() { + bail!("failed to compile NovaSeal lifecycle"); + } + Ok(()) +} + +fn bootstrap(funding: &Value, lifecycle_hash: &str, deps: Vec, header: &str, data: &[u8]) -> Result { + let total = funding["total_capacity"].as_u64().unwrap(); + let change = total.checked_sub(STATE_CAPACITY).context("bootstrap funding capacity is too small")?; + if change == 0 { + bail!("bootstrap funding capacity is too small"); + } + let type_script = json!({"code_hash": lifecycle_hash, "hash_type": "data2", "args": "0x"}); + let witness = witness(OP_BOOTSTRAP, data, &[0_u8; 254], &ZERO_HASH, &[0_u8; 96], None)?; + let cells = funding_cells(funding); + let mut witnesses = vec![witness]; + witnesses.extend(vec!["0x".into(); cells.len().saturating_sub(1)]); + Ok(transaction( + cells, + vec![ + json!({"capacity": format!("0x{STATE_CAPACITY:x}"), "lock": always_success_lock("0x"), "type": type_script}), + json!({"capacity": format!("0x{change:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + ], + vec![hex0x(data), "0x".into()], + deps, + witnesses, + vec![header.into()], + )) +} + +#[allow(clippy::too_many_arguments)] +fn transition( + old_ref: &Value, + old: &CoreState, + lifecycle_hash: &str, + deps: Vec, + header: &str, + funding: &Value, + new_hash: [u8; 32], + mutate: bool, +) -> Result<(Value, CoreState, TransitionMaterial)> { + let old_data = pack_cell(old); + let material = material(old_ref["tx_hash"].as_str().unwrap(), old_ref["index"].as_u64().unwrap(), old, new_hash)?; + let mut signature = material.signature_payload.clone(); + if mutate { + *signature.last_mut().unwrap() ^= 1; + } + let witness = witness( + OP_TRANSITION, + &old_data, + &material.signed_intent, + &material.state_hash_commitment, + &signature, + Some(&material.flat_header), + )?; + let total = funding["total_capacity"].as_u64().unwrap(); + let change = total.checked_sub(RECEIPT_CAPACITY).context("transition funding capacity is too small")?; + if change == 0 { + bail!("transition funding capacity is too small"); + } + let type_script = json!({"code_hash": lifecycle_hash, "hash_type": "data2", "args": "0x"}); + let mut inputs = vec![old_ref.clone()]; + inputs.extend_from_slice(funding_cells(funding)); + let mut witnesses = vec![witness]; + witnesses.extend(vec!["0x".into(); funding_cells(funding).len()]); + let tx = transaction( + &inputs, + vec![ + json!({"capacity": format!("0x{:x}", old_ref["capacity"].as_u64().unwrap()), "lock": always_success_lock("0x"), "type": type_script}), + json!({"capacity": format!("0x{RECEIPT_CAPACITY:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + json!({"capacity": format!("0x{change:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + ], + vec![hex0x(&material.new_cell_data), hex0x(&material.receipt_data), "0x".into()], + deps, + witnesses, + vec![header.into()], + ); + let next = CoreState { + authority: old.authority, + state: material.new_state_hash, + policy: old.policy, + receipt: material.receipt_hash, + nonce: old.nonce + 1, + expiry: old.expiry, + }; + Ok((tx, next, material)) +} + +#[allow(clippy::too_many_arguments)] +pub fn run( + root: &Path, + ckb_repo: Option<&Path>, + ckb_bin: Option<&Path>, + output: Option<&Path>, + run_dir: Option<&Path>, + pretty: bool, + keep_node: bool, +) -> Result { + let root = fs::canonicalize(root)?; + let ckb_repo = fs::canonicalize(ckb_repo.map(Path::to_path_buf).unwrap_or_else(|| root.parent().unwrap().join("ckb")))?; + let ckb_bin = resolve_ckb_bin(&ckb_repo, ckb_bin)?; + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let run_dir = + run_dir.map(Path::to_path_buf).unwrap_or_else(|| root.join(format!("target/novaseal-devnet-stateful-live/{timestamp}"))); + fs::create_dir_all(&run_dir)?; + let run_dir = fs::canonicalize(run_dir)?; + let lifecycle_path = run_dir.join("novaseal-lifecycle-type.elf"); + compile(&root, &lifecycle_path)?; + let verifier_path = root.join("proposals/novaseal/v0-mvp-skeleton/target/novaseal-btc-verifier-riscv-shell-release.elf"); + if !verifier_path.is_file() { + bail!("missing verifier ELF: {}", verifier_path.display()); + } + let mut devnet = CkbDevnet::new(ckb_repo.clone(), ckb_bin.clone(), run_dir.clone())?; + let mut report = json!({"schema": "novaseal-devnet-stateful-live-v0.1", "status": "running", + "scenario": "core_bootstrap_then_key_auth_transition", "repo_root": root.display().to_string(), "ckb_repo": ckb_repo.display().to_string(), + "ckb_bin": ckb_bin.display().to_string(), "run_dir": run_dir.display().to_string()}); + let scenario = (|| -> Result<()> { + devnet.start()?; + let genesis = devnet.get_block_by_number(0)?; + let always = always_success_dep(genesis["transactions"][0]["hash"].as_str().unwrap()); + let verifier_bytes = fs::read(&verifier_path)?; + let lifecycle_bytes = fs::read(&lifecycle_path)?; + let verifier = deploy_code(&mut devnet, "cellscript_btc_bip340_verifier_riscv", &verifier_bytes, &always)?; + let lifecycle = deploy_code(&mut devnet, "novaseal_lifecycle_type", &lifecycle_bytes, &always)?; + let deps = vec![verifier["cell_dep"].clone(), lifecycle["cell_dep"].clone(), always]; + let source_paths = [ + "proposals/novaseal/v0-mvp-skeleton/Cell.toml", + "proposals/novaseal/v0-mvp-skeleton/src", + "proposals/novaseal/v0-mvp-skeleton/schemas", + "proposals/novaseal/v0-mvp-skeleton/verifier/novaseal_btc_verifier", + "crates/cellscript-tools/src/novaseal_core_live.rs", + "crates/cellscript-tools/src/ckb_devnet.rs", + ] + .into_iter() + .map(PathBuf::from) + .collect::>(); + let artifacts = BTreeMap::from([("verifier".into(), verifier_path.clone()), ("lifecycle".into(), lifecycle_path.clone())]); + let source_provenance = provenance(&root, &source_paths, &artifacts)?; + let header = devnet.rpc("get_tip_header", vec![])?["hash"].as_str().unwrap().to_owned(); + let initial = CoreState { + authority: xonly_pubkey(&TEST_SECRET_KEY)?, + state: ckb_hash(b"novaseal devnet initial state"), + policy: ckb_hash(b"novaseal devnet policy"), + receipt: ZERO_HASH, + nonce: 0, + expiry: (1_u64 << 63) - 1, + }; + let initial_data = pack_cell(&initial); + let bootstrap_funding = devnet.collect_spendable(STATE_CAPACITY + 100 * crate::ckb_devnet::SHANNONS)?; + let bootstrap_tx = + bootstrap(&bootstrap_funding, lifecycle["data_hash"].as_str().unwrap(), deps.clone(), &header, &initial_data)?; + fs::write(run_dir.join("bootstrap-tx.json"), format!("{}\n", stable_json_pretty(&bootstrap_tx)?))?; + let bootstrap_dry = devnet.rpc("dry_run_transaction", vec![bootstrap_tx.clone()])?; + let bootstrap_commit = devnet.submit_and_commit(&bootstrap_tx, "novaseal bootstrap")?; + let type_script = json!({"code_hash": lifecycle["data_hash"], "hash_type": "data2", "args": "0x"}); + let bootstrap_live = devnet.assert_live_cell( + bootstrap_commit["tx_hash"].as_str().unwrap(), + 0, + "bootstrap state", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&initial_data), + )?; + let old_ref = json!({"tx_hash": bootstrap_commit["tx_hash"], "index": 0, "capacity": STATE_CAPACITY}); + let transition_header = devnet.rpc("get_tip_header", vec![])?["hash"].as_str().unwrap().to_owned(); + let transition_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * crate::ckb_devnet::SHANNONS)?; + let (transition_tx, next, transition_material) = transition( + &old_ref, + &initial, + lifecycle["data_hash"].as_str().unwrap(), + deps.clone(), + &transition_header, + &transition_funding, + ckb_hash(b"novaseal devnet state after transition"), + false, + )?; + fs::write(run_dir.join("transition-tx.json"), format!("{}\n", stable_json_pretty(&transition_tx)?))?; + let transition_dry = devnet.rpc("dry_run_transaction", vec![transition_tx.clone()])?; + let transition_commit = devnet.submit_and_commit(&transition_tx, "novaseal key-auth transition")?; + let bootstrap_dead = devnet.wait_dead_cell(bootstrap_commit["tx_hash"].as_str().unwrap(), 0)?; + let new_live = devnet.assert_live_cell( + transition_commit["tx_hash"].as_str().unwrap(), + 0, + "transition new state", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&transition_material.new_cell_data), + )?; + let receipt_live = devnet.assert_live_cell( + transition_commit["tx_hash"].as_str().unwrap(), + 1, + "transition receipt", + Some(RECEIPT_CAPACITY), + Some(&always_success_lock("0x")), + Some(&Value::Null), + Some(&transition_material.receipt_data), + )?; + let negative_header = devnet.rpc("get_tip_header", vec![])?["hash"].as_str().unwrap().to_owned(); + let negative_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * crate::ckb_devnet::SHANNONS)?; + let negative_ref = json!({"tx_hash": transition_commit["tx_hash"], "index": 0, "capacity": STATE_CAPACITY}); + let (negative_tx, _, _) = transition( + &negative_ref, + &next, + lifecycle["data_hash"].as_str().unwrap(), + deps, + &negative_header, + &negative_funding, + ckb_hash(b"novaseal devnet rejected state"), + true, + )?; + fs::write(run_dir.join("wrong-signature-tx.json"), format!("{}\n", stable_json_pretty(&negative_tx)?))?; + let rejection = devnet.dry_run_rejects( + &negative_tx, + "wrong signature transition", + Some("Inputs[0].Type"), + lifecycle["data_hash"].as_str(), + Some(56), + )?; + let still_live = devnet.assert_live_cell( + transition_commit["tx_hash"].as_str().unwrap(), + 0, + "post-negative state", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&transition_material.new_cell_data), + )?; + report.as_object_mut().unwrap().extend(json!({"status": "passed", "live_devnet_rpc_executed": true, "stateful_lifecycle_executed": true, + "ckb_log": devnet.log_path.display().to_string(), "rpc_url": devnet.rpc_url, "artifacts": {"verifier": verifier, "lifecycle": lifecycle}, + "provenance": source_provenance, + "bootstrap": {"dry_run_cycles": bootstrap_dry["cycles"], "commit": bootstrap_commit, "state_cell_live": bootstrap_live["status"] == "live", "state_data_hash": hex0x(&ckb_hash(&initial_data))}, + "transition": {"dry_run_cycles": transition_dry["cycles"], "commit": transition_commit, "old_state_not_live": bootstrap_dead["status"] != "live", + "new_state_live": new_live["status"] == "live", "receipt_live": receipt_live["status"] == "live", "signed_intent_hash": hex0x(&transition_material.signed_intent_hash), "latest_receipt_hash": hex0x(&next.receipt)}, + "negative_cases": {"wrong_signature_dry_run": rejection, "post_negative_state_still_live": still_live["status"] == "live"} + }).as_object().unwrap().clone()); + Ok(()) + })(); + if let Err(error) = scenario { + report["status"] = json!("failed"); + report["error"] = json!(error.to_string()); + report["ckb_log"] = json!(devnet.log_path.display().to_string()); + report["rpc_url"] = json!(devnet.rpc_url); + } + if !keep_node { + devnet.stop(); + } + let output = match output { + Some(path) if path.is_absolute() => path.to_path_buf(), + Some(path) => root.join(path), + None => root.join("target/novaseal-devnet-stateful-live.json"), + }; + fs::create_dir_all(output.parent().context("output path has no parent")?)?; + let text = if pretty { stable_json_pretty(&report)? } else { stable_json_spaced(&report)? }; + fs::write(&output, format!("{text}\n"))?; + println!( + "wrote {} status={} live_devnet_rpc_executed={}", + output.display(), + report["status"].as_str().unwrap_or("failed"), + report["live_devnet_rpc_executed"].as_bool().unwrap_or(false) + ); + Ok(if report["status"] == "passed" { 0 } else { 1 }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deterministic_signer_matches_expected_xonly_key() { + assert_eq!( + hex0x(&xonly_pubkey(&TEST_SECRET_KEY).unwrap()), + "0xc89fe99d72fcfa969434ddd87bb186a48213e9df3ec4b8a77042cf9559fc5765" + ); + } +} diff --git a/crates/cellscript-tools/src/novaseal_planned_btc_tx.rs b/crates/cellscript-tools/src/novaseal_planned_btc_tx.rs new file mode 100644 index 00000000..3e369211 --- /dev/null +++ b/crates/cellscript-tools/src/novaseal_planned_btc_tx.rs @@ -0,0 +1,661 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::{bail, Context, Result}; +use serde_json::{json, Value}; + +use crate::ckb_devnet::{ + always_success_dep, always_success_lock, ckb_hash, deploy_code, entry_witness_input_type_hex, funding_cells, hex0x, provenance, + resolve_ckb_bin, schnorr_sign, transaction, u16_bytes, u32_bytes, u64_bytes, u8_bytes, xonly_pubkey, CkbDevnet, RECEIPT_CAPACITY, + SHANNONS, STATE_CAPACITY, TEST_AUX_RAND, TEST_SECRET_KEY, ZERO_HASH, +}; +use crate::novaseal_planned_live::{compile_contract, contract_report_header, lifecycle_type, Contract}; + +const OP_COMMIT: u64 = 0; +const OP_INITIALIZE: u64 = 255; +const STATUS_ACTIVE: u64 = 1; +const STATUS_COMMITTED: u64 = 2; +type Hash = [u8; 32]; + +#[derive(Clone)] +struct Base { + seal: Hash, + policy: Hash, + committer: Hash, + initial_state: Hash, + committed_state: Hash, + txid: Hash, + wtxid: Hash, + output_index: u64, + amount_sats: u64, + expiry: u64, +} + +#[derive(Clone)] +struct Cell { + seal: Hash, + policy: Hash, + committer: Hash, + btc_commitment: Hash, + state: Hash, + status: u64, + receipt: Hash, + nonce: u64, + expiry: u64, +} + +struct Material { + old_cell_data: Vec, + new_cell: Cell, + new_cell_data: Vec, + receipt_data: Vec, + signed_intent: Vec, + signed_hash: Hash, + signature: Vec, + txid: Hash, + wtxid: Hash, + output_index: u64, + amount_sats: u64, + btc_commitment: Hash, + transition_commitment: Hash, + receipt_hash: Hash, +} + +fn append(out: &mut Vec, chunks: &[&[u8]]) { + for chunk in chunks { + out.extend_from_slice(chunk); + } +} + +fn base(label: &str) -> Result { + Ok(Base { + seal: ckb_hash(format!("NovaSeal BTC transaction seal {label}").as_bytes()), + policy: ckb_hash(format!("NovaSeal BTC transaction policy {label}").as_bytes()), + committer: xonly_pubkey(&TEST_SECRET_KEY)?, + initial_state: ckb_hash(format!("NovaSeal BTC transaction active state {label}").as_bytes()), + committed_state: ckb_hash(format!("NovaSeal BTC transaction committed state {label}").as_bytes()), + txid: ckb_hash(format!("NovaSeal BTC txid {label}").as_bytes()), + wtxid: ckb_hash(format!("NovaSeal BTC wtxid {label}").as_bytes()), + output_index: 2, + amount_sats: 125_000, + expiry: (1_u64 << 63) - 1, + }) +} + +fn zero_cell() -> Cell { + Cell { + seal: ZERO_HASH, + policy: ZERO_HASH, + committer: ZERO_HASH, + btc_commitment: ZERO_HASH, + state: ZERO_HASH, + status: 0, + receipt: ZERO_HASH, + nonce: 0, + expiry: 0, + } +} + +fn pack_state(cell: &Cell) -> Vec { + let mut out = u16_bytes(0); + append( + &mut out, + &[ + &cell.seal, + &cell.policy, + &cell.committer, + &cell.btc_commitment, + &cell.state, + &u8_bytes(cell.status), + &u64_bytes(cell.nonce), + &u64_bytes(cell.expiry), + ], + ); + out +} + +fn pack_cell(cell: &Cell) -> Vec { + let mut out = u16_bytes(0); + append( + &mut out, + &[ + &cell.seal, + &cell.policy, + &cell.committer, + &cell.btc_commitment, + &cell.state, + &u8_bytes(cell.status), + &cell.receipt, + &u64_bytes(cell.nonce), + &u64_bytes(cell.expiry), + ], + ); + out +} + +fn public_commitment(txid: &Hash, wtxid: &Hash, output_index: u64, amount: u64, transition: &Hash) -> Vec { + let mut out = Vec::new(); + append(&mut out, &[txid, wtxid, &u32_bytes(output_index as usize), &u64_bytes(amount), transition]); + out +} + +#[allow(clippy::too_many_arguments)] +fn pack_core( + op: u64, + base: &Base, + txid: &Hash, + wtxid: &Hash, + output_index: u64, + amount: u64, + old_state: &Hash, + new_state: &Hash, + transition: &Hash, + old_status: u64, + new_status: u64, + old_nonce: u64, + new_nonce: u64, +) -> Vec { + let mut out = Vec::new(); + append( + &mut out, + &[ + &u8_bytes(op), + &base.seal, + &base.policy, + &base.committer, + txid, + wtxid, + &u32_bytes(output_index as usize), + &u64_bytes(amount), + old_state, + new_state, + transition, + &u8_bytes(old_status), + &u8_bytes(new_status), + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + &u64_bytes(base.expiry), + &ZERO_HASH, + ], + ); + out +} + +#[allow(clippy::too_many_arguments)] +fn pack_receipt( + base: &Base, + btc_commitment: &Hash, + old_state: &Hash, + new_state: &Hash, + old_nonce: u64, + new_nonce: u64, + core_hash: &Hash, + signed_hash: Option<&Hash>, + receipt_hash: Option<&Hash>, +) -> Vec { + let mut out = Vec::new(); + append( + &mut out, + &[ + &u8_bytes(OP_COMMIT), + &base.seal, + &base.policy, + &base.committer, + btc_commitment, + old_state, + new_state, + &u8_bytes(STATUS_ACTIVE), + &u8_bytes(STATUS_COMMITTED), + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + core_hash, + ], + ); + if let (Some(signed_hash), Some(receipt_hash)) = (signed_hash, receipt_hash) { + append(&mut out, &[signed_hash, &ZERO_HASH, receipt_hash, &base.committer, &u64_bytes(base.expiry)]); + } else { + out.extend_from_slice(&ZERO_HASH); + } + out +} + +#[allow(clippy::too_many_arguments)] +fn canonical(op: u64, base: &Base, old_state: &Hash, new_state: &Hash, old_nonce: u64, new_nonce: u64, body: &Hash) -> Hash { + let mut out = Vec::new(); + append( + &mut out, + &[ + &base.seal, + &base.policy, + &u8_bytes(op), + &u8_bytes(op), + &base.seal, + old_state, + new_state, + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + &u64_bytes(base.expiry), + &base.committer, + body, + &ZERO_HASH, + ], + ); + ckb_hash(&out) +} + +fn material(op: u64, base: &Base, old: Option<&Cell>, mutate: bool, zero_txid: bool, mismatch: bool) -> Result { + let ( + old_status, + new_status, + old_nonce, + new_nonce, + old_state, + new_state, + txid, + wtxid, + output_index, + amount, + transition, + btc_commitment, + mut next, + ) = match op { + OP_INITIALIZE => ( + 0, + STATUS_ACTIVE, + 0, + 0, + ZERO_HASH, + base.initial_state, + ZERO_HASH, + ZERO_HASH, + 0, + 0, + ZERO_HASH, + ZERO_HASH, + Cell { + seal: base.seal, + policy: base.policy, + committer: base.committer, + btc_commitment: ZERO_HASH, + state: base.initial_state, + status: STATUS_ACTIVE, + receipt: ZERO_HASH, + nonce: 0, + expiry: base.expiry, + }, + ), + OP_COMMIT => { + let old = old.context("BTC transaction commit material requires an old cell")?; + let txid = if zero_txid { ZERO_HASH } else { base.txid }; + let transition = + if mismatch { ckb_hash(b"NovaSeal BTC transaction mismatched transition") } else { ckb_hash(&base.committed_state) }; + let commitment = ckb_hash(&public_commitment(&txid, &base.wtxid, base.output_index, base.amount_sats, &transition)); + ( + STATUS_ACTIVE, + STATUS_COMMITTED, + old.nonce, + old.nonce + 1, + old.state, + base.committed_state, + txid, + base.wtxid, + base.output_index, + base.amount_sats, + transition, + commitment, + Cell { + seal: old.seal, + policy: old.policy, + committer: old.committer, + btc_commitment: commitment, + state: base.committed_state, + status: STATUS_COMMITTED, + receipt: ZERO_HASH, + nonce: old.nonce + 1, + expiry: old.expiry, + }, + ) + } + _ => bail!("unknown BTC transaction op {op}"), + }; + let old_commitment = old.map(|value| ckb_hash(&pack_state(value))).unwrap_or(ZERO_HASH); + let new_commitment = ckb_hash(&pack_state(&next)); + let core = pack_core( + op, + base, + &txid, + &wtxid, + output_index, + amount, + &old_state, + &new_state, + &transition, + old_status, + new_status, + old_nonce, + new_nonce, + ); + let core_hash = ckb_hash(&core); + let receipt_hash = if op == OP_COMMIT { + ckb_hash(&pack_receipt(base, &btc_commitment, &old_state, &new_state, old_nonce, new_nonce, &core_hash, None, None)) + } else { + ZERO_HASH + }; + if op == OP_COMMIT { + next.receipt = receipt_hash; + } + let canonical = canonical(op, base, &old_commitment, &new_commitment, old_nonce, new_nonce, &core_hash); + let mut signed_intent = core; + append(&mut signed_intent, &[&canonical, &receipt_hash]); + let signed_hash = ckb_hash(&signed_intent); + let receipt_data = if op == OP_COMMIT { + pack_receipt( + base, + &btc_commitment, + &old_state, + &new_state, + old_nonce, + new_nonce, + &core_hash, + Some(&signed_hash), + Some(&receipt_hash), + ) + } else { + Vec::new() + }; + let (public, signed) = schnorr_sign(&signed_hash, &TEST_SECRET_KEY, &TEST_AUX_RAND)?; + let mut signature = Vec::with_capacity(96); + signature.extend_from_slice(&public); + signature.extend_from_slice(&signed); + if mutate { + *signature.last_mut().unwrap() ^= 1; + } + Ok(Material { + old_cell_data: pack_cell(old.unwrap_or(&zero_cell())), + new_cell_data: pack_cell(&next), + new_cell: next, + receipt_data, + signed_intent, + signed_hash, + signature, + txid, + wtxid, + output_index, + amount_sats: amount, + btc_commitment, + transition_commitment: transition, + receipt_hash, + }) +} + +fn witness(op: u64, material: &Material) -> String { + let mut out = b"CSARGv1\0".to_vec(); + out.extend_from_slice(&u8_bytes(op)); + for value in [material.old_cell_data.as_slice(), material.signed_intent.as_slice(), material.signature.as_slice()] { + out.extend_from_slice(&u32_bytes(value.len())); + out.extend_from_slice(value); + } + entry_witness_input_type_hex(&out) +} + +fn build_initialize(funding: &Value, lifecycle_hash: &str, deps: Vec, header: &str, material: &Material) -> Result { + let total = funding["total_capacity"].as_u64().context("BTC transaction initialize funding total is missing")?; + let change = total.checked_sub(STATE_CAPACITY).context("BTC transaction initialize funding capacity is too small")?; + if change == 0 { + bail!("BTC transaction initialize funding capacity is too small"); + } + let cells = funding_cells(funding); + let mut witnesses = vec![witness(OP_INITIALIZE, material)]; + witnesses.extend(vec!["0x".into(); cells.len().saturating_sub(1)]); + Ok(transaction( + cells, + vec![ + json!({"capacity": format!("0x{STATE_CAPACITY:x}"), "lock": always_success_lock("0x"), "type": lifecycle_type(lifecycle_hash)}), + json!({"capacity": format!("0x{change:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + ], + vec![hex0x(&material.new_cell_data), "0x".into()], + deps, + witnesses, + vec![header.into()], + )) +} + +fn build_commit( + old_ref: &Value, + funding: &Value, + lifecycle_hash: &str, + deps: Vec, + header: &str, + material: &Material, +) -> Result { + let total = funding["total_capacity"].as_u64().context("BTC transaction commit funding total is missing")?; + let change = total.checked_sub(RECEIPT_CAPACITY).context("BTC transaction commit funding capacity is too small")?; + if change == 0 { + bail!("BTC transaction commit funding capacity is too small"); + } + let mut inputs = vec![old_ref.clone()]; + inputs.extend_from_slice(funding_cells(funding)); + let mut witnesses = vec![witness(OP_COMMIT, material)]; + witnesses.extend(vec!["0x".into(); funding_cells(funding).len()]); + Ok(transaction( + &inputs, + vec![ + json!({"capacity": format!("0x{:x}", old_ref["capacity"].as_u64().unwrap()), "lock": always_success_lock("0x"), "type": lifecycle_type(lifecycle_hash)}), + json!({"capacity": format!("0x{RECEIPT_CAPACITY:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + json!({"capacity": format!("0x{change:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + ], + vec![hex0x(&material.new_cell_data), hex0x(&material.receipt_data), "0x".into()], + deps, + witnesses, + vec![header.into()], + )) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn run( + root: &Path, + ckb_repo: Option<&Path>, + ckb_bin: Option<&Path>, + run_dir: Option<&Path>, + contract: Contract, + keep_node: bool, +) -> Result { + let root = fs::canonicalize(root)?; + let ckb_repo = fs::canonicalize(ckb_repo.map(Path::to_path_buf).unwrap_or_else(|| root.parent().unwrap().join("ckb")))?; + let ckb_bin = resolve_ckb_bin(&ckb_repo, ckb_bin)?; + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let run_dir = run_dir + .map(Path::to_path_buf) + .unwrap_or_else(|| root.join(format!("target/novaseal-btc-transaction-commitment-devnet-stateful-live/{timestamp}"))); + fs::create_dir_all(&run_dir)?; + let run_dir = fs::canonicalize(run_dir)?; + let lifecycle_path = run_dir.join("nova-btc-transaction-commitment-lifecycle-type.elf"); + compile_contract(&root, contract, &lifecycle_path)?; + let verifier_path = root.join("proposals/novaseal/v0-mvp-skeleton/target/novaseal-btc-verifier-riscv-shell-release.elf"); + if !verifier_path.is_file() { + bail!("missing verifier ELF: {}", verifier_path.display()); + } + let mut devnet = CkbDevnet::new(ckb_repo.clone(), ckb_bin.clone(), run_dir.clone())?; + let mut report = + contract_report_header(contract, "btc_transaction_commitment_initialize_then_commit", &root, &ckb_repo, &ckb_bin, &run_dir); + report["btc_public_verification_scope"] = json!( + "live CKB transition executes the BIP340 runtime verifier and binds a declared BTC txid/wtxid/output tuple; SPV/indexer finality remains separate production evidence" + ); + let mut stage = "initializing"; + let scenario = (|| -> Result<()> { + stage = "start devnet"; + devnet.start()?; + stage = "deploy artifacts"; + let genesis = devnet.get_block_by_number(0)?; + let always = always_success_dep(genesis["transactions"][0]["hash"].as_str().context("genesis hash is missing")?); + let verifier = deploy_code(&mut devnet, "cellscript_btc_bip340_verifier_riscv", &fs::read(&verifier_path)?, &always)?; + let lifecycle = + deploy_code(&mut devnet, "nova_btc_transaction_commitment_lifecycle_type", &fs::read(&lifecycle_path)?, &always)?; + let lifecycle_hash = lifecycle["data_hash"].as_str().context("lifecycle hash is missing")?.to_owned(); + let deps = vec![verifier["cell_dep"].clone(), lifecycle["cell_dep"].clone(), always]; + let source_paths = [ + "proposals/novaseal/btc-transaction-commitment-profile-v0/Cell.toml", + "proposals/novaseal/btc-transaction-commitment-profile-v0/src", + "proposals/novaseal/btc-transaction-commitment-profile-v0/schemas", + "proposals/novaseal/v0-mvp-skeleton/verifier/novaseal_btc_verifier", + "crates/cellscript-tools/src/novaseal_planned_btc_tx.rs", + "crates/cellscript-tools/src/ckb_devnet.rs", + ] + .into_iter() + .map(PathBuf::from) + .collect::>(); + let artifacts = BTreeMap::from([("verifier".into(), verifier_path.clone()), ("lifecycle".into(), lifecycle_path.clone())]); + let source_provenance = provenance(&root, &source_paths, &artifacts)?; + let base = base("live")?; + let type_script = lifecycle_type(&lifecycle_hash); + + stage = "valid initialize"; + let initialize = material(OP_INITIALIZE, &base, None, false, false, false)?; + let header = devnet.rpc("get_tip_header", vec![])?; + let funding = devnet.collect_spendable(STATE_CAPACITY + 100 * SHANNONS)?; + let tx = build_initialize(&funding, &lifecycle_hash, deps.clone(), header["hash"].as_str().unwrap(), &initialize)?; + let initialize_dry = devnet.rpc("dry_run_transaction", vec![tx.clone()])?; + let initialize_commit = devnet.submit_and_commit(&tx, "BTC transaction commitment initialize")?; + let initialize_hash = initialize_commit["tx_hash"].as_str().unwrap(); + let initial_live = devnet.assert_live_cell( + initialize_hash, + 0, + "BTC transaction active state", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&initialize.new_cell_data), + )?; + let initial_ref = json!({"tx_hash": initialize_hash, "index": 0, "capacity": STATE_CAPACITY}); + + stage = "negative wrong committer signature"; + let negative_header = devnet.rpc("get_tip_header", vec![])?; + let wrong = material(OP_COMMIT, &base, Some(&initialize.new_cell), true, false, false)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = + build_commit(&initial_ref, &funding, &lifecycle_hash, deps.clone(), negative_header["hash"].as_str().unwrap(), &wrong)?; + let wrong_reject = devnet.dry_run_rejects( + &tx, + "BTC transaction wrong committer signature", + Some("Inputs[0].Type"), + Some(&lifecycle_hash), + Some(56), + )?; + + stage = "negative zero BTC txid"; + let zero = material(OP_COMMIT, &base, Some(&initialize.new_cell), false, true, false)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = + build_commit(&initial_ref, &funding, &lifecycle_hash, deps.clone(), negative_header["hash"].as_str().unwrap(), &zero)?; + let zero_reject = + devnet.dry_run_rejects(&tx, "BTC transaction zero txid", Some("Inputs[0].Type"), Some(&lifecycle_hash), Some(5))?; + + stage = "negative transition hash mismatch"; + let mismatch = material(OP_COMMIT, &base, Some(&initialize.new_cell), false, false, true)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = + build_commit(&initial_ref, &funding, &lifecycle_hash, deps.clone(), negative_header["hash"].as_str().unwrap(), &mismatch)?; + let mismatch_reject = devnet.dry_run_rejects( + &tx, + "BTC transaction transition hash mismatch", + Some("Inputs[0].Type"), + Some(&lifecycle_hash), + Some(5), + )?; + let post_negative = devnet.assert_live_cell( + initialize_hash, + 0, + "post-negative BTC transaction active state", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&initialize.new_cell_data), + )?; + + stage = "valid commit transaction"; + let header = devnet.rpc("get_tip_header", vec![])?; + let commit_material = material(OP_COMMIT, &base, Some(&initialize.new_cell), false, false, false)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = build_commit(&initial_ref, &funding, &lifecycle_hash, deps, header["hash"].as_str().unwrap(), &commit_material)?; + let commit_dry = devnet.rpc("dry_run_transaction", vec![tx.clone()])?; + let commit = devnet.submit_and_commit(&tx, "BTC transaction commitment transition")?; + let old_dead = devnet.wait_dead_cell(initialize_hash, 0)?; + let commit_hash = commit["tx_hash"].as_str().unwrap(); + let committed_live = devnet.assert_live_cell( + commit_hash, + 0, + "BTC transaction committed state", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&commit_material.new_cell_data), + )?; + let receipt_live = devnet.assert_live_cell( + commit_hash, + 1, + "BTC transaction commitment receipt", + Some(RECEIPT_CAPACITY), + Some(&always_success_lock("0x")), + Some(&Value::Null), + Some(&commit_material.receipt_data), + )?; + report.as_object_mut().unwrap().extend( + json!({ + "status": "passed", "live_devnet_rpc_executed": true, "stateful_lifecycle_executed": true, + "ckb_log": devnet.log_path.display().to_string(), "rpc_url": devnet.rpc_url, + "artifacts": {"verifier": verifier, "lifecycle": lifecycle}, "provenance": source_provenance, + "initialize": {"dry_run_cycles": initialize_dry["cycles"], "commit": initialize_commit, + "state_live": initial_live["status"] == "live", "state_data_hash": hex0x(&ckb_hash(&initialize.new_cell_data))}, + "commit_transaction": {"dry_run_cycles": commit_dry["cycles"], "commit": commit, + "old_state_not_live": old_dead["status"] != "live", "new_state_live": committed_live["status"] == "live", + "receipt_live": receipt_live["status"] == "live", + "btc_tx_tuple_bound": commit_material.new_cell.btc_commitment == commit_material.btc_commitment && commit_material.btc_commitment != ZERO_HASH, + "transition_commitment_bound": commit_material.transition_commitment == ckb_hash(&base.committed_state), + "public_btc_verification_executed": true, + "public_btc_verification_scope": "BIP340 runtime verifier execution over the signed BTC commitment intent", + "btc_tx_commitment_hash": hex0x(&commit_material.btc_commitment), + "public_btc_anchor": {"kind": "btc_transaction_commitment", "anchor_source": "local_deterministic_fixture", + "btc_txid": hex0x(&commit_material.txid), "btc_wtxid": hex0x(&commit_material.wtxid), + "btc_output_index": commit_material.output_index, "btc_amount_sats": commit_material.amount_sats, + "ckb_btc_commitment_hash": hex0x(&commit_material.btc_commitment)}, + "signed_intent_hash": hex0x(&commit_material.signed_hash), "receipt_hash": hex0x(&commit_material.receipt_hash)}, + "negative_cases": {"wrong_committer_signature_dry_run": wrong_reject, "zero_btc_txid_dry_run": zero_reject, + "transition_hash_mismatch_dry_run": mismatch_reject, "post_negative_state_still_live": post_negative["status"] == "live"}, + }) + .as_object() + .unwrap() + .clone(), + ); + Ok(()) + })(); + if let Err(error) = scenario { + report["status"] = json!("failed"); + report["stage"] = json!(stage); + report["error"] = json!(error.to_string()); + report["ckb_log"] = json!(devnet.log_path.display().to_string()); + report["rpc_url"] = json!(devnet.rpc_url); + } + if !keep_node { + devnet.stop(); + } + Ok(report) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn initialization_material_is_deterministic() { + let base = base("parity").unwrap(); + let initial = material(OP_INITIALIZE, &base, None, false, false, false).unwrap(); + let committed = material(OP_COMMIT, &base, Some(&initial.new_cell), false, false, false).unwrap(); + assert_eq!(hex0x(&ckb_hash(&initial.new_cell_data)), "0x52b95b87ee55d01594d590d042c5f10dcae64e31182a0c8bb6e2388693a4dbc7"); + assert_eq!(hex0x(&ckb_hash(&committed.new_cell_data)), "0xa67add7f0f8033d4b772ed4eeb973a9a27e7f0ab277659ff2e1ea6928f7adc20"); + assert_eq!(hex0x(&committed.signed_hash), "0xff54214ef0cf24022aaa693b833741c7c57270f4b77951fe3587811175b70a2b"); + assert_eq!(hex0x(&committed.receipt_hash), "0xb92df287af5040fe4684125cc6db43e7d2fa65604315dd1c0b51ce338fde0d2b"); + assert_eq!(hex0x(&ckb_hash(&committed.receipt_data)), "0x5e0868fd60f32d5e613e69a4ebd418bbb075c5e6019240b348c6483771abe7ec"); + } +} diff --git a/crates/cellscript-tools/src/novaseal_planned_btc_utxo.rs b/crates/cellscript-tools/src/novaseal_planned_btc_utxo.rs new file mode 100644 index 00000000..e3dfcb67 --- /dev/null +++ b/crates/cellscript-tools/src/novaseal_planned_btc_utxo.rs @@ -0,0 +1,661 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::{bail, Context, Result}; +use serde_json::{json, Value}; + +use crate::ckb_devnet::{ + always_success_dep, always_success_lock, ckb_hash, deploy_code, entry_witness_input_type_hex, funding_cells, hex0x, provenance, + resolve_ckb_bin, schnorr_sign, transaction, u16_bytes, u32_bytes, u64_bytes, u8_bytes, xonly_pubkey, CkbDevnet, RECEIPT_CAPACITY, + SHANNONS, STATE_CAPACITY, TEST_AUX_RAND, TEST_SECRET_KEY, ZERO_HASH, +}; +use crate::novaseal_planned_live::{compile_contract, contract_report_header, lifecycle_type, Contract}; + +const OP_CLOSE: u64 = 0; +const OP_INITIALIZE: u64 = 255; +const STATUS_ACTIVE: u64 = 1; +const STATUS_CLOSED: u64 = 2; +type Hash = [u8; 32]; + +#[derive(Clone)] +struct Base { + seal: Hash, + policy: Hash, + owner: Hash, + initial_state: Hash, + closed_state: Hash, + txid: Hash, + vout: u64, + amount_sats: u64, + script_pubkey: Hash, + spend_txid: Hash, + spend_wtxid: Hash, + spend_input: u64, + expiry: u64, +} + +#[derive(Clone)] +struct Cell { + seal: Hash, + policy: Hash, + owner: Hash, + sealed_utxo: Hash, + state: Hash, + status: u64, + receipt: Hash, + nonce: u64, + expiry: u64, +} + +struct Material { + old_cell_data: Vec, + new_cell: Cell, + new_cell_data: Vec, + receipt_data: Vec, + signed_intent: Vec, + signed_hash: Hash, + signature: Vec, + txid: Hash, + vout: u64, + amount_sats: u64, + script_pubkey: Hash, + spend_txid: Hash, + spend_wtxid: Hash, + spend_input: u64, + sealed_utxo: Hash, + closure: Hash, + receipt_hash: Hash, +} + +fn append(out: &mut Vec, chunks: &[&[u8]]) { + for chunk in chunks { + out.extend_from_slice(chunk); + } +} + +fn base(label: &str) -> Result { + Ok(Base { + seal: ckb_hash(format!("NovaSeal BTC UTXO seal {label}").as_bytes()), + policy: ckb_hash(format!("NovaSeal BTC UTXO policy {label}").as_bytes()), + owner: xonly_pubkey(&TEST_SECRET_KEY)?, + initial_state: ckb_hash(format!("NovaSeal BTC UTXO active state {label}").as_bytes()), + closed_state: ckb_hash(format!("NovaSeal BTC UTXO closed state {label}").as_bytes()), + txid: ckb_hash(format!("NovaSeal BTC UTXO txid {label}").as_bytes()), + vout: 1, + amount_sats: 250_000, + script_pubkey: ckb_hash(format!("NovaSeal BTC UTXO script pubkey {label}").as_bytes()), + spend_txid: ckb_hash(format!("NovaSeal BTC UTXO spend txid {label}").as_bytes()), + spend_wtxid: ckb_hash(format!("NovaSeal BTC UTXO spend wtxid {label}").as_bytes()), + spend_input: 0, + expiry: (1_u64 << 63) - 1, + }) +} + +fn zero_cell() -> Cell { + Cell { + seal: ZERO_HASH, + policy: ZERO_HASH, + owner: ZERO_HASH, + sealed_utxo: ZERO_HASH, + state: ZERO_HASH, + status: 0, + receipt: ZERO_HASH, + nonce: 0, + expiry: 0, + } +} + +fn pack_state(cell: &Cell) -> Vec { + let mut out = u16_bytes(0); + append( + &mut out, + &[ + &cell.seal, + &cell.policy, + &cell.owner, + &cell.sealed_utxo, + &cell.state, + &u8_bytes(cell.status), + &u64_bytes(cell.nonce), + &u64_bytes(cell.expiry), + ], + ); + out +} + +fn pack_cell(cell: &Cell) -> Vec { + let mut out = u16_bytes(0); + append( + &mut out, + &[ + &cell.seal, + &cell.policy, + &cell.owner, + &cell.sealed_utxo, + &cell.state, + &u8_bytes(cell.status), + &cell.receipt, + &u64_bytes(cell.nonce), + &u64_bytes(cell.expiry), + ], + ); + out +} + +fn utxo_commitment(txid: &Hash, vout: u64, amount: u64, script_pubkey: &Hash) -> Vec { + let mut out = Vec::new(); + append(&mut out, &[txid, &u32_bytes(vout as usize), &u64_bytes(amount), script_pubkey]); + out +} + +fn closure_commitment(sealed: &Hash, spend_txid: &Hash, spend_wtxid: &Hash, spend_input: u64, transition: &Hash) -> Vec { + let mut out = Vec::new(); + append(&mut out, &[sealed, spend_txid, spend_wtxid, &u32_bytes(spend_input as usize), transition, &ZERO_HASH]); + out +} + +#[allow(clippy::too_many_arguments)] +fn pack_core( + op: u64, + base: &Base, + txid: &Hash, + spend_txid: &Hash, + spend_wtxid: &Hash, + old_state: &Hash, + new_state: &Hash, + transition: &Hash, + old_status: u64, + new_status: u64, + old_nonce: u64, + new_nonce: u64, +) -> Vec { + let mut out = Vec::new(); + append( + &mut out, + &[ + &u8_bytes(op), + &base.seal, + &base.policy, + &base.owner, + txid, + &u32_bytes(base.vout as usize), + &u64_bytes(base.amount_sats), + &base.script_pubkey, + spend_txid, + spend_wtxid, + &u32_bytes(base.spend_input as usize), + old_state, + new_state, + transition, + &u8_bytes(old_status), + &u8_bytes(new_status), + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + &u64_bytes(base.expiry), + &ZERO_HASH, + ], + ); + out +} + +#[allow(clippy::too_many_arguments)] +fn pack_receipt( + base: &Base, + sealed: &Hash, + closure: &Hash, + old_state: &Hash, + new_state: &Hash, + old_nonce: u64, + new_nonce: u64, + core_hash: &Hash, + signed_hash: Option<&Hash>, + receipt_hash: Option<&Hash>, +) -> Vec { + let mut out = Vec::new(); + append( + &mut out, + &[ + &u8_bytes(OP_CLOSE), + &base.seal, + &base.policy, + &base.owner, + sealed, + closure, + old_state, + new_state, + &u8_bytes(STATUS_ACTIVE), + &u8_bytes(STATUS_CLOSED), + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + core_hash, + ], + ); + if let (Some(signed_hash), Some(receipt_hash)) = (signed_hash, receipt_hash) { + append(&mut out, &[signed_hash, &ZERO_HASH, receipt_hash, &base.owner, &u64_bytes(base.expiry)]); + } else { + out.extend_from_slice(&ZERO_HASH); + } + out +} + +#[allow(clippy::too_many_arguments)] +fn canonical(op: u64, base: &Base, old_state: &Hash, new_state: &Hash, old_nonce: u64, new_nonce: u64, body: &Hash) -> Hash { + let mut out = Vec::new(); + append( + &mut out, + &[ + &base.seal, + &base.policy, + &u8_bytes(op), + &u8_bytes(op), + &base.seal, + old_state, + new_state, + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + &u64_bytes(base.expiry), + &base.owner, + body, + &ZERO_HASH, + ], + ); + ckb_hash(&out) +} + +fn material(op: u64, base: &Base, old: Option<&Cell>, mutate: bool, mismatch: bool, zero_spend: bool) -> Result { + let txid = if mismatch { ckb_hash(b"NovaSeal mismatched UTXO txid") } else { base.txid }; + let sealed = ckb_hash(&utxo_commitment(&txid, base.vout, base.amount_sats, &base.script_pubkey)); + let ( + old_status, + new_status, + old_nonce, + new_nonce, + old_state, + new_state, + spend_txid, + spend_wtxid, + transition, + closure, + mut next, + new_commitment, + ) = match op { + OP_INITIALIZE => { + let next = Cell { + seal: base.seal, + policy: base.policy, + owner: base.owner, + sealed_utxo: sealed, + state: base.initial_state, + status: STATUS_ACTIVE, + receipt: ZERO_HASH, + nonce: 0, + expiry: base.expiry, + }; + let new_commitment = ckb_hash(&pack_state(&next)); + (0, STATUS_ACTIVE, 0, 0, ZERO_HASH, base.initial_state, ZERO_HASH, ZERO_HASH, ZERO_HASH, ZERO_HASH, next, new_commitment) + } + OP_CLOSE => { + let old = old.context("BTC UTXO close material requires an old cell")?; + let spend_txid = if zero_spend { ZERO_HASH } else { base.spend_txid }; + let transition = ckb_hash(&base.closed_state); + let closure = ckb_hash(&closure_commitment(&sealed, &spend_txid, &base.spend_wtxid, base.spend_input, &transition)); + ( + STATUS_ACTIVE, + STATUS_CLOSED, + old.nonce, + old.nonce + 1, + old.state, + base.closed_state, + spend_txid, + base.spend_wtxid, + transition, + closure, + Cell { + seal: old.seal, + policy: old.policy, + owner: old.owner, + sealed_utxo: sealed, + state: base.closed_state, + status: STATUS_CLOSED, + receipt: ZERO_HASH, + nonce: old.nonce + 1, + expiry: old.expiry, + }, + closure, + ) + } + _ => bail!("unknown BTC UTXO op {op}"), + }; + let old_commitment = old.map(|value| ckb_hash(&pack_state(value))).unwrap_or(ZERO_HASH); + let core = pack_core( + op, + base, + &txid, + &spend_txid, + &spend_wtxid, + &old_state, + &new_state, + &transition, + old_status, + new_status, + old_nonce, + new_nonce, + ); + let core_hash = ckb_hash(&core); + let receipt_hash = if op == OP_CLOSE { + ckb_hash(&pack_receipt(base, &sealed, &closure, &old_state, &new_state, old_nonce, new_nonce, &core_hash, None, None)) + } else { + ZERO_HASH + }; + if op == OP_CLOSE { + next.receipt = receipt_hash; + } + let canonical = canonical(op, base, &old_commitment, &new_commitment, old_nonce, new_nonce, &core_hash); + let mut signed_intent = core; + append(&mut signed_intent, &[&canonical, &receipt_hash]); + let mut signing_digest = Vec::new(); + append(&mut signing_digest, &[&core_hash, &canonical, &receipt_hash]); + let signed_hash = ckb_hash(&signing_digest); + let receipt_data = if op == OP_CLOSE { + pack_receipt( + base, + &sealed, + &closure, + &old_state, + &new_state, + old_nonce, + new_nonce, + &core_hash, + Some(&signed_hash), + Some(&receipt_hash), + ) + } else { + Vec::new() + }; + let (public, signed) = schnorr_sign(&signed_hash, &TEST_SECRET_KEY, &TEST_AUX_RAND)?; + let mut signature = Vec::with_capacity(96); + signature.extend_from_slice(&public); + signature.extend_from_slice(&signed); + if mutate { + *signature.last_mut().unwrap() ^= 1; + } + Ok(Material { + old_cell_data: pack_cell(old.unwrap_or(&zero_cell())), + new_cell_data: pack_cell(&next), + new_cell: next, + receipt_data, + signed_intent, + signed_hash, + signature, + txid, + vout: base.vout, + amount_sats: base.amount_sats, + script_pubkey: base.script_pubkey, + spend_txid, + spend_wtxid, + spend_input: base.spend_input, + sealed_utxo: sealed, + closure, + receipt_hash, + }) +} + +fn witness(op: u64, material: &Material) -> String { + let mut out = b"CSARGv1\0".to_vec(); + out.extend_from_slice(&u8_bytes(op)); + for value in [material.old_cell_data.as_slice(), material.signed_intent.as_slice(), material.signature.as_slice()] { + out.extend_from_slice(&u32_bytes(value.len())); + out.extend_from_slice(value); + } + entry_witness_input_type_hex(&out) +} + +fn build_initialize(funding: &Value, lifecycle_hash: &str, deps: Vec, header: &str, material: &Material) -> Result { + let total = funding["total_capacity"].as_u64().context("BTC UTXO initialize funding total is missing")?; + let change = total.checked_sub(STATE_CAPACITY).context("BTC UTXO initialize funding capacity is too small")?; + if change == 0 { + bail!("BTC UTXO initialize funding capacity is too small"); + } + let cells = funding_cells(funding); + let mut witnesses = vec![witness(OP_INITIALIZE, material)]; + witnesses.extend(vec!["0x".into(); cells.len().saturating_sub(1)]); + Ok(transaction( + cells, + vec![ + json!({"capacity": format!("0x{STATE_CAPACITY:x}"), "lock": always_success_lock("0x"), "type": lifecycle_type(lifecycle_hash)}), + json!({"capacity": format!("0x{change:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + ], + vec![hex0x(&material.new_cell_data), "0x".into()], + deps, + witnesses, + vec![header.into()], + )) +} + +fn build_close( + old_ref: &Value, + funding: &Value, + lifecycle_hash: &str, + deps: Vec, + header: &str, + material: &Material, +) -> Result { + let total = funding["total_capacity"].as_u64().context("BTC UTXO close funding total is missing")?; + let change = total.checked_sub(RECEIPT_CAPACITY).context("BTC UTXO close funding capacity is too small")?; + if change == 0 { + bail!("BTC UTXO close funding capacity is too small"); + } + let mut inputs = vec![old_ref.clone()]; + inputs.extend_from_slice(funding_cells(funding)); + let mut witnesses = vec![witness(OP_CLOSE, material)]; + witnesses.extend(vec!["0x".into(); funding_cells(funding).len()]); + Ok(transaction( + &inputs, + vec![ + json!({"capacity": format!("0x{:x}", old_ref["capacity"].as_u64().unwrap()), "lock": always_success_lock("0x"), "type": lifecycle_type(lifecycle_hash)}), + json!({"capacity": format!("0x{RECEIPT_CAPACITY:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + json!({"capacity": format!("0x{change:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + ], + vec![hex0x(&material.new_cell_data), hex0x(&material.receipt_data), "0x".into()], + deps, + witnesses, + vec![header.into()], + )) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn run( + root: &Path, + ckb_repo: Option<&Path>, + ckb_bin: Option<&Path>, + run_dir: Option<&Path>, + contract: Contract, + keep_node: bool, +) -> Result { + let root = fs::canonicalize(root)?; + let ckb_repo = fs::canonicalize(ckb_repo.map(Path::to_path_buf).unwrap_or_else(|| root.parent().unwrap().join("ckb")))?; + let ckb_bin = resolve_ckb_bin(&ckb_repo, ckb_bin)?; + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let run_dir = run_dir + .map(Path::to_path_buf) + .unwrap_or_else(|| root.join(format!("target/novaseal-btc-utxo-seal-devnet-stateful-live/{timestamp}"))); + fs::create_dir_all(&run_dir)?; + let run_dir = fs::canonicalize(run_dir)?; + let lifecycle_path = run_dir.join("nova-btc-utxo-seal-lifecycle-type.elf"); + compile_contract(&root, contract, &lifecycle_path)?; + let verifier_path = root.join("proposals/novaseal/v0-mvp-skeleton/target/novaseal-btc-verifier-riscv-shell-release.elf"); + if !verifier_path.is_file() { + bail!("missing verifier ELF: {}", verifier_path.display()); + } + let mut devnet = CkbDevnet::new(ckb_repo.clone(), ckb_bin.clone(), run_dir.clone())?; + let mut report = contract_report_header(contract, "btc_utxo_seal_initialize_then_close", &root, &ckb_repo, &ckb_bin, &run_dir); + report["btc_public_verification_scope"] = json!( + "live CKB closure executes the BIP340 runtime verifier and binds a declared BTC UTXO/spend tuple; SPV/indexer spend-finality evidence remains separate production evidence" + ); + let mut stage = "initializing"; + let scenario = (|| -> Result<()> { + stage = "start devnet"; + devnet.start()?; + stage = "deploy artifacts"; + let genesis = devnet.get_block_by_number(0)?; + let always = always_success_dep(genesis["transactions"][0]["hash"].as_str().context("genesis hash is missing")?); + let verifier = deploy_code(&mut devnet, "cellscript_btc_bip340_verifier_riscv", &fs::read(&verifier_path)?, &always)?; + let lifecycle = deploy_code(&mut devnet, "nova_btc_utxo_seal_lifecycle_type", &fs::read(&lifecycle_path)?, &always)?; + let lifecycle_hash = lifecycle["data_hash"].as_str().context("lifecycle hash is missing")?.to_owned(); + let deps = vec![verifier["cell_dep"].clone(), lifecycle["cell_dep"].clone(), always]; + let source_paths = [ + "proposals/novaseal/btc-utxo-seal-profile-v0/Cell.toml", + "proposals/novaseal/btc-utxo-seal-profile-v0/src", + "proposals/novaseal/btc-utxo-seal-profile-v0/schemas", + "proposals/novaseal/v0-mvp-skeleton/verifier/novaseal_btc_verifier", + "crates/cellscript-tools/src/novaseal_planned_btc_utxo.rs", + "crates/cellscript-tools/src/ckb_devnet.rs", + ] + .into_iter() + .map(PathBuf::from) + .collect::>(); + let artifacts = BTreeMap::from([("verifier".into(), verifier_path.clone()), ("lifecycle".into(), lifecycle_path.clone())]); + let source_provenance = provenance(&root, &source_paths, &artifacts)?; + let base = base("live")?; + let type_script = lifecycle_type(&lifecycle_hash); + + stage = "valid initialize"; + let initialize = material(OP_INITIALIZE, &base, None, false, false, false)?; + let header = devnet.rpc("get_tip_header", vec![])?; + let funding = devnet.collect_spendable(STATE_CAPACITY + 100 * SHANNONS)?; + let tx = build_initialize(&funding, &lifecycle_hash, deps.clone(), header["hash"].as_str().unwrap(), &initialize)?; + let initialize_dry = devnet.rpc("dry_run_transaction", vec![tx.clone()])?; + let initialize_commit = devnet.submit_and_commit(&tx, "BTC UTXO seal initialize")?; + let initialize_hash = initialize_commit["tx_hash"].as_str().unwrap(); + let initial_live = devnet.assert_live_cell( + initialize_hash, + 0, + "BTC UTXO active seal", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&initialize.new_cell_data), + )?; + let initial_ref = json!({"tx_hash": initialize_hash, "index": 0, "capacity": STATE_CAPACITY}); + + stage = "negative wrong owner signature"; + let negative_header = devnet.rpc("get_tip_header", vec![])?; + let wrong = material(OP_CLOSE, &base, Some(&initialize.new_cell), true, false, false)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = + build_close(&initial_ref, &funding, &lifecycle_hash, deps.clone(), negative_header["hash"].as_str().unwrap(), &wrong)?; + let wrong_reject = + devnet.dry_run_rejects(&tx, "BTC UTXO wrong owner signature", Some("Inputs[0].Type"), Some(&lifecycle_hash), Some(56))?; + + stage = "negative UTXO commitment mismatch"; + let mismatch = material(OP_CLOSE, &base, Some(&initialize.new_cell), false, true, false)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = + build_close(&initial_ref, &funding, &lifecycle_hash, deps.clone(), negative_header["hash"].as_str().unwrap(), &mismatch)?; + let mismatch_reject = + devnet.dry_run_rejects(&tx, "BTC UTXO commitment mismatch", Some("Inputs[0].Type"), Some(&lifecycle_hash), Some(5))?; + + stage = "negative zero spend txid"; + let zero = material(OP_CLOSE, &base, Some(&initialize.new_cell), false, false, true)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = build_close(&initial_ref, &funding, &lifecycle_hash, deps.clone(), negative_header["hash"].as_str().unwrap(), &zero)?; + let zero_reject = + devnet.dry_run_rejects(&tx, "BTC UTXO zero spend txid", Some("Inputs[0].Type"), Some(&lifecycle_hash), Some(5))?; + let post_negative = devnet.assert_live_cell( + initialize_hash, + 0, + "post-negative BTC UTXO active seal", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&initialize.new_cell_data), + )?; + + stage = "valid close UTXO seal"; + let header = devnet.rpc("get_tip_header", vec![])?; + let close_material = material(OP_CLOSE, &base, Some(&initialize.new_cell), false, false, false)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = build_close(&initial_ref, &funding, &lifecycle_hash, deps, header["hash"].as_str().unwrap(), &close_material)?; + let close_dry = devnet.rpc("dry_run_transaction", vec![tx.clone()])?; + let close = devnet.submit_and_commit(&tx, "BTC UTXO seal closure")?; + let old_dead = devnet.wait_dead_cell(initialize_hash, 0)?; + let close_hash = close["tx_hash"].as_str().unwrap(); + let closed_live = devnet.assert_live_cell( + close_hash, + 0, + "BTC UTXO closed seal", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&close_material.new_cell_data), + )?; + let receipt_live = devnet.assert_live_cell( + close_hash, + 1, + "BTC UTXO closure receipt", + Some(RECEIPT_CAPACITY), + Some(&always_success_lock("0x")), + Some(&Value::Null), + Some(&close_material.receipt_data), + )?; + report.as_object_mut().unwrap().extend( + json!({ + "status": "passed", "live_devnet_rpc_executed": true, "stateful_lifecycle_executed": true, + "ckb_log": devnet.log_path.display().to_string(), "rpc_url": devnet.rpc_url, + "artifacts": {"verifier": verifier, "lifecycle": lifecycle}, "provenance": source_provenance, + "initialize": {"dry_run_cycles": initialize_dry["cycles"], "commit": initialize_commit, + "state_live": initial_live["status"] == "live", "state_data_hash": hex0x(&ckb_hash(&initialize.new_cell_data))}, + "close_utxo_seal": {"dry_run_cycles": close_dry["cycles"], "commit": close, + "old_state_not_live": old_dead["status"] != "live", "new_state_live": closed_live["status"] == "live", + "receipt_live": receipt_live["status"] == "live", "sealed_utxo_tuple_bound": initialize.new_cell.sealed_utxo == close_material.sealed_utxo, + "spend_tuple_bound": close_material.closure != ZERO_HASH, "public_btc_spend_verification_executed": true, + "public_btc_verification_scope": "BIP340 runtime verifier execution over the signed BTC UTXO closure intent", + "sealed_utxo_commitment_hash": hex0x(&close_material.sealed_utxo), "closure_commitment_hash": hex0x(&close_material.closure), + "public_btc_anchor": {"kind": "btc_utxo_spend", "anchor_source": "local_deterministic_fixture", + "sealed_btc_txid": hex0x(&close_material.txid), "sealed_btc_vout_index": close_material.vout, + "sealed_btc_amount_sats": close_material.amount_sats, "script_pubkey_hash": hex0x(&close_material.script_pubkey), + "btc_txid": hex0x(&close_material.spend_txid), "btc_wtxid": hex0x(&close_material.spend_wtxid), + "spend_input_index": close_material.spend_input, "ckb_btc_commitment_hash": hex0x(&close_material.closure), + "sealed_utxo_commitment_hash": hex0x(&close_material.sealed_utxo)}, + "signed_intent_hash": hex0x(&close_material.signed_hash), "receipt_hash": hex0x(&close_material.receipt_hash)}, + "negative_cases": {"wrong_owner_signature_dry_run": wrong_reject, + "utxo_commitment_mismatch_dry_run": mismatch_reject, "zero_spend_txid_dry_run": zero_reject, + "post_negative_state_still_live": post_negative["status"] == "live"}, + }) + .as_object() + .unwrap() + .clone(), + ); + Ok(()) + })(); + if let Err(error) = scenario { + report["status"] = json!("failed"); + report["stage"] = json!(stage); + report["error"] = json!(error.to_string()); + report["ckb_log"] = json!(devnet.log_path.display().to_string()); + report["rpc_url"] = json!(devnet.rpc_url); + } + if !keep_node { + devnet.stop(); + } + Ok(report) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn close_material_is_deterministic() { + let base = base("parity").unwrap(); + let initial = material(OP_INITIALIZE, &base, None, false, false, false).unwrap(); + let closed = material(OP_CLOSE, &base, Some(&initial.new_cell), false, false, false).unwrap(); + assert_eq!(hex0x(&ckb_hash(&initial.new_cell_data)), "0xdd07b127b77136877a21d67d7f2fdae74b72dcef2f98d31ba33a0c7257881a31"); + assert_eq!(hex0x(&ckb_hash(&closed.new_cell_data)), "0xfcbd780069f1541b9c5619d41a4a6a159f31726c4a5599ace5451ecfe1d9862d"); + assert_eq!(hex0x(&closed.signed_hash), "0xcc66217dabfe2b031c9899dbe314ee7d5a39a7c1e59611120e6296a56f38aa46"); + assert_eq!(hex0x(&closed.receipt_hash), "0xa4e3127e6e0a3ae4c92207acbd4b91bb8969b4efed3af44449955b55a40eee11"); + assert_eq!(hex0x(&ckb_hash(&closed.receipt_data)), "0xddcafd05403466f543ef27a9b22f45af0c6df7f6ed1211e2bb449436700cd2d2"); + } +} diff --git a/crates/cellscript-tools/src/novaseal_planned_dual.rs b/crates/cellscript-tools/src/novaseal_planned_dual.rs new file mode 100644 index 00000000..7b38b5fa --- /dev/null +++ b/crates/cellscript-tools/src/novaseal_planned_dual.rs @@ -0,0 +1,618 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::{bail, Context, Result}; +use serde_json::{json, Value}; + +use crate::ckb_devnet::{ + always_success_dep, always_success_lock, ckb_hash, deploy_code, entry_witness_input_type_hex, funding_cells, hex0x, provenance, + resolve_ckb_bin, schnorr_sign, transaction, u16_bytes, u32_bytes, u64_bytes, u8_bytes, xonly_pubkey, CkbDevnet, RECEIPT_CAPACITY, + SHANNONS, STATE_CAPACITY, TEST_AUX_RAND, TEST_SECRET_KEY, ZERO_HASH, +}; +use crate::novaseal_planned_live::{compile_contract, contract_report_header, lifecycle_type, Contract}; + +const OP_FINALIZE: u64 = 0; +const OP_INITIALIZE: u64 = 255; +const STATUS_ACTIVE: u64 = 1; +const STATUS_FINALIZED: u64 = 2; +const CKB_SECRET: [u8; 32] = [0x22; 32]; +const CKB_AUX: [u8; 32] = [0x42; 32]; +type Hash = [u8; 32]; + +#[derive(Clone)] +struct Base { + seal: Hash, + policy: Hash, + btc_owner: Hash, + ckb_authority: Hash, + sealed_txid: Hash, + sealed_vout: u64, + sealed_amount: u64, + script_pubkey: Hash, + sealed_utxo: Hash, + initial_state: Hash, + final_state: Hash, + btc_closure: Hash, + btc_txid: Hash, + btc_wtxid: Hash, + spend_input: u64, + maturity: u64, + expiry: u64, +} + +#[derive(Clone)] +struct Cell { + seal: Hash, + policy: Hash, + btc_owner: Hash, + ckb_authority: Hash, + sealed_utxo: Hash, + state: Hash, + status: u64, + receipt: Hash, + nonce: u64, + maturity: u64, + expiry: u64, +} + +struct Material { + old_cell: Cell, + old_cell_data: Vec, + new_cell: Cell, + new_cell_data: Vec, + receipt_data: Vec, + signed_intent: Vec, + signed_hash: Hash, + btc_signature: Vec, + ckb_signature: Vec, + finality: Hash, + btc_closure: Hash, + receipt_hash: Hash, +} + +fn append(out: &mut Vec, chunks: &[&[u8]]) { + for chunk in chunks { + out.extend_from_slice(chunk); + } +} + +fn utxo_commitment(txid: &Hash, vout: u64, amount: u64, script_pubkey: &Hash) -> Vec { + let mut out = Vec::new(); + append(&mut out, &[txid, &u32_bytes(vout as usize), &u64_bytes(amount), script_pubkey]); + out +} + +fn base(label: &str) -> Result { + let sealed_txid = ckb_hash(format!("NovaSeal dual sealed BTC txid {label}").as_bytes()); + let sealed_vout = 1; + let sealed_amount = 350_000; + let script_pubkey = ckb_hash(format!("NovaSeal dual sealed BTC script pubkey {label}").as_bytes()); + let sealed_utxo = ckb_hash(&utxo_commitment(&sealed_txid, sealed_vout, sealed_amount, &script_pubkey)); + Ok(Base { + seal: ckb_hash(format!("NovaSeal dual seal {label}").as_bytes()), + policy: ckb_hash(format!("NovaSeal dual policy {label}").as_bytes()), + btc_owner: xonly_pubkey(&TEST_SECRET_KEY)?, + ckb_authority: xonly_pubkey(&CKB_SECRET)?, + sealed_txid, + sealed_vout, + sealed_amount, + script_pubkey, + sealed_utxo, + initial_state: ckb_hash(format!("NovaSeal dual active CKB state {label}").as_bytes()), + final_state: ckb_hash(format!("NovaSeal dual finalized CKB state {label}").as_bytes()), + btc_closure: ckb_hash(format!("NovaSeal dual BTC closure {label}").as_bytes()), + btc_txid: ckb_hash(format!("NovaSeal dual BTC closure txid {label}").as_bytes()), + btc_wtxid: ckb_hash(format!("NovaSeal dual BTC closure wtxid {label}").as_bytes()), + spend_input: 0, + maturity: 0, + expiry: (1_u64 << 63) - 1, + }) +} + +fn zero_cell() -> Cell { + Cell { + seal: ZERO_HASH, + policy: ZERO_HASH, + btc_owner: ZERO_HASH, + ckb_authority: ZERO_HASH, + sealed_utxo: ZERO_HASH, + state: ZERO_HASH, + status: 0, + receipt: ZERO_HASH, + nonce: 0, + maturity: 0, + expiry: 0, + } +} + +fn pack_state(cell: &Cell) -> Vec { + let mut out = u16_bytes(0); + append( + &mut out, + &[ + &cell.seal, + &cell.policy, + &cell.btc_owner, + &cell.ckb_authority, + &cell.sealed_utxo, + &cell.state, + &u8_bytes(cell.status), + &u64_bytes(cell.nonce), + &u64_bytes(cell.maturity), + &u64_bytes(cell.expiry), + ], + ); + out +} + +fn pack_cell(cell: &Cell) -> Vec { + let mut out = u16_bytes(0); + append( + &mut out, + &[ + &cell.seal, + &cell.policy, + &cell.btc_owner, + &cell.ckb_authority, + &cell.sealed_utxo, + &cell.state, + &u8_bytes(cell.status), + &cell.receipt, + &u64_bytes(cell.nonce), + &u64_bytes(cell.maturity), + &u64_bytes(cell.expiry), + ], + ); + out +} + +fn finality(sealed: &Hash, closure: &Hash, old_state: &Hash, new_state: &Hash, maturity: u64) -> Vec { + let mut out = Vec::new(); + append(&mut out, &[sealed, closure, old_state, new_state, &u64_bytes(maturity), &ZERO_HASH]); + out +} + +#[allow(clippy::too_many_arguments)] +fn pack_core( + op: u64, + base: &Base, + closure: &Hash, + old_state: &Hash, + new_state: &Hash, + old_status: u64, + new_status: u64, + old_nonce: u64, + new_nonce: u64, +) -> Vec { + let mut out = Vec::new(); + append( + &mut out, + &[ + &u8_bytes(op), + &base.seal, + &base.policy, + &base.btc_owner, + &base.ckb_authority, + &base.sealed_utxo, + closure, + old_state, + new_state, + &u64_bytes(base.maturity), + &u8_bytes(old_status), + &u8_bytes(new_status), + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + &u64_bytes(base.expiry), + &ZERO_HASH, + ], + ); + out +} + +#[allow(clippy::too_many_arguments)] +fn pack_receipt( + base: &Base, + closure: &Hash, + old_state: &Hash, + new_state: &Hash, + old_nonce: u64, + new_nonce: u64, + core_hash: &Hash, + signed_hash: Option<&Hash>, + receipt_hash: Option<&Hash>, +) -> Vec { + let mut out = Vec::new(); + append( + &mut out, + &[ + &u8_bytes(OP_FINALIZE), + &base.seal, + &base.policy, + &base.btc_owner, + &base.ckb_authority, + &base.sealed_utxo, + closure, + old_state, + new_state, + &u8_bytes(STATUS_ACTIVE), + &u8_bytes(STATUS_FINALIZED), + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + core_hash, + ], + ); + if let (Some(signed_hash), Some(receipt_hash)) = (signed_hash, receipt_hash) { + append( + &mut out, + &[signed_hash, &ZERO_HASH, receipt_hash, &base.ckb_authority, &u64_bytes(base.maturity), &u64_bytes(base.expiry)], + ); + } else { + out.extend_from_slice(&ZERO_HASH); + } + out +} + +fn canonical(op: u64, base: &Base, old_state: &Hash, new_state: &Hash, old_nonce: u64, new_nonce: u64, body: &Hash) -> Hash { + let mut out = Vec::new(); + append( + &mut out, + &[ + &base.seal, + &base.policy, + &u8_bytes(op), + &u8_bytes(op), + &base.seal, + old_state, + new_state, + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + &u64_bytes(base.expiry), + &base.ckb_authority, + body, + &ZERO_HASH, + ], + ); + ckb_hash(&out) +} + +fn signature(secret: &[u8; 32], aux: &[u8; 32], hash: &Hash, mutate: bool) -> Result> { + let (public, signed) = schnorr_sign(hash, secret, aux)?; + let mut out = Vec::with_capacity(96); + out.extend_from_slice(&public); + out.extend_from_slice(&signed); + if mutate { + *out.last_mut().unwrap() ^= 1; + } + Ok(out) +} + +fn material(op: u64, base: &Base, old: Option<&Cell>, mutate_btc: bool, mutate_ckb: bool, zero_closure: bool) -> Result { + let (old_status, new_status, old_nonce, new_nonce, old_state, new_state, closure, new_cell, new_commitment, old_commitment) = + match op { + OP_INITIALIZE => { + let next = Cell { + seal: base.seal, + policy: base.policy, + btc_owner: base.btc_owner, + ckb_authority: base.ckb_authority, + sealed_utxo: base.sealed_utxo, + state: base.initial_state, + status: STATUS_ACTIVE, + receipt: ZERO_HASH, + nonce: 0, + maturity: base.maturity, + expiry: base.expiry, + }; + let new_commitment = ckb_hash(&pack_state(&next)); + (0, STATUS_ACTIVE, 0, 0, ZERO_HASH, base.initial_state, ZERO_HASH, next, new_commitment, ZERO_HASH) + } + OP_FINALIZE => { + let old = old.context("dual-seal finalization material requires an old cell")?; + let closure = if zero_closure { ZERO_HASH } else { base.btc_closure }; + let finality = ckb_hash(&finality(&old.sealed_utxo, &closure, &old.state, &base.final_state, old.maturity)); + ( + STATUS_ACTIVE, + STATUS_FINALIZED, + old.nonce, + old.nonce + 1, + old.state, + base.final_state, + closure, + zero_cell(), + finality, + ckb_hash(&pack_state(old)), + ) + } + _ => bail!("unknown dual-seal op {op}"), + }; + let core = pack_core(op, base, &closure, &old_state, &new_state, old_status, new_status, old_nonce, new_nonce); + let core_hash = ckb_hash(&core); + let receipt_hash = if op == OP_FINALIZE { + ckb_hash(&pack_receipt(base, &closure, &old_state, &new_state, old_nonce, new_nonce, &core_hash, None, None)) + } else { + ZERO_HASH + }; + let canonical = canonical(op, base, &old_commitment, &new_commitment, old_nonce, new_nonce, &core_hash); + let mut signed_intent = core; + append(&mut signed_intent, &[&canonical, &receipt_hash]); + let signed_hash = ckb_hash(&signed_intent); + let receipt_data = if op == OP_FINALIZE { + pack_receipt(base, &closure, &old_state, &new_state, old_nonce, new_nonce, &core_hash, Some(&signed_hash), Some(&receipt_hash)) + } else { + Vec::new() + }; + let old_value = old.cloned().unwrap_or_else(zero_cell); + Ok(Material { + old_cell_data: pack_cell(&old_value), + old_cell: old_value, + new_cell: new_cell.clone(), + new_cell_data: pack_cell(&new_cell), + receipt_data, + signed_intent, + signed_hash, + btc_signature: signature(&TEST_SECRET_KEY, &TEST_AUX_RAND, &signed_hash, mutate_btc)?, + ckb_signature: signature(&CKB_SECRET, &CKB_AUX, &signed_hash, mutate_ckb)?, + finality: new_commitment, + btc_closure: closure, + receipt_hash, + }) +} + +fn witness(op: u64, material: &Material) -> String { + let mut out = b"CSARGv1\0".to_vec(); + out.extend_from_slice(&u8_bytes(op)); + for value in [ + material.old_cell_data.as_slice(), + material.signed_intent.as_slice(), + material.btc_signature.as_slice(), + material.ckb_signature.as_slice(), + ] { + out.extend_from_slice(&u32_bytes(value.len())); + out.extend_from_slice(value); + } + entry_witness_input_type_hex(&out) +} + +fn build_initialize(funding: &Value, lifecycle_hash: &str, deps: Vec, header: &str, material: &Material) -> Result { + let total = funding["total_capacity"].as_u64().context("dual-seal initialize funding total is missing")?; + let change = total.checked_sub(STATE_CAPACITY).context("dual-seal initialize funding capacity is too small")?; + if change == 0 { + bail!("dual-seal initialize funding capacity is too small"); + } + let cells = funding_cells(funding); + let mut witnesses = vec![witness(OP_INITIALIZE, material)]; + witnesses.extend(vec!["0x".into(); cells.len().saturating_sub(1)]); + Ok(transaction( + cells, + vec![ + json!({"capacity": format!("0x{STATE_CAPACITY:x}"), "lock": always_success_lock("0x"), "type": lifecycle_type(lifecycle_hash)}), + json!({"capacity": format!("0x{change:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + ], + vec![hex0x(&material.new_cell_data), "0x".into()], + deps, + witnesses, + vec![header.into()], + )) +} + +fn build_finalize(old_ref: &Value, funding: &Value, deps: Vec, header: &str, material: &Material) -> Result { + let total = old_ref["capacity"].as_u64().context("dual-seal old ref capacity is missing")? + + funding["total_capacity"].as_u64().context("dual-seal funding total is missing")?; + let change = total.checked_sub(RECEIPT_CAPACITY).context("dual-seal finalize funding capacity is too small")?; + if change == 0 { + bail!("dual-seal finalize funding capacity is too small"); + } + let mut inputs = vec![old_ref.clone()]; + inputs.extend_from_slice(funding_cells(funding)); + let mut witnesses = vec![witness(OP_FINALIZE, material)]; + witnesses.extend(vec!["0x".into(); funding_cells(funding).len()]); + Ok(transaction( + &inputs, + vec![ + json!({"capacity": format!("0x{RECEIPT_CAPACITY:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + json!({"capacity": format!("0x{change:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + ], + vec![hex0x(&material.receipt_data), "0x".into()], + deps, + witnesses, + vec![header.into()], + )) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn run( + root: &Path, + ckb_repo: Option<&Path>, + ckb_bin: Option<&Path>, + run_dir: Option<&Path>, + contract: Contract, + keep_node: bool, +) -> Result { + let root = fs::canonicalize(root)?; + let ckb_repo = fs::canonicalize(ckb_repo.map(Path::to_path_buf).unwrap_or_else(|| root.parent().unwrap().join("ckb")))?; + let ckb_bin = resolve_ckb_bin(&ckb_repo, ckb_bin)?; + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let run_dir = run_dir + .map(Path::to_path_buf) + .unwrap_or_else(|| root.join(format!("target/novaseal-dual-seal-devnet-stateful-live/{timestamp}"))); + fs::create_dir_all(&run_dir)?; + let run_dir = fs::canonicalize(run_dir)?; + let lifecycle_path = run_dir.join("nova-dual-seal-lifecycle-type.elf"); + compile_contract(&root, contract, &lifecycle_path)?; + let verifier_path = root.join("proposals/novaseal/v0-mvp-skeleton/target/novaseal-btc-verifier-riscv-shell-release.elf"); + if !verifier_path.is_file() { + bail!("missing verifier ELF: {}", verifier_path.display()); + } + let mut devnet = CkbDevnet::new(ckb_repo.clone(), ckb_bin.clone(), run_dir.clone())?; + let mut report = contract_report_header(contract, "dual_seal_initialize_then_finalize", &root, &ckb_repo, &ckb_bin, &run_dir); + report["finality_scope"] = json!( + "live CKB finalisation executes the maturity guard and both BIP340 authorities over a declared BTC closure commitment; public BTC SPV/indexer closure evidence remains separate production evidence" + ); + let mut stage = "initializing"; + let scenario = (|| -> Result<()> { + stage = "start devnet"; + devnet.start()?; + stage = "deploy artifacts"; + let genesis = devnet.get_block_by_number(0)?; + let always = always_success_dep(genesis["transactions"][0]["hash"].as_str().context("genesis hash is missing")?); + let verifier = deploy_code(&mut devnet, "cellscript_btc_bip340_verifier_riscv", &fs::read(&verifier_path)?, &always)?; + let lifecycle = deploy_code(&mut devnet, "nova_dual_seal_lifecycle_type", &fs::read(&lifecycle_path)?, &always)?; + let lifecycle_hash = lifecycle["data_hash"].as_str().context("lifecycle hash is missing")?.to_owned(); + let deps = vec![verifier["cell_dep"].clone(), lifecycle["cell_dep"].clone(), always]; + let source_paths = [ + "proposals/novaseal/dual-seal-profile-v0/Cell.toml", + "proposals/novaseal/dual-seal-profile-v0/src", + "proposals/novaseal/dual-seal-profile-v0/schemas", + "proposals/novaseal/v0-mvp-skeleton/verifier/novaseal_btc_verifier", + "crates/cellscript-tools/src/novaseal_planned_dual.rs", + "crates/cellscript-tools/src/ckb_devnet.rs", + ] + .into_iter() + .map(PathBuf::from) + .collect::>(); + let artifacts = BTreeMap::from([("verifier".into(), verifier_path.clone()), ("lifecycle".into(), lifecycle_path.clone())]); + let source_provenance = provenance(&root, &source_paths, &artifacts)?; + let base = base("live")?; + let type_script = lifecycle_type(&lifecycle_hash); + + stage = "valid initialize"; + let initialize = material(OP_INITIALIZE, &base, None, false, false, false)?; + let header = devnet.rpc("get_tip_header", vec![])?; + let funding = devnet.collect_spendable(STATE_CAPACITY + 100 * SHANNONS)?; + let tx = build_initialize(&funding, &lifecycle_hash, deps.clone(), header["hash"].as_str().unwrap(), &initialize)?; + let initialize_dry = devnet.rpc("dry_run_transaction", vec![tx.clone()])?; + let initialize_commit = devnet.submit_and_commit(&tx, "dual-seal initialize")?; + let initialize_hash = initialize_commit["tx_hash"].as_str().unwrap(); + let initial_live = devnet.assert_live_cell( + initialize_hash, + 0, + "dual-seal active state", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&initialize.new_cell_data), + )?; + let initial_ref = json!({"tx_hash": initialize_hash, "index": 0, "capacity": STATE_CAPACITY}); + + stage = "negative wrong BTC owner signature"; + let negative_header = devnet.rpc("get_tip_header", vec![])?; + let wrong_btc = material(OP_FINALIZE, &base, Some(&initialize.new_cell), true, false, false)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = build_finalize(&initial_ref, &funding, deps.clone(), negative_header["hash"].as_str().unwrap(), &wrong_btc)?; + let wrong_btc_reject = devnet.dry_run_rejects( + &tx, + "dual-seal wrong BTC owner signature", + Some("Inputs[0].Type"), + Some(&lifecycle_hash), + Some(56), + )?; + + stage = "negative wrong CKB authority signature"; + let wrong_ckb = material(OP_FINALIZE, &base, Some(&initialize.new_cell), false, true, false)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = build_finalize(&initial_ref, &funding, deps.clone(), negative_header["hash"].as_str().unwrap(), &wrong_ckb)?; + let wrong_ckb_reject = devnet.dry_run_rejects( + &tx, + "dual-seal wrong CKB authority signature", + Some("Inputs[0].Type"), + Some(&lifecycle_hash), + Some(56), + )?; + + stage = "negative missing BTC closure"; + let missing = material(OP_FINALIZE, &base, Some(&initialize.new_cell), false, false, true)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = build_finalize(&initial_ref, &funding, deps.clone(), negative_header["hash"].as_str().unwrap(), &missing)?; + let missing_reject = devnet.dry_run_rejects( + &tx, + "dual-seal missing BTC closure commitment", + Some("Inputs[0].Type"), + Some(&lifecycle_hash), + Some(5), + )?; + let post_negative = devnet.assert_live_cell( + initialize_hash, + 0, + "post-negative dual-seal active state", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&initialize.new_cell_data), + )?; + + stage = "valid finalize"; + let header = devnet.rpc("get_tip_header", vec![])?; + let finalize = material(OP_FINALIZE, &base, Some(&initialize.new_cell), false, false, false)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = build_finalize(&initial_ref, &funding, deps, header["hash"].as_str().unwrap(), &finalize)?; + let finalize_dry = devnet.rpc("dry_run_transaction", vec![tx.clone()])?; + let commit = devnet.submit_and_commit(&tx, "dual-seal finalization")?; + let old_dead = devnet.wait_dead_cell(initialize_hash, 0)?; + let receipt_live = devnet.assert_live_cell( + commit["tx_hash"].as_str().unwrap(), + 0, + "dual-seal final receipt", + Some(RECEIPT_CAPACITY), + Some(&always_success_lock("0x")), + Some(&Value::Null), + Some(&finalize.receipt_data), + )?; + report.as_object_mut().unwrap().extend( + json!({ + "status": "passed", "live_devnet_rpc_executed": true, "stateful_lifecycle_executed": true, + "ckb_log": devnet.log_path.display().to_string(), "rpc_url": devnet.rpc_url, + "artifacts": {"verifier": verifier, "lifecycle": lifecycle}, "provenance": source_provenance, + "initialize": {"dry_run_cycles": initialize_dry["cycles"], "commit": initialize_commit, + "state_live": initial_live["status"] == "live", "state_data_hash": hex0x(&ckb_hash(&initialize.new_cell_data))}, + "finalize_dual_seal": {"dry_run_cycles": finalize_dry["cycles"], "commit": commit, + "old_state_not_live": old_dead["status"] != "live", "receipt_live": receipt_live["status"] == "live", + "btc_closure_bound": finalize.btc_closure != ZERO_HASH, "ckb_maturity_executed": base.maturity == 0, + "dual_authority_executed": true, "finality_commitment_hash": hex0x(&finalize.finality), + "btc_closure_commitment_hash": hex0x(&finalize.btc_closure), + "public_btc_anchor": {"kind": "dual_seal_btc_closure", "anchor_source": "local_deterministic_fixture", + "sealed_btc_txid": hex0x(&base.sealed_txid), "sealed_btc_vout_index": base.sealed_vout, + "sealed_btc_amount_sats": base.sealed_amount, "script_pubkey_hash": hex0x(&base.script_pubkey), + "btc_txid": hex0x(&base.btc_txid), "btc_wtxid": hex0x(&base.btc_wtxid), + "spend_input_index": base.spend_input, "ckb_btc_commitment_hash": hex0x(&finalize.btc_closure), + "sealed_utxo_commitment_hash": hex0x(&finalize.old_cell.sealed_utxo)}, + "signed_intent_hash": hex0x(&finalize.signed_hash), "receipt_hash": hex0x(&finalize.receipt_hash)}, + "negative_cases": {"wrong_btc_owner_signature_dry_run": wrong_btc_reject, + "wrong_ckb_authority_signature_dry_run": wrong_ckb_reject, + "btc_closure_commitment_missing_dry_run": missing_reject, "post_negative_state_still_live": post_negative["status"] == "live"}, + }) + .as_object() + .unwrap() + .clone(), + ); + Ok(()) + })(); + if let Err(error) = scenario { + report["status"] = json!("failed"); + report["stage"] = json!(stage); + report["error"] = json!(error.to_string()); + report["ckb_log"] = json!(devnet.log_path.display().to_string()); + report["rpc_url"] = json!(devnet.rpc_url); + } + if !keep_node { + devnet.stop(); + } + Ok(report) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn finalization_material_is_deterministic() { + let base = base("parity").unwrap(); + let initial = material(OP_INITIALIZE, &base, None, false, false, false).unwrap(); + let finalized = material(OP_FINALIZE, &base, Some(&initial.new_cell), false, false, false).unwrap(); + assert_eq!(hex0x(&ckb_hash(&initial.new_cell_data)), "0xc1598e096376a0a4c7e4ed7bd627823729191b22a48b6e520868fbfd58c0ddb9"); + assert_eq!(hex0x(&finalized.signed_hash), "0x6654d1cc26fb7ad081c1f78fd9c76c0c83113993c3bee9d562fcc7234a45f5c7"); + assert_eq!(hex0x(&finalized.receipt_hash), "0x6bfbe13c9fa540ee1695536077a92a60ff693845300662ea85f3f8b27588c5f3"); + assert_eq!(hex0x(&ckb_hash(&finalized.receipt_data)), "0x4ff529a399edc077ff0fc108e198d5d186892e21b00f7d80de7b27ca5ad66c3a"); + } +} diff --git a/crates/cellscript-tools/src/novaseal_planned_fiber.rs b/crates/cellscript-tools/src/novaseal_planned_fiber.rs new file mode 100644 index 00000000..e3c09961 --- /dev/null +++ b/crates/cellscript-tools/src/novaseal_planned_fiber.rs @@ -0,0 +1,576 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::{bail, Context, Result}; +use serde_json::{json, Value}; + +use crate::ckb_devnet::{ + always_success_dep, always_success_lock, ckb_hash, deploy_code, entry_witness_input_type_hex, funding_cells, hex0x, provenance, + resolve_ckb_bin, schnorr_sign, transaction, u16_bytes, u32_bytes, u64_bytes, u8_bytes, xonly_pubkey, CkbDevnet, RECEIPT_CAPACITY, + SHANNONS, STATE_CAPACITY, TEST_AUX_RAND, TEST_SECRET_KEY, ZERO_HASH, +}; +use crate::novaseal_planned_live::{compile_contract, contract_report_header, lifecycle_type, Contract}; + +const OP_SETTLE: u64 = 0; +const OP_INITIALIZE: u64 = 255; +const STATUS_ACTIVE: u64 = 1; +const STATUS_SETTLED: u64 = 2; +type Hash = [u8; 32]; + +#[derive(Clone)] +struct Base { + candidate: Hash, + policy: Hash, + operator: Hash, + channel: Hash, + initial_balance: Hash, + settled_balance: Hash, + route: Hash, + payment: Hash, + amount: u64, + expiry: u64, +} + +#[derive(Clone)] +struct Cell { + candidate: Hash, + policy: Hash, + operator: Hash, + channel: Hash, + balance: Hash, + status: u64, + receipt: Hash, + nonce: u64, + expiry: u64, +} + +struct Material { + old_cell_data: Vec, + new_cell: Cell, + new_cell_data: Vec, + receipt_data: Vec, + signed_intent: Vec, + signed_hash: Hash, + signature: Vec, + settlement: Hash, + receipt_hash: Hash, +} + +fn append(out: &mut Vec, chunks: &[&[u8]]) { + for chunk in chunks { + out.extend_from_slice(chunk); + } +} + +fn base(label: &str) -> Result { + Ok(Base { + candidate: ckb_hash(format!("NovaSeal Fiber candidate {label}").as_bytes()), + policy: ckb_hash(format!("NovaSeal Fiber policy {label}").as_bytes()), + operator: xonly_pubkey(&TEST_SECRET_KEY)?, + channel: ckb_hash(format!("NovaSeal Fiber channel {label}").as_bytes()), + initial_balance: ckb_hash(format!("NovaSeal Fiber initial balance {label}").as_bytes()), + settled_balance: ckb_hash(format!("NovaSeal Fiber settled balance {label}").as_bytes()), + route: ckb_hash(format!("NovaSeal Fiber route {label}").as_bytes()), + payment: ckb_hash(format!("NovaSeal Fiber payment {label}").as_bytes()), + amount: 42_000, + expiry: (1_u64 << 63) - 1, + }) +} + +fn zero_cell() -> Cell { + Cell { + candidate: ZERO_HASH, + policy: ZERO_HASH, + operator: ZERO_HASH, + channel: ZERO_HASH, + balance: ZERO_HASH, + status: 0, + receipt: ZERO_HASH, + nonce: 0, + expiry: 0, + } +} + +fn pack_state(cell: &Cell) -> Vec { + let mut out = u16_bytes(0); + append( + &mut out, + &[ + &cell.candidate, + &cell.policy, + &cell.operator, + &cell.channel, + &cell.balance, + &u8_bytes(cell.status), + &u64_bytes(cell.nonce), + &u64_bytes(cell.expiry), + ], + ); + out +} + +fn pack_cell(cell: &Cell) -> Vec { + let mut out = u16_bytes(0); + append( + &mut out, + &[ + &cell.candidate, + &cell.policy, + &cell.operator, + &cell.channel, + &cell.balance, + &u8_bytes(cell.status), + &cell.receipt, + &u64_bytes(cell.nonce), + &u64_bytes(cell.expiry), + ], + ); + out +} + +fn settlement(base: &Base, old_balance: &Hash, new_balance: &Hash) -> Vec { + let mut out = Vec::new(); + append(&mut out, &[&base.channel, &base.route, &base.payment, old_balance, new_balance, &u64_bytes(base.amount), &ZERO_HASH]); + out +} + +#[allow(clippy::too_many_arguments)] +fn pack_core( + op: u64, + base: &Base, + route: &Hash, + payment: &Hash, + old_balance: &Hash, + new_balance: &Hash, + amount: u64, + old_status: u64, + new_status: u64, + old_nonce: u64, + new_nonce: u64, +) -> Vec { + let mut out = Vec::new(); + append( + &mut out, + &[ + &u8_bytes(op), + &base.candidate, + &base.policy, + &base.operator, + &base.channel, + route, + payment, + old_balance, + new_balance, + &u64_bytes(amount), + &u8_bytes(old_status), + &u8_bytes(new_status), + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + &u64_bytes(base.expiry), + &ZERO_HASH, + ], + ); + out +} + +#[allow(clippy::too_many_arguments)] +fn pack_receipt( + base: &Base, + old_balance: &Hash, + new_balance: &Hash, + old_nonce: u64, + new_nonce: u64, + core_hash: &Hash, + signed_hash: Option<&Hash>, + receipt_hash: Option<&Hash>, +) -> Vec { + let mut out = Vec::new(); + append( + &mut out, + &[ + &u8_bytes(OP_SETTLE), + &base.candidate, + &base.policy, + &base.operator, + &base.channel, + &base.route, + &base.payment, + old_balance, + new_balance, + &u64_bytes(base.amount), + &u8_bytes(STATUS_ACTIVE), + &u8_bytes(STATUS_SETTLED), + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + core_hash, + ], + ); + if let (Some(signed_hash), Some(receipt_hash)) = (signed_hash, receipt_hash) { + append(&mut out, &[signed_hash, &ZERO_HASH, receipt_hash, &base.operator, &u64_bytes(base.expiry)]); + } else { + out.extend_from_slice(&ZERO_HASH); + } + out +} + +fn canonical(op: u64, base: &Base, old_state: &Hash, new_state: &Hash, old_nonce: u64, new_nonce: u64, body: &Hash) -> Hash { + let mut out = Vec::new(); + append( + &mut out, + &[ + &base.candidate, + &base.policy, + &u8_bytes(op), + &u8_bytes(op), + &base.candidate, + old_state, + new_state, + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + &u64_bytes(base.expiry), + &base.operator, + body, + &ZERO_HASH, + ], + ); + ckb_hash(&out) +} + +fn material(op: u64, base: &Base, old: Option<&Cell>, mutate: bool, replay: bool) -> Result { + let (old_balance, new_balance, route, payment, amount, old_status, new_status, old_nonce, new_nonce, mut next) = match op { + OP_INITIALIZE => ( + ZERO_HASH, + base.initial_balance, + ZERO_HASH, + ZERO_HASH, + 0, + 0, + STATUS_ACTIVE, + 0, + 0, + Cell { + candidate: base.candidate, + policy: base.policy, + operator: base.operator, + channel: base.channel, + balance: base.initial_balance, + status: STATUS_ACTIVE, + receipt: ZERO_HASH, + nonce: 0, + expiry: base.expiry, + }, + ), + OP_SETTLE => { + let old = old.context("Fiber settle material requires an old cell")?; + let balance = if replay { old.balance } else { base.settled_balance }; + ( + old.balance, + balance, + base.route, + base.payment, + base.amount, + STATUS_ACTIVE, + STATUS_SETTLED, + old.nonce, + old.nonce + 1, + Cell { + candidate: old.candidate, + policy: old.policy, + operator: old.operator, + channel: old.channel, + balance, + status: STATUS_SETTLED, + receipt: ZERO_HASH, + nonce: old.nonce + 1, + expiry: old.expiry, + }, + ) + } + _ => bail!("unknown Fiber op {op}"), + }; + let old_commitment = old.map(|value| ckb_hash(&pack_state(value))).unwrap_or(ZERO_HASH); + let new_commitment = ckb_hash(&pack_state(&next)); + let core = pack_core(op, base, &route, &payment, &old_balance, &new_balance, amount, old_status, new_status, old_nonce, new_nonce); + let core_hash = ckb_hash(&core); + let receipt_hash = if op == OP_SETTLE { + ckb_hash(&pack_receipt(base, &old_balance, &new_balance, old_nonce, new_nonce, &core_hash, None, None)) + } else { + ZERO_HASH + }; + if op == OP_SETTLE { + next.receipt = receipt_hash; + } + let canonical = canonical(op, base, &old_commitment, &new_commitment, old_nonce, new_nonce, &core_hash); + let mut signed_intent = core; + append(&mut signed_intent, &[&canonical, &receipt_hash]); + let signed_hash = ckb_hash(&signed_intent); + let receipt_data = if op == OP_SETTLE { + pack_receipt(base, &old_balance, &new_balance, old_nonce, new_nonce, &core_hash, Some(&signed_hash), Some(&receipt_hash)) + } else { + Vec::new() + }; + let settlement = if op == OP_SETTLE { ckb_hash(&settlement(base, &old_balance, &new_balance)) } else { ZERO_HASH }; + let (public, signed) = schnorr_sign(&signed_hash, &TEST_SECRET_KEY, &TEST_AUX_RAND)?; + let mut signature = Vec::with_capacity(96); + signature.extend_from_slice(&public); + signature.extend_from_slice(&signed); + if mutate { + *signature.last_mut().unwrap() ^= 1; + } + Ok(Material { + old_cell_data: pack_cell(old.unwrap_or(&zero_cell())), + new_cell_data: pack_cell(&next), + new_cell: next, + receipt_data, + signed_intent, + signed_hash, + signature, + settlement, + receipt_hash, + }) +} + +fn witness(op: u64, material: &Material) -> String { + let mut out = b"CSARGv1\0".to_vec(); + out.extend_from_slice(&u8_bytes(op)); + for value in [material.old_cell_data.as_slice(), material.signed_intent.as_slice(), material.signature.as_slice()] { + out.extend_from_slice(&u32_bytes(value.len())); + out.extend_from_slice(value); + } + entry_witness_input_type_hex(&out) +} + +fn build_initialize(funding: &Value, lifecycle_hash: &str, deps: Vec, header: &str, material: &Material) -> Result { + let total = funding["total_capacity"].as_u64().context("Fiber initialize funding total is missing")?; + let change = total.checked_sub(STATE_CAPACITY).context("Fiber initialize funding capacity is too small")?; + if change == 0 { + bail!("Fiber initialize funding capacity is too small"); + } + let cells = funding_cells(funding); + let mut witnesses = vec![witness(OP_INITIALIZE, material)]; + witnesses.extend(vec!["0x".into(); cells.len().saturating_sub(1)]); + Ok(transaction( + cells, + vec![ + json!({"capacity": format!("0x{STATE_CAPACITY:x}"), "lock": always_success_lock("0x"), "type": lifecycle_type(lifecycle_hash)}), + json!({"capacity": format!("0x{change:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + ], + vec![hex0x(&material.new_cell_data), "0x".into()], + deps, + witnesses, + vec![header.into()], + )) +} + +fn build_settle( + old_ref: &Value, + funding: &Value, + lifecycle_hash: &str, + deps: Vec, + header: &str, + material: &Material, +) -> Result { + let total = funding["total_capacity"].as_u64().context("Fiber settle funding total is missing")?; + let change = total.checked_sub(RECEIPT_CAPACITY).context("Fiber settle funding capacity is too small")?; + if change == 0 { + bail!("Fiber settle funding capacity is too small"); + } + let mut inputs = vec![old_ref.clone()]; + inputs.extend_from_slice(funding_cells(funding)); + let mut witnesses = vec![witness(OP_SETTLE, material)]; + witnesses.extend(vec!["0x".into(); funding_cells(funding).len()]); + Ok(transaction( + &inputs, + vec![ + json!({"capacity": format!("0x{:x}", old_ref["capacity"].as_u64().unwrap()), "lock": always_success_lock("0x"), "type": lifecycle_type(lifecycle_hash)}), + json!({"capacity": format!("0x{RECEIPT_CAPACITY:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + json!({"capacity": format!("0x{change:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + ], + vec![hex0x(&material.new_cell_data), hex0x(&material.receipt_data), "0x".into()], + deps, + witnesses, + vec![header.into()], + )) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn run( + root: &Path, + ckb_repo: Option<&Path>, + ckb_bin: Option<&Path>, + run_dir: Option<&Path>, + contract: Contract, + keep_node: bool, +) -> Result { + let root = fs::canonicalize(root)?; + let ckb_repo = fs::canonicalize(ckb_repo.map(Path::to_path_buf).unwrap_or_else(|| root.parent().unwrap().join("ckb")))?; + let ckb_bin = resolve_ckb_bin(&ckb_repo, ckb_bin)?; + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let run_dir = run_dir + .map(Path::to_path_buf) + .unwrap_or_else(|| root.join(format!("target/novaseal-fiber-candidate-devnet-stateful-live/{timestamp}"))); + fs::create_dir_all(&run_dir)?; + let run_dir = fs::canonicalize(run_dir)?; + let lifecycle_path = run_dir.join("nova-fiber-candidate-lifecycle-type.elf"); + compile_contract(&root, contract, &lifecycle_path)?; + let verifier_path = root.join("proposals/novaseal/v0-mvp-skeleton/target/novaseal-btc-verifier-riscv-shell-release.elf"); + if !verifier_path.is_file() { + bail!("missing verifier ELF: {}", verifier_path.display()); + } + let mut devnet = CkbDevnet::new(ckb_repo.clone(), ckb_bin.clone(), run_dir.clone())?; + let mut report = contract_report_header(contract, "fiber_candidate_initialize_then_settle", &root, &ckb_repo, &ckb_bin, &run_dir); + report["fiber_execution_scope"] = + json!("live CKB stateful settlement path; real Fiber node/channel execution remains a later external experiment"); + let mut stage = "initializing"; + let scenario = (|| -> Result<()> { + stage = "start devnet"; + devnet.start()?; + stage = "deploy artifacts"; + let genesis = devnet.get_block_by_number(0)?; + let always = always_success_dep(genesis["transactions"][0]["hash"].as_str().context("genesis hash is missing")?); + let verifier = deploy_code(&mut devnet, "cellscript_btc_bip340_verifier_riscv", &fs::read(&verifier_path)?, &always)?; + let lifecycle = deploy_code(&mut devnet, "nova_fiber_candidate_lifecycle_type", &fs::read(&lifecycle_path)?, &always)?; + let lifecycle_hash = lifecycle["data_hash"].as_str().context("lifecycle hash is missing")?.to_owned(); + let deps = vec![verifier["cell_dep"].clone(), lifecycle["cell_dep"].clone(), always]; + let source_paths = [ + "proposals/novaseal/fiber-candidate-profile-v0/Cell.toml", + "proposals/novaseal/fiber-candidate-profile-v0/src", + "proposals/novaseal/fiber-candidate-profile-v0/schemas", + "proposals/novaseal/v0-mvp-skeleton/verifier/novaseal_btc_verifier", + "crates/cellscript-tools/src/novaseal_planned_fiber.rs", + "crates/cellscript-tools/src/ckb_devnet.rs", + ] + .into_iter() + .map(PathBuf::from) + .collect::>(); + let artifacts = BTreeMap::from([("verifier".into(), verifier_path.clone()), ("lifecycle".into(), lifecycle_path.clone())]); + let source_provenance = provenance(&root, &source_paths, &artifacts)?; + let base = base("live")?; + let type_script = lifecycle_type(&lifecycle_hash); + + stage = "valid initialize"; + let initialize = material(OP_INITIALIZE, &base, None, false, false)?; + let header = devnet.rpc("get_tip_header", vec![])?; + let funding = devnet.collect_spendable(STATE_CAPACITY + 100 * SHANNONS)?; + let tx = build_initialize(&funding, &lifecycle_hash, deps.clone(), header["hash"].as_str().unwrap(), &initialize)?; + let initialize_dry = devnet.rpc("dry_run_transaction", vec![tx.clone()])?; + let initialize_commit = devnet.submit_and_commit(&tx, "Fiber candidate initialize")?; + let initialize_hash = initialize_commit["tx_hash"].as_str().unwrap(); + let initial_live = devnet.assert_live_cell( + initialize_hash, + 0, + "Fiber active candidate", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&initialize.new_cell_data), + )?; + let initial_ref = json!({"tx_hash": initialize_hash, "index": 0, "capacity": STATE_CAPACITY}); + + stage = "negative wrong operator signature"; + let negative_header = devnet.rpc("get_tip_header", vec![])?; + let wrong = material(OP_SETTLE, &base, Some(&initialize.new_cell), true, false)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = + build_settle(&initial_ref, &funding, &lifecycle_hash, deps.clone(), negative_header["hash"].as_str().unwrap(), &wrong)?; + let wrong_reject = + devnet.dry_run_rejects(&tx, "Fiber wrong operator signature", Some("Inputs[0].Type"), Some(&lifecycle_hash), Some(56))?; + + stage = "negative balance replay"; + let replay = material(OP_SETTLE, &base, Some(&initialize.new_cell), false, true)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = + build_settle(&initial_ref, &funding, &lifecycle_hash, deps.clone(), negative_header["hash"].as_str().unwrap(), &replay)?; + let replay_reject = + devnet.dry_run_rejects(&tx, "Fiber balance commitment replay", Some("Inputs[0].Type"), Some(&lifecycle_hash), Some(5))?; + let post_negative = devnet.assert_live_cell( + initialize_hash, + 0, + "post-negative Fiber active candidate", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&initialize.new_cell_data), + )?; + + stage = "valid settle"; + let header = devnet.rpc("get_tip_header", vec![])?; + let settle = material(OP_SETTLE, &base, Some(&initialize.new_cell), false, false)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = build_settle(&initial_ref, &funding, &lifecycle_hash, deps, header["hash"].as_str().unwrap(), &settle)?; + let settle_dry = devnet.rpc("dry_run_transaction", vec![tx.clone()])?; + let commit = devnet.submit_and_commit(&tx, "Fiber candidate settlement")?; + let old_dead = devnet.wait_dead_cell(initialize_hash, 0)?; + let commit_hash = commit["tx_hash"].as_str().unwrap(); + let settled_live = devnet.assert_live_cell( + commit_hash, + 0, + "Fiber settled candidate", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&settle.new_cell_data), + )?; + let receipt_live = devnet.assert_live_cell( + commit_hash, + 1, + "Fiber settlement receipt", + Some(RECEIPT_CAPACITY), + Some(&always_success_lock("0x")), + Some(&Value::Null), + Some(&settle.receipt_data), + )?; + report.as_object_mut().unwrap().extend( + json!({ + "status": "passed", "live_devnet_rpc_executed": true, "stateful_lifecycle_executed": true, + "ckb_log": devnet.log_path.display().to_string(), "rpc_url": devnet.rpc_url, + "artifacts": {"verifier": verifier, "lifecycle": lifecycle}, "provenance": source_provenance, + "initialize": {"dry_run_cycles": initialize_dry["cycles"], "commit": initialize_commit, + "candidate_live": initial_live["status"] == "live", "candidate_data_hash": hex0x(&ckb_hash(&initialize.new_cell_data))}, + "settle_fiber_candidate": {"dry_run_cycles": settle_dry["cycles"], "commit": commit, + "old_candidate_not_live": old_dead["status"] != "live", "new_candidate_live": settled_live["status"] == "live", + "receipt_live": receipt_live["status"] == "live", "balance_commitment_progressed": settle.new_cell.balance != initialize.new_cell.balance, + "fiber_execution_executed": true, + "fiber_execution_scope": "profile-level live CKB settlement path; external Fiber node experiment is still separate", + "settlement_commitment_hash": hex0x(&settle.settlement), "signed_intent_hash": hex0x(&settle.signed_hash), + "receipt_hash": hex0x(&settle.receipt_hash)}, + "negative_cases": {"wrong_operator_signature_dry_run": wrong_reject, + "balance_commitment_replay_dry_run": replay_reject, "post_negative_state_still_live": post_negative["status"] == "live"}, + }) + .as_object() + .unwrap() + .clone(), + ); + Ok(()) + })(); + if let Err(error) = scenario { + report["status"] = json!("failed"); + report["stage"] = json!(stage); + report["error"] = json!(error.to_string()); + report["ckb_log"] = json!(devnet.log_path.display().to_string()); + report["rpc_url"] = json!(devnet.rpc_url); + } + if !keep_node { + devnet.stop(); + } + Ok(report) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn settlement_material_is_deterministic() { + let base = base("parity").unwrap(); + let initial = material(OP_INITIALIZE, &base, None, false, false).unwrap(); + let settled = material(OP_SETTLE, &base, Some(&initial.new_cell), false, false).unwrap(); + assert_eq!(hex0x(&ckb_hash(&initial.new_cell_data)), "0xc5c1cb82e0d3ab0f573925695adf1306bf8cfcd94cfec0fbd9f71a826342b039"); + assert_eq!(hex0x(&ckb_hash(&settled.new_cell_data)), "0xc7171e970e8243289832031bd4318c61b55da960f66bdb7a6eceaa026834ec44"); + assert_eq!(hex0x(&settled.signed_hash), "0x0a988c16445df31a8b389cbd9f3a81f7c0d8e50ef0a23c39da85f72b8970aa35"); + assert_eq!(hex0x(&settled.receipt_hash), "0xd0b2f179086571d61c3fca3a048b76faf46c97feeb88c58069d5d6068ac1bf88"); + assert_eq!(hex0x(&ckb_hash(&settled.receipt_data)), "0xdac0f576c6d79fb355828819cce8b06af90e1173e38b0e2772bb2a75083555f9"); + } +} diff --git a/crates/cellscript-tools/src/novaseal_planned_fungible.rs b/crates/cellscript-tools/src/novaseal_planned_fungible.rs new file mode 100644 index 00000000..44cdc60f --- /dev/null +++ b/crates/cellscript-tools/src/novaseal_planned_fungible.rs @@ -0,0 +1,787 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::{bail, Context, Result}; +use serde_json::{json, Value}; + +use crate::ckb_devnet::{ + always_success_dep, always_success_lock, ckb_hash, deploy_code, entry_witness_input_type_hex, funding_cells, hex0x, provenance, + resolve_ckb_bin, schnorr_sign, transaction, u16_bytes, u32_bytes, u64_bytes, u8_bytes, xonly_pubkey, CkbDevnet, RECEIPT_CAPACITY, + SHANNONS, STATE_CAPACITY, TEST_AUX_RAND, TEST_SECRET_KEY, ZERO_HASH, +}; +use crate::novaseal_planned_live::{compile_contract, contract_report_header, lifecycle_type, Contract}; + +const OP_ISSUE: u64 = 0; +const OP_TRANSFER: u64 = 1; +const OP_SETTLE: u64 = 2; +const STATUS_ACTIVE: u64 = 1; +const STATUS_SETTLED: u64 = 2; +const HOLDER_SECRET: [u8; 32] = [0x22; 32]; +const HOLDER_AUX: [u8; 32] = [0x42; 32]; +const RECEIVER_SECRET: [u8; 32] = [0x33; 32]; +const RECEIVER_AUX: [u8; 32] = [0x66; 32]; + +type Hash = [u8; 32]; + +#[derive(Clone)] +struct Base { + asset: Hash, + xudt: Hash, + issuer: Hash, + holder: Hash, + amount: u64, + expiry: u64, +} + +#[derive(Clone)] +struct Cell { + asset: Hash, + xudt: Hash, + issuer: Hash, + holder: Hash, + amount: u64, + status: u64, + receipt: Hash, + nonce: u64, + expiry: u64, +} + +struct Material { + old_cell_data: Vec, + new_cell: Cell, + new_cell_data: Vec, + receipt_data: Vec, + signed_intent: Vec, + receipt_hash: Hash, + signature: Vec, +} + +fn append(target: &mut Vec, chunks: &[&[u8]]) { + for chunk in chunks { + target.extend_from_slice(chunk); + } +} + +fn zero_cell() -> Cell { + Cell { + asset: ZERO_HASH, + xudt: ZERO_HASH, + issuer: ZERO_HASH, + holder: ZERO_HASH, + amount: 0, + status: 0, + receipt: ZERO_HASH, + nonce: 0, + expiry: 0, + } +} + +fn pack_state(cell: &Cell) -> Vec { + let mut out = u16_bytes(0); + append( + &mut out, + &[ + &cell.asset, + &cell.xudt, + &cell.issuer, + &cell.holder, + &u64_bytes(cell.amount), + &u8_bytes(cell.status), + &u64_bytes(cell.nonce), + &u64_bytes(cell.expiry), + ], + ); + out +} + +fn pack_cell(cell: &Cell) -> Vec { + let mut out = u16_bytes(0); + append( + &mut out, + &[ + &cell.asset, + &cell.xudt, + &cell.issuer, + &cell.holder, + &u64_bytes(cell.amount), + &u8_bytes(cell.status), + &cell.receipt, + &u64_bytes(cell.nonce), + &u64_bytes(cell.expiry), + ], + ); + out +} + +#[allow(clippy::too_many_arguments)] +fn pack_core( + op: u64, + base: &Base, + old_holder: &Hash, + new_holder: &Hash, + old_status: u64, + new_status: u64, + old_amount: u64, + transfer_amount: u64, + new_amount: u64, + old_nonce: u64, + new_nonce: u64, +) -> Vec { + let mut out = Vec::new(); + append( + &mut out, + &[ + &u8_bytes(op), + &base.asset, + &base.xudt, + &base.issuer, + old_holder, + new_holder, + &u8_bytes(old_status), + &u8_bytes(new_status), + &u64_bytes(old_amount), + &u64_bytes(transfer_amount), + &u64_bytes(new_amount), + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + &u64_bytes(base.expiry), + &ZERO_HASH, + ], + ); + out +} + +#[allow(clippy::too_many_arguments)] +fn canonical( + op: u64, + base: &Base, + old_state: &Hash, + new_state: &Hash, + old_nonce: u64, + new_nonce: u64, + authority: &Hash, + body: &Hash, +) -> Hash { + let mut packed = Vec::new(); + append( + &mut packed, + &[ + &base.asset, + &base.xudt, + &u8_bytes(op), + &u8_bytes(op), + &base.asset, + old_state, + new_state, + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + &u64_bytes(base.expiry), + authority, + body, + &ZERO_HASH, + ], + ); + ckb_hash(&packed) +} + +#[allow(clippy::too_many_arguments)] +fn receipt_commitment( + op: u64, + base: &Base, + old_holder: &Hash, + new_holder: &Hash, + old_status: u64, + new_status: u64, + old_amount: u64, + transfer_amount: u64, + new_amount: u64, + old_nonce: u64, + new_nonce: u64, + core_hash: &Hash, +) -> Vec { + let mut out = Vec::new(); + append( + &mut out, + &[ + &u8_bytes(op), + &base.asset, + &base.xudt, + old_holder, + new_holder, + &u8_bytes(old_status), + &u8_bytes(new_status), + &u64_bytes(old_amount), + &u64_bytes(transfer_amount), + &u64_bytes(new_amount), + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + core_hash, + &ZERO_HASH, + ], + ); + out +} + +#[allow(clippy::too_many_arguments)] +fn receipt( + op: u64, + base: &Base, + old_holder: &Hash, + new_holder: &Hash, + old_status: u64, + new_status: u64, + old_amount: u64, + transfer_amount: u64, + new_amount: u64, + old_nonce: u64, + new_nonce: u64, + core_hash: &Hash, + signed_hash: &Hash, + receipt_hash: &Hash, + authority: &Hash, +) -> Vec { + let mut out = Vec::new(); + append( + &mut out, + &[ + &u8_bytes(op), + &base.asset, + &base.xudt, + old_holder, + new_holder, + &u8_bytes(old_status), + &u8_bytes(new_status), + &u64_bytes(old_amount), + &u64_bytes(transfer_amount), + &u64_bytes(new_amount), + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + core_hash, + signed_hash, + &ZERO_HASH, + receipt_hash, + authority, + &u64_bytes(base.expiry), + ], + ); + out +} + +fn signature(secret: &[u8; 32], aux: &[u8; 32], hash: &Hash, mutate: bool) -> Result> { + let (public, signed) = schnorr_sign(hash, secret, aux)?; + let mut out = Vec::with_capacity(96); + out.extend_from_slice(&public); + out.extend_from_slice(&signed); + if mutate { + *out.last_mut().unwrap() ^= 1; + } + Ok(out) +} + +fn base(label: &str) -> Result { + Ok(Base { + asset: ckb_hash(format!("NovaSeal fungible xUDT asset {label}").as_bytes()), + xudt: ckb_hash(format!("NovaSeal fungible xUDT type {label}").as_bytes()), + issuer: xonly_pubkey(&TEST_SECRET_KEY)?, + holder: xonly_pubkey(&HOLDER_SECRET)?, + amount: 1_000, + expiry: (1_u64 << 63) - 1, + }) +} + +fn material(op: u64, base: &Base, old: Option<&Cell>, mutate: bool, amount_override: Option) -> Result { + let ( + old_holder, + new_holder, + old_status, + new_status, + old_amount, + transfer_amount, + new_amount, + old_nonce, + new_nonce, + authority, + secret, + aux, + mut next, + ) = match op { + OP_ISSUE => { + let next = Cell { + asset: base.asset, + xudt: base.xudt, + issuer: base.issuer, + holder: base.holder, + amount: base.amount, + status: STATUS_ACTIVE, + receipt: ZERO_HASH, + nonce: 0, + expiry: base.expiry, + }; + ( + ZERO_HASH, + base.holder, + 0, + STATUS_ACTIVE, + 0, + base.amount, + base.amount, + 0, + 0, + base.issuer, + &TEST_SECRET_KEY, + &TEST_AUX_RAND, + next, + ) + } + OP_TRANSFER => { + let old = old.context("xUDT transfer material requires an old cell")?; + let receiver = xonly_pubkey(&RECEIVER_SECRET)?; + let mut next = old.clone(); + next.holder = receiver; + next.receipt = ZERO_HASH; + next.nonce += 1; + ( + old.holder, + receiver, + STATUS_ACTIVE, + STATUS_ACTIVE, + old.amount, + amount_override.unwrap_or(old.amount), + old.amount, + old.nonce, + old.nonce + 1, + old.holder, + &HOLDER_SECRET, + &HOLDER_AUX, + next, + ) + } + OP_SETTLE => { + let old = old.context("xUDT settle material requires an old cell")?; + ( + old.holder, + old.holder, + STATUS_ACTIVE, + STATUS_SETTLED, + old.amount, + old.amount, + 0, + old.nonce, + old.nonce + 1, + old.holder, + &RECEIVER_SECRET, + &RECEIVER_AUX, + zero_cell(), + ) + } + _ => bail!("unknown xUDT op {op}"), + }; + let old_state = old.map(|cell| ckb_hash(&pack_state(cell))).unwrap_or(ZERO_HASH); + let new_state = if op == OP_SETTLE { ZERO_HASH } else { ckb_hash(&pack_state(&next)) }; + let core = pack_core( + op, + base, + &old_holder, + &new_holder, + old_status, + new_status, + old_amount, + transfer_amount, + new_amount, + old_nonce, + new_nonce, + ); + let core_hash = ckb_hash(&core); + let receipt_hash = ckb_hash(&receipt_commitment( + op, + base, + &old_holder, + &new_holder, + old_status, + new_status, + old_amount, + transfer_amount, + new_amount, + old_nonce, + new_nonce, + &core_hash, + )); + let canonical = canonical(op, base, &old_state, &new_state, old_nonce, new_nonce, &authority, &core_hash); + let mut signed_intent = core; + signed_intent.extend_from_slice(&canonical); + signed_intent.extend_from_slice(&receipt_hash); + let signed_hash = ckb_hash(&signed_intent); + let receipt_data = receipt( + op, + base, + &old_holder, + &new_holder, + old_status, + new_status, + old_amount, + transfer_amount, + new_amount, + old_nonce, + new_nonce, + &core_hash, + &signed_hash, + &receipt_hash, + &authority, + ); + if op != OP_SETTLE { + next.receipt = receipt_hash; + } + Ok(Material { + old_cell_data: pack_cell(old.unwrap_or(&zero_cell())), + new_cell_data: pack_cell(&next), + new_cell: next, + receipt_data, + signed_intent, + receipt_hash, + signature: signature(secret, aux, &signed_hash, mutate)?, + }) +} + +fn witness(op: u64, material: &Material) -> String { + let mut out = b"CSARGv1\0".to_vec(); + out.extend_from_slice(&u8_bytes(op)); + for value in [ + material.old_cell_data.as_slice(), + material.new_cell_data.as_slice(), + material.signed_intent.as_slice(), + material.signature.as_slice(), + ] { + out.extend_from_slice(&u32_bytes(value.len())); + out.extend_from_slice(value); + } + entry_witness_input_type_hex(&out) +} + +fn build_issue(funding: &Value, lifecycle_hash: &str, deps: Vec, header: &str, material: &Material) -> Result { + let total = funding["total_capacity"].as_u64().context("xUDT issue funding total is missing")?; + let change = total.checked_sub(STATE_CAPACITY + RECEIPT_CAPACITY).context("xUDT issue funding capacity is too small")?; + if change == 0 { + bail!("xUDT issue funding capacity is too small"); + } + let cells = funding_cells(funding); + let mut witnesses = vec![witness(OP_ISSUE, material)]; + witnesses.extend(vec!["0x".into(); cells.len().saturating_sub(1)]); + Ok(transaction( + cells, + vec![ + json!({"capacity": format!("0x{STATE_CAPACITY:x}"), "lock": always_success_lock("0x"), "type": lifecycle_type(lifecycle_hash)}), + json!({"capacity": format!("0x{RECEIPT_CAPACITY:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + json!({"capacity": format!("0x{change:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + ], + vec![hex0x(&material.new_cell_data), hex0x(&material.receipt_data), "0x".into()], + deps, + witnesses, + vec![header.into()], + )) +} + +fn build_transfer( + old_ref: &Value, + funding: &Value, + lifecycle_hash: &str, + deps: Vec, + header: &str, + material: &Material, +) -> Result { + let total = funding["total_capacity"].as_u64().context("xUDT transfer funding total is missing")?; + let change = total.checked_sub(RECEIPT_CAPACITY).context("xUDT transfer funding capacity is too small")?; + if change == 0 { + bail!("xUDT transfer funding capacity is too small"); + } + let mut inputs = vec![old_ref.clone()]; + inputs.extend_from_slice(funding_cells(funding)); + let mut witnesses = vec![witness(OP_TRANSFER, material)]; + witnesses.extend(vec!["0x".into(); funding_cells(funding).len()]); + Ok(transaction( + &inputs, + vec![ + json!({"capacity": format!("0x{:x}", old_ref["capacity"].as_u64().unwrap()), "lock": always_success_lock("0x"), "type": lifecycle_type(lifecycle_hash)}), + json!({"capacity": format!("0x{RECEIPT_CAPACITY:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + json!({"capacity": format!("0x{change:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + ], + vec![hex0x(&material.new_cell_data), hex0x(&material.receipt_data), "0x".into()], + deps, + witnesses, + vec![header.into()], + )) +} + +fn build_settle(old_ref: &Value, funding: &Value, deps: Vec, header: &str, material: &Material) -> Result { + let total = + old_ref["capacity"].as_u64().unwrap() + funding["total_capacity"].as_u64().context("xUDT settle funding total is missing")?; + let change = total.checked_sub(RECEIPT_CAPACITY).context("xUDT settle funding capacity is too small")?; + if change == 0 { + bail!("xUDT settle funding capacity is too small"); + } + let mut inputs = vec![old_ref.clone()]; + inputs.extend_from_slice(funding_cells(funding)); + let mut witnesses = vec![witness(OP_SETTLE, material)]; + witnesses.extend(vec!["0x".into(); funding_cells(funding).len()]); + Ok(transaction( + &inputs, + vec![ + json!({"capacity": format!("0x{RECEIPT_CAPACITY:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + json!({"capacity": format!("0x{change:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + ], + vec![hex0x(&material.receipt_data), "0x".into()], + deps, + witnesses, + vec![header.into()], + )) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn run( + root: &Path, + ckb_repo: Option<&Path>, + ckb_bin: Option<&Path>, + run_dir: Option<&Path>, + contract: Contract, + keep_node: bool, +) -> Result { + let root = fs::canonicalize(root)?; + let ckb_repo = fs::canonicalize(ckb_repo.map(Path::to_path_buf).unwrap_or_else(|| root.parent().unwrap().join("ckb")))?; + let ckb_bin = resolve_ckb_bin(&ckb_repo, ckb_bin)?; + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let run_dir = run_dir + .map(Path::to_path_buf) + .unwrap_or_else(|| root.join(format!("target/novaseal-fungible-xudt-devnet-stateful-live/{timestamp}"))); + fs::create_dir_all(&run_dir)?; + let run_dir = fs::canonicalize(run_dir)?; + let lifecycle_path = run_dir.join("nova-fungible-xudt-lifecycle-type.elf"); + compile_contract(&root, contract, &lifecycle_path)?; + let verifier_path = root.join("proposals/novaseal/v0-mvp-skeleton/target/novaseal-btc-verifier-riscv-shell-release.elf"); + if !verifier_path.is_file() { + bail!("missing verifier ELF: {}", verifier_path.display()); + } + let mut devnet = CkbDevnet::new(ckb_repo.clone(), ckb_bin.clone(), run_dir.clone())?; + let mut report = contract_report_header(contract, "fungible_xudt_issue_transfer_settle", &root, &ckb_repo, &ckb_bin, &run_dir); + let mut stage = "initializing"; + let scenario = (|| -> Result<()> { + stage = "start devnet"; + devnet.start()?; + stage = "deploy artifacts"; + let genesis = devnet.get_block_by_number(0)?; + let always = always_success_dep(genesis["transactions"][0]["hash"].as_str().context("genesis hash is missing")?); + let verifier = deploy_code(&mut devnet, "cellscript_btc_bip340_verifier_riscv", &fs::read(&verifier_path)?, &always)?; + let lifecycle = deploy_code(&mut devnet, "nova_fungible_xudt_lifecycle_type", &fs::read(&lifecycle_path)?, &always)?; + let lifecycle_hash = lifecycle["data_hash"].as_str().context("lifecycle hash is missing")?.to_owned(); + let deps = vec![verifier["cell_dep"].clone(), lifecycle["cell_dep"].clone(), always]; + let source_paths = [ + "proposals/novaseal/fungible-xudt-profile-v0/Cell.toml", + "proposals/novaseal/fungible-xudt-profile-v0/src", + "proposals/novaseal/fungible-xudt-profile-v0/schemas", + "proposals/novaseal/v0-mvp-skeleton/verifier/novaseal_btc_verifier", + "crates/cellscript-tools/src/novaseal_planned_fungible.rs", + "crates/cellscript-tools/src/ckb_devnet.rs", + ] + .into_iter() + .map(PathBuf::from) + .collect::>(); + let artifacts = BTreeMap::from([("verifier".into(), verifier_path.clone()), ("lifecycle".into(), lifecycle_path.clone())]); + let source_provenance = provenance(&root, &source_paths, &artifacts)?; + let base = base("live")?; + + stage = "valid issue"; + let issue_material = material(OP_ISSUE, &base, None, false, None)?; + let header = devnet.rpc("get_tip_header", vec![])?; + let funding = devnet.collect_spendable(STATE_CAPACITY + RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = build_issue(&funding, &lifecycle_hash, deps.clone(), header["hash"].as_str().unwrap(), &issue_material)?; + let issue_dry = devnet.rpc("dry_run_transaction", vec![tx.clone()])?; + let issue_commit = devnet.submit_and_commit(&tx, "fungible xUDT issue")?; + let issue_hash = issue_commit["tx_hash"].as_str().unwrap(); + let type_script = lifecycle_type(&lifecycle_hash); + let issue_balance_live = devnet.assert_live_cell( + issue_hash, + 0, + "xUDT issued balance", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&issue_material.new_cell_data), + )?; + let issue_receipt_live = devnet.assert_live_cell( + issue_hash, + 1, + "xUDT issue receipt", + Some(RECEIPT_CAPACITY), + Some(&always_success_lock("0x")), + Some(&Value::Null), + Some(&issue_material.receipt_data), + )?; + let issued_ref = json!({"tx_hash": issue_hash, "index": 0, "capacity": STATE_CAPACITY}); + + stage = "negative transfer wrong holder signature"; + let negative_header = devnet.rpc("get_tip_header", vec![])?; + let wrong = material(OP_TRANSFER, &base, Some(&issue_material.new_cell), true, None)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = + build_transfer(&issued_ref, &funding, &lifecycle_hash, deps.clone(), negative_header["hash"].as_str().unwrap(), &wrong)?; + let wrong_signature = devnet.dry_run_rejects( + &tx, + "xUDT wrong holder signature transfer", + Some("Inputs[0].Type"), + Some(&lifecycle_hash), + Some(56), + )?; + + stage = "negative transfer amount mismatch"; + let mismatch = material(OP_TRANSFER, &base, Some(&issue_material.new_cell), false, Some(issue_material.new_cell.amount - 1))?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = build_transfer( + &issued_ref, + &funding, + &lifecycle_hash, + deps.clone(), + negative_header["hash"].as_str().unwrap(), + &mismatch, + )?; + let amount_mismatch = + devnet.dry_run_rejects(&tx, "xUDT transfer amount mismatch", Some("Inputs[0].Type"), Some(&lifecycle_hash), Some(5))?; + let post_transfer_negative = devnet.assert_live_cell( + issue_hash, + 0, + "post-negative xUDT issued balance", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&issue_material.new_cell_data), + )?; + + stage = "valid transfer"; + let transfer_header = devnet.rpc("get_tip_header", vec![])?; + let transfer_material = material(OP_TRANSFER, &base, Some(&issue_material.new_cell), false, None)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = build_transfer( + &issued_ref, + &funding, + &lifecycle_hash, + deps.clone(), + transfer_header["hash"].as_str().unwrap(), + &transfer_material, + )?; + let transfer_dry = devnet.rpc("dry_run_transaction", vec![tx.clone()])?; + let transfer_commit = devnet.submit_and_commit(&tx, "fungible xUDT transfer")?; + let old_dead = devnet.wait_dead_cell(issue_hash, 0)?; + let transfer_hash = transfer_commit["tx_hash"].as_str().unwrap(); + let receiver_live = devnet.assert_live_cell( + transfer_hash, + 0, + "xUDT receiver balance", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&transfer_material.new_cell_data), + )?; + let transfer_receipt_live = devnet.assert_live_cell( + transfer_hash, + 1, + "xUDT transfer receipt", + Some(RECEIPT_CAPACITY), + Some(&always_success_lock("0x")), + Some(&Value::Null), + Some(&transfer_material.receipt_data), + )?; + let receiver_ref = json!({"tx_hash": transfer_hash, "index": 0, "capacity": STATE_CAPACITY}); + + stage = "negative settle wrong holder signature"; + let settle_negative_header = devnet.rpc("get_tip_header", vec![])?; + let wrong_settle = material(OP_SETTLE, &base, Some(&transfer_material.new_cell), true, None)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = build_settle(&receiver_ref, &funding, deps.clone(), settle_negative_header["hash"].as_str().unwrap(), &wrong_settle)?; + let settle_wrong_signature = devnet.dry_run_rejects( + &tx, + "xUDT wrong holder signature settle", + Some("Inputs[0].Type"), + Some(&lifecycle_hash), + Some(56), + )?; + let post_negative = devnet.assert_live_cell( + transfer_hash, + 0, + "post-negative xUDT receiver balance", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&transfer_material.new_cell_data), + )?; + + stage = "valid settle"; + let settle_header = devnet.rpc("get_tip_header", vec![])?; + let settle_material = material(OP_SETTLE, &base, Some(&transfer_material.new_cell), false, None)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = build_settle(&receiver_ref, &funding, deps, settle_header["hash"].as_str().unwrap(), &settle_material)?; + let settle_dry = devnet.rpc("dry_run_transaction", vec![tx.clone()])?; + let settle_commit = devnet.submit_and_commit(&tx, "fungible xUDT settle")?; + let receiver_dead = devnet.wait_dead_cell(transfer_hash, 0)?; + let settle_live = devnet.assert_live_cell( + settle_commit["tx_hash"].as_str().unwrap(), + 0, + "xUDT settlement receipt", + Some(RECEIPT_CAPACITY), + Some(&always_success_lock("0x")), + Some(&Value::Null), + Some(&settle_material.receipt_data), + )?; + + report.as_object_mut().unwrap().extend( + json!({ + "status": "passed", "live_devnet_rpc_executed": true, "stateful_lifecycle_executed": true, + "ckb_log": devnet.log_path.display().to_string(), "rpc_url": devnet.rpc_url, + "artifacts": {"verifier": verifier, "lifecycle": lifecycle}, "provenance": source_provenance, + "issue": {"dry_run_cycles": issue_dry["cycles"], "commit": issue_commit, + "balance_live": issue_balance_live["status"] == "live", "receipt_live": issue_receipt_live["status"] == "live", + "balance_data_hash": hex0x(&ckb_hash(&issue_material.new_cell_data)), "receipt_hash": hex0x(&issue_material.receipt_hash)}, + "transfer": {"dry_run_cycles": transfer_dry["cycles"], "commit": transfer_commit, + "old_balance_not_live": old_dead["status"] != "live", "sender_balance_live": post_transfer_negative["status"] == "live", + "receiver_balance_live": receiver_live["status"] == "live", "receipt_live": transfer_receipt_live["status"] == "live", + "amount_conserved": transfer_material.new_cell.amount == issue_material.new_cell.amount, + "receipt_hash": hex0x(&transfer_material.receipt_hash)}, + "settle": {"dry_run_cycles": settle_dry["cycles"], "commit": settle_commit, + "old_balance_not_live": receiver_dead["status"] != "live", "settlement_receipt_live": settle_live["status"] == "live", + "receipt_hash": hex0x(&settle_material.receipt_hash)}, + "negative_cases": {"wrong_holder_signature_dry_run": wrong_signature, + "transfer_amount_mismatch_dry_run": amount_mismatch, "settle_wrong_holder_signature_dry_run": settle_wrong_signature, + "post_negative_state_still_live": post_negative["status"] == "live"}, + }) + .as_object() + .unwrap() + .clone(), + ); + Ok(()) + })(); + if let Err(error) = scenario { + report["status"] = json!("failed"); + report["stage"] = json!(stage); + report["error"] = json!(error.to_string()); + report["ckb_log"] = json!(devnet.log_path.display().to_string()); + report["rpc_url"] = json!(devnet.rpc_url); + } + if !keep_node { + devnet.stop(); + } + Ok(report) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn issue_material_is_stable() { + let base = base("parity").unwrap(); + let value = material(OP_ISSUE, &base, None, false, None).unwrap(); + assert_eq!(value.new_cell.amount, 1_000); + assert_eq!(hex0x(&ckb_hash(&value.new_cell_data)), "0x93a3f78c8cde6463adb34d4fbb112577bad13dcc9d13fbdede8ced2c69c707dd"); + assert_eq!(hex0x(&ckb_hash(&value.signed_intent)), "0x9935e84f62134cd4760cd08c5b47256d179b0fdf9388a8820cacffe111b01e5e"); + assert_eq!(hex0x(&value.receipt_hash), "0xedbd62d6f61220475c7284cb1e624d26b6147fb6792abc1554b95888cda0a990"); + assert_eq!(hex0x(&ckb_hash(&value.receipt_data)), "0xc456ae4d35cf68a160eb8c15a0b4abe2204f74ba482485466fcf451b552eef48"); + } +} diff --git a/crates/cellscript-tools/src/novaseal_planned_live.rs b/crates/cellscript-tools/src/novaseal_planned_live.rs new file mode 100644 index 00000000..55c86c49 --- /dev/null +++ b/crates/cellscript-tools/src/novaseal_planned_live.rs @@ -0,0 +1,361 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::Path; +use std::process::Command; + +use anyhow::{bail, Context, Result}; +use serde_json::{json, Value}; + +use crate::shared::{stable_json_pretty, stable_json_spaced}; + +#[derive(Clone, Copy)] +pub(crate) struct Contract { + pub(crate) profile: &'static str, + pub(crate) output: &'static str, + pub(crate) source: &'static str, + pub(crate) source_actions: &'static [&'static str], + pub(crate) lifecycle_action: &'static str, + pub(crate) tx_hashes: &'static [(&'static str, &'static str)], + pub(crate) live_checks: &'static [(&'static str, &'static str)], + pub(crate) negative_cases: &'static [(&'static str, &'static str)], +} + +const FUNGIBLE: Contract = Contract { + profile: "fungible-xudt", + output: "target/novaseal-fungible-xudt-devnet-stateful-live.json", + source: "proposals/novaseal/fungible-xudt-profile-v0/src/nova_fungible_xudt_lifecycle_type.cell", + source_actions: &["issue_xudt", "transfer_xudt", "settle_xudt", "nova_fungible_xudt_lifecycle"], + lifecycle_action: "nova_fungible_xudt_lifecycle", + tx_hashes: &[("issue", "/issue/commit/tx_hash"), ("transfer", "/transfer/commit/tx_hash"), ("settle", "/settle/commit/tx_hash")], + live_checks: &[ + ("issue_balance_live", "/issue/balance_live"), + ("issue_receipt_live", "/issue/receipt_live"), + ("transfer_old_balance_not_live", "/transfer/old_balance_not_live"), + ("transfer_sender_balance_live", "/transfer/sender_balance_live"), + ("transfer_receiver_balance_live", "/transfer/receiver_balance_live"), + ("transfer_receipt_live", "/transfer/receipt_live"), + ("transfer_amount_conserved", "/transfer/amount_conserved"), + ("settle_old_balance_not_live", "/settle/old_balance_not_live"), + ("settlement_receipt_live", "/settle/settlement_receipt_live"), + ("post_negative_state_still_live", "/negative_cases/post_negative_state_still_live"), + ], + negative_cases: &[ + ("wrong_holder_signature_rejected", "wrong_holder_signature_dry_run"), + ("transfer_amount_mismatch_rejected", "transfer_amount_mismatch_dry_run"), + ("settle_wrong_holder_signature_rejected", "settle_wrong_holder_signature_dry_run"), + ], +}; + +const RWA: Contract = Contract { + profile: "rwa-receipt", + output: "target/novaseal-rwa-receipt-devnet-stateful-live.json", + source: "proposals/novaseal/rwa-receipt-profile-v0/src/nova_rwa_receipt_lifecycle_type.cell", + source_actions: &["materialize_rwa_receipt", "claim_rwa_receipt", "settle_rwa_receipt", "nova_rwa_receipt_lifecycle"], + lifecycle_action: "nova_rwa_receipt_lifecycle", + tx_hashes: &[ + ("materialize", "/materialize/commit/tx_hash"), + ("claim", "/claim/commit/tx_hash"), + ("settle", "/settle/commit/tx_hash"), + ], + live_checks: &[ + ("materialized_receipt_live", "/materialize/receipt_live"), + ("materialized_audit_event_live", "/materialize/audit_event_live"), + ("claim_old_receipt_not_live", "/claim/old_receipt_not_live"), + ("claimed_receipt_live", "/claim/claimed_receipt_live"), + ("claim_event_live", "/claim/claim_event_live"), + ("settle_old_claim_not_live", "/settle/old_claim_not_live"), + ("settlement_receipt_live", "/settle/settlement_receipt_live"), + ("settlement_event_live", "/settle/settlement_event_live"), + ("amount_conserved", "/settle/amount_conserved"), + ("post_negative_state_still_live", "/negative_cases/post_negative_state_still_live"), + ], + negative_cases: &[ + ("wrong_holder_claim_rejected", "wrong_holder_claim_dry_run"), + ("wrong_issuer_settlement_rejected", "wrong_issuer_settlement_dry_run"), + ("amount_mutation_rejected", "amount_mutation_dry_run"), + ], +}; + +const BTC_TX: Contract = Contract { + profile: "btc-transaction-commitment", + output: "target/novaseal-btc-transaction-commitment-devnet-stateful-live.json", + source: "proposals/novaseal/btc-transaction-commitment-profile-v0/src/nova_btc_transaction_commitment_type.cell", + source_actions: &["commit_btc_transaction_transition", "nova_btc_transaction_commitment_lifecycle"], + lifecycle_action: "nova_btc_transaction_commitment_lifecycle", + tx_hashes: &[("commit_transaction", "/commit_transaction/commit/tx_hash")], + live_checks: &[ + ("old_state_not_live", "/commit_transaction/old_state_not_live"), + ("new_state_live", "/commit_transaction/new_state_live"), + ("receipt_live", "/commit_transaction/receipt_live"), + ("btc_tx_tuple_bound", "/commit_transaction/btc_tx_tuple_bound"), + ("transition_commitment_bound", "/commit_transaction/transition_commitment_bound"), + ("public_btc_verification_executed", "/commit_transaction/public_btc_verification_executed"), + ("post_negative_state_still_live", "/negative_cases/post_negative_state_still_live"), + ], + negative_cases: &[ + ("wrong_committer_signature_rejected", "wrong_committer_signature_dry_run"), + ("zero_btc_txid_rejected", "zero_btc_txid_dry_run"), + ("transition_hash_mismatch_rejected", "transition_hash_mismatch_dry_run"), + ], +}; + +const BTC_UTXO: Contract = Contract { + profile: "btc-utxo-seal", + output: "target/novaseal-btc-utxo-seal-devnet-stateful-live.json", + source: "proposals/novaseal/btc-utxo-seal-profile-v0/src/nova_btc_utxo_seal_type.cell", + source_actions: &["close_btc_utxo_seal", "nova_btc_utxo_seal_lifecycle"], + lifecycle_action: "nova_btc_utxo_seal_lifecycle", + tx_hashes: &[("close_utxo_seal", "/close_utxo_seal/commit/tx_hash")], + live_checks: &[ + ("old_state_not_live", "/close_utxo_seal/old_state_not_live"), + ("new_state_live", "/close_utxo_seal/new_state_live"), + ("receipt_live", "/close_utxo_seal/receipt_live"), + ("sealed_utxo_tuple_bound", "/close_utxo_seal/sealed_utxo_tuple_bound"), + ("spend_tuple_bound", "/close_utxo_seal/spend_tuple_bound"), + ("public_btc_spend_verification_executed", "/close_utxo_seal/public_btc_spend_verification_executed"), + ("post_negative_state_still_live", "/negative_cases/post_negative_state_still_live"), + ], + negative_cases: &[ + ("wrong_owner_signature_rejected", "wrong_owner_signature_dry_run"), + ("utxo_commitment_mismatch_rejected", "utxo_commitment_mismatch_dry_run"), + ("zero_spend_txid_rejected", "zero_spend_txid_dry_run"), + ], +}; + +const DUAL: Contract = Contract { + profile: "dual-seal", + output: "target/novaseal-dual-seal-devnet-stateful-live.json", + source: "proposals/novaseal/dual-seal-profile-v0/src/nova_dual_seal_type.cell", + source_actions: &["finalize_dual_seal", "nova_dual_seal_lifecycle"], + lifecycle_action: "nova_dual_seal_lifecycle", + tx_hashes: &[("finalize_dual_seal", "/finalize_dual_seal/commit/tx_hash")], + live_checks: &[ + ("old_state_not_live", "/finalize_dual_seal/old_state_not_live"), + ("receipt_live", "/finalize_dual_seal/receipt_live"), + ("btc_closure_bound", "/finalize_dual_seal/btc_closure_bound"), + ("ckb_maturity_executed", "/finalize_dual_seal/ckb_maturity_executed"), + ("dual_authority_executed", "/finalize_dual_seal/dual_authority_executed"), + ("post_negative_state_still_live", "/negative_cases/post_negative_state_still_live"), + ], + negative_cases: &[ + ("wrong_btc_owner_signature_rejected", "wrong_btc_owner_signature_dry_run"), + ("wrong_ckb_authority_signature_rejected", "wrong_ckb_authority_signature_dry_run"), + ("btc_closure_commitment_missing_rejected", "btc_closure_commitment_missing_dry_run"), + ], +}; + +const FIBER: Contract = Contract { + profile: "fiber-candidate", + output: "target/novaseal-fiber-candidate-devnet-stateful-live.json", + source: "proposals/novaseal/fiber-candidate-profile-v0/src/nova_fiber_candidate_type.cell", + source_actions: &["settle_fiber_candidate", "nova_fiber_candidate_lifecycle"], + lifecycle_action: "nova_fiber_candidate_lifecycle", + tx_hashes: &[("settle_fiber_candidate", "/settle_fiber_candidate/commit/tx_hash")], + live_checks: &[ + ("old_candidate_not_live", "/settle_fiber_candidate/old_candidate_not_live"), + ("new_candidate_live", "/settle_fiber_candidate/new_candidate_live"), + ("receipt_live", "/settle_fiber_candidate/receipt_live"), + ("balance_commitment_progressed", "/settle_fiber_candidate/balance_commitment_progressed"), + ("fiber_execution_executed", "/settle_fiber_candidate/fiber_execution_executed"), + ("post_negative_state_still_live", "/negative_cases/post_negative_state_still_live"), + ], + negative_cases: &[ + ("wrong_operator_signature_rejected", "wrong_operator_signature_dry_run"), + ("balance_commitment_replay_rejected", "balance_commitment_replay_dry_run"), + ], +}; + +fn contract(profile: &str) -> Result { + match profile { + "fungible-xudt" => Ok(FUNGIBLE), + "rwa-receipt" => Ok(RWA), + "btc-transaction-commitment" => Ok(BTC_TX), + "btc-utxo-seal" => Ok(BTC_UTXO), + "dual-seal" => Ok(DUAL), + "fiber-candidate" => Ok(FIBER), + _ => bail!("unsupported planned profile {profile}"), + } +} + +fn rows(rows: &[(&str, &str)], pointer_name: &str) -> Vec { + rows.iter().map(|(name, pointer)| json!({"name": name, (pointer_name): pointer})).collect() +} + +pub(crate) fn lifecycle_type(data_hash: &str) -> Value { + json!({"code_hash": data_hash, "hash_type": "data2", "args": "0x"}) +} + +pub(crate) fn contract_report_header( + contract: Contract, + scenario: &str, + root: &Path, + ckb_repo: &Path, + ckb_bin: &Path, + run_dir: &Path, +) -> Value { + json!({ + "schema": "novaseal-planned-profile-devnet-stateful-live-v0.1", + "profile": contract.profile, + "status": "running", + "scenario": scenario, + "repo_root": root.display().to_string(), + "ckb_repo": ckb_repo.display().to_string(), + "ckb_bin": ckb_bin.display().to_string(), + "run_dir": run_dir.display().to_string(), + "expected_tx_hashes": rows(contract.tx_hashes, "pointer"), + "required_live_checks": rows(contract.live_checks, "pointer"), + "required_negative_cases": rows(contract.negative_cases, "key"), + }) +} + +fn not_run(contract: Contract) -> Value { + let negative: BTreeMap<_, _> = contract + .negative_cases + .iter() + .map(|(_, key)| ((*key).to_owned(), json!({"status": "not_run", "matched_expected": false}))) + .collect(); + json!({ + "schema": "novaseal-planned-profile-devnet-stateful-live-v0.1", + "profile": contract.profile, + "status": "not_run", + "live_devnet_rpc_executed": false, + "stateful_lifecycle_executed": false, + "artifact_contract": { + "source": contract.source, + "source_actions": contract.source_actions, + "lifecycle_action": contract.lifecycle_action, + "stable_lifecycle_artifact_required": true, + "dispatcher_required": false, + "dispatcher_gap": Value::Null, + }, + "expected_tx_hashes": rows(contract.tx_hashes, "pointer"), + "required_live_checks": rows(contract.live_checks, "pointer"), + "required_negative_cases": rows(contract.negative_cases, "key"), + "provenance": {"repo_commit": Value::Null, "source_tree": Value::Null, "artifacts": Value::Null}, + "negative_cases": negative, + "next_engineering_step": "Replace this contract report with profile-specific live CKB devnet transaction evidence, including fresh source/artifact provenance.", + }) +} + +fn render(value: &Value, pretty: bool) -> Result { + if pretty { + stable_json_pretty(value) + } else { + stable_json_spaced(value) + } +} + +fn prepare(root: &Path, contract: Contract) -> Result { + let output = root + .join("target/novaseal-planned-profile-artifacts") + .join(contract.profile) + .join(format!("{}.elf", contract.lifecycle_action)); + fs::create_dir_all(output.parent().context("artifact output has no parent")?)?; + let args = [ + "run", + "--quiet", + "--bin", + "cellc", + "--", + contract.source, + "--target-profile", + "ckb", + "--target", + "riscv64-elf", + "--entry-action", + contract.lifecycle_action, + "-o", + output.to_str().context("artifact path is not UTF-8")?, + ]; + let completed = Command::new("cargo").args(args).current_dir(root).output()?; + let command: Vec<_> = std::iter::once("cargo").chain(args).collect(); + let mut report = json!({ + "schema": "novaseal-planned-profile-artifact-prep-v0.1", "profile": contract.profile, + "source": contract.source, "lifecycle_action": contract.lifecycle_action, + "artifact": output.to_string_lossy(), "status": if completed.status.success() { "passed" } else { "failed" }, "command": command, + }); + if completed.status.success() { + report["size_bytes"] = json!(fs::metadata(output)?.len()); + } else { + report["stderr"] = json!(String::from_utf8_lossy(&completed.stderr)); + report["stdout"] = json!(String::from_utf8_lossy(&completed.stdout)); + } + Ok(report) +} + +pub(crate) fn compile_contract(root: &Path, contract: Contract, output: &Path) -> Result<()> { + fs::create_dir_all(output.parent().context("lifecycle artifact path has no parent")?)?; + let status = Command::new("cargo") + .args([ + "run", + "--quiet", + "--locked", + "--bin", + "cellc", + "--", + contract.source, + "--target-profile", + "ckb", + "--target", + "riscv64-elf", + "--entry-action", + contract.lifecycle_action, + "-o", + output.to_str().context("lifecycle artifact path is not UTF-8")?, + ]) + .current_dir(root) + .status()?; + if !status.success() { + bail!("failed to compile {} lifecycle", contract.profile); + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +pub fn run( + root: &Path, + profile: &str, + output: Option<&Path>, + ckb_repo: Option<&Path>, + ckb_bin: Option<&Path>, + run_dir: Option<&Path>, + pretty: bool, + keep_node: bool, + list_contract: bool, + prepare_artifacts: bool, + live: bool, +) -> Result { + let contract = contract(profile)?; + if prepare_artifacts { + let report = prepare(root, contract)?; + println!("{}", render(&report, pretty)?); + return Ok(if report["status"] == "passed" { 0 } else { 1 }); + } + let mut report = not_run(contract); + if list_contract { + println!("{}", render(&report, pretty)?); + return Ok(1); + } + if live { + report = match profile { + "fungible-xudt" => crate::novaseal_planned_fungible::run(root, ckb_repo, ckb_bin, run_dir, contract, keep_node)?, + "rwa-receipt" => crate::novaseal_planned_rwa::run(root, ckb_repo, ckb_bin, run_dir, contract, keep_node)?, + "btc-transaction-commitment" => { + crate::novaseal_planned_btc_tx::run(root, ckb_repo, ckb_bin, run_dir, contract, keep_node)? + } + "btc-utxo-seal" => crate::novaseal_planned_btc_utxo::run(root, ckb_repo, ckb_bin, run_dir, contract, keep_node)?, + "dual-seal" => crate::novaseal_planned_dual::run(root, ckb_repo, ckb_bin, run_dir, contract, keep_node)?, + "fiber-candidate" => crate::novaseal_planned_fiber::run(root, ckb_repo, ckb_bin, run_dir, contract, keep_node)?, + _ => bail!("{profile} Rust live runner is not wired yet; refusing to emit synthetic devnet evidence"), + }; + } + let output = match output { + Some(path) if path.is_absolute() => path.to_path_buf(), + Some(path) => root.join(path), + None => root.join(contract.output), + }; + fs::create_dir_all(output.parent().context("output path has no parent")?)?; + fs::write(&output, format!("{}\n", render(&report, pretty)?))?; + println!("wrote {} status={} profile={profile}", output.display(), report["status"].as_str().unwrap_or("failed")); + Ok(if report["status"] == "passed" { 0 } else { 1 }) +} diff --git a/crates/cellscript-tools/src/novaseal_planned_rwa.rs b/crates/cellscript-tools/src/novaseal_planned_rwa.rs new file mode 100644 index 00000000..2799bc87 --- /dev/null +++ b/crates/cellscript-tools/src/novaseal_planned_rwa.rs @@ -0,0 +1,715 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::{bail, Context, Result}; +use serde_json::{json, Value}; + +use crate::ckb_devnet::{ + always_success_dep, always_success_lock, ckb_hash, deploy_code, entry_witness_input_type_hex, funding_cells, hex0x, provenance, + resolve_ckb_bin, schnorr_sign, transaction, u16_bytes, u32_bytes, u64_bytes, u8_bytes, xonly_pubkey, CkbDevnet, RECEIPT_CAPACITY, + SHANNONS, STATE_CAPACITY, TEST_AUX_RAND, TEST_SECRET_KEY, ZERO_HASH, +}; +use crate::novaseal_planned_live::{compile_contract, contract_report_header, lifecycle_type, Contract}; + +const OP_MATERIALIZE: u64 = 0; +const OP_CLAIM: u64 = 1; +const OP_SETTLE: u64 = 2; +const STATUS_MATERIALIZED: u64 = 1; +const STATUS_CLAIMED: u64 = 2; +const STATUS_SETTLED: u64 = 3; +const HOLDER_SECRET: [u8; 32] = [0x22; 32]; +const HOLDER_AUX: [u8; 32] = [0x42; 32]; + +type Hash = [u8; 32]; + +#[derive(Clone)] +struct Base { + receipt_id: Hash, + registry: Hash, + asset: Hash, + document: Hash, + issuer: Hash, + holder: Hash, + amount: u64, + expiry: u64, +} + +#[derive(Clone)] +struct Cell { + receipt_id: Hash, + registry: Hash, + asset: Hash, + document: Hash, + issuer: Hash, + holder: Hash, + amount: u64, + status: u64, + receipt: Hash, + nonce: u64, + expiry: u64, +} + +struct Material { + old_cell: Cell, + old_cell_data: Vec, + new_cell: Cell, + new_cell_data: Vec, + event_data: Vec, + signed_intent: Vec, + receipt_hash: Hash, + signer_signature: Vec, + cosigner_signature: Vec, +} + +fn append(out: &mut Vec, chunks: &[&[u8]]) { + for chunk in chunks { + out.extend_from_slice(chunk); + } +} + +fn zero_cell() -> Cell { + Cell { + receipt_id: ZERO_HASH, + registry: ZERO_HASH, + asset: ZERO_HASH, + document: ZERO_HASH, + issuer: ZERO_HASH, + holder: ZERO_HASH, + amount: 0, + status: 0, + receipt: ZERO_HASH, + nonce: 0, + expiry: 0, + } +} + +fn base(label: &str) -> Result { + Ok(Base { + receipt_id: ckb_hash(format!("NovaSeal RWA receipt {label}").as_bytes()), + registry: ckb_hash(format!("NovaSeal RWA registry {label}").as_bytes()), + asset: ckb_hash(format!("NovaSeal RWA asset {label}").as_bytes()), + document: ckb_hash(format!("NovaSeal RWA document {label}").as_bytes()), + issuer: xonly_pubkey(&TEST_SECRET_KEY)?, + holder: xonly_pubkey(&HOLDER_SECRET)?, + amount: 10_000, + expiry: (1_u64 << 63) - 1, + }) +} + +fn pack_state(cell: &Cell) -> Vec { + let mut out = u16_bytes(0); + append( + &mut out, + &[ + &cell.receipt_id, + &cell.registry, + &cell.asset, + &cell.document, + &cell.issuer, + &cell.holder, + &u64_bytes(cell.amount), + &u8_bytes(cell.status), + &u64_bytes(cell.nonce), + &u64_bytes(cell.expiry), + ], + ); + out +} + +fn pack_cell(cell: &Cell) -> Vec { + let mut out = u16_bytes(0); + append( + &mut out, + &[ + &cell.receipt_id, + &cell.registry, + &cell.asset, + &cell.document, + &cell.issuer, + &cell.holder, + &u64_bytes(cell.amount), + &u8_bytes(cell.status), + &cell.receipt, + &u64_bytes(cell.nonce), + &u64_bytes(cell.expiry), + ], + ); + out +} + +#[allow(clippy::too_many_arguments)] +fn pack_core( + op: u64, + base: &Base, + old_status: u64, + new_status: u64, + old_amount: u64, + settlement_amount: u64, + old_nonce: u64, + new_nonce: u64, +) -> Vec { + let mut out = Vec::new(); + append( + &mut out, + &[ + &u8_bytes(op), + &base.receipt_id, + &base.registry, + &base.asset, + &base.document, + &base.issuer, + &base.holder, + &u8_bytes(old_status), + &u8_bytes(new_status), + &u64_bytes(old_amount), + &u64_bytes(settlement_amount), + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + &u64_bytes(base.expiry), + &ZERO_HASH, + ], + ); + out +} + +#[allow(clippy::too_many_arguments)] +fn pack_event( + op: u64, + base: &Base, + old_status: u64, + new_status: u64, + old_amount: u64, + settlement_amount: u64, + old_nonce: u64, + new_nonce: u64, + core_hash: &Hash, + receipt_hash: Option<&Hash>, + signer: Option<&Hash>, +) -> Vec { + let mut out = Vec::new(); + append( + &mut out, + &[ + &u8_bytes(op), + &base.receipt_id, + &base.registry, + &base.asset, + &base.document, + &base.issuer, + &base.holder, + &u8_bytes(old_status), + &u8_bytes(new_status), + &u64_bytes(old_amount), + &u64_bytes(settlement_amount), + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + core_hash, + &ZERO_HASH, + ], + ); + if let (Some(receipt_hash), Some(signer)) = (receipt_hash, signer) { + append(&mut out, &[receipt_hash, signer, &u64_bytes(base.expiry)]); + } + out +} + +#[allow(clippy::too_many_arguments)] +fn canonical( + op: u64, + base: &Base, + old_state: &Hash, + new_state: &Hash, + old_nonce: u64, + new_nonce: u64, + authority: &Hash, + body: &Hash, +) -> Hash { + let mut out = Vec::new(); + append( + &mut out, + &[ + &base.receipt_id, + &base.registry, + &u8_bytes(op), + &u8_bytes(op), + &base.receipt_id, + old_state, + new_state, + &u64_bytes(old_nonce), + &u64_bytes(new_nonce), + &u64_bytes(base.expiry), + authority, + body, + &ZERO_HASH, + ], + ); + ckb_hash(&out) +} + +fn signature(secret: &[u8; 32], aux: &[u8; 32], hash: &Hash, mutate: bool) -> Result> { + let (public, signed) = schnorr_sign(hash, secret, aux)?; + let mut out = Vec::with_capacity(96); + out.extend_from_slice(&public); + out.extend_from_slice(&signed); + if mutate { + *out.last_mut().unwrap() ^= 1; + } + Ok(out) +} + +fn material( + op: u64, + base: &Base, + old: Option<&Cell>, + mutate_issuer: bool, + mutate_holder: bool, + amount_override: Option, +) -> Result { + let (old_status, new_status, old_amount, settlement_amount, old_nonce, new_nonce, authority, mut next) = match op { + OP_MATERIALIZE => ( + 0, + STATUS_MATERIALIZED, + 0, + base.amount, + 0, + 0, + base.issuer, + Cell { + receipt_id: base.receipt_id, + registry: base.registry, + asset: base.asset, + document: base.document, + issuer: base.issuer, + holder: base.holder, + amount: base.amount, + status: STATUS_MATERIALIZED, + receipt: ZERO_HASH, + nonce: 0, + expiry: base.expiry, + }, + ), + OP_CLAIM => { + let old = old.context("RWA claim material requires an old cell")?; + let mut next = old.clone(); + next.status = STATUS_CLAIMED; + next.receipt = ZERO_HASH; + next.nonce += 1; + ( + STATUS_MATERIALIZED, + STATUS_CLAIMED, + old.amount, + amount_override.unwrap_or(old.amount), + old.nonce, + old.nonce + 1, + old.holder, + next, + ) + } + OP_SETTLE => { + let old = old.context("RWA settle material requires an old cell")?; + ( + STATUS_CLAIMED, + STATUS_SETTLED, + old.amount, + amount_override.unwrap_or(old.amount), + old.nonce, + old.nonce + 1, + old.issuer, + zero_cell(), + ) + } + _ => bail!("unknown RWA op {op}"), + }; + let old_value = old.cloned().unwrap_or_else(zero_cell); + let old_state = old.map(|value| ckb_hash(&pack_state(value))).unwrap_or(ZERO_HASH); + let new_state = if op == OP_SETTLE { ZERO_HASH } else { ckb_hash(&pack_state(&next)) }; + let core = pack_core(op, base, old_status, new_status, old_amount, settlement_amount, old_nonce, new_nonce); + let core_hash = ckb_hash(&core); + let receipt_hash = ckb_hash(&pack_event( + op, + base, + old_status, + new_status, + old_amount, + settlement_amount, + old_nonce, + new_nonce, + &core_hash, + None, + None, + )); + let canonical = canonical(op, base, &old_state, &new_state, old_nonce, new_nonce, &authority, &core_hash); + if op != OP_SETTLE { + next.receipt = receipt_hash; + } + let new_cell_data = pack_cell(&next); + let event_data = pack_event( + op, + base, + old_status, + new_status, + old_amount, + settlement_amount, + old_nonce, + new_nonce, + &core_hash, + Some(&receipt_hash), + Some(&authority), + ); + let mut signed_intent = core; + append( + &mut signed_intent, + &[&canonical, &receipt_hash, &if op == OP_SETTLE { ZERO_HASH } else { ckb_hash(&new_cell_data) }, &ckb_hash(&event_data)], + ); + let signed_hash = ckb_hash(&signed_intent); + let issuer_signature = signature(&TEST_SECRET_KEY, &TEST_AUX_RAND, &signed_hash, mutate_issuer)?; + let holder_signature = signature(&HOLDER_SECRET, &HOLDER_AUX, &signed_hash, mutate_holder)?; + let signer_signature = if op == OP_CLAIM { holder_signature.clone() } else { issuer_signature.clone() }; + let cosigner_signature = if op == OP_SETTLE { holder_signature } else { issuer_signature }; + Ok(Material { + old_cell: old_value.clone(), + old_cell_data: pack_cell(&old_value), + new_cell: next, + new_cell_data, + event_data, + signed_intent, + receipt_hash, + signer_signature, + cosigner_signature, + }) +} + +fn witness(op: u64, material: &Material) -> String { + let mut out = b"CSARGv1\0".to_vec(); + out.extend_from_slice(&u8_bytes(op)); + for value in [ + material.old_cell_data.as_slice(), + material.signed_intent.as_slice(), + material.signer_signature.as_slice(), + material.cosigner_signature.as_slice(), + ] { + out.extend_from_slice(&u32_bytes(value.len())); + out.extend_from_slice(value); + } + entry_witness_input_type_hex(&out) +} + +fn build_state_event( + op: u64, + old_ref: Option<&Value>, + funding: &Value, + lifecycle_hash: &str, + deps: Vec, + header: &str, + material: &Material, +) -> Result { + let funding_total = funding["total_capacity"].as_u64().context("RWA funding total is missing")?; + let (inputs, change, state_capacity, extra_witnesses) = if op == OP_MATERIALIZE { + ( + funding_cells(funding).to_vec(), + funding_total.checked_sub(STATE_CAPACITY + RECEIPT_CAPACITY), + STATE_CAPACITY, + funding_cells(funding).len().saturating_sub(1), + ) + } else { + let old_ref = old_ref.context("RWA state/event tx requires an old ref")?; + let mut inputs = vec![old_ref.clone()]; + inputs.extend_from_slice(funding_cells(funding)); + ( + inputs, + funding_total.checked_sub(RECEIPT_CAPACITY), + old_ref["capacity"].as_u64().context("RWA old ref capacity is missing")?, + funding_cells(funding).len(), + ) + }; + let change = change.context("RWA state/event funding capacity is too small")?; + if change == 0 { + bail!("RWA state/event funding capacity is too small"); + } + let mut witnesses = vec![witness(op, material)]; + witnesses.extend(vec!["0x".into(); extra_witnesses]); + Ok(transaction( + &inputs, + vec![ + json!({"capacity": format!("0x{state_capacity:x}"), "lock": always_success_lock("0x"), "type": lifecycle_type(lifecycle_hash)}), + json!({"capacity": format!("0x{RECEIPT_CAPACITY:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + json!({"capacity": format!("0x{change:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + ], + vec![hex0x(&material.new_cell_data), hex0x(&material.event_data), "0x".into()], + deps, + witnesses, + vec![header.into()], + )) +} + +fn build_settle(old_ref: &Value, funding: &Value, deps: Vec, header: &str, material: &Material) -> Result { + let total = old_ref["capacity"].as_u64().context("RWA old ref capacity is missing")? + + funding["total_capacity"].as_u64().context("RWA funding total is missing")?; + let change = total.checked_sub(RECEIPT_CAPACITY).context("RWA settle funding capacity is too small")?; + if change == 0 { + bail!("RWA settle funding capacity is too small"); + } + let mut inputs = vec![old_ref.clone()]; + inputs.extend_from_slice(funding_cells(funding)); + let mut witnesses = vec![witness(OP_SETTLE, material)]; + witnesses.extend(vec!["0x".into(); funding_cells(funding).len()]); + Ok(transaction( + &inputs, + vec![ + json!({"capacity": format!("0x{RECEIPT_CAPACITY:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + json!({"capacity": format!("0x{change:x}"), "lock": always_success_lock("0x"), "type": Value::Null}), + ], + vec![hex0x(&material.event_data), "0x".into()], + deps, + witnesses, + vec![header.into()], + )) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn run( + root: &Path, + ckb_repo: Option<&Path>, + ckb_bin: Option<&Path>, + run_dir: Option<&Path>, + contract: Contract, + keep_node: bool, +) -> Result { + let root = fs::canonicalize(root)?; + let ckb_repo = fs::canonicalize(ckb_repo.map(Path::to_path_buf).unwrap_or_else(|| root.parent().unwrap().join("ckb")))?; + let ckb_bin = resolve_ckb_bin(&ckb_repo, ckb_bin)?; + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let run_dir = run_dir + .map(Path::to_path_buf) + .unwrap_or_else(|| root.join(format!("target/novaseal-rwa-receipt-devnet-stateful-live/{timestamp}"))); + fs::create_dir_all(&run_dir)?; + let run_dir = fs::canonicalize(run_dir)?; + let lifecycle_path = run_dir.join("nova-rwa-receipt-lifecycle-type.elf"); + compile_contract(&root, contract, &lifecycle_path)?; + let verifier_path = root.join("proposals/novaseal/v0-mvp-skeleton/target/novaseal-btc-verifier-riscv-shell-release.elf"); + if !verifier_path.is_file() { + bail!("missing verifier ELF: {}", verifier_path.display()); + } + let mut devnet = CkbDevnet::new(ckb_repo.clone(), ckb_bin.clone(), run_dir.clone())?; + let mut report = contract_report_header(contract, "rwa_receipt_materialize_claim_settle", &root, &ckb_repo, &ckb_bin, &run_dir); + let mut stage = "initializing"; + let scenario = (|| -> Result<()> { + stage = "start devnet"; + devnet.start()?; + stage = "deploy artifacts"; + let genesis = devnet.get_block_by_number(0)?; + let always = always_success_dep(genesis["transactions"][0]["hash"].as_str().context("genesis hash is missing")?); + let verifier = deploy_code(&mut devnet, "cellscript_btc_bip340_verifier_riscv", &fs::read(&verifier_path)?, &always)?; + let lifecycle = deploy_code(&mut devnet, "nova_rwa_receipt_lifecycle_type", &fs::read(&lifecycle_path)?, &always)?; + let lifecycle_hash = lifecycle["data_hash"].as_str().context("lifecycle hash is missing")?.to_owned(); + let deps = vec![verifier["cell_dep"].clone(), lifecycle["cell_dep"].clone(), always]; + let source_paths = [ + "proposals/novaseal/rwa-receipt-profile-v0/Cell.toml", + "proposals/novaseal/rwa-receipt-profile-v0/src", + "proposals/novaseal/rwa-receipt-profile-v0/schemas", + "proposals/novaseal/v0-mvp-skeleton/verifier/novaseal_btc_verifier", + "crates/cellscript-tools/src/novaseal_planned_rwa.rs", + "crates/cellscript-tools/src/ckb_devnet.rs", + ] + .into_iter() + .map(PathBuf::from) + .collect::>(); + let artifacts = BTreeMap::from([("verifier".into(), verifier_path.clone()), ("lifecycle".into(), lifecycle_path.clone())]); + let source_provenance = provenance(&root, &source_paths, &artifacts)?; + let base = base("live")?; + let type_script = lifecycle_type(&lifecycle_hash); + + stage = "valid materialize"; + let materialize = material(OP_MATERIALIZE, &base, None, false, false, None)?; + let header = devnet.rpc("get_tip_header", vec![])?; + let funding = devnet.collect_spendable(STATE_CAPACITY + RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = build_state_event( + OP_MATERIALIZE, + None, + &funding, + &lifecycle_hash, + deps.clone(), + header["hash"].as_str().unwrap(), + &materialize, + )?; + let materialize_dry = devnet.rpc("dry_run_transaction", vec![tx.clone()])?; + let materialize_commit = devnet.submit_and_commit(&tx, "RWA receipt materialize")?; + let materialize_hash = materialize_commit["tx_hash"].as_str().unwrap(); + let materialized_live = devnet.assert_live_cell( + materialize_hash, + 0, + "RWA materialized receipt", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&materialize.new_cell_data), + )?; + let materialized_event = devnet.assert_live_cell( + materialize_hash, + 1, + "RWA materialized audit event", + Some(RECEIPT_CAPACITY), + Some(&always_success_lock("0x")), + Some(&Value::Null), + Some(&materialize.event_data), + )?; + let materialized_ref = json!({"tx_hash": materialize_hash, "index": 0, "capacity": STATE_CAPACITY}); + + stage = "negative claim wrong holder signature"; + let header = devnet.rpc("get_tip_header", vec![])?; + let wrong_claim = material(OP_CLAIM, &base, Some(&materialize.new_cell), false, true, None)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = build_state_event( + OP_CLAIM, + Some(&materialized_ref), + &funding, + &lifecycle_hash, + deps.clone(), + header["hash"].as_str().unwrap(), + &wrong_claim, + )?; + let wrong_claim_reject = + devnet.dry_run_rejects(&tx, "RWA wrong holder claim", Some("Inputs[0].Type"), Some(&lifecycle_hash), Some(56))?; + let _post_claim_negative = devnet.assert_live_cell( + materialize_hash, + 0, + "post-negative RWA materialized receipt", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&materialize.new_cell_data), + )?; + + stage = "valid claim"; + let header = devnet.rpc("get_tip_header", vec![])?; + let claim = material(OP_CLAIM, &base, Some(&materialize.new_cell), false, false, None)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = build_state_event( + OP_CLAIM, + Some(&materialized_ref), + &funding, + &lifecycle_hash, + deps.clone(), + header["hash"].as_str().unwrap(), + &claim, + )?; + let claim_dry = devnet.rpc("dry_run_transaction", vec![tx.clone()])?; + let claim_commit = devnet.submit_and_commit(&tx, "RWA receipt claim")?; + let old_dead = devnet.wait_dead_cell(materialize_hash, 0)?; + let claim_hash = claim_commit["tx_hash"].as_str().unwrap(); + let claimed_live = devnet.assert_live_cell( + claim_hash, + 0, + "RWA claimed receipt", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&claim.new_cell_data), + )?; + let claim_event = devnet.assert_live_cell( + claim_hash, + 1, + "RWA claim event", + Some(RECEIPT_CAPACITY), + Some(&always_success_lock("0x")), + Some(&Value::Null), + Some(&claim.event_data), + )?; + let claimed_ref = json!({"tx_hash": claim_hash, "index": 0, "capacity": STATE_CAPACITY}); + + stage = "negative settlement wrong issuer signature"; + let header = devnet.rpc("get_tip_header", vec![])?; + let wrong_settle = material(OP_SETTLE, &base, Some(&claim.new_cell), true, false, None)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = build_settle(&claimed_ref, &funding, deps.clone(), header["hash"].as_str().unwrap(), &wrong_settle)?; + let wrong_settle_reject = + devnet.dry_run_rejects(&tx, "RWA wrong issuer settlement", Some("Inputs[0].Type"), Some(&lifecycle_hash), Some(56))?; + + stage = "negative settlement amount mutation"; + let amount_mutation = material(OP_SETTLE, &base, Some(&claim.new_cell), false, false, Some(claim.new_cell.amount - 1))?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = build_settle(&claimed_ref, &funding, deps.clone(), header["hash"].as_str().unwrap(), &amount_mutation)?; + let amount_reject = + devnet.dry_run_rejects(&tx, "RWA settlement amount mutation", Some("Inputs[0].Type"), Some(&lifecycle_hash), Some(5))?; + let post_negative = devnet.assert_live_cell( + claim_hash, + 0, + "post-negative RWA claimed receipt", + Some(STATE_CAPACITY), + Some(&always_success_lock("0x")), + Some(&type_script), + Some(&claim.new_cell_data), + )?; + + stage = "valid settle"; + let header = devnet.rpc("get_tip_header", vec![])?; + let settle = material(OP_SETTLE, &base, Some(&claim.new_cell), false, false, None)?; + let funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS)?; + let tx = build_settle(&claimed_ref, &funding, deps, header["hash"].as_str().unwrap(), &settle)?; + let settle_dry = devnet.rpc("dry_run_transaction", vec![tx.clone()])?; + let settle_commit = devnet.submit_and_commit(&tx, "RWA receipt settle")?; + let claim_dead = devnet.wait_dead_cell(claim_hash, 0)?; + let settle_event = devnet.assert_live_cell( + settle_commit["tx_hash"].as_str().unwrap(), + 0, + "RWA settlement event", + Some(RECEIPT_CAPACITY), + Some(&always_success_lock("0x")), + Some(&Value::Null), + Some(&settle.event_data), + )?; + + report.as_object_mut().unwrap().extend( + json!({ + "status": "passed", "live_devnet_rpc_executed": true, "stateful_lifecycle_executed": true, + "ckb_log": devnet.log_path.display().to_string(), "rpc_url": devnet.rpc_url, + "artifacts": {"verifier": verifier, "lifecycle": lifecycle}, "provenance": source_provenance, + "materialize": {"dry_run_cycles": materialize_dry["cycles"], "commit": materialize_commit, + "receipt_live": materialized_live["status"] == "live", "audit_event_live": materialized_event["status"] == "live", + "event_hash": hex0x(&materialize.receipt_hash)}, + "claim": {"dry_run_cycles": claim_dry["cycles"], "commit": claim_commit, + "old_receipt_not_live": old_dead["status"] != "live", "claimed_receipt_live": claimed_live["status"] == "live", + "claim_event_live": claim_event["status"] == "live", "event_hash": hex0x(&claim.receipt_hash)}, + "settle": {"dry_run_cycles": settle_dry["cycles"], "commit": settle_commit, + "old_claim_not_live": claim_dead["status"] != "live", "settlement_receipt_live": settle_event["status"] == "live", + "settlement_event_live": settle_event["status"] == "live", "amount_conserved": settle.old_cell.amount == claim.new_cell.amount, + "event_hash": hex0x(&settle.receipt_hash)}, + "negative_cases": {"wrong_holder_claim_dry_run": wrong_claim_reject, + "wrong_issuer_settlement_dry_run": wrong_settle_reject, "amount_mutation_dry_run": amount_reject, + "post_negative_state_still_live": post_negative["status"] == "live"}, + }) + .as_object() + .unwrap() + .clone(), + ); + Ok(()) + })(); + if let Err(error) = scenario { + report["status"] = json!("failed"); + report["stage"] = json!(stage); + report["error"] = json!(error.to_string()); + report["ckb_log"] = json!(devnet.log_path.display().to_string()); + report["rpc_url"] = json!(devnet.rpc_url); + } + if !keep_node { + devnet.stop(); + } + Ok(report) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn materialize_material_matches_legacy_vectors() { + let base = base("parity").unwrap(); + let value = material(OP_MATERIALIZE, &base, None, false, false, None).unwrap(); + assert_eq!(hex0x(&ckb_hash(&value.new_cell_data)), "0xa6022d9b654a0e062d2eefaea34e008ee12ac020f6f74c54bfedc7dcddfc1a3e"); + assert_eq!(hex0x(&ckb_hash(&value.signed_intent)), "0x265e8ffa7c5adaeeb7942713e8507bd53269953c5be222174b1b2804192a275f"); + assert_eq!(hex0x(&value.receipt_hash), "0xf85aeee6b63d3b9fc7eda2c9969cb31844cd1aa14eccf03c9f484dd1f7cc4790"); + assert_eq!(hex0x(&ckb_hash(&value.event_data)), "0xbadc9d1806c37c8223e7583455454e2aa754b4a517f9e077aeb8f91e165d0380"); + } +} diff --git a/crates/cellscript-tools/src/production_evidence.rs b/crates/cellscript-tools/src/production_evidence.rs new file mode 100644 index 00000000..d7688577 --- /dev/null +++ b/crates/cellscript-tools/src/production_evidence.rs @@ -0,0 +1,1247 @@ +//! Production CKB acceptance-evidence validation. +//! +//! This is the Rust implementation of the release-critical validator that +//! historically lived in the script-based release harness. +//! Keep the evidence schema and all fail-closed checks stable: old reports are +//! part of the repository's audit trail. + +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{bail, Context, Result}; +use serde_json::{json, Map, Value}; +use sha2::{Digest, Sha256}; + +use crate::crypto::{ckb_blake2b256, hex0x, sha256_hex}; + +pub(crate) const SOURCE_PROVENANCE_SCHEMA: &str = "cellscript-ckb-acceptance-source-provenance-v0.22"; +pub(crate) const BUILD_REPORT_SCHEMA: &str = "cellscript-ckb-build-report-v0.20"; +const EXPECTED_STATUS: &str = "passed"; +const EXPECTED_MODE: &str = "production"; +const EXPECTED_ACTION_COUNT: u64 = 43; + +pub(crate) const SOURCE_PROVENANCE_PATHS: &[&str] = &[ + "Cargo.lock", + "Cargo.toml", + "rust-toolchain.toml", + ".github/workflows/release.yml", + "src", + "examples", + "scripts/cellscript_gate.sh", + "scripts/cellscript_ckb_release_gate.sh", + "scripts/ckb_acceptance_pin.json", + "scripts/ckb_cellscript_acceptance.sh", + "crates/cellscript-tools/fixtures/ckb_acceptance/transactions-v0.23.json", + "crates/cellscript-tools/src/ckb_acceptance.rs", + "crates/cellscript-tools/src/ckb_acceptance_live.rs", + "crates/cellscript-tools/src/production_evidence.rs", +]; + +pub(crate) const EXPECTED_EXAMPLES: &[&str] = + &["amm_pool.cell", "launch.cell", "multisig.cell", "nft.cell", "timelock.cell", "token.cell", "vesting.cell"]; + +pub(crate) const EXPECTED_NON_PRODUCTION_EXAMPLES: &[&str] = &["registry.cell", "atomic_swap.cell", "multi_phase_dao.cell"]; + +pub(crate) const EXPECTED_LANGUAGE_EXAMPLES: &[&str] = &[ + "canonical_style.cell", + "order_book.cell", + "registry.cell", + "stdlib.cell", + "v0_14_capacity_time.cell", + "v0_14_ckb_type_id_create.cell", + "v0_14_delegate_verify.cell", + "v0_14_hash_blake2b.cell", + "v0_14_multi_step_pipeline.cell", + "v0_14_witness_source.cell", + "v0_15_identity_lifecycle.cell", + "v0_15_scoped_invariant.cell", + "v0_22_borrow.cell", + "v0_22_bounded_lifecycle.cell", + "v0_22_transaction_views.cell", +]; + +pub(crate) const EXPECTED_CRITICAL_ELF_ABI_EXAMPLES: &[&str] = &["launch.cell", "token.cell", "amm_pool.cell"]; + +pub(crate) const EXPECTED_END_TO_END_STATEFUL_SCENARIOS: &[&str] = &[ + "token.mint-with-authority-transfer-mint-with-authority-merge-burn", + "nft.mint-list-transfer-by-listing", + "timelock.create-lock-lock-asset-request-release-execute", + "launch.launch-token-then-mint-with-authority", + "amm.seed-add-swap-remove", + "vesting.create-config-grant-revoke", + "multisig.create-propose-approve-approve-execute", +]; + +pub(crate) const ACTION_RUNS: &[(&str, &str, &[&str])] = &[ + ("token_action_runs", "token.cell", &["mint_with_authority", "transfer_token", "burn", "merge"]), + ( + "nft_action_runs", + "nft.cell", + &[ + "create_collection", + "mint", + "transfer", + "create_listing", + "cancel_listing", + "buy_from_listing", + "create_offer", + "accept_offer", + "burn", + "batch_mint", + ], + ), + ( + "timelock_action_runs", + "timelock.cell", + &[ + "create_absolute_lock", + "create_relative_lock", + "lock_asset", + "request_release", + "request_emergency_release", + "approve_emergency_release", + "extend_lock", + "execute_release", + "execute_emergency_release", + "batch_create_locks", + ], + ), + ( + "multisig_action_runs", + "multisig.cell", + &[ + "create_wallet", + "propose_transfer", + "record_approval", + "execute_proposal", + "cancel_proposal", + "propose_add_signer", + "propose_remove_signer", + "propose_change_threshold", + ], + ), + ( + "vesting_action_runs", + "vesting.cell", + &["create_vesting_config", "grant_vesting", "claim_vested", "claim_fully_vested", "revoke_grant"], + ), + ("amm_action_runs", "amm_pool.cell", &["seed_pool", "swap_a_for_b", "add_liquidity", "remove_liquidity"]), + ("launch_action_runs", "launch.cell", &["launch_token", "bootstrap_token"]), +]; + +pub(crate) const PUBLIC_TIMELOCK_ACTIONS: &[&str] = &[ + "create_absolute_lock", + "create_relative_lock", + "lock_asset", + "request_release", + "execute_release", + "request_emergency_release", + "approve_emergency_release", + "execute_emergency_release", + "extend_lock", + "batch_create_locks", +]; + +pub(crate) const LOCKS: &[(&str, &[&str])] = &[ + ("multisig.cell", &["is_signer_lock", "can_execute", "can_cancel", "has_enough_approvals", "not_expired"]), + ("nft.cell", &["nft_ownership", "listing_seller", "offer_buyer", "valid_royalty", "collection_creator"]), + ("timelock.cell", &["can_unlock_lock", "is_owner", "lock_id_commitment", "asset_matches", "not_expired", "emergency_approved"]), + ("vesting.cell", &["vesting_admin"]), +]; + +fn invalid(message: impl std::fmt::Display) -> anyhow::Error { + anyhow::anyhow!("invalid CKB CellScript production evidence: {message}") +} + +fn require(condition: bool, message: impl std::fmt::Display) -> Result<()> { + if !condition { + bail!(invalid(message)); + } + Ok(()) +} + +fn object<'a>(value: &'a Value, context: &str) -> Result<&'a Map> { + value.as_object().ok_or_else(|| invalid(format!("{context} must be an object"))) +} + +fn array<'a>(value: Option<&'a Value>, context: &str) -> Result<&'a Vec> { + value.and_then(Value::as_array).ok_or_else(|| invalid(format!("{context} must be a list"))) +} + +fn nonempty_string<'a>(value: Option<&'a Value>, context: &str) -> Result<&'a str> { + let value = value.and_then(Value::as_str).ok_or_else(|| invalid(format!("{context} must be a non-empty string")))?; + require(!value.is_empty(), format!("{context} must be a non-empty string"))?; + Ok(value) +} + +fn require_field(mapping: &Map, key: &str, expected: Value, context: &str) -> Result<()> { + let actual = mapping.get(key).unwrap_or(&Value::Null); + let prefix = if context.is_empty() { String::new() } else { format!("{context}.") }; + require(actual == &expected, format!("{prefix}{key} must be {expected:?}, got {actual:?}")) +} + +fn require_empty(mapping: &Map, key: &str, context: &str) -> Result<()> { + require_field(mapping, key, json!([]), context) +} + +fn positive(value: Option<&Value>, context: &str) -> Result { + let number = value.and_then(Value::as_u64).filter(|number| *number > 0); + number.ok_or_else(|| invalid(format!("{context} must be a positive integer, got {:?}", value.unwrap_or(&Value::Null)))) +} + +fn boolean(value: Option<&Value>, context: &str) -> Result { + value + .and_then(Value::as_bool) + .ok_or_else(|| invalid(format!("{context} must be a boolean, got {:?}", value.unwrap_or(&Value::Null)))) +} + +fn hex_hash<'a>(value: Option<&'a Value>, context: &str) -> Result<&'a str> { + let value = value.and_then(Value::as_str).unwrap_or_default(); + require( + value.len() == 66 && value.starts_with("0x") && value[2..].bytes().all(|byte| byte.is_ascii_hexdigit()), + format!("{context} must be a 32-byte 0x-prefixed hex hash, got {value:?}"), + )?; + Ok(value) +} + +fn load_json(path: &Path) -> Result { + let bytes = fs::read(path).with_context(|| format!("missing CKB production evidence: {}", path.display()))?; + let value: Value = serde_json::from_slice(&bytes).with_context(|| format!("invalid JSON in {}", path.display()))?; + require(value.is_object(), format!("{} must contain a JSON object", path.display()))?; + Ok(value) +} + +fn git_stdout(repo_root: &Path, args: &[&str]) -> Result { + let output = Command::new("git") + .args(args) + .current_dir(repo_root) + .output() + .with_context(|| format!("failed to query git source provenance in {}", repo_root.display()))?; + require( + output.status.success(), + format!( + "failed to query git source provenance in {}: {}", + repo_root.display(), + String::from_utf8_lossy(&output.stderr).trim() + ), + )?; + Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned()) +} + +fn file_sha256(path: &Path) -> Result { + Ok(sha256_hex(&fs::read(path).with_context(|| format!("failed to read {}", path.display()))?)) +} + +fn expected_action_ids() -> Vec { + let mut ids = ACTION_RUNS + .iter() + .flat_map(|(_, example, actions)| actions.iter().map(move |action| Value::String(format!("{example}:{action}")))) + .collect::>(); + ids.sort_by(|left, right| left.as_str().cmp(&right.as_str())); + ids +} + +fn expected_lock_names() -> Vec { + LOCKS.iter().flat_map(|(example, locks)| locks.iter().map(move |lock| format!("{example}:{lock}"))).collect() +} + +fn expected_lock_scope() -> Value { + let mut map = Map::new(); + for (example, locks) in LOCKS { + map.insert((*example).to_owned(), json!(locks)); + } + Value::Object(map) +} + +fn expected_lock_count() -> u64 { + LOCKS.iter().map(|(_, locks)| locks.len() as u64).sum() +} + +fn public_actions(example: &str) -> &'static [&'static str] { + if example == "timelock.cell" { + return PUBLIC_TIMELOCK_ACTIONS; + } + ACTION_RUNS.iter().find(|(_, candidate, _)| *candidate == example).map(|(_, _, actions)| *actions).unwrap_or(&[]) +} + +fn validate_elf_entry_abi_gate(report: &Map) -> Result<()> { + let gate = object(report.get("ckb_elf_entry_abi_gate").unwrap_or(&Value::Null), "ckb_elf_entry_abi_gate")?; + for (key, expected) in [ + ("schema", json!("cellscript-ckb-elf-entry-abi-gate-v0.22")), + ("status", json!(EXPECTED_STATUS)), + ("requires_ckb_vm_stack_pointer_preserved", json!(true)), + ("requires_entry_trampoline_call_sequence", json!(true)), + ("requires_rx_only_executable_segment", json!(true)), + ("requires_no_fake_stack_load_segment", json!(true)), + ("critical_examples", json!(EXPECTED_CRITICAL_ELF_ABI_EXAMPLES)), + ] { + require_field(gate, key, expected, "ckb_elf_entry_abi_gate")?; + } + require_empty(gate, "failures", "ckb_elf_entry_abi_gate")?; + positive(gate.get("audited_artifact_count"), "ckb_elf_entry_abi_gate.audited_artifact_count")?; + + let critical = object(gate.get("critical_example_gate").unwrap_or(&Value::Null), "ckb_elf_entry_abi_gate.critical_example_gate")?; + for example in EXPECTED_CRITICAL_ELF_ABI_EXAMPLES { + let context = format!("ckb_elf_entry_abi_gate.critical_example_gate.{example}"); + let row = object(critical.get(*example).unwrap_or(&Value::Null), &context)?; + require_field(row, "status", json!(EXPECTED_STATUS), &context)?; + require_field(row, "missing", json!(false), &context)?; + require_empty(row, "failures", &context)?; + positive(row.get("artifact_count"), &format!("{context}.artifact_count"))?; + } + + let rows = array(gate.get("rows"), "ckb_elf_entry_abi_gate.rows")?; + require(!rows.is_empty(), "ckb_elf_entry_abi_gate.rows must be a non-empty list")?; + for (index, value) in rows.iter().enumerate() { + let context = format!("ckb_elf_entry_abi_gate.rows[{index}]"); + let row = object(value, &context)?; + for (key, expected) in [ + ("status", json!(EXPECTED_STATUS)), + ("preserves_ckb_vm_stack_pointer", json!(true)), + ("entry_trampoline_calls_with_ra", json!(true)), + ("executable_segment_rx_only", json!(true)), + ("executable_segment_file_size_equals_memory_size", json!(true)), + ("first_instruction_le_hex", json!("0x00000097")), + ("trampoline_instructions_le_hex", json!(["0x00000097", "0x014080e7", "0x000008b7", "0x05d88893", "0x00000073"])), + ("trampoline_bytes_hex", json!("97000000e7804001b70800009388d80573000000")), + ("exit_syscall_number", json!(93)), + ("exit_sequence_exact", json!(true)), + ] { + require_field(row, key, expected, &context)?; + } + nonempty_string(row.get("artifact"), &format!("{context}.artifact"))?; + require_field(row, "call_target", row.get("expected_call_target").cloned().unwrap_or(Value::Null), &context)?; + } + Ok(()) +} + +fn tracked_source_files(repo_root: &Path) -> Result> { + let mut args = vec!["ls-files", "--"]; + args.extend(SOURCE_PROVENANCE_PATHS); + Ok(git_stdout(repo_root, &args)? + .lines() + .filter(|line| !line.is_empty() && repo_root.join(line).is_file()) + .map(str::to_owned) + .collect()) +} + +fn tracked_source_sha256(repo_root: &Path, files: &[String]) -> Result { + let mut digest = Sha256::new(); + for relative in files { + digest.update(relative.as_bytes()); + digest.update([0]); + digest.update(file_sha256(&repo_root.join(relative))?.as_bytes()); + digest.update(b"\n"); + } + Ok(format!("0x{}", hex::encode(digest.finalize()))) +} + +pub(crate) fn current_source_provenance(repo_root: &Path) -> Result> { + let files = tracked_source_files(repo_root)?; + let mut current = Map::new(); + current.insert("repo_commit".into(), json!(git_stdout(repo_root, &["rev-parse", "HEAD"])?)); + current.insert("git_dirty".into(), json!(!git_stdout(repo_root, &["status", "--porcelain", "--untracked-files=all"])?.is_empty())); + current.insert("tracked_source_paths".into(), json!(SOURCE_PROVENANCE_PATHS)); + current.insert("tracked_source_files".into(), json!(files)); + current.insert("tracked_source_file_count".into(), json!(files.len())); + current.insert("tracked_source_sha256".into(), json!(tracked_source_sha256(repo_root, &files)?)); + current.insert( + "acceptance_script_sha256".into(), + json!(format!("0x{}", file_sha256(&repo_root.join("scripts/ckb_cellscript_acceptance.sh"))?)), + ); + current.insert( + "validator_script_sha256".into(), + json!(format!("0x{}", file_sha256(&repo_root.join("crates/cellscript-tools/src/production_evidence.rs"))?)), + ); + Ok(current) +} + +fn validate_source_provenance(report: &Map, repo_root: &Path) -> Result<()> { + let provenance = object(report.get("source_provenance").unwrap_or(&Value::Null), "source_provenance")?; + require_field(provenance, "schema", json!(SOURCE_PROVENANCE_SCHEMA), "source_provenance")?; + require( + provenance.get("generated_at_utc").is_some_and(Value::is_string), + "source_provenance.generated_at_utc must be a timestamp string", + )?; + require_field(provenance, "git_dirty", json!(false), "source_provenance")?; + let current = current_source_provenance(repo_root)?; + for key in [ + "repo_commit", + "git_dirty", + "tracked_source_paths", + "tracked_source_files", + "tracked_source_file_count", + "tracked_source_sha256", + "acceptance_script_sha256", + "validator_script_sha256", + ] { + require_field(provenance, key, current.get(key).cloned().unwrap_or(Value::Null), "source_provenance")?; + } + Ok(()) +} + +fn recursive_files(root: &Path) -> Result> { + fn visit(path: &Path, files: &mut Vec) -> Result<()> { + let mut entries = fs::read_dir(path)?.collect::, _>>()?; + entries.sort_by_key(std::fs::DirEntry::file_name); + for entry in entries { + let path = entry.path(); + if path.is_dir() { + visit(&path, files)?; + } else if path.is_file() { + files.push(path); + } + } + Ok(()) + } + let mut files = Vec::new(); + visit(root, &mut files)?; + files.sort(); + Ok(files) +} + +fn validate_public_builder_contracts(report: &Map) -> Result<()> { + let gate = object(report.get("public_builder_contracts").unwrap_or(&Value::Null), "public_builder_contracts")?; + for (key, expected) in [ + ("schema", json!("cellscript-public-builder-contract-gate-v0.22")), + ("status", json!(EXPECTED_STATUS)), + ("example_count", json!(EXPECTED_EXAMPLES.len())), + ("action_count", json!(EXPECTED_ACTION_COUNT)), + ("requires_gen_builder", json!(true)), + ("requires_action_build", json!(true)), + ("transaction_origin_claim", json!("acceptance-rust-harness-not-generated-builder")), + ] { + require_field(gate, key, expected, "public_builder_contracts")?; + } + let contracts = array(gate.get("contracts"), "public_builder_contracts.contracts")?; + let actual_examples = contracts.iter().filter_map(|row| row.get("example")).cloned().collect::>(); + require( + actual_examples == json!(EXPECTED_EXAMPLES).as_array().cloned().unwrap(), + "public builder examples must match exact release scope", + )?; + let mut seen_action_ids = Vec::new(); + for contract_value in contracts { + let contract = object(contract_value, "public_builder_contracts.contracts[]")?; + let example = nonempty_string(contract.get("example"), "public_builder_contracts.contracts[].example")?; + let context = format!("public_builder_contracts.{example}"); + let actions = public_actions(example); + for (key, expected) in [ + ("status", json!(EXPECTED_STATUS)), + ("generator_schema", json!("cellscript-generated-builder-summary-v0.20")), + ("builder_manifest_schema", json!("cellscript-generated-action-builder-v0.23-edition-2026")), + ("target", json!("typescript")), + ("target_profile", json!("ckb")), + ("actions", json!(actions)), + ("action_count", json!(actions.len())), + ("runtime_adapter_execution", json!("not-proven-by-this-contract-gate")), + ] { + require_field(contract, key, expected, &context)?; + } + hex_hash(contract.get("manifest_sha256"), &format!("{context}.manifest_sha256"))?; + hex_hash(contract.get("generated_tree_sha256"), &format!("{context}.generated_tree_sha256"))?; + positive(contract.get("generated_file_count"), &format!("{context}.generated_file_count"))?; + let manifest_path = PathBuf::from(contract.get("manifest_path").and_then(Value::as_str).unwrap_or_default()); + require(manifest_path.is_file(), format!("{context}.manifest_path does not exist: {}", manifest_path.display()))?; + require_field(contract, "manifest_sha256", json!(format!("0x{}", file_sha256(&manifest_path)?)), &context)?; + let manifest = load_json(&manifest_path)?; + let manifest = object(&manifest, &format!("{context}.manifest"))?; + let manifest_actions = array(manifest.get("actions"), &format!("{context}.manifest.actions"))?; + let manifest_names = + manifest_actions.iter().map(|value| value.get("name").cloned().unwrap_or(Value::Null)).collect::>(); + require(manifest_names == json!(actions).as_array().cloned().unwrap(), format!("{context} manifest action mismatch"))?; + + let generated_files = recursive_files(manifest_path.parent().context("builder manifest has no parent directory")?)?; + let mut tree_hash = Sha256::new(); + for path in &generated_files { + let relative = path.strip_prefix(manifest_path.parent().unwrap())?.to_string_lossy().replace('\\', "/"); + tree_hash.update(relative.as_bytes()); + tree_hash.update([0]); + tree_hash.update(Sha256::digest(fs::read(path)?)); + } + require_field(contract, "generated_file_count", json!(generated_files.len()), &context)?; + require_field(contract, "generated_tree_sha256", json!(format!("0x{}", hex::encode(tree_hash.finalize()))), &context)?; + + let plans = array(contract.get("action_plans"), &format!("{context}.action_plans"))?; + require(plans.len() == actions.len(), format!("{context}.action_plans must cover every action"))?; + for (plan_value, action) in plans.iter().zip(actions.iter()) { + let plan = object(plan_value, &format!("{context}.action_plans.{action}"))?; + let plan_context = format!("{context}.action_plans.{action}"); + let contract_id = format!("{example}:{action}"); + for (key, expected) in [ + ("action", json!(action)), + ("contract_id", json!(contract_id)), + ("policy", json!("cellscript-action-builder-plan-v1")), + ("status", json!(EXPECTED_STATUS)), + ] { + require_field(plan, key, expected, &plan_context)?; + } + hex_hash(plan.get("plan_sha256"), &format!("{plan_context}.plan_sha256"))?; + let plan_path = PathBuf::from(plan.get("plan_path").and_then(Value::as_str).unwrap_or_default()); + require(plan_path.is_file(), format!("{plan_context}.plan_path does not exist: {}", plan_path.display()))?; + require_field(plan, "plan_sha256", json!(format!("0x{}", file_sha256(&plan_path)?)), &plan_context)?; + let plan_json = load_json(&plan_path)?; + let plan_json = object(&plan_json, &format!("{plan_context}.file"))?; + for (key, expected) in [ + ("status", json!("ok")), + ("policy", json!("cellscript-action-builder-plan-v1")), + ("action", json!(action)), + ("target_profile", json!("ckb")), + ] { + require_field(plan_json, key, expected, &format!("{plan_context}.file"))?; + } + seen_action_ids.push(Value::String(contract_id)); + } + } + seen_action_ids.sort_by(|left, right| left.as_str().cmp(&right.as_str())); + require(seen_action_ids == expected_action_ids(), "public builder action contracts must match the exact production action matrix") +} + +fn validate_ckb_runtime_provenance(report: &Map, repo_root: &Path, report_dir: &Path) -> Result<()> { + let pin_path = repo_root.join("scripts/ckb_acceptance_pin.json"); + let pin_value = load_json(&pin_path)?; + let pin = object(&pin_value, "ckb_acceptance_pin")?; + require_field(pin, "schema", json!("cellscript-ckb-acceptance-pin-v0.22"), "ckb_acceptance_pin")?; + + let provenance = object(report.get("ckb_runtime_provenance").unwrap_or(&Value::Null), "ckb_runtime_provenance")?; + let context = "ckb_runtime_provenance"; + for (key, expected) in [ + ("schema", json!("cellscript-ckb-runtime-provenance-v0.22")), + ("pin_schema", pin.get("schema").cloned().unwrap_or(Value::Null)), + ("pin_file_sha256", json!(format!("0x{}", file_sha256(&pin_path)?))), + ("repository", pin.get("repository").cloned().unwrap_or(Value::Null)), + ("revision", pin.get("revision").cloned().unwrap_or(Value::Null)), + ("repo_head", pin.get("revision").cloned().unwrap_or(Value::Null)), + ("repo_dirty", json!(false)), + ("version", pin.get("version").cloned().unwrap_or(Value::Null)), + ("build_mode", json!("fresh-dedicated-cargo-target")), + ("cxxflags", json!("-include cstdint")), + ("cxx_compatibility_contract", json!("ckb-librocksdb-sys-8.5.4-explicit-cstdint-v1")), + ("binary_archived_with_report", json!(true)), + ] { + require_field(provenance, key, expected, context)?; + } + let version = nonempty_string(pin.get("version"), "ckb_acceptance_pin.version")?; + let revision = nonempty_string(pin.get("revision"), "ckb_acceptance_pin.revision")?; + let version_output = nonempty_string(provenance.get("version_output"), &format!("{context}.version_output"))?; + require( + version_output.contains(version) && version_output.contains(&revision[..7]), + format!("{context}.version_output must bind version and revision, got {version_output:?}"), + )?; + + let ckb_repo = fs::canonicalize(PathBuf::from(report.get("ckb_repo").and_then(Value::as_str).unwrap_or_default())) + .unwrap_or_else(|_| PathBuf::from(report.get("ckb_repo").and_then(Value::as_str).unwrap_or_default())); + require(ckb_repo.is_dir(), format!("ckb_repo does not exist: {}", ckb_repo.display()))?; + require(git_stdout(&ckb_repo, &["rev-parse", "HEAD"])? == revision, "current CKB checkout does not match pin")?; + require( + git_stdout(&ckb_repo, &["status", "--porcelain", "--untracked-files=all"])?.is_empty(), + "current CKB checkout must be clean", + )?; + + let binary_path = fs::canonicalize(PathBuf::from(provenance.get("binary_path").and_then(Value::as_str).unwrap_or_default())) + .unwrap_or_else(|_| PathBuf::from(provenance.get("binary_path").and_then(Value::as_str).unwrap_or_default())); + require(binary_path.is_file(), format!("{context}.binary_path does not exist: {}", binary_path.display()))?; + let expected_binary = fs::canonicalize(report_dir.join("ckb-runtime/ckb")).unwrap_or_else(|_| report_dir.join("ckb-runtime/ckb")); + require_field(provenance, "binary_path", json!(expected_binary.to_string_lossy()), context)?; + require_field(provenance, "binary_sha256", json!(format!("0x{}", file_sha256(&binary_path)?)), context)?; + let binary_version = Command::new(&binary_path) + .arg("--version") + .output() + .with_context(|| format!("failed to execute {} --version", binary_path.display()))?; + require(binary_version.status.success(), format!("{} --version failed", binary_path.display()))?; + require_field(provenance, "version_output", json!(String::from_utf8_lossy(&binary_version.stdout).trim()), context)?; + + let templates = array(pin.get("template_paths"), "ckb_acceptance_pin.template_paths")?; + require(templates.len() >= 2, "ckb_acceptance_pin.template_paths must contain config and spec paths")?; + for (key, template) in [("source_template_path", &templates[0]), ("source_spec_path", &templates[1])] { + let path = ckb_repo.join(nonempty_string(Some(template), &format!("ckb_acceptance_pin.{key}"))?); + require_field(provenance, key, json!(path.to_string_lossy()), context)?; + require(path.is_file(), format!("{context}.{key} does not exist: {}", path.display()))?; + require_field(provenance, &key.replace("_path", "_sha256"), json!(format!("0x{}", file_sha256(&path)?)), context)?; + } + for key in ["effective_config", "effective_spec"] { + let path = PathBuf::from(provenance.get(&format!("{key}_path")).and_then(Value::as_str).unwrap_or_default()); + require(path.is_file(), format!("{context}.{key}_path does not exist: {}", path.display()))?; + require_field(provenance, &format!("{key}_sha256"), json!(format!("0x{}", file_sha256(&path)?)), context)?; + } + hex_hash(provenance.get("genesis_hash"), &format!("{context}.genesis_hash"))?; + let onchain_genesis = report.get("onchain").and_then(|value| value.get("genesis_hash")).cloned().unwrap_or(Value::Null); + require_field(provenance, "genesis_hash", onchain_genesis, context) +} + +fn validate_build_reports(report: &Map, compile_only: bool) -> Result<()> { + let build_index = object(report.get("cellscript_build_reports").unwrap_or(&Value::Null), "cellscript_build_reports")?; + for (key, expected) in [ + ("schema", json!("cellscript-ckb-build-report-index-v0.20")), + ("target_profile", json!("ckb")), + ("vm_profile", json!("ckb-vm")), + ("artifact_format", json!("riscv64-elf")), + ("artifact_hash_algorithm", json!("ckb-blake2b256")), + ("requires_exact_artifact_hash", json!(true)), + ("requires_elf_entry_abi_gate", json!(true)), + ("requires_live_code_cell_data_hash_match", json!(true)), + ("status", json!(EXPECTED_STATUS)), + ] { + require_field(build_index, key, expected, "cellscript_build_reports")?; + } + let rows = array(build_index.get("reports"), "cellscript_build_reports.reports")?; + require(!rows.is_empty(), "cellscript_build_reports.reports must be a non-empty list")?; + require_field(build_index, "artifact_count", json!(rows.len()), "cellscript_build_reports")?; + let elf_gate = report.get("ckb_elf_entry_abi_gate").and_then(Value::as_object).cloned().unwrap_or_default(); + require_field( + build_index, + "artifact_count", + elf_gate.get("audited_artifact_count").cloned().unwrap_or(Value::Null), + "cellscript_build_reports", + )?; + + let mut seen_artifacts = BTreeSet::new(); + for (index, value) in rows.iter().enumerate() { + let context = format!("cellscript_build_reports.reports[{index}]"); + let row = object(value, &context)?; + for (key, expected) in [ + ("schema", json!(BUILD_REPORT_SCHEMA)), + ("target_profile", json!("ckb")), + ("vm_profile", json!("ckb-vm")), + ("artifact_format", json!("riscv64-elf")), + ("artifact_hash_algorithm", json!("ckb-blake2b256")), + ("deployment_hash_type_used_by_gate", json!("data1")), + ("verify_artifact_status", json!("passed")), + ("verify_target_profile", json!("ckb")), + ("elf_entry_abi_status", json!("passed")), + ("abi_trailer_stripped", json!(true)), + ] { + require_field(row, key, expected, &context)?; + } + let artifact_size = positive(row.get("artifact_size_bytes"), &format!("{context}.artifact_size_bytes"))?; + hex_hash(row.get("deployable_elf_hash"), &format!("{context}.deployable_elf_hash"))?; + hex_hash(row.get("artifact_sha256"), &format!("{context}.artifact_sha256"))?; + let artifact_path = nonempty_string(row.get("artifact_path"), &format!("{context}.artifact_path"))?; + require(seen_artifacts.insert(artifact_path.to_owned()), format!("duplicate build report artifact_path: {artifact_path}"))?; + let artifact = PathBuf::from(artifact_path); + require(artifact.exists(), format!("{context}.artifact_path does not exist: {}", artifact.display()))?; + let bytes = fs::read(&artifact)?; + require(bytes.len() as u64 == artifact_size, format!("{context}.artifact_size_bytes does not match artifact"))?; + require_field(row, "deployable_elf_hash", json!(hex0x(&ckb_blake2b256(&bytes)?)), &context)?; + require_field(row, "artifact_sha256", json!(format!("0x{}", sha256_hex(&bytes))), &context)?; + let deployments = array(row.get("onchain_deployments"), &format!("{context}.onchain_deployments"))?; + if compile_only { + require(deployments.is_empty(), format!("{context}.onchain_deployments must be empty for compile-only reports"))?; + } else { + require(!deployments.is_empty(), format!("{context}.onchain_deployments must contain live deployment evidence"))?; + for (deployment_index, deployment_value) in deployments.iter().enumerate() { + let deployment_context = format!("{context}.onchain_deployments[{deployment_index}]"); + let deployment = object(deployment_value, &deployment_context)?; + for (key, expected) in [ + ("code_cell_live", json!(true)), + ("live_code_cell_data_hash_matches_artifact", json!(true)), + ("artifact_ckb_data_hash_blake2b", row.get("deployable_elf_hash").cloned().unwrap_or(Value::Null)), + ("live_code_cell_data_hash", row.get("deployable_elf_hash").cloned().unwrap_or(Value::Null)), + ] { + require_field(deployment, key, expected, &deployment_context)?; + } + let out_point = + object(deployment.get("out_point").unwrap_or(&Value::Null), &format!("{deployment_context}.out_point"))?; + for key in ["tx_hash", "index"] { + let value = out_point.get(key).and_then(Value::as_str).unwrap_or_default(); + require(value.starts_with("0x"), format!("{deployment_context}.out_point.{key} must be hex"))?; + } + } + } + } + if compile_only { + require( + build_index.get("onchain_deployed_artifact_count").is_none_or(|value| value.is_null() || value == &json!(0)), + "compile-only build reports must not record onchain deployments", + )?; + } else { + require_field(build_index, "onchain_deployed_artifact_count", json!(rows.len()), "cellscript_build_reports")?; + require_field(build_index, "live_code_cell_data_hash_match_count", json!(rows.len()), "cellscript_build_reports")?; + for key in ["missing_onchain_deployments", "live_code_cell_data_hash_mismatches", "unexpected_onchain_artifacts"] { + require_empty(build_index, key, "cellscript_build_reports")?; + } + } + Ok(()) +} + +fn validate_compile_gate(report: &Map, compile_only: bool) -> Result<()> { + for (key, expected) in [ + ("acceptance_mode", json!(EXPECTED_MODE)), + ("status", json!(EXPECTED_STATUS)), + ("production_ready", json!(!compile_only)), + ("bundled_examples_count", json!(EXPECTED_EXAMPLES.len())), + ("bundled_examples_exact_order", json!(EXPECTED_EXAMPLES)), + ("non_production_examples", json!(EXPECTED_NON_PRODUCTION_EXAMPLES)), + ("language_examples_count", json!(EXPECTED_LANGUAGE_EXAMPLES.len())), + ("language_examples_exact_order", json!(EXPECTED_LANGUAGE_EXAMPLES)), + ("original_scoped_action_count", json!(EXPECTED_ACTION_COUNT)), + ("original_scoped_lock_count", json!(expected_lock_count())), + ("original_scoped_action_fail_closed_count", json!(0)), + ("original_scoped_lock_fail_closed_count", json!(0)), + ] { + require_field(report, key, expected, "")?; + } + for key in [ + "strict_original_ckb_compile_policy_fail_closed", + "strict_original_ckb_compile_unexpected_failures", + "original_scoped_action_fail_closed", + "original_scoped_lock_fail_closed", + ] { + require_empty(report, key, "")?; + } + + let gate = object(report.get("production_gate").unwrap_or(&Value::Null), "production_gate")?; + for (key, expected) in [ + ("status", json!(EXPECTED_STATUS)), + ("requires_original_scoped_harnesses", json!(true)), + ("requires_no_expected_fail_closed_entries", json!(true)), + ("requires_all_bundled_examples_strict_original_ckb", json!(true)), + ("requires_ckb_elf_entry_abi_gate", json!(true)), + ("requires_cellscript_build_reports", json!(true)), + ("requires_public_builder_contracts", json!(true)), + ] { + require_field(gate, key, expected, "production_gate")?; + } + require_empty(gate, "failures", "production_gate")?; + validate_elf_entry_abi_gate(report)?; + validate_build_reports(report, compile_only)?; + + let coverage = object(report.get("ckb_business_coverage").unwrap_or(&Value::Null), "ckb_business_coverage")?; + require_field(coverage, "strict_compile_coverage_complete", json!(true), "ckb_business_coverage")?; + require_field(coverage, "expected_fail_closed_action_count", json!(0), "ckb_business_coverage")?; + require_field(coverage, "expected_fail_closed_lock_count", json!(0), "ckb_business_coverage")?; + if compile_only { + for (key, expected) in [ + ("status", json!("incomplete")), + ("onchain_action_coverage_complete", json!(false)), + ("ckb_onchain_action_count", json!(0)), + ] { + require_field(coverage, key, expected, "ckb_business_coverage")?; + } + let onchain = object(report.get("onchain").unwrap_or(&Value::Null), "onchain")?; + require_field(onchain, "status", json!("skipped"), "onchain")?; + require_field(onchain, "reason", json!("compile-only"), "onchain")?; + } else { + require_field(coverage, "status", json!("complete"), "ckb_business_coverage")?; + require_field(coverage, "onchain_action_coverage_complete", json!(true), "ckb_business_coverage")?; + require_field(coverage, "ckb_onchain_action_count", json!(EXPECTED_ACTION_COUNT), "ckb_business_coverage")?; + let missing = coverage.get("missing_ckb_onchain_actions").unwrap_or(&Value::Null); + require( + missing.is_null() || missing.as_object().is_some_and(Map::is_empty), + format!("ckb_business_coverage.missing_ckb_onchain_actions must be empty, got {missing:?}"), + )?; + } + + let example_scope = object(report.get("example_scope").unwrap_or(&Value::Null), "example_scope")?; + for (key, expected) in [ + ("production_bundled_examples", json!(EXPECTED_EXAMPLES)), + ("non_production_top_level_examples", json!(EXPECTED_NON_PRODUCTION_EXAMPLES)), + ("non_production_language_examples", json!(EXPECTED_LANGUAGE_EXAMPLES)), + ] { + require_field(example_scope, key, expected, "example_scope")?; + } + let scope_note = example_scope.get("production_scope_note").and_then(Value::as_str).unwrap_or_default(); + require( + scope_note.contains("Only production_bundled_examples") + && scope_note.contains("non_production_top_level_examples") + && scope_note.contains("non_production_language_examples"), + "example_scope.production_scope_note must state the production/non-production example boundary", + )?; + + let source_layout = object(report.get("example_source_layout").unwrap_or(&Value::Null), "example_source_layout")?; + require( + source_layout.get("canonical_bundled_examples").is_some_and(Value::is_string), + "example_source_layout must record canonical_bundled_examples", + )?; + require( + source_layout.get("language_examples").is_some_and(Value::is_string), + "example_source_layout must record language_examples", + )?; + require( + !source_layout.contains_key("production_acceptance_examples") + && !source_layout.contains_key("canonical_business_examples") + && !source_layout.contains_key("flat_business_compatibility_examples"), + "example_source_layout must not advertise the removed business/acceptance split", + )?; + let layout_note = source_layout.get("canonical_examples_note").and_then(Value::as_str).unwrap_or_default(); + require( + layout_note.contains("top-level examples/*.cell directly") + && layout_note.contains("examples/business and examples/acceptance"), + "example_source_layout.canonical_examples_note must state the single-source example layout", + )?; + + let lock_scope = object(report.get("lock_acceptance_scope").unwrap_or(&Value::Null), "lock_acceptance_scope")?; + if lock_scope.get("onchain_lock_spend_matrix") == Some(&json!(true)) { + require_field(lock_scope, "strict_compile_only", json!(false), "lock_acceptance_scope")?; + require_field(lock_scope, "onchain_lock_spend_matrix_scope", expected_lock_scope(), "lock_acceptance_scope")?; + require_field(lock_scope, "required_cases_per_lock", json!(["valid_spend", "invalid_spend"]), "lock_acceptance_scope")?; + } else { + require_field(lock_scope, "strict_compile_only", json!(true), "lock_acceptance_scope")?; + require_field(lock_scope, "onchain_lock_spend_matrix", json!(false), "lock_acceptance_scope")?; + require_field(lock_scope, "pending_onchain_lock_spend_matrix", expected_lock_scope(), "lock_acceptance_scope")?; + require_field( + lock_scope, + "required_cases_per_lock_when_promoted", + json!(["valid_spend", "invalid_spend"]), + "lock_acceptance_scope", + )?; + } + let lock_note = lock_scope.get("scope_note").and_then(Value::as_str).unwrap_or_default(); + require(lock_note.contains("strict-compiled"), "lock_acceptance_scope.scope_note must mention strict compilation") +} + +fn all_action_runs(report: &Map) -> Result>> { + let onchain = object(report.get("onchain").unwrap_or(&Value::Null), "onchain")?; + let mut runs = Vec::new(); + for (key, _, expected_actions) in ACTION_RUNS { + let values = array(onchain.get(*key), &format!("onchain.{key}"))?; + let actual_actions = values + .iter() + .filter_map(Value::as_object) + .map(|row| row.get("action").cloned().unwrap_or(Value::Null)) + .collect::>(); + let mut sorted_actual = actual_actions.clone(); + sorted_actual.sort_by(|left, right| left.as_str().cmp(&right.as_str())); + let mut sorted_expected = json!(expected_actions).as_array().cloned().unwrap(); + sorted_expected.sort_by(|left, right| left.as_str().cmp(&right.as_str())); + require( + sorted_actual == sorted_expected && actual_actions.len() == expected_actions.len(), + format!("onchain.{key} actions must match the production matrix, got {actual_actions:?}"), + )?; + let unique = actual_actions.iter().filter_map(Value::as_str).collect::>(); + require( + unique.len() == actual_actions.len(), + format!("onchain.{key} must not contain duplicate actions, got {actual_actions:?}"), + )?; + for value in values { + runs.push(object(value, &format!("onchain.{key} entries"))?); + } + } + Ok(runs) +} + +fn validate_code_section(row: &Map, name: &str) -> Result<()> { + let code = object(row.get("code").unwrap_or(&Value::Null), &format!("{name}.code"))?; + boolean(code.get("code_cell_live"), &format!("{name}.code.code_cell_live"))?; + positive(code.get("artifact_size_bytes"), &format!("{name}.code.artifact_size_bytes"))?; + require_field(code, "live_code_cell_data_hash_matches_artifact", json!(true), &format!("{name}.code"))?; + hex_hash(code.get("artifact_ckb_data_hash_blake2b"), &format!("{name}.code.artifact_ckb_data_hash_blake2b"))?; + require_field( + code, + "live_code_cell_data_hash", + code.get("artifact_ckb_data_hash_blake2b").cloned().unwrap_or(Value::Null), + &format!("{name}.code"), + ) +} + +fn validate_measured_constraints(measured: &Map, name: &str, require_output_lists: bool) -> Result<()> { + let context = format!("{name}.measured_constraints"); + for (key, expected) in [ + ("cycles_status", json!("dry-run-measured")), + ("tx_size_status", json!("measured-by-cellscript-ckb-tx-measure")), + ("occupied_capacity_status", json!("derived-by-cellscript-ckb-tx-measure")), + ] { + require_field(measured, key, expected, &context)?; + } + positive(measured.get("measured_cycles"), &format!("{context}.measured_cycles"))?; + positive(measured.get("consensus_serialized_tx_size_bytes"), &format!("{context}.consensus_serialized_tx_size_bytes"))?; + let occupied = positive(measured.get("occupied_capacity_shannons"), &format!("{context}.occupied_capacity_shannons"))?; + let output_capacity = positive(measured.get("output_capacity_shannons"), &format!("{context}.output_capacity_shannons"))?; + require(output_capacity >= occupied, format!("{name} output capacity is below occupied capacity"))?; + if require_output_lists { + let output_count = positive(measured.get("output_count"), &format!("{context}.output_count"))? as usize; + let capacities = + array(measured.get("measured_output_capacity_shannons"), &format!("{context}.measured_output_capacity_shannons"))?; + let occupied_capacities = + array(measured.get("output_occupied_capacity_shannons"), &format!("{context}.output_occupied_capacity_shannons"))?; + require(capacities.len() == output_count, format!("{name} measured output capacity count does not match output_count"))?; + require( + occupied_capacities.len() == output_count, + format!("{name} occupied output capacity count does not match output_count"), + )?; + for (index, (capacity, occupied_capacity)) in capacities.iter().zip(occupied_capacities).enumerate() { + let capacity = positive(Some(capacity), &format!("{context}.measured_output_capacity_shannons[{index}]"))?; + let occupied_capacity = + positive(Some(occupied_capacity), &format!("{context}.output_occupied_capacity_shannons[{index}]"))?; + require(capacity >= occupied_capacity, format!("{name} output {index} capacity is below occupied capacity"))?; + } + } + require(measured.get("capacity_is_sufficient") == Some(&json!(true)), format!("{name} has insufficient capacity"))?; + require(measured.get("under_capacity_output_indexes") == Some(&json!([])), format!("{name} has under-capacity outputs")) +} + +fn validate_stateful_scenarios(onchain: &Map) -> Result<()> { + let stateful = object(onchain.get("stateful_scenarios").unwrap_or(&Value::Null), "onchain.stateful_scenarios")?; + require_field(stateful, "status", json!(EXPECTED_STATUS), "onchain.stateful_scenarios")?; + let scenario_count = positive(stateful.get("scenario_count"), "onchain.stateful_scenarios.scenario_count")? as usize; + positive(stateful.get("step_count"), "onchain.stateful_scenarios.step_count")?; + require_field( + stateful, + "end_to_end_scenario_count", + json!(EXPECTED_END_TO_END_STATEFUL_SCENARIOS.len()), + "onchain.stateful_scenarios", + )?; + require_field( + stateful, + "action_branch_scenario_count", + json!(scenario_count - EXPECTED_END_TO_END_STATEFUL_SCENARIOS.len()), + "onchain.stateful_scenarios", + )?; + let coverage = object( + stateful.get("stateful_action_coverage").unwrap_or(&Value::Null), + "onchain.stateful_scenarios.stateful_action_coverage", + )?; + for (key, expected) in [ + ("status", json!(EXPECTED_STATUS)), + ("required_action_count", json!(EXPECTED_ACTION_COUNT)), + ("covered_action_count", json!(EXPECTED_ACTION_COUNT)), + ("required_action_ids", Value::Array(expected_action_ids())), + ("covered_action_ids", Value::Array(expected_action_ids())), + ] { + require_field(coverage, key, expected, "stateful_action_coverage")?; + } + for key in ["missing_action_ids", "missing_artifact_ids", "unexpected_artifact_ids"] { + require_empty(coverage, key, "stateful_action_coverage")?; + } + let runs = array(stateful.get("runs"), "onchain.stateful_scenarios.runs")?; + require(runs.len() == scenario_count, "stateful scenario runs must match scenario_count")?; + let leading_names = runs + .iter() + .take(EXPECTED_END_TO_END_STATEFUL_SCENARIOS.len()) + .map(|run| run.get("name").cloned().unwrap_or(Value::Null)) + .collect::>(); + require( + leading_names == json!(EXPECTED_END_TO_END_STATEFUL_SCENARIOS).as_array().cloned().unwrap(), + "stateful end-to-end scenario names/order must match the production matrix", + )?; + + let expected_ids = + expected_action_ids().into_iter().filter_map(|value| value.as_str().map(str::to_owned)).collect::>(); + let mut seen_names = BTreeSet::new(); + let mut main_action_ids = BTreeSet::new(); + let mut branch_action_ids = Vec::new(); + let mut observed_step_count = 0_usize; + for (index, value) in runs.iter().enumerate() { + let context = format!("onchain.stateful_scenarios.runs[{index}]"); + let run = object(value, &context)?; + let name = nonempty_string(run.get("name"), &format!("{context}.name"))?; + require(seen_names.insert(name.to_owned()), format!("duplicate stateful scenario name: {name}"))?; + for (key, expected) in [ + ("status", json!(EXPECTED_STATUS)), + ("builder_backed", json!(false)), + ("transaction_origin", json!("acceptance-rust-harness")), + ("harness_origin", json!("rust-transaction-recipe-replay")), + ] { + require_field(run, key, expected, &context)?; + } + nonempty_string(run.get("acceptance_harness_name"), &format!("{context}.acceptance_harness_name"))?; + let action_ids = array(run.get("action_ids"), &format!("{context}.action_ids"))?; + require(!action_ids.is_empty(), format!("{context}.action_ids must be a non-empty list"))?; + let action_id_strings = action_ids.iter().filter_map(Value::as_str).map(str::to_owned).collect::>(); + require( + action_id_strings.len() == action_ids.len() && action_id_strings.iter().all(|action_id| expected_ids.contains(action_id)), + format!("{context}.action_ids contains actions outside the production matrix"), + )?; + let steps = array(run.get("steps"), &format!("{context}.steps"))?; + require(!steps.is_empty(), format!("{context}.steps must be a non-empty list"))?; + observed_step_count += steps.len(); + if index < EXPECTED_END_TO_END_STATEFUL_SCENARIOS.len() { + require_field(run, "kind", json!("stateful-scenario"), &context)?; + require(steps.len() >= 2, format!("{context} end-to-end scenario must contain at least two committed steps"))?; + main_action_ids.extend(action_id_strings); + } else { + require_field(run, "kind", json!("stateful-action-branch"), &context)?; + require( + action_ids.len() == 1 && steps.len() == 1, + format!("{context} branch scenario must bind exactly one action and one step"), + )?; + branch_action_ids.extend(action_id_strings); + } + for (step_index, step_value) in steps.iter().enumerate() { + let step_context = format!("{context}.steps[{step_index}]"); + let step = object(step_value, &step_context)?; + nonempty_string(step.get("step"), &format!("{step_context}.step"))?; + require_field(step, "status", json!(EXPECTED_STATUS), &step_context)?; + let dry_run = object(step.get("dry_run").unwrap_or(&Value::Null), &format!("{step_context}.dry_run"))?; + require( + dry_run.get("cycles").and_then(Value::as_str).is_some_and(|value| value.starts_with("0x")), + format!("{step_context}.dry_run.cycles must be a hex quantity"), + )?; + let commit = object(step.get("commit").unwrap_or(&Value::Null), &format!("{step_context}.commit"))?; + hex_hash(commit.get("tx_hash"), &format!("{step_context}.commit.tx_hash"))?; + let commit_status = object(commit.get("status").unwrap_or(&Value::Null), &format!("{step_context}.commit.status"))?; + require_field(commit_status, "status", json!("committed"), &format!("{step_context}.commit.status"))?; + let constraints = + object(step.get("measured_constraints").unwrap_or(&Value::Null), &format!("{step_context}.measured_constraints"))?; + positive(constraints.get("measured_cycles"), &format!("{step_context}.measured_constraints.measured_cycles"))?; + positive( + constraints.get("consensus_serialized_tx_size_bytes"), + &format!("{step_context}.measured_constraints.consensus_serialized_tx_size_bytes"), + )?; + positive( + constraints.get("occupied_capacity_shannons"), + &format!("{step_context}.measured_constraints.occupied_capacity_shannons"), + )?; + require_field(constraints, "capacity_is_sufficient", json!(true), &format!("{step_context}.measured_constraints"))?; + require_empty(constraints, "under_capacity_output_indexes", &format!("{step_context}.measured_constraints"))?; + let consumed = array(step.get("consumed_inputs"), &format!("{step_context}.consumed_inputs"))?; + require( + consumed.iter().all(|cell| cell.as_object().is_some_and(|cell| cell.get("status") != Some(&json!("live")))), + format!("{step_context}.consumed_inputs contains a still-live or malformed cell"), + )?; + let outputs_live = object(step.get("outputs_live").unwrap_or(&Value::Null), &format!("{step_context}.outputs_live"))?; + require( + outputs_live.values().all(|value| value == &json!(true)), + format!("{step_context}.outputs_live contains a dead output"), + )?; + } + } + require_field(stateful, "step_count", json!(observed_step_count), "onchain.stateful_scenarios")?; + let expected_branch_ids = expected_ids.difference(&main_action_ids).cloned().collect::>(); + branch_action_ids.sort(); + require( + branch_action_ids == expected_branch_ids, + "stateful branch scenarios must cover every action absent from end-to-end flows exactly once", + ) +} + +fn validate_action_runs(report: &Map) -> Result<()> { + let runs = all_action_runs(report)?; + require( + runs.len() == EXPECTED_ACTION_COUNT as usize, + format!("expected {EXPECTED_ACTION_COUNT} action runs, got {}", runs.len()), + )?; + let mut seen_names = BTreeSet::new(); + for run in runs { + let name = nonempty_string(run.get("name"), "action run name")?; + require(seen_names.insert(name.to_owned()), format!("duplicate action run name: {name}"))?; + let action = nonempty_string(run.get("action"), &format!("{name}.action"))?; + require(name.ends_with(&format!(":{action}")), format!("{name} must end with action suffix :{action}"))?; + for (key, expected) in [ + ("status", json!(EXPECTED_STATUS)), + ("builder_backed", json!(false)), + ("transaction_origin", json!("acceptance-rust-harness")), + ("harness_origin", json!("rust-transaction-recipe-replay")), + ("public_builder_contract_id", json!(name)), + ("public_builder_contract_verified", json!(true)), + ] { + require_field(run, key, expected, name)?; + } + nonempty_string(run.get("acceptance_harness_name"), &format!("{name}.acceptance_harness_name"))?; + nonempty_string(run.get("acceptance_harness_implementation"), &format!("{name}.acceptance_harness_implementation"))?; + validate_code_section(run, name)?; + let valid_dry_run = object(run.get("valid_dry_run").unwrap_or(&Value::Null), &format!("{name}.valid_dry_run"))?; + require( + valid_dry_run.get("cycles").and_then(Value::as_str).is_some_and(|value| value.starts_with("0x")), + format!("{name} missing hex dry-run cycles"), + )?; + object(run.get("valid_commit").unwrap_or(&Value::Null), &format!("{name}.valid_commit"))?; + let malformed = object(run.get("malformed_transaction").unwrap_or(&Value::Null), &format!("{name}.malformed_transaction"))?; + for (key, expected) in + [("status", json!("rejected")), ("expected_reason_matched", json!(true)), ("policy_or_capacity_reason", json!(false))] + { + require_field(malformed, key, expected, &format!("{name}.malformed_transaction"))?; + } + let measured = object(run.get("measured_constraints").unwrap_or(&Value::Null), &format!("{name}.measured_constraints"))?; + validate_measured_constraints(measured, name, true)?; + } + Ok(()) +} + +fn validate_lock_runs(onchain: &Map) -> Result<()> { + let runs = array(onchain.get("lock_spend_matrix_runs"), "onchain.lock_spend_matrix_runs")?; + let lock_names = runs + .iter() + .filter_map(Value::as_object) + .map(|row| row.get("name").and_then(Value::as_str).unwrap_or_default().to_owned()) + .collect::>(); + let mut actual_sorted = lock_names.clone(); + actual_sorted.sort(); + let mut expected_sorted = expected_lock_names(); + expected_sorted.sort(); + require( + actual_sorted == expected_sorted && lock_names.len() == expected_lock_count() as usize, + format!("lock spend matrix must cover {expected_sorted:?}, got {lock_names:?}"), + )?; + require( + lock_names.iter().collect::>().len() == lock_names.len(), + format!("lock spend matrix must not contain duplicates, got {lock_names:?}"), + )?; + for value in runs { + let run = object(value, "lock spend matrix entry")?; + let name = nonempty_string(run.get("name"), "lock run name")?; + let lock = nonempty_string(run.get("lock"), &format!("{name}.lock"))?; + require(name.ends_with(&format!(":{lock}")), format!("{name} must end with lock suffix :{lock}"))?; + for (key, expected) in [ + ("status", json!(EXPECTED_STATUS)), + ("builder_backed", json!(false)), + ("transaction_origin", json!("acceptance-rust-harness")), + ("harness_origin", json!("rust-transaction-recipe-replay")), + ] { + require_field(run, key, expected, name)?; + } + nonempty_string(run.get("acceptance_harness_name"), &format!("{name}.acceptance_harness_name"))?; + nonempty_string(run.get("acceptance_harness_implementation"), &format!("{name}.acceptance_harness_implementation"))?; + validate_code_section(run, name)?; + + let valid_spend = object(run.get("valid_spend").unwrap_or(&Value::Null), &format!("{name}.valid_spend"))?; + require_field(valid_spend, "status", json!(EXPECTED_STATUS), &format!("{name}.valid_spend"))?; + require_field(valid_spend, "output_live", json!(true), &format!("{name}.valid_spend"))?; + let valid_dry_run = object(valid_spend.get("dry_run").unwrap_or(&Value::Null), &format!("{name}.valid_spend.dry_run"))?; + require( + valid_dry_run.get("cycles").and_then(Value::as_str).is_some_and(|value| value.starts_with("0x")), + format!("{name}.valid_spend missing hex dry-run cycles"), + )?; + object(valid_spend.get("commit").unwrap_or(&Value::Null), &format!("{name}.valid_spend.commit"))?; + + let invalid_spend = object(run.get("invalid_spend").unwrap_or(&Value::Null), &format!("{name}.invalid_spend"))?; + require_field(invalid_spend, "status", json!("rejected"), &format!("{name}.invalid_spend"))?; + let rejection = object(invalid_spend.get("rejection").unwrap_or(&Value::Null), &format!("{name}.invalid_spend.rejection"))?; + for (key, expected) in + [("status", json!("rejected")), ("expected_reason_matched", json!(true)), ("policy_or_capacity_reason", json!(false))] + { + require_field(rejection, key, expected, &format!("{name}.invalid_spend.rejection"))?; + } + let reason = nonempty_string(rejection.get("reason"), &format!("{name}.invalid_spend.rejection.reason"))?; + for fragment in ["source: Inputs[0].Lock", "ValidationFailure", "error code 5"] { + require( + reason.contains(fragment), + format!("{name}.invalid_spend.rejection must show lock predicate error fragment {fragment:?}"), + )?; + } + let live_after = array( + invalid_spend.get("input_cells_live_after_rejection"), + &format!("{name}.invalid_spend.input_cells_live_after_rejection"), + )?; + require( + !live_after.is_empty() && live_after.iter().all(|value| value == &json!(true)), + format!("{name}.invalid_spend must keep rejected input cells live"), + )?; + let measured = object(run.get("measured_constraints").unwrap_or(&Value::Null), &format!("{name}.measured_constraints"))?; + validate_measured_constraints(measured, name, false)?; + } + Ok(()) +} + +fn validate_onchain_gate(report: &Map) -> Result<()> { + let onchain = object(report.get("onchain").unwrap_or(&Value::Null), "onchain")?; + for (key, expected) in [ + ("status", json!(EXPECTED_STATUS)), + ("all_artifacts_deployed_and_spent", json!(true)), + ("all_bundled_examples_deployed", json!(true)), + ("bundled_examples_deployed", json!(EXPECTED_EXAMPLES)), + ("all_token_actions_exercised", json!(true)), + ("all_nft_actions_exercised", json!(true)), + ("all_timelock_actions_exercised", json!(true)), + ("all_multisig_actions_exercised", json!(true)), + ("all_vesting_actions_exercised", json!(true)), + ("all_amm_actions_exercised", json!(true)), + ("all_launch_actions_exercised", json!(true)), + ("builder_backed_action_count", json!(0)), + ("acceptance_harness_action_count", json!(EXPECTED_ACTION_COUNT)), + ("public_builder_contract_action_count", json!(EXPECTED_ACTION_COUNT)), + ("measured_cycles_action_count", json!(EXPECTED_ACTION_COUNT)), + ("tx_size_measured_action_count", json!(EXPECTED_ACTION_COUNT)), + ("occupied_capacity_measured_action_count", json!(EXPECTED_ACTION_COUNT)), + ("lock_spend_matrix_count", json!(expected_lock_count())), + ("builder_backed_lock_spend_matrix_count", json!(0)), + ("acceptance_harness_lock_spend_matrix_count", json!(expected_lock_count())), + ("lock_valid_spend_count", json!(expected_lock_count())), + ("lock_invalid_spend_count", json!(expected_lock_count())), + ("measured_cycles_lock_count", json!(expected_lock_count())), + ("tx_size_measured_lock_count", json!(expected_lock_count())), + ("occupied_capacity_measured_lock_count", json!(expected_lock_count())), + ("all_locks_behavior_exercised", json!(true)), + ] { + require_field(onchain, key, expected, "onchain")?; + } + let resource_scope = + object(onchain.get("resource_identity_evidence_scope").unwrap_or(&Value::Null), "onchain.resource_identity_evidence_scope")?; + for (key, expected) in [ + ("status", json!("fixture-only")), + ("always_success_resource_types", json!(true)), + ("production_resource_identity_proven", json!(false)), + ] { + require_field(resource_scope, key, expected, "onchain.resource_identity_evidence_scope")?; + } + + let deployments = array(onchain.get("bundled_example_deployment_runs"), "onchain.bundled_example_deployment_runs")?; + require( + deployments.len() == EXPECTED_EXAMPLES.len(), + format!("expected {} bundled example deployment runs, got {}", EXPECTED_EXAMPLES.len(), deployments.len()), + )?; + let deployment_names = + deployments.iter().filter_map(Value::as_object).map(|row| row.get("name").cloned().unwrap_or(Value::Null)).collect::>(); + require( + deployment_names == json!(EXPECTED_EXAMPLES).as_array().cloned().unwrap(), + format!("bundled example deployment order must match release scope, got {deployment_names:?}"), + )?; + for value in deployments { + let run = object(value, "bundled example deployment run")?; + let name = nonempty_string(run.get("name"), "bundled example deployment run name")?; + require_field(run, "status", json!(EXPECTED_STATUS), name)?; + require_field(run, "kind", json!("bundled-example-strict-original"), name)?; + boolean(run.get("code_cell_live"), &format!("{name}.code_cell_live"))?; + positive(run.get("artifact_size_bytes"), &format!("{name}.artifact_size_bytes"))?; + require_field(run, "live_code_cell_data_hash_matches_artifact", json!(true), name)?; + hex_hash(run.get("artifact_ckb_data_hash_blake2b"), &format!("{name}.artifact_ckb_data_hash_blake2b"))?; + require_field( + run, + "live_code_cell_data_hash", + run.get("artifact_ckb_data_hash_blake2b").cloned().unwrap_or(Value::Null), + name, + )?; + let dry_run = object(run.get("valid_deploy_dry_run").unwrap_or(&Value::Null), &format!("{name}.valid_deploy_dry_run"))?; + require( + dry_run.get("cycles").and_then(Value::as_str).is_some_and(|value| value.starts_with("0x")), + format!("{name} missing hex deploy dry-run cycles"), + )?; + } + + let final_gate = object(report.get("final_production_hardening_gate").unwrap_or(&Value::Null), "final_production_hardening_gate")?; + for (key, expected) in [ + ("status", json!(EXPECTED_STATUS)), + ("ready", json!(true)), + ("requires_builder_generated_transactions", json!(false)), + ("requires_public_builder_contracts", json!(true)), + ("requires_acceptance_harness_transactions", json!(true)), + ("requires_measured_cycles", json!(true)), + ("requires_consensus_serialized_tx_size", json!(true)), + ("requires_exact_occupied_capacity", json!(true)), + ("requires_stateful_action_coverage", json!(true)), + ("production_resource_identity_claim", json!(false)), + ("resource_identity_evidence_scope", json!("always-success-fixture-only")), + ("requires_build_report_live_artifact_linkage", json!(true)), + ] { + require_field(final_gate, key, expected, "final_production_hardening_gate")?; + } + require_empty(final_gate, "failures", "final_production_hardening_gate")?; + validate_stateful_scenarios(onchain)?; + validate_action_runs(report)?; + validate_lock_runs(onchain) +} + +pub fn run(repo_root: &Path, report: &Path, explicit_repo_root: Option<&Path>, compile_only: bool) -> Result { + let report_path = fs::canonicalize(report).with_context(|| format!("missing CKB production evidence: {}", report.display()))?; + let source_root = match explicit_repo_root { + Some(path) => fs::canonicalize(path).with_context(|| format!("failed to resolve repository root {}", path.display()))?, + None => repo_root.to_path_buf(), + }; + let report_value = load_json(&report_path)?; + let report_object = object(&report_value, &report_path.display().to_string())?; + validate_source_provenance(report_object, &source_root)?; + validate_public_builder_contracts(report_object)?; + validate_compile_gate(report_object, compile_only)?; + if !compile_only { + validate_ckb_runtime_provenance( + report_object, + &source_root, + report_path.parent().context("production evidence report has no parent directory")?, + )?; + validate_onchain_gate(report_object)?; + } + let mode = if compile_only { "compile-only " } else { "" }; + println!("valid CKB CellScript {mode}production evidence: {}", report_path.display()); + Ok(0) +} diff --git a/crates/cellscript-tools/src/profile_operator.rs b/crates/cellscript-tools/src/profile_operator.rs new file mode 100644 index 00000000..f9ffddc1 --- /dev/null +++ b/crates/cellscript-tools/src/profile_operator.rs @@ -0,0 +1,407 @@ +//! NovaSeal profile-operator fixture generator. + +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result}; +use serde_json::{json, Value}; + +use crate::btc_anchor::public_btc_anchor_shape_matches_profile; +use crate::crypto::{canonical_report_hash, ckb_blake2b256, hex0x, sha256_hex}; +use crate::shared::{lexical_path, stable_json_compact, stable_json_pretty}; + +const REPORT_PERSON: &[u8] = b"NovaProfileFxV0"; +const PACKED_DOMAIN: &[u8] = b"NovaSealProfileOperatorFixtureV0\0"; + +#[derive(Clone, Copy)] +struct ActionCase { + action: &'static str, + fixture: &'static str, + signers: &'static [&'static str], + tx_pointer: Option<&'static str>, +} + +#[derive(Clone, Copy)] +struct ProfileCase { + profile: &'static str, + root: &'static str, + signed_type: &'static str, + live_report: Option<&'static str>, + public_btc_anchor: Option<&'static str>, + fiber_report: Option<&'static str>, + external_boundary: Option<&'static str>, + cases: &'static [ActionCase], +} + +const FUNGIBLE_CASES: &[ActionCase] = &[ + ActionCase { action: "issue_xudt", fixture: "issue_valid.json", signers: &["issuer"], tx_pointer: Some("/issue/commit/tx_hash") }, + ActionCase { + action: "transfer_xudt", + fixture: "transfer_valid.json", + signers: &["holder"], + tx_pointer: Some("/transfer/commit/tx_hash"), + }, + ActionCase { + action: "settle_xudt", + fixture: "settle_valid.json", + signers: &["holder"], + tx_pointer: Some("/settle/commit/tx_hash"), + }, +]; + +const RWA_CASES: &[ActionCase] = &[ + ActionCase { + action: "materialize_rwa_receipt", + fixture: "materialize_valid.json", + signers: &["issuer"], + tx_pointer: Some("/materialize/commit/tx_hash"), + }, + ActionCase { + action: "claim_rwa_receipt", + fixture: "claim_valid.json", + signers: &["holder"], + tx_pointer: Some("/claim/commit/tx_hash"), + }, + ActionCase { + action: "settle_rwa_receipt", + fixture: "settle_valid.json", + signers: &["issuer", "holder"], + tx_pointer: Some("/settle/commit/tx_hash"), + }, +]; + +const BTC_TRANSACTION_CASES: &[ActionCase] = &[ActionCase { + action: "commit_btc_transaction_transition", + fixture: "commit_transaction_valid.json", + signers: &["committer"], + tx_pointer: Some("/commit_transaction/commit/tx_hash"), +}]; + +const BTC_UTXO_CASES: &[ActionCase] = &[ActionCase { + action: "close_btc_utxo_seal", + fixture: "close_utxo_seal_valid.json", + signers: &["owner"], + tx_pointer: Some("/close_utxo_seal/commit/tx_hash"), +}]; + +const DUAL_SEAL_CASES: &[ActionCase] = &[ActionCase { + action: "finalize_dual_seal", + fixture: "finalize_dual_seal_valid.json", + signers: &["btc_owner", "ckb_authority"], + tx_pointer: Some("/finalize_dual_seal/commit/tx_hash"), +}]; + +const FIBER_CASES: &[ActionCase] = &[ActionCase { + action: "settle_fiber_candidate", + fixture: "settle_fiber_candidate_valid.json", + signers: &["operator"], + tx_pointer: Some("/settle_fiber_candidate/commit/tx_hash"), +}]; + +const PROFILE_CASES: &[ProfileCase] = &[ + ProfileCase { + profile: "fungible-xudt-profile-v0", + root: "proposals/novaseal/fungible-xudt-profile-v0", + signed_type: "NovaFungibleXudtSignedIntentV0", + live_report: Some("target/novaseal-fungible-xudt-devnet-stateful-live.json"), + public_btc_anchor: None, + fiber_report: None, + external_boundary: None, + cases: FUNGIBLE_CASES, + }, + ProfileCase { + profile: "rwa-receipt-profile-v0", + root: "proposals/novaseal/rwa-receipt-profile-v0", + signed_type: "NovaRwaReceiptSignedIntentV0", + live_report: Some("target/novaseal-rwa-receipt-devnet-stateful-live.json"), + public_btc_anchor: None, + fiber_report: None, + external_boundary: None, + cases: RWA_CASES, + }, + ProfileCase { + profile: "btc-transaction-commitment-profile-v0", + root: "proposals/novaseal/btc-transaction-commitment-profile-v0", + signed_type: "NovaBtcTransactionCommitmentSignedIntentV0", + live_report: Some("target/novaseal-btc-transaction-commitment-devnet-stateful-live.json"), + public_btc_anchor: Some("/commit_transaction/public_btc_anchor"), + fiber_report: None, + external_boundary: None, + cases: BTC_TRANSACTION_CASES, + }, + ProfileCase { + profile: "btc-utxo-seal-profile-v0", + root: "proposals/novaseal/btc-utxo-seal-profile-v0", + signed_type: "NovaBtcUtxoSealSignedIntentV0", + live_report: Some("target/novaseal-btc-utxo-seal-devnet-stateful-live.json"), + public_btc_anchor: Some("/close_utxo_seal/public_btc_anchor"), + fiber_report: None, + external_boundary: None, + cases: BTC_UTXO_CASES, + }, + ProfileCase { + profile: "dual-seal-profile-v0", + root: "proposals/novaseal/dual-seal-profile-v0", + signed_type: "NovaDualSealSignedIntentV0", + live_report: Some("target/novaseal-dual-seal-devnet-stateful-live.json"), + public_btc_anchor: Some("/finalize_dual_seal/public_btc_anchor"), + fiber_report: None, + external_boundary: None, + cases: DUAL_SEAL_CASES, + }, + ProfileCase { + profile: "fiber-candidate-profile-v0", + root: "proposals/novaseal/fiber-candidate-profile-v0", + signed_type: "NovaFiberCandidateSignedIntentV0", + live_report: Some("target/novaseal-fiber-candidate-devnet-stateful-live.json"), + public_btc_anchor: None, + fiber_report: Some("target/novaseal-fiber-node-experiments.json"), + external_boundary: None, + cases: FIBER_CASES, + }, +]; + +fn report_hash(label: &str, value: &Value) -> Result { + canonical_report_hash(REPORT_PERSON, label, value) +} + +fn read_json(path: &Path) -> Result { + serde_json::from_slice(&fs::read(path).with_context(|| format!("failed to read {}", path.display()))?) + .with_context(|| format!("{} is not valid JSON", path.display())) +} + +fn json_file_hash(path: &Path) -> Result { + let label = path.file_name().and_then(|name| name.to_str()).context("JSON file name is not UTF-8")?; + report_hash(label, &read_json(path)?) +} + +fn matching_files(directory: &Path, extension: &str) -> Result> { + let mut files = Vec::new(); + for entry in fs::read_dir(directory).with_context(|| format!("failed to read {}", directory.display()))? { + let candidate = entry?.path(); + if candidate.extension().and_then(|value| value.to_str()) == Some(extension) { + files.push(candidate); + } + } + files.sort(); + Ok(files) +} + +fn file_set_hash(root: &Path, paths: &[PathBuf]) -> Result { + let mut entries = Vec::new(); + for candidate in paths { + if candidate.is_symlink() || !candidate.is_file() { + continue; + } + let relative = + candidate.strip_prefix(root).with_context(|| format!("{} is outside {}", candidate.display(), root.display()))?; + entries.push(json!({ + "path": relative.to_string_lossy(), + "sha256": sha256_hex(&fs::read(candidate).with_context(|| format!("failed to read {}", candidate.display()))?), + })); + } + report_hash("file_set", &Value::Array(entries)) +} + +fn packed_hash(type_name: &str, packed: &[u8]) -> Result<(String, String)> { + let length = u32::try_from(packed.len()).context("packed operator fixture exceeds u32")?; + let mut preimage = Vec::with_capacity(PACKED_DOMAIN.len() + type_name.len() + 1 + 4 + packed.len()); + preimage.extend_from_slice(PACKED_DOMAIN); + preimage.extend_from_slice(type_name.as_bytes()); + preimage.push(0); + preimage.extend_from_slice(&length.to_le_bytes()); + preimage.extend_from_slice(packed); + Ok((hex0x(&preimage), hex0x(&ckb_blake2b256(&preimage)?))) +} + +fn json_truthy(value: &Value) -> bool { + match value { + Value::Null => false, + Value::Bool(value) => *value, + Value::Number(value) => value.as_f64().is_some_and(|number| number != 0.0), + Value::String(value) => !value.is_empty(), + Value::Array(value) => !value.is_empty(), + Value::Object(value) => !value.is_empty(), + } +} + +fn optional_json(root: &Path, relative: Option<&str>) -> Result> { + let Some(relative) = relative else { + return Ok(None); + }; + let candidate = root.join(relative); + if candidate.is_file() { + Ok(Some(read_json(&candidate)?)) + } else { + Ok(None) + } +} + +fn pointer(value: Option<&Value>, pointer: Option<&str>) -> Value { + value.zip(pointer).and_then(|(value, pointer)| value.pointer(pointer)).cloned().unwrap_or(Value::Null) +} + +fn build_case(root: &Path, evidence_root: &Path, profile: &ProfileCase, action_case: &ActionCase) -> Result { + let profile_root = root.join(profile.root); + let fixture_path = profile_root.join("fixtures").join(action_case.fixture); + let fixture = read_json(&fixture_path)?; + let source_hash = file_set_hash(root, &matching_files(&profile_root.join("src"), "cell")?)?; + let schema_hash = file_set_hash(root, &matching_files(&profile_root.join("schemas"), "schema")?)?; + let proof_hash = json_file_hash(&profile_root.join("proofs/invariant_matrix.json"))?; + let live_report = optional_json(evidence_root, profile.live_report)?; + let fiber_report = optional_json(evidence_root, profile.fiber_report)?; + let live_tx_hash = pointer(live_report.as_ref(), action_case.tx_pointer); + let public_btc_anchor = pointer(live_report.as_ref(), profile.public_btc_anchor); + let public_btc_required = + matches!(profile.profile, "btc-transaction-commitment-profile-v0" | "btc-utxo-seal-profile-v0" | "dual-seal-profile-v0"); + let signers = action_case.signers; + let display = json!({ + "profile": profile.profile, + "action": action_case.action, + "fixture": action_case.fixture, + "fixture_description": fixture.get("description").cloned().unwrap_or(Value::Null), + "signers": signers, + "signed_type": profile.signed_type, + "source_tree_hash": source_hash, + "schema_set_hash": schema_hash, + "proof_matrix_hash": proof_hash, + "live_devnet_tx_hash": live_tx_hash, + "public_btc_anchor": public_btc_anchor, + "external_boundary": profile.external_boundary, + }); + let signature_witnesses: Vec = signers.iter().map(|signer| format!("{signer}_sig")).collect(); + let witness_shape = json!({ + "signed_intent": profile.signed_type, + "signature_witnesses": signature_witnesses, + "fixture_expected": fixture.get("expected").cloned().unwrap_or(Value::Null), + "live_report": profile.live_report, + "fiber_report": profile.fiber_report, + }); + let live_report_hash = match (&live_report, profile.live_report) { + (Some(report), Some(label)) => Value::String(report_hash(label, report)?), + _ => Value::Null, + }; + let fiber_report_hash = match (&fiber_report, profile.fiber_report) { + (Some(report), Some(label)) => Value::String(report_hash(label, report)?), + _ => Value::Null, + }; + let intent_body = json!({ + "schema": "novaseal-profile-operator-intent-v0.1", + "profile": profile.profile, + "action": action_case.action, + "fixture": action_case.fixture, + "fixture_hash": json_file_hash(&fixture_path)?, + "source_tree_hash": source_hash, + "schema_set_hash": schema_hash, + "proof_matrix_hash": proof_hash, + "signers": signers, + "witness_shape_hash": report_hash("witness_shape", &witness_shape)?, + "live_report_hash": live_report_hash, + "fiber_report_hash": fiber_report_hash, + "live_tx_hash": live_tx_hash, + "public_btc_anchor": public_btc_anchor, + "external_boundary": profile.external_boundary, + }); + let packed = stable_json_compact(&intent_body)?.into_bytes(); + let (preimage, digest) = packed_hash(profile.signed_type, &packed)?; + let tx_skeleton = json!({ + "profile": profile.profile, + "action": action_case.action, + "fixture": action_case.fixture, + "live_tx_hash": live_tx_hash, + "source_tree_hash": source_hash, + "witness_shape_hash": intent_body["witness_shape_hash"], + "public_btc_anchor": public_btc_anchor, + }); + let fixture_expected = fixture.get("expected").and_then(Value::as_str) == Some("accepted"); + let fixture_action = fixture.get("action").and_then(Value::as_str) == Some(action_case.action); + let live_passed = live_report.as_ref().and_then(|report| report.get("status")).and_then(Value::as_str) == Some("passed") + || profile.external_boundary == Some("package_fixture_only_external_btc_and_ckb_finality_required"); + let fiber_passed = fiber_report.as_ref().is_none_or(|report| { + !json_truthy(report) || report.pointer("/workflow_coverage/all_required_workflows_executed_passed") == Some(&Value::Bool(true)) + }); + let anchor_present = !public_btc_required || json_truthy(&public_btc_anchor); + let anchor_shape = !public_btc_required || public_btc_anchor_shape_matches_profile(profile.profile, Some(&public_btc_anchor)); + let checks = json!({ + "fixture_expected_accepted": fixture_expected, + "fixture_action_matches": fixture_action, + "live_status_passed_or_external_boundary": live_passed, + "fiber_execution_passed_when_required": fiber_passed, + "public_btc_anchor_present_when_required": anchor_present, + "public_btc_anchor_shape_matches_profile": anchor_shape, + }); + let passed = checks.as_object().context("operator checks are not an object")?.values().all(|check| check == &Value::Bool(true)); + Ok(json!({ + "profile": profile.profile, + "action": action_case.action, + "fixture": action_case.fixture, + "status": if passed { "passed" } else { "failed" }, + "checks": checks, + "signers": signers, + "signed_type": profile.signed_type, + "signed_intent_hash": digest, + "signed_intent_hash_preimage_hex": preimage, + "signed_intent_body_hex": hex0x(&packed), + "bip340_message_hash": digest, + "witness_shape_hash": intent_body["witness_shape_hash"], + "tx_skeleton_hash": report_hash("tx_skeleton", &tx_skeleton)?, + "fixture_hash": intent_body["fixture_hash"], + "source_tree_hash": source_hash, + "schema_set_hash": schema_hash, + "proof_matrix_hash": proof_hash, + "live_report_hash": intent_body["live_report_hash"], + "fiber_report_hash": intent_body["fiber_report_hash"], + "live_devnet_tx_hash": live_tx_hash, + "public_btc_anchor": public_btc_anchor, + "wallet_display": display, + "operator_witness_shape": witness_shape, + })) +} + +fn build_report(root: &Path, evidence_root: &Path) -> Result { + let mut cases = Vec::new(); + for profile in PROFILE_CASES { + for action_case in profile.cases { + cases.push(build_case(root, evidence_root, profile, action_case)?); + } + } + let profiles: BTreeSet<&str> = cases.iter().filter_map(|case| case["profile"].as_str()).collect(); + let matched = cases.iter().filter(|case| case["status"] == "passed").count(); + let passed = !cases.is_empty() && matched == cases.len(); + Ok(json!({ + "schema": "novaseal-profile-operator-fixtures-v0.1", + "status": if passed { "passed" } else { "failed" }, + "hash_algorithm": "ckb_blake2b_256", + "signature_scheme": "BIP340 Schnorr over 32-byte signed profile intent hash", + "fixture_boundary": "wallet/service fixtures bind declared profile actions to source, schema, invariant, witness, and live-report evidence; external BTC/CellDep/TCB attestations remain separate production gates", + "summary": { + "total": cases.len(), + "matched": matched, + "profile_count": profiles.len(), + "profiles": profiles, + }, + "profiles": profiles, + "cases": cases, + })) +} + +pub fn run(root: &Path, evidence_root: Option<&Path>, output: Option<&Path>, pretty: bool) -> Result { + let default_output = root.join("target/novaseal-profile-operator-fixtures.json"); + let output = lexical_path(output.unwrap_or(&default_output)); + let report = build_report(root, evidence_root.unwrap_or(root))?; + let parent = output.parent().filter(|parent| !parent.as_os_str().is_empty()).unwrap_or_else(|| Path::new(".")); + fs::create_dir_all(parent).with_context(|| format!("failed to create {}", parent.display()))?; + fs::write(&output, format!("{}\n", stable_json_pretty(&report)?)) + .with_context(|| format!("failed to write {}", output.display()))?; + if pretty { + println!( + "wrote {} status={} profiles={} cases={}", + output.display(), + report["status"].as_str().unwrap_or("failed"), + report["summary"]["profile_count"].as_u64().unwrap_or(0), + report["summary"]["total"].as_u64().unwrap_or(0), + ); + } + Ok(if report["status"] == "passed" { 0 } else { 1 }) +} diff --git a/crates/cellscript-tools/src/repository_checks.rs b/crates/cellscript-tools/src/repository_checks.rs new file mode 100644 index 00000000..7cdbab8b --- /dev/null +++ b/crates/cellscript-tools/src/repository_checks.rs @@ -0,0 +1,443 @@ +//! Native repository-policy checks used by every gate mode. + +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{bail, Context, Result}; +use percent_encoding::percent_decode_str; +use regex::Regex; + +fn tracked_paths(root: &Path) -> Result> { + let output = Command::new("git") + .args(["ls-files", "--recurse-submodules", "-z"]) + .current_dir(root) + .output() + .context("failed to enumerate tracked repository and submodule files")?; + if !output.status.success() { + bail!("git ls-files --recurse-submodules failed: {}", String::from_utf8_lossy(&output.stderr).trim()); + } + output + .stdout + .split(|byte| *byte == 0) + .filter(|path| !path.is_empty()) + .map(|path| std::str::from_utf8(path).map(PathBuf::from).context("tracked path is not valid UTF-8")) + .filter(|path| path.as_ref().is_ok_and(|path| root.join(path).is_file())) + .collect() +} + +fn forbidden_source_artifact(path: &Path) -> bool { + let forbidden_extension = path + .extension() + .and_then(|extension| extension.to_str()) + .is_some_and(|extension| matches!(extension, "py" | "pyi" | "pyc" | "pyo")); + forbidden_extension + || path.file_name().and_then(|name| name.to_str()) == Some(".DS_Store") + || path + .components() + .any(|component| matches!(component.as_os_str().to_str(), Some("__pycache__" | ".cap" | ".playwright-mcp"))) +} + +fn active_tooling_source(path: &Path) -> bool { + if path.file_name().and_then(|name| name.to_str()).is_some_and(|name| matches!(name, "package.json" | "Makefile" | "Justfile")) { + return true; + } + path.extension().and_then(|extension| extension.to_str()).is_some_and(|extension| { + matches!(extension, "rs" | "sh" | "bash" | "zsh" | "yml" | "yaml" | "toml" | "mjs" | "js" | "ts" | "tsx") + }) +} + +pub fn check_source_policy(root: &Path) -> Result<()> { + let retired_runtime_name = ["py", "thon"].concat(); + let mut forbidden = Vec::new(); + let mut runtime_residue = Vec::new(); + for relative in tracked_paths(root)? { + if forbidden_source_artifact(&relative) { + forbidden.push(relative.clone()); + } + if active_tooling_source(&relative) { + let path = root.join(&relative); + let text = + fs::read_to_string(&path).with_context(|| format!("failed to read active tooling source {}", path.display()))?; + if text.to_ascii_lowercase().contains(&retired_runtime_name) { + runtime_residue.push(relative); + } + } + } + if forbidden.is_empty() && runtime_residue.is_empty() { + return Ok(()); + } + eprintln!("Repository source-language policy failed:"); + for path in forbidden { + eprintln!(" forbidden source or generated artifact: {}", path.display()); + } + for path in runtime_residue { + eprintln!(" retired runtime residue in active tooling source: {}", path.display()); + } + bail!("repository source-language policy failed") +} + +fn normalized_head(path: &Path, lines: usize) -> Result { + let text = fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; + Ok(text.lines().take(lines).flat_map(str::split_whitespace).collect::>().join(" ")) +} + +fn check_document_contract( + root: &Path, + relative: &str, + required: &[&str], + forbidden: &[&str], + failures: &mut Vec, +) -> Result<()> { + let path = root.join(relative); + if !path.is_file() { + failures.push(format!("required current-contract document is missing: {relative}")); + return Ok(()); + } + let text = fs::read_to_string(&path).with_context(|| format!("failed to read {}", path.display()))?; + let normalized = text.split_whitespace().collect::>().join(" "); + let searchable = normalized.to_ascii_lowercase(); + for marker in required { + if !searchable.contains(&marker.to_ascii_lowercase()) { + failures.push(format!("{relative} is missing current-contract marker: {marker}")); + } + } + for marker in forbidden { + if searchable.contains(&marker.to_ascii_lowercase()) { + failures.push(format!("{relative} retains forbidden stale marker: {marker}")); + } + } + Ok(()) +} + +pub fn check_doc_status(root: &Path) -> Result<()> { + let readme = fs::read_to_string(root.join("README.md"))?; + let link_re = Regex::new(r"\]\((docs/CELLSCRIPT_[^)#]+\.md)(?:#[^)]+)?\)")?; + let mut docs = link_re + .captures_iter(&readme) + .filter_map(|capture| capture.get(1).map(|value| value.as_str().to_owned())) + .collect::>(); + let tracked = Command::new("git").args(["ls-files", "docs/CELLSCRIPT_*.md"]).current_dir(root).output(); + if let Ok(output) = tracked + && output.status.success() + { + for relative in String::from_utf8_lossy(&output.stdout).lines() { + if root.join(relative).is_file() { + docs.insert(relative.to_owned()); + } + } + } + for entry in fs::read_dir(root.join("docs"))? { + let entry = entry?; + let name = entry.file_name(); + let name = name.to_string_lossy(); + if entry.path().is_file() && name.starts_with("CELLSCRIPT_") && name.ends_with(".md") { + docs.insert(format!("docs/{name}")); + } + } + let stale_patterns = [ + "formal 0.19 headless Rust adapter crate", + "0.19 scope compatibility contract", + "Active 0.19 grammar-governance contract", + "Proposed. Implementation gated", + "**Status**: In progress", + ]; + let mut failures = Vec::new(); + for relative in docs { + let path = root.join(&relative); + if !path.is_file() { + failures.push(format!("README-linked CellScript doc is missing: {relative}")); + continue; + } + let head = normalized_head(&path, 40)?; + for pattern in stale_patterns { + if head.contains(pattern) { + failures.push(format!("{relative} has stale Status header pattern: {pattern}")); + } + } + } + for (relative, marker) in [ + ("docs/CELLSCRIPT_CKB_ADAPTER.md", "production contract for the current CellScript CKB profile"), + ("docs/CELLSCRIPT_CKB_STD_COMPAT.md", "production compatibility contract for the current CellScript CKB profile"), + ("docs/CELLSCRIPT_GRAMMAR_GOVERNANCE_RFC.md", "Active grammar-governance contract"), + ("docs/CELLSCRIPT_WEBSITE_PARADIGM_UPGRADE_RFC.md", "Implemented across the 0.20-0.23 line"), + ] { + if !normalized_head(&root.join(relative), 20)?.contains(marker) { + failures.push(format!("{relative} Status header is missing freshness marker: {marker}")); + } + } + + let lib_source = fs::read_to_string(root.join("src/lib.rs"))?; + let schema_re = Regex::new(r"METADATA_SCHEMA_VERSION:\s*u32\s*=\s*(\d+)")?; + let schema_version = schema_re + .captures(&lib_source) + .and_then(|captures| captures.get(1)) + .map(|value| value.as_str().to_owned()) + .context("src/lib.rs is missing METADATA_SCHEMA_VERSION")?; + let current_schema = format!("current metadata schema {schema_version}"); + let schema_number = format!("metadata schema {schema_version}"); + + check_document_contract( + root, + "README.md", + &["0.23 release notes", "0.24 release notes", "0.24 roadmap", "cellc publish --authorise"], + &[], + &mut failures, + )?; + check_document_contract( + root, + "docs/README.md", + &[ + schema_number.as_str(), + "CELLSCRIPT_0_23_RELEASE_NOTES.md", + "CELLSCRIPT_0_24_RELEASE_NOTES.md", + "CELLSCRIPT_0_24_ROADMAP.md", + "CELLSCRIPT_VERIFIED_ARTIFACT_BOUNDARY.md", + ], + &[], + &mut failures, + )?; + for relative in ["docs/CELLSCRIPT_RUNTIME_ERROR_CODES.md", "docs/wiki/Tutorial-06-Metadata-Verification-and-Production-Gates.md"] { + check_document_contract(root, relative, &[current_schema.as_str()], &["current schema 55"], &mut failures)?; + } + for relative in ["docs/skills/cellscript-metadata-audit/SKILL.md", "docs/releases/CELLSCRIPT_0_24_RELEASE_NOTES.md"] { + check_document_contract(root, relative, &[schema_number.as_str()], &["metadata schema 57"], &mut failures)?; + } + check_document_contract( + root, + "docs/releases/CELLSCRIPT_0_23_RELEASE_NOTES.md", + &["metadata schema 57", "0.24 roadmap"], + &["metadata schema 58"], + &mut failures, + )?; + + for relative in [ + "docs/CELLSCRIPT_REGISTRY_PHASE1.md", + "docs/CELLSCRIPT_REGISTRY_PRODUCTION_BOUNDARY_ADR.md", + "docs/wiki/Tutorial-12-Phase1-Registry-End-to-End.md", + "services/registry-api/README.md", + "docs/releases/CELLSCRIPT_0_23_RELEASE_NOTES.md", + ] { + check_document_contract( + root, + relative, + &["cellc publish --authorise", "get_live_cell", "get_transaction", "tx_status", "Pudge"], + &[], + &mut failures, + )?; + } + check_document_contract( + root, + "docs/CELLSCRIPT_GATE_POLICY.md", + &["Node 22", "npm --prefix website run build:ci", "native source-policy enforcement"], + &[], + &mut failures, + )?; + check_document_contract( + root, + "roadmap/CELLSCRIPT_0_23_ROADMAP.md", + &["implementation scope frozen", "final scope decision", "0.24 roadmap", "attested adapter"], + &["Status: Draft, pending release-line coordination"], + &mut failures, + )?; + check_document_contract( + root, + "roadmap/CELLSCRIPT_0_24_ROADMAP.md", + &[ + "Verified Artifact Boundary", + "Executable Package Tests", + "Myelin Adapter Re-Convergence", + "independent checker", + "Exit Criteria", + ], + &[], + &mut failures, + )?; + check_document_contract( + root, + "docs/wiki/Tutorial-14-Verified-Artifacts-and-Executable-Tests.md", + &["four-file bundle", "cellc test --backend all", "structurally_verified"], + &[], + &mut failures, + )?; + for relative in ["roadmap/CELLSCRIPT_ROADMAP.md", "roadmap/CELLSCRIPT_ROADMAP_OVERVIEW.md"] { + check_document_contract( + root, + relative, + &["0.24", "independent", "CELLSCRIPT_0_24_ROADMAP.md"], + &["Myelin vendored fork re-converges"], + &mut failures, + )?; + } + + let mut wiki_docs = Vec::new(); + collect_markdown(&root.join("docs/wiki"), &mut wiki_docs)?; + for path in wiki_docs { + let text = fs::read_to_string(&path).with_context(|| format!("failed to read {}", path.display()))?; + if text.contains("/blob/nightly-0.22/") { + let relative = path.strip_prefix(root).unwrap_or(&path).display(); + failures.push(format!("{relative} retains an active link to nightly-0.22")); + } + } + if !failures.is_empty() { + eprintln!("CellScript documentation Status freshness check failed:"); + for failure in failures { + eprintln!(" - {failure}"); + } + bail!("documentation status freshness check failed"); + } + Ok(()) +} + +fn collect_markdown(path: &Path, output: &mut Vec) -> Result<()> { + if path.is_file() { + output.push(path.to_owned()); + return Ok(()); + } + if !path.is_dir() { + return Ok(()); + } + for entry in fs::read_dir(path)? { + let entry = entry?; + let entry_path = entry.path(); + if entry_path.is_dir() { + let name = entry.file_name(); + if [".git", ".mavis", "dist", "node_modules", "target"].iter().any(|skip| name == *skip) { + continue; + } + collect_markdown(&entry_path, output)?; + } else if entry_path.extension().and_then(|value| value.to_str()) == Some("md") { + output.push(entry_path); + } + } + Ok(()) +} + +pub fn check_markdown_links(root: &Path) -> Result<()> { + let starts = [ + root.join("README.md"), + root.join("docs"), + root.join("roadmap"), + root.join("editors/vscode-cellscript/README.md"), + root.join("editors/vscode-cellscript/docs"), + ]; + let mut files = Vec::new(); + for start in starts { + collect_markdown(&start, &mut files)?; + } + files.sort(); + let link_re = Regex::new(r#"(!?)\[[^\]]+\]\(([^)\s]+(?:\s+\"[^\"]*\")?)\)"#)?; + let mut failures = Vec::new(); + for path in files { + let text = fs::read_to_string(&path).with_context(|| format!("failed to read {}", path.display()))?; + for (index, line) in text.lines().enumerate() { + for capture in link_re.captures_iter(line) { + if capture.get(1).is_some_and(|marker| marker.as_str() == "!") { + continue; + } + let mut raw = capture[2].trim().to_owned(); + if raw.contains(' ') && !raw.starts_with('<') { + raw.truncate(raw.find(' ').unwrap_or(raw.len())); + } + raw = raw.trim_matches(['<', '>']).to_owned(); + let target = raw.split('#').next().unwrap_or(""); + if target.is_empty() + || target.starts_with("http://") + || target.starts_with("https://") + || target.starts_with("mailto:") + || target.starts_with("tel:") + || target.starts_with("app://") + || target.starts_with('/') + { + continue; + } + let decoded = percent_decode_str(target).decode_utf8_lossy(); + let candidate = path.parent().unwrap_or(root).join(decoded.as_ref()); + if !candidate.exists() { + let relative = path.strip_prefix(root).unwrap_or(&path).to_string_lossy().replace('\\', "/"); + failures.push(format!("{relative}:{}: missing local markdown link target {raw}", index + 1)); + } + } + } + } + if !failures.is_empty() { + eprintln!("Local markdown link check failed:"); + for failure in failures { + eprintln!(" - {failure}"); + } + bail!("local Markdown link check failed"); + } + Ok(()) +} + +pub fn check_package_contents(path: &Path) -> Result<()> { + let allowed_files = [ + ".cargo_vcs_info.json", + "Cargo.lock", + "Cargo.toml", + "Cargo.toml.orig", + "CHANGELOG.md", + "CODING_STYLE.md", + "LICENSE-MIT", + "README.md", + ]; + let allowed_dirs = ["assets", "examples", "roadmap", "scripts", "src", "tests"]; + let mut unexpected = Vec::new(); + let contents = fs::read_to_string(path)?; + for raw in contents.lines() { + let item = raw.trim(); + if item.is_empty() { + continue; + } + let root = item.split('/').next().unwrap_or(item); + if item.ends_with(".pyc") + || item.ends_with(".pyo") + || item.contains("__pycache__/") + || (!item.contains('/') && !allowed_files.contains(&item)) + || (item.contains('/') && !allowed_dirs.contains(&root)) + { + unexpected.push(item); + } + } + if !unexpected.is_empty() { + eprintln!("crates.io package includes repository-only files:"); + for item in unexpected { + eprintln!(" {item}"); + } + bail!("package contents check failed"); + } + Ok(()) +} + +pub fn workspace_version(root: &Path) -> Result { + let manifest: toml::Value = fs::read_to_string(root.join("Cargo.toml"))?.parse()?; + manifest + .get("package") + .and_then(|package| package.get("version")) + .and_then(toml::Value::as_str) + .map(ToOwned::to_owned) + .context("Cargo.toml package.version is missing") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn source_policy_recognizes_forbidden_artifact_paths() { + assert!(forbidden_source_artifact(Path::new("scripts/legacy.py"))); + assert!(forbidden_source_artifact(Path::new("src/__pycache__/legacy.pyc"))); + assert!(forbidden_source_artifact(Path::new(".cap/logs/run.log"))); + assert!(forbidden_source_artifact(Path::new(".playwright-mcp/page.yml"))); + assert!(!forbidden_source_artifact(Path::new("src/main.rs"))); + } + + #[test] + fn active_tooling_source_scope_excludes_historical_prose() { + assert!(active_tooling_source(Path::new("src/main.rs"))); + assert!(active_tooling_source(Path::new(".github/workflows/ci.yml"))); + assert!(active_tooling_source(Path::new("website/package.json"))); + assert!(!active_tooling_source(Path::new("docs/archive/history.md"))); + } +} diff --git a/crates/cellscript-tools/src/service_builder.rs b/crates/cellscript-tools/src/service_builder.rs new file mode 100644 index 00000000..9aab769c --- /dev/null +++ b/crates/cellscript-tools/src/service_builder.rs @@ -0,0 +1,199 @@ +//! NovaSeal service-builder fixture generator. + +use std::collections::BTreeSet; +use std::fs; +use std::path::Path; + +use anyhow::{Context, Result}; +use serde_json::{json, Map, Value}; + +use crate::btc_anchor::public_btc_anchor_shape_matches_profile; +use crate::crypto::{canonical_report_hash, nonzero_hex32}; +use crate::shared::{lexical_path, stable_json_pretty}; + +const REPORT_PERSON: &[u8] = b"NovaSvcBuildV0"; + +fn report_hash(label: &str, value: &Value) -> Result { + canonical_report_hash(REPORT_PERSON, label, value) +} + +fn required<'value>(object: &'value Map, key: &str) -> Result<&'value Value> { + object.get(key).with_context(|| format!("operator fixture case is missing {key}")) +} + +fn required_string<'value>(object: &'value Map, key: &str) -> Result<&'value str> { + required(object, key)?.as_str().with_context(|| format!("operator fixture case field {key} is not a string")) +} + +fn external_inputs(profile: &str) -> Vec<&'static str> { + let mut required = vec!["public_shared_cell_dep_attestation", "external_bip340_tcb_review_attestation"]; + if matches!(profile, "btc-transaction-commitment-profile-v0" | "btc-utxo-seal-profile-v0" | "dual-seal-profile-v0") { + required.push("public_btc_spv_evidence"); + } + if profile == "rwa-receipt-profile-v0" { + required.push("legal_registry_review_evidence"); + } + required +} + +fn build_case(operator_case: &Value) -> Result { + let operator = operator_case.as_object().context("operator fixture case is not an object")?; + let profile = required_string(operator, "profile")?; + let action = required_string(operator, "action")?; + let fixture = required_string(operator, "fixture")?; + let signers = required(operator, "signers")?.clone(); + let operator_fixture_hash = report_hash("operator_case", operator_case)?; + let idempotency = json!([profile, action, fixture, required(operator, "signed_intent_hash")?,]); + let required_live_inputs = json!({ + "live_report_hash": operator.get("live_report_hash").cloned().unwrap_or(Value::Null), + "live_devnet_tx_hash": operator.get("live_devnet_tx_hash").cloned().unwrap_or(Value::Null), + "fiber_report_hash": operator.get("fiber_report_hash").cloned().unwrap_or(Value::Null), + "public_btc_anchor": operator.get("public_btc_anchor").cloned().unwrap_or(Value::Null), + }); + let request = json!({ + "schema": "novaseal-service-builder-request-v0.1", + "builder_name": "novaseal-profile-service-builder-v0", + "profile": profile, + "action": action, + "fixture": fixture, + "idempotency_key": report_hash("idempotency", &idempotency)?, + "operator_fixture_hash": operator_fixture_hash, + "signers": signers, + "required_profile_inputs": { + "source_tree_hash": required(operator, "source_tree_hash")?, + "schema_set_hash": required(operator, "schema_set_hash")?, + "proof_matrix_hash": required(operator, "proof_matrix_hash")?, + "fixture_hash": required(operator, "fixture_hash")?, + }, + "required_live_inputs": required_live_inputs, + "production_external_inputs": external_inputs(profile), + }); + let tx_skeleton = json!({ + "schema": "novaseal-service-builder-tx-skeleton-v0.1", + "profile": profile, + "action": action, + "fixture": fixture, + "builder_name": "novaseal-profile-service-builder-v0", + "operator_fixture_hash": operator_fixture_hash, + "signed_intent_hash": required(operator, "signed_intent_hash")?, + "witness_shape_hash": required(operator, "witness_shape_hash")?, + "source_tree_hash": required(operator, "source_tree_hash")?, + "live_devnet_tx_hash": operator.get("live_devnet_tx_hash").cloned().unwrap_or(Value::Null), + "public_btc_anchor": operator.get("public_btc_anchor").cloned().unwrap_or(Value::Null), + }); + let tx_skeleton_hash = report_hash("tx_skeleton", &tx_skeleton)?; + let receipt_binding = json!({ + "profile": profile, + "action": action, + "fixture": fixture, + "signed_intent_hash": required(operator, "signed_intent_hash")?, + "tx_skeleton_hash": tx_skeleton_hash, + "operator_fixture_hash": operator_fixture_hash, + }); + let builder_trace = json!({"request": request, "tx_skeleton": tx_skeleton}); + let service_queue = json!([profile, action, fixture, request["idempotency_key"]]); + let response = json!({ + "schema": "novaseal-service-builder-response-v0.1", + "builder_name": "novaseal-profile-service-builder-v0", + "profile": profile, + "action": action, + "fixture": fixture, + "service_queue_key": report_hash("service_queue", &service_queue)?, + "tx_skeleton_hash": tx_skeleton_hash, + "witness_shape_hash": required(operator, "witness_shape_hash")?, + "signed_intent_hash": required(operator, "signed_intent_hash")?, + "bip340_message_hash": required(operator, "bip340_message_hash")?, + "receipt_binding_hash": report_hash("receipt_binding", &receipt_binding)?, + "builder_trace_hash": report_hash("builder_trace", &builder_trace)?, + }); + let production_inputs = request["production_external_inputs"].as_array().context("production inputs are not an array")?; + let btc_required = production_inputs.iter().any(|item| item.as_str() == Some("public_btc_spv_evidence")); + let request_anchor = request["required_live_inputs"].get("public_btc_anchor"); + let skeleton_anchor = tx_skeleton.get("public_btc_anchor"); + let profile_inputs_valid = + request["required_profile_inputs"].as_object().context("profile inputs are not an object")?.values().all(nonzero_hex32); + let signed_intent = response.get("signed_intent_hash").context("response signed intent is missing")?; + let bip340_message = response.get("bip340_message_hash").context("response BIP340 message is missing")?; + let witness_shape = response.get("witness_shape_hash").context("response witness shape is missing")?; + let checks = json!({ + "operator_case_passed": operator.get("status").and_then(Value::as_str) == Some("passed"), + "request_hashes_present": profile_inputs_valid, + "signed_intent_hash_bound": nonzero_hex32(signed_intent) && signed_intent == required(operator, "signed_intent_hash")?, + "bip340_message_hash_bound": nonzero_hex32(bip340_message) && bip340_message == required(operator, "bip340_message_hash")?, + "witness_shape_hash_bound": nonzero_hex32(witness_shape) && witness_shape == required(operator, "witness_shape_hash")?, + "tx_skeleton_hash_present": response.get("tx_skeleton_hash").is_some_and(nonzero_hex32), + "receipt_binding_hash_present": response.get("receipt_binding_hash").is_some_and(nonzero_hex32), + "service_queue_key_present": response.get("service_queue_key").is_some_and(nonzero_hex32), + "external_requirements_named": !production_inputs.is_empty(), + "public_btc_anchor_bound_when_required": !btc_required || request_anchor.is_some_and(|anchor| !anchor.is_null() && anchor.as_bool() != Some(false)), + "public_btc_anchor_shape_matches_profile": !btc_required || public_btc_anchor_shape_matches_profile(profile, request_anchor), + "tx_skeleton_public_btc_anchor_shape_matches_profile": !btc_required || public_btc_anchor_shape_matches_profile(profile, skeleton_anchor), + }); + let passed = checks.as_object().context("checks are not an object")?.values().all(|check| check == &Value::Bool(true)); + Ok(json!({ + "profile": profile, + "action": action, + "fixture": fixture, + "status": if passed { "passed" } else { "failed" }, + "checks": checks, + "builder_name": "novaseal-profile-service-builder-v0", + "operator_fixture_hash": operator_fixture_hash, + "signers": signers, + "request": request, + "response": response, + "tx_skeleton": tx_skeleton, + })) +} + +fn build_report(operator_fixtures: &Value) -> Result { + let cases = operator_fixtures + .get("cases") + .and_then(Value::as_array) + .map(|cases| cases.iter().map(build_case).collect::>>()) + .transpose()? + .unwrap_or_default(); + let profiles: BTreeSet<&str> = cases.iter().filter_map(|case| case.get("profile").and_then(Value::as_str)).collect(); + let passed = !cases.is_empty() && cases.iter().all(|case| case.get("status").and_then(Value::as_str) == Some("passed")); + let matched = cases.iter().filter(|case| case.get("status").and_then(Value::as_str) == Some("passed")).count(); + Ok(json!({ + "schema": "novaseal-service-builder-fixtures-v0.1", + "status": if passed { "passed" } else { "failed" }, + "builder_name": "novaseal-profile-service-builder-v0", + "source_operator_fixture_report": "target/novaseal-profile-operator-fixtures.json", + "source_operator_fixture_report_hash": report_hash("operator_report", operator_fixtures)?, + "fixture_boundary": "builder fixtures model reproducible service request/response hashes for local profile evidence; public BTC SPV, public CellDep, external TCB, and legal registry evidence remain production inputs", + "summary": { + "total": cases.len(), + "matched": matched, + "profile_count": profiles.len(), + "profiles": profiles, + }, + "profiles": profiles, + "cases": cases, + })) +} + +pub fn run(root: &Path, operator_fixtures: Option<&Path>, output: Option<&Path>, pretty: bool) -> Result { + let default_operator = root.join("target/novaseal-profile-operator-fixtures.json"); + let default_output = root.join("target/novaseal-service-builder-fixtures.json"); + let operator_path = lexical_path(operator_fixtures.unwrap_or(&default_operator)); + let output_path = lexical_path(output.unwrap_or(&default_output)); + let operator: Value = + serde_json::from_slice(&fs::read(&operator_path).with_context(|| format!("failed to read {}", operator_path.display()))?) + .with_context(|| format!("{} is not valid JSON", operator_path.display()))?; + let report = build_report(&operator)?; + let parent = output_path.parent().filter(|parent| !parent.as_os_str().is_empty()).unwrap_or_else(|| Path::new(".")); + fs::create_dir_all(parent).with_context(|| format!("failed to create {}", parent.display()))?; + fs::write(&output_path, format!("{}\n", stable_json_pretty(&report)?)) + .with_context(|| format!("failed to write {}", output_path.display()))?; + if pretty { + println!( + "wrote {} status={} profiles={} cases={}", + output_path.display(), + report["status"].as_str().unwrap_or("failed"), + report["summary"]["profile_count"].as_u64().unwrap_or(0), + report["summary"]["total"].as_u64().unwrap_or(0), + ); + } + Ok(if report["status"] == "passed" { 0 } else { 1 }) +} diff --git a/crates/cellscript-tools/src/shared.rs b/crates/cellscript-tools/src/shared.rs new file mode 100644 index 00000000..a8e69182 --- /dev/null +++ b/crates/cellscript-tools/src/shared.rs @@ -0,0 +1,141 @@ +//! Shared helpers for the cellscript-tools binaries. +//! +//! These helpers preserve stable report encodings and path semantics so native +//! tools remain compatible with existing evidence. + +use std::fs; +use std::path::{Path, PathBuf}; + +use serde_json::Value; + +/// Resolve the CellScript repository root. +/// +/// Resolution walks up from the current directory until a `Cargo.toml` +/// declaring `name = "cellscript"` is found. +/// +/// `--root` overrides the walk and is canonicalised. This matters on platforms +/// such as macOS where `/var` resolves to `/private/var`. +pub fn resolve_repo_root(override_root: Option<&Path>) -> anyhow::Result { + if let Some(root) = override_root { + return fs::canonicalize(root).map_err(|e| anyhow::anyhow!("failed to resolve repository root {}: {e}", root.display())); + } + let cwd = std::env::current_dir().map_err(|e| anyhow::anyhow!("failed to read current directory: {e}"))?; + for dir in cwd.ancestors() { + let manifest = dir.join("Cargo.toml"); + if manifest.is_file() + && let Ok(text) = fs::read_to_string(&manifest) + && text.lines().any(|line| line.trim() == "name = \"cellscript\"") + { + return Ok(dir.to_path_buf()); + } + } + anyhow::bail!( + "could not locate the CellScript repository root \ + (no Cargo.toml with name = \"cellscript\" found by walking up from cwd); \ + pass --root explicitly" + ) +} + +/// Read a UTF-8 text file relative to the repo root. +/// +/// The path is resolved beneath `root` and decoded as UTF-8. +pub fn read_text(root: &Path, relative: &str) -> anyhow::Result { + let full = root.join(relative); + fs::read_to_string(&full).map_err(|e| anyhow::anyhow!("failed to read {}: {e}", full.display())) +} + +/// Substring containment check. +/// +/// This is a plain substring match, not a line-based one. Tokens may contain +/// embedded newlines; the match is byte-for-byte on the original text. +pub fn contains(text: &str, token: &str) -> bool { + text.contains(token) +} + +/// Slice the text strictly between two marker substrings. +/// +/// Returns the text after the first `start` and before the first subsequent +/// `end`, with a diagnostic naming either missing marker. +pub fn slice_between<'a>(text: &'a str, start: &str, end: &str) -> anyhow::Result<&'a str> { + let after_start = text + .split_once(start) + .map(|(_, rest)| rest) + .ok_or_else(|| anyhow::anyhow!("slice_between: start marker not found: {start:?}"))?; + let before_end = after_start + .split_once(end) + .map(|(before, _)| before) + .ok_or_else(|| anyhow::anyhow!("slice_between: end marker not found: {end:?}"))?; + Ok(before_end) +} + +/// Collapse repeated separators and `.` components without resolving symlinks +/// or parent components. +pub fn lexical_path(path: &Path) -> PathBuf { + path.components().collect() +} + +/// Render stable pretty JSON with sorted object keys and ASCII-only escapes. +pub fn stable_json_pretty(value: &Value) -> anyhow::Result { + let json = serde_json::to_string_pretty(value)?; + Ok(escape_json_non_ascii(&json)) +} + +/// Render stable compact JSON with sorted object keys and ASCII-only escapes. +pub fn stable_json_compact(value: &Value) -> anyhow::Result { + let json = serde_json::to_string(value)?; + Ok(escape_json_non_ascii(&json)) +} + +/// Render stable single-line JSON with one space after commas and colons. +pub fn stable_json_spaced(value: &Value) -> anyhow::Result { + let json = serde_json::to_string(value)?; + let mut rendered = String::with_capacity(json.len() + json.len() / 8); + let mut in_string = false; + let mut escaped = false; + for character in json.chars() { + rendered.push(character); + if in_string { + if escaped { + escaped = false; + } else if character == '\\' { + escaped = true; + } else if character == '"' { + in_string = false; + } + } else if character == '"' { + in_string = true; + } else if matches!(character, ',' | ':') { + rendered.push(' '); + } + } + Ok(escape_json_non_ascii(&rendered)) +} + +/// Escape non-ASCII text as UTF-16 `\u` units, including surrogate pairs for +/// non-BMP characters, so report bytes remain platform-independent. +fn escape_json_non_ascii(json: &str) -> String { + let mut escaped = String::with_capacity(json.len()); + for character in json.chars() { + if character.is_ascii() { + escaped.push(character); + } else { + for unit in character.encode_utf16(&mut [0; 2]) { + use std::fmt::Write as _; + write!(escaped, "\\u{unit:04x}").expect("writing to String cannot fail"); + } + } + } + escaped +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + #[test] + fn stable_spaced_json_spacing_ignores_string_punctuation() { + assert_eq!(stable_json_spaced(&json!({"a": [1, 2], "b": "x,y:z\""})).unwrap(), r#"{"a": [1, 2], "b": "x,y:z\""}"#); + } +} diff --git a/crates/cellscript-tools/src/skill_pack.rs b/crates/cellscript-tools/src/skill_pack.rs new file mode 100644 index 00000000..078b6b67 --- /dev/null +++ b/crates/cellscript-tools/src/skill_pack.rs @@ -0,0 +1,297 @@ +//! CellScript skill-pack validator used by the repository gate. +//! +//! Validates that the CellScript programming skill-pack stays fresh against +//! the current CLI: every expected skill directory exists, each `SKILL.md` +//! carries the required YAML front-matter, every referenced file exists and +//! stays inside the repo, and every `cellc` command token used in a skill is +//! present in the live CLI registry extracted from `src/cli/commands.rs`. +//! +//! Stable behavioural contract: +//! - always emits exactly one JSON document on stdout (pass or fail); +//! - exit 0 iff no failures; exit 1 if any failure was recorded; +//! - a structurally malformed `SKILL.md` is a hard error returned before any +//! JSON is printed. + +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; + +use regex::Regex; +use serde_json::json; +use std::sync::OnceLock; + +use crate::shared::stable_json_pretty; + +/// Expected skill directory names, kept sorted for readability. +const EXPECTED_SKILLS: &[&str] = &[ + "cellscript-ckb-model", + "cellscript-diagnostics", + "cellscript-language-basics", + "cellscript-metadata-audit", + "cellscript-builder-deployment", + "cellscript-package-cli", +]; + +/// Extract visible CLI command names from `src/cli/commands.rs`. +fn cli_command_regex() -> &'static Regex { + static RE: OnceLock = OnceLock::new(); + RE.get_or_init(|| Regex::new(r#"ClapCommand::new\("([^"]+)"\)"#).expect("CLI command regex must compile")) +} + +/// A parsed front-matter field. Scalars remain distinct from lists so the +/// validator can reject scalar `references` and `commands`. +enum FrontMatterValue { + Scalar(String), + List(Vec), +} + +#[derive(Default)] +struct FrontMatter { + fields: std::collections::BTreeMap, +} + +/// Hand-rolled YAML front-matter parser for the deliberately narrow skill-pack +/// schema. +/// +/// Semantics mirrored exactly: +/// - the file MUST start with `---\n` at byte 0 (no leading whitespace, no +/// CRLF tolerance); +/// - the closing `---\n` is found by splitting the text on `---\n` at most +/// twice and taking part `[1]`; +/// - a missing closing delimiter is `unterminated front matter`; +/// - a list item must begin with exactly `" - "` (two spaces, hyphen, space) +/// and must immediately follow a list-head line (the `current_list` reset +/// happens at the top of each non-list line); +/// - tabs are NOT accepted as indentation; +/// - a scalar line must contain `:`; `key: value` (value non-empty) stores a +/// scalar, `key:` (value empty) starts a list. +fn parse_front_matter(text: &str, path: &Path) -> anyhow::Result { + if !text.starts_with("---\n") { + return Err(anyhow::anyhow!("{} is missing YAML-style front matter", path.display())); + } + // Split into at most three parts on the literal delimiter. The first is + // before the opening `---\n` (empty, since the file starts with it); the + // second part is the front matter; the third is the body. + let parts: Vec<&str> = text.splitn(3, "---\n").collect(); + let header = parts.get(1).ok_or_else(|| anyhow::anyhow!("{} has unterminated front matter", path.display()))?; + + let mut fm = FrontMatter::default(); + let mut current_list: Option = None; + + for raw_line in header.split('\n') { + // Strip trailing whitespace after line splitting. + let line = raw_line.trim_end(); + if line.is_empty() { + continue; + } + // List item: must be exactly two-space indent + `- `. + if let Some(rest) = line.strip_prefix(" - ") { + let key = + current_list.as_ref().ok_or_else(|| anyhow::anyhow!("{} has a list item outside a list: {}", path.display(), line))?; + let value = rest.trim().to_string(); + match fm.fields.get_mut(key) { + Some(FrontMatterValue::List(values)) => values.push(value), + _ => unreachable!("current_list always names a list field"), + } + continue; + } + // Any non-list line resets the current list context. + current_list = None; + let Some((key, value)) = line.split_once(':') else { + return Err(anyhow::anyhow!("{} has malformed front matter line: {}", path.display(), line)); + }; + let key = key.trim().to_string(); + let value = value.trim(); + if !value.is_empty() { + // Scalar values replace any prior field value. + fm.fields.insert(key, FrontMatterValue::Scalar(value.to_string())); + } else { + // A list head replaces any prior field value with a fresh list. + fm.fields.insert(key.clone(), FrontMatterValue::List(Vec::new())); + current_list = Some(key); + } + } + Ok(fm) +} + +/// Return a field only when the front matter represented it as a YAML list. +fn list<'a>(fm: &'a FrontMatter, key: &str) -> Option<&'a [String]> { + match fm.fields.get(key) { + Some(FrontMatterValue::List(values)) => Some(values), + _ => None, + } +} + +/// Collect every `cellc` command token known to the live CLI, plus the +/// top-level `cellc` binary name. Mirrors `visible_command_names()`. +fn visible_command_names(root: &Path) -> anyhow::Result> { + let source = fs::read_to_string(root.join("src/cli/commands.rs")) + .map_err(|e| anyhow::anyhow!("failed to read src/cli/commands.rs: {e}"))?; + let mut names: BTreeSet = + cli_command_regex().captures_iter(&source).filter_map(|c| c.get(1).map(|m| m.as_str().to_string())).collect(); + names.insert("cellc".to_string()); + Ok(names) +} + +/// Discover every `docs/skills/cellscript-*/SKILL.md` and return the sorted +/// list of (absolute_path, skill_dir_name) pairs. Mirrors +/// `sorted((repo_root / "docs/skills").glob("cellscript-*/SKILL.md"))`. +fn discover_skills(root: &Path) -> anyhow::Result> { + let base = root.join("docs/skills"); + let mut found: Vec<(PathBuf, String)> = Vec::new(); + if !base.is_dir() { + return Ok(found); + } + for entry in fs::read_dir(&base)? { + let entry = entry?; + if !entry.file_type()?.is_dir() { + continue; + } + let dir_name = entry.file_name().to_string_lossy().to_string(); + if !dir_name.starts_with("cellscript-") { + continue; + } + let skill_md = entry.path().join("SKILL.md"); + if skill_md.is_file() { + found.push((skill_md, dir_name)); + } + } + // Keep discovery deterministic by sorting absolute paths lexically. + found.sort_by(|a, b| a.0.cmp(&b.0)); + Ok(found) +} + +/// Validate a single skill file, appending any failure messages to `failures`. +/// Mirrors `validate_skill()` line by line, including the exact error message +/// wording and the `{path}` / `{reference}` / `{command}` / `{part}` +/// interpolation. +fn validate_skill(skill_md: &Path, fm: &FrontMatter, root: &Path, command_names: &BTreeSet, failures: &mut Vec) { + let path_str = skill_md.display().to_string(); + + // name: present and non-empty. + let name_is_missing = match fm.fields.get("name") { + None => true, + Some(FrontMatterValue::Scalar(value)) => value.trim().is_empty(), + // Any list value counts as present here and fails later type-specific + // validation where appropriate. + Some(FrontMatterValue::List(_)) => false, + }; + if name_is_missing { + failures.push(format!("{path_str}: missing name")); + } + + // references: non-empty list. + let references = list(fm, "references").unwrap_or(&[]); + if references.is_empty() { + failures.push(format!("{path_str}: missing references list")); + } + let mut has_current_doc_or_example = false; + for reference in references { + // Strip any `#anchor` suffix before path checks. + let ref_path = reference.split('#').next().unwrap_or(reference); + if ref_path.starts_with("../") || ref_path.contains("/../") { + failures.push(format!("{path_str}: reference escapes repo root: {reference}")); + continue; + } + let full = root.join(ref_path); + if !full.exists() { + failures.push(format!("{path_str}: referenced file does not exist: {reference}")); + continue; + } + if ref_path.starts_with("docs/wiki/") || ref_path.starts_with("docs/CELLSCRIPT_") || ref_path.starts_with("examples/") { + has_current_doc_or_example = true; + } + } + if !has_current_doc_or_example { + failures.push(format!("{path_str}: references must include current docs/wiki, docs/CELLSCRIPT_*, or examples files")); + } + + // commands: non-empty list. + let commands = list(fm, "commands").unwrap_or(&[]); + if commands.is_empty() { + failures.push(format!("{path_str}: missing commands list")); + } + for command in commands { + let mut parts = command.split_whitespace(); + let first = parts.next(); + if first != Some("cellc") { + failures.push(format!("{path_str}: command must start with 'cellc': {command}")); + continue; + } + for part in parts { + if part.starts_with('-') || part.starts_with('<') { + continue; + } + if !command_names.contains(part) { + failures.push(format!("{path_str}: command token is not present in CLI registry: {command} ({part})")); + } + } + } +} + +/// Entry point. Returns the exit code the binary should propagate. +/// +/// A structurally malformed `SKILL.md` propagates an `anyhow::Error`; `main.rs` +/// prints it to stderr and returns exit code 1 without printing JSON. +pub fn run(root: &Path) -> anyhow::Result { + let skill_files = discover_skills(root)?; + let found: BTreeSet = skill_files.iter().map(|(_, name)| name.clone()).collect(); + let expected: BTreeSet = EXPECTED_SKILLS.iter().map(|s| s.to_string()).collect(); + + let mut failures: Vec = Vec::new(); + + // Directory-level failures: missing then extra, in that fixed order. + let missing: Vec<&String> = expected.difference(&found).collect(); + if !missing.is_empty() { + let joined = missing.iter().map(|s| s.as_str()).collect::>().join(", "); + failures.push(format!("missing skill directories: {joined}")); + } + let extra: Vec<&String> = found.difference(&expected).collect(); + if !extra.is_empty() { + let joined = extra.iter().map(|s| s.as_str()).collect::>().join(", "); + failures.push(format!("unexpected CellScript skill directories: {joined}")); + } + + let command_names = visible_command_names(root)?; + for (skill_md, _name) in &skill_files { + let text = fs::read_to_string(skill_md)?; + // A malformed file propagates as a hard error with no JSON emitted. + let fm = parse_front_matter(&text, skill_md)?; + validate_skill(skill_md, &fm, root, &command_names, &mut failures); + } + + let compiler_source = fs::read_to_string(root.join("src/lib.rs"))?; + let schema_re = Regex::new(r"METADATA_SCHEMA_VERSION:\s*u32\s*=\s*(\d+)")?; + let current_schema = + schema_re.captures(&compiler_source).and_then(|captures| captures.get(1)).map(|value| value.as_str().to_owned()); + match current_schema { + Some(schema) => { + let metadata_skill = root.join("docs/skills/cellscript-metadata-audit/SKILL.md"); + let text = fs::read_to_string(&metadata_skill)?; + let normalized = text.split_whitespace().collect::>().join(" "); + for marker in + [format!("current metadata schema {schema}"), "Edition 2026".to_owned(), "resolved compatibility profile".to_owned()] + { + if !normalized.contains(&marker) { + failures.push(format!("{}: missing current metadata contract marker: {marker}", metadata_skill.display())); + } + } + } + None => failures.push("src/lib.rs: missing METADATA_SCHEMA_VERSION for skill-pack freshness".to_owned()), + } + + let status = if failures.is_empty() { "passed" } else { "failed" }; + let skills_sorted: Vec<&String> = found.iter().collect::>(); + let report = json!({ + "schema": "cellscript-skill-pack-freshness-v0.24", + "status": status, + "skills": skills_sorted.iter().map(|s| s.as_str()).collect::>(), + "skill_count": skill_files.len(), + "failures": failures, + }); + // Stable pretty JSON uses sorted keys; `println!` adds the required final + // newline. + println!("{}", stable_json_pretty(&report)?); + + Ok(if failures.is_empty() { 0 } else { 1 }) +} diff --git a/crates/cellscript-tools/src/strict_backend.rs b/crates/cellscript-tools/src/strict_backend.rs new file mode 100644 index 00000000..757557c6 --- /dev/null +++ b/crates/cellscript-tools/src/strict_backend.rs @@ -0,0 +1,315 @@ +//! Strict backend audit implementation used by the repository gate. + +use std::collections::BTreeSet; +use std::env; +use std::fs; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitStatus}; +use std::time::Instant; + +use anyhow::{Context, Result}; +use serde_json::{json, Value}; +use time::OffsetDateTime; + +use crate::shared::{lexical_path, stable_json_pretty}; + +const FEATURE_IDS: &[&str] = &[ + "ir.cfg.block-id-uniqueness", + "ir.cfg.terminator-targets", + "ir.cfg.reachability", + "ir.defs.must-define-before-use", + "ir.abi.call-arg-types", + "ir.abi.return-types", + "codegen.psabi.sp-delta-alignment", + "codegen.psabi.outgoing-stack-args-0-through-20", + "codegen.tuple-return-register-contract", + "codegen.runtime-fail-closed-syscall-contracts", + "riscv.oracle.core-instruction-bytes", + "riscv.oracle.immediate-boundaries", + "riscv.branch-relaxation.near-and-far", + "riscv.machine-cfg.layout-coverage", + "riscv.elf.header-and-segment-layout", + "edge.match-wildcard-order", + "edge.tuple-projection-through-branching", + "edge.bytestring-length", + "edge.import-alias-callable-rename", + "metamorphic.numeric-type-equality-commutative", + "acceptance.syntax-combo", + "acceptance.ckb-stateful-scenarios", +]; + +#[derive(Clone, Debug)] +struct CommandSpec { + id: &'static str, + feature_ids: &'static [&'static str], + argv: &'static [&'static str], +} + +fn command_plan(mode: &str) -> Vec { + let mut commands = vec![ + CommandSpec { + id: "strict-rust-contract-tests", + feature_ids: &[ + "ir.cfg.block-id-uniqueness", + "ir.cfg.terminator-targets", + "ir.cfg.reachability", + "ir.defs.must-define-before-use", + "ir.abi.call-arg-types", + "ir.abi.return-types", + "codegen.psabi.sp-delta-alignment", + "riscv.oracle.core-instruction-bytes", + "riscv.oracle.immediate-boundaries", + "riscv.elf.header-and-segment-layout", + ], + argv: &["cargo", "test", "--locked", "-p", "cellscript", "strict_audit", "--", "--nocapture"], + }, + CommandSpec { + id: "outgoing-stack-abi-matrix", + feature_ids: &["codegen.psabi.outgoing-stack-args-0-through-20"], + argv: &[ + "cargo", + "test", + "--locked", + "-p", + "cellscript", + "outgoing_stack_arg_area_is_16_byte_aligned_at_call_boundaries", + "--", + "--nocapture", + ], + }, + CommandSpec { + id: "assembler-emitted-surface", + feature_ids: &["riscv.machine-cfg.layout-coverage"], + argv: &[ + "cargo", + "test", + "--locked", + "-p", + "cellscript", + "internal_assembler_encodes_emitted_instruction_surface", + "--", + "--nocapture", + ], + }, + CommandSpec { + id: "branch-relaxation-contracts", + feature_ids: &["riscv.branch-relaxation.near-and-far"], + argv: &["cargo", "test", "--locked", "-p", "cellscript", "relaxes", "--", "--nocapture"], + }, + CommandSpec { + id: "tuple-return-abi-contracts", + feature_ids: &["codegen.tuple-return-register-contract"], + argv: &[ + "cargo", + "test", + "--locked", + "-p", + "cellscript", + "tuple_return_abi_rejects_more_than_eight_fields", + "--", + "--nocapture", + ], + }, + CommandSpec { + id: "runtime-fail-closed-contracts", + feature_ids: &["codegen.runtime-fail-closed-syscall-contracts"], + argv: &[ + "cargo", + "test", + "--locked", + "-p", + "cellscript", + "ckb_u64_syscall_helpers_check_return_code_and_size", + "--", + "--nocapture", + ], + }, + CommandSpec { + id: "backend-shape-contracts", + feature_ids: &["riscv.machine-cfg.layout-coverage"], + argv: &[ + "cargo", + "test", + "--locked", + "-p", + "cellscript", + "bundled_examples_stay_within_backend_shape_budgets", + "--", + "--nocapture", + ], + }, + CommandSpec { + id: "wildcard-match-order-contract", + feature_ids: &["edge.match-wildcard-order"], + argv: &[ + "cargo", + "test", + "--locked", + "-p", + "cellscript", + "compile_rejects_invalid_enum_match_patterns", + "--", + "--nocapture", + ], + }, + CommandSpec { + id: "tuple-projection-branching-contracts", + feature_ids: &["edge.tuple-projection-through-branching"], + argv: &["cargo", "test", "--locked", "-p", "cellscript", "compile_preserves_", "--", "--nocapture"], + }, + CommandSpec { + id: "bytestring-length-contracts", + feature_ids: &["edge.bytestring-length"], + argv: &["cargo", "test", "--locked", "-p", "cellscript", "byte_string", "--", "--nocapture"], + }, + CommandSpec { + id: "import-alias-callable-rename-contract", + feature_ids: &["edge.import-alias-callable-rename"], + argv: &[ + "cargo", + "test", + "--locked", + "-p", + "cellscript", + "compile_package_import_alias_emits_matching_external_callable", + "--", + "--nocapture", + ], + }, + CommandSpec { + id: "numeric-type-equality-metamorphic-contract", + feature_ids: &["metamorphic.numeric-type-equality-commutative"], + argv: &[ + "cargo", + "test", + "--locked", + "-p", + "cellscript", + "numeric_named_type_equality_is_commutative", + "--", + "--nocapture", + ], + }, + ]; + if matches!(mode, "ci" | "full" | "nightly") { + commands.push(CommandSpec { + id: "syntax-combo-audit", + feature_ids: &["acceptance.syntax-combo"], + argv: &["scripts/cellscript_syntax_combo_audit.sh", "ci"], + }); + } + if matches!(mode, "full" | "nightly") { + commands.push(CommandSpec { + id: "ckb-stateful-scenarios", + feature_ids: &["acceptance.ckb-stateful-scenarios"], + argv: &["scripts/cellscript_ckb_stateful_scenarios.sh"], + }); + } + commands +} + +fn exit_code(status: ExitStatus) -> i32 { + if let Some(code) = status.code() { + return code; + } + #[cfg(unix)] + { + use std::os::unix::process::ExitStatusExt; + -status.signal().unwrap_or(1) + } + #[cfg(not(unix))] + { + 1 + } +} + +fn tail_chars(text: &str, limit: usize) -> String { + let trimmed = text.trim(); + let count = trimmed.chars().count(); + trimmed.chars().skip(count.saturating_sub(limit)).collect() +} + +fn run_command(root: &Path, spec: &CommandSpec) -> Result { + let started = Instant::now(); + let output = Command::new(spec.argv[0]) + .args(&spec.argv[1..]) + .current_dir(root) + .output() + .with_context(|| format!("failed to run {}", spec.argv.join(" ")))?; + let duration = (started.elapsed().as_secs_f64() * 1000.0).round() / 1000.0; + let stdout = String::from_utf8(output.stdout).context("strict audit command stdout is not UTF-8")?; + let stderr = String::from_utf8(output.stderr).context("strict audit command stderr is not UTF-8")?; + let combined = format!("{stdout}\n{stderr}"); + let code = exit_code(output.status); + Ok(json!({ + "id": spec.id, + "feature_ids": spec.feature_ids, + "argv": spec.argv, + "status": if code == 0 { "passed" } else { "failed" }, + "exit_code": code, + "duration_seconds": duration, + "output_tail": tail_chars(&combined, 12_000), + })) +} + +fn default_report_path(root: &Path, mode: &str) -> Result { + let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()); + let format = time::format_description::parse("[year][month][day]-[hour][minute][second]")?; + let stamp = now.format(&format)?; + Ok(root.join("target/cellscript-strict-backend-audit").join(format!("strict-backend-audit-{mode}-{stamp}.json"))) +} + +pub fn run(root: &Path, mode: &str) -> Result { + if !matches!(mode, "quick" | "ci" | "full" | "nightly") { + eprintln!("usage: cellscript-tools strict-backend [quick|ci|full|nightly]"); + return Ok(2); + } + + let report_path = match env::var_os("CELLSCRIPT_STRICT_BACKEND_AUDIT_REPORT") { + // Collapse repeated separators and `.` without resolving symlinks or + // `..` components. + Some(path) => lexical_path(&PathBuf::from(path)), + None => default_report_path(root, mode)?, + }; + let report_parent = report_path.parent().filter(|parent| !parent.as_os_str().is_empty()).unwrap_or_else(|| Path::new(".")); + fs::create_dir_all(report_parent).with_context(|| format!("failed to create report directory {}", report_parent.display()))?; + + let commands = command_plan(mode); + let mut results = Vec::with_capacity(commands.len()); + let mut tested = BTreeSet::new(); + for spec in &commands { + println!("==> {}: {}", spec.id, spec.argv.join(" ")); + io::stdout().flush().context("failed to flush strict audit progress")?; + let result = run_command(root, spec)?; + if result.get("status").and_then(Value::as_str) == Some("passed") { + tested.extend(spec.feature_ids.iter().copied()); + } + results.push(result); + } + + let mut missing: Vec<&str> = FEATURE_IDS.iter().copied().filter(|feature| !tested.contains(feature)).collect(); + missing.sort_unstable(); + let failed: Vec<&str> = results + .iter() + .filter(|result| result.get("status").and_then(Value::as_str) != Some("passed")) + .filter_map(|result| result.get("id").and_then(Value::as_str)) + .collect(); + let passed = failed.is_empty(); + let report = json!({ + "audit": "cellscript-strict-codegen-ir-riscv", + "mode": mode, + "status": if passed { "passed" } else { "failed" }, + "feature_ids": FEATURE_IDS, + "tested_feature_ids": tested, + "missing_feature_ids": missing, + "failed_commands": failed, + "artifact_hashes": [], + "ckb_vm": {"cycles": Value::Null, "transaction_size_bytes": Value::Null}, + "commands": results, + }); + fs::write(&report_path, format!("{}\n", stable_json_pretty(&report)?)) + .with_context(|| format!("failed to write {}", report_path.display()))?; + println!("strict backend audit report: {}", report_path.display()); + Ok(if passed { 0 } else { 1 }) +} diff --git a/crates/cellscript-tools/src/syntax_combo.rs b/crates/cellscript-tools/src/syntax_combo.rs new file mode 100644 index 00000000..83938f91 --- /dev/null +++ b/crates/cellscript-tools/src/syntax_combo.rs @@ -0,0 +1,1315 @@ +//! Rust runner for the matrix-driven CellScript syntax-combination audit. +//! +//! The deterministic case declarations are frozen in +//! `tests/syntax_combo/cases.json`. Runtime behaviour, seed annotations, +//! compiler execution, metadata oracles, shrinking, and report generation +//! remain implemented here so the gate has one native implementation. + +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::thread; +use std::time::Duration; + +use anyhow::{bail, Context, Result}; +use blake2b_ref::Blake2bBuilder; +use serde::Deserialize; +use serde_json::{json, Value}; +use time::format_description; +use time::OffsetDateTime; +use wait_timeout::ChildExt; + +use crate::shared::{stable_json_compact, stable_json_pretty}; + +const DEFAULT_SEED: u64 = 20_260_503; + +#[derive(Clone, Debug, Deserialize)] +struct Expected { + phase: String, + #[serde(default)] + contains: Vec, +} + +#[derive(Clone, Debug, Default, Deserialize)] +struct Oracle { + action: Option, + #[serde(default)] + consume_bindings: Vec, + #[serde(default)] + create_bindings: Vec, + #[serde(default)] + locked_outputs: Vec, + #[serde(default)] + create_fields: BTreeMap>, + #[serde(default)] + obligation_contains: Vec, + validity_type: Option, + #[serde(default)] + validity_tiers: Vec, + borrow_scope: Option, + borrow_view_type: Option, + capability_operation: Option, + capability_type: Option, + payload_enum: Option, + protocol_role_action: Option, + protocol_role: Option, + protocol_role_source: Option, + protocol_role_conflict: Option, +} + +#[derive(Clone, Debug, Deserialize)] +struct AuditCase { + name: String, + source: String, + expected: Expected, + #[serde(default)] + oracle: Oracle, + #[serde(default = "generated_origin")] + origin: String, +} + +fn generated_origin() -> String { + "generated".to_owned() +} + +impl AuditCase { + fn case_id(&self) -> String { + let input = format!("{}\n{}", self.name, self.source); + let mut state = Blake2bBuilder::new(6).build(); + state.update(input.as_bytes()); + let mut digest = [0_u8; 6]; + state.finalize(&mut digest); + hex::encode(digest) + } +} + +#[derive(Debug, Deserialize)] +struct Manifest { + cases: Vec, + governance_release_matrix: Value, + bug_class_contracts: Vec, +} + +struct CommandOutput { + success: bool, + output: String, +} + +fn run_cmd(root: &Path, argv: &[String], timeout: Duration) -> Result { + let (program, args) = argv.split_first().context("audit command is empty")?; + let mut child = Command::new(program) + .args(args) + .current_dir(root) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .with_context(|| format!("failed to run {}", argv.join(" ")))?; + let stdout = child.stdout.take().context("child stdout was not piped")?; + let stderr = child.stderr.take().context("child stderr was not piped")?; + let stdout_reader = thread::spawn(move || { + let mut bytes = Vec::new(); + let mut reader = stdout; + let _ = reader.read_to_end(&mut bytes); + bytes + }); + let stderr_reader = thread::spawn(move || { + let mut bytes = Vec::new(); + let mut reader = stderr; + let _ = reader.read_to_end(&mut bytes); + bytes + }); + let status = match child.wait_timeout(timeout)? { + Some(status) => status, + None => { + child.kill().with_context(|| format!("failed to kill timed-out command {}", argv.join(" ")))?; + let _ = child.wait(); + bail!("command timed out after {}s: {}", timeout.as_secs(), argv.join(" ")); + } + }; + let mut bytes = stdout_reader.join().unwrap_or_default(); + bytes.extend(stderr_reader.join().unwrap_or_default()); + Ok(CommandOutput { success: status.success(), output: String::from_utf8_lossy(&bytes).into_owned() }) +} + +fn compact(root: &Path, text: &str, limit: usize) -> String { + let text = text.replace(&root.display().to_string(), "$ROOT"); + if text.chars().count() <= limit { + return text; + } + let prefix: String = text.chars().take(limit).collect(); + format!("{prefix}\n......") +} + +fn cellc_bin(root: &Path) -> Result { + if let Some(value) = std::env::var_os("CELLC_BIN") { + let path = PathBuf::from(value); + if path.is_file() { + return Ok(path); + } + bail!("missing required tool: {}", path.display()); + } + let target_dir = std::env::var_os("CARGO_TARGET_DIR").map(PathBuf::from).unwrap_or_else(|| root.join("target")); + let target_dir = if target_dir.is_absolute() { target_dir } else { root.join(target_dir) }; + let candidate = target_dir.join("debug/cellc"); + if candidate.is_file() { + return Ok(candidate); + } + let build = + run_cmd(root, &["cargo".into(), "build".into(), "--locked".into(), "--bin".into(), "cellc".into()], Duration::from_secs(120))?; + if !build.success { + bail!("{}", compact(root, &build.output, 4_000)); + } + Ok(candidate) +} + +fn parse_seed(root: &Path, path: &Path) -> Result { + let text = fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; + let mut expected = Expected { phase: "accept".to_owned(), contains: Vec::new() }; + let mut oracle = Oracle::default(); + for line in text.lines() { + let Some(payload) = line.trim().strip_prefix("// audit:") else { + continue; + }; + let Some((key, value)) = payload.trim().split_once('=') else { + continue; + }; + let value = value.trim().to_owned(); + match key.trim() { + "phase" => expected.phase = value, + "contains" => expected.contains.push(value), + "validity_type" => oracle.validity_type = Some(value), + "validity_tier" => oracle.validity_tiers.push(value), + "borrow_scope" => oracle.borrow_scope = Some(value), + "borrow_view_type" => oracle.borrow_view_type = Some(value), + "capability_operation" => oracle.capability_operation = Some(value), + "capability_type" => oracle.capability_type = Some(value), + "payload_enum" => oracle.payload_enum = Some(value), + "protocol_role_action" => oracle.protocol_role_action = Some(value), + "protocol_role" => oracle.protocol_role = Some(value), + "protocol_role_source" => oracle.protocol_role_source = Some(value), + "protocol_role_conflict" => oracle.protocol_role_conflict = Some(value.eq_ignore_ascii_case("true")), + _ => {} + } + } + let stem = path.file_stem().and_then(|value| value.to_str()).context("seed path has no UTF-8 stem")?; + Ok(AuditCase { + name: format!("seed-{stem}"), + source: text, + expected, + oracle, + origin: path.strip_prefix(root).unwrap_or(path).to_string_lossy().replace('\\', "/"), + }) +} + +/// Frozen MT19937 integer-seed and bounded-selection implementation used solely +/// to preserve historical deep-audit case IDs. +struct StableMt19937 { + state: [u32; 624], + index: usize, +} + +impl StableMt19937 { + fn new(seed: u64) -> Self { + let key = [seed as u32, (seed >> 32) as u32]; + let key = if key[1] == 0 { &key[..1] } else { &key[..] }; + let mut state = [0_u32; 624]; + state[0] = 19_650_218; + for index in 1..624 { + state[index] = 1_812_433_253_u32.wrapping_mul(state[index - 1] ^ (state[index - 1] >> 30)).wrapping_add(index as u32); + } + let (mut i, mut j) = (1_usize, 0_usize); + for _ in 0..624.max(key.len()) { + state[i] = + (state[i] ^ (state[i - 1] ^ (state[i - 1] >> 30)).wrapping_mul(1_664_525)).wrapping_add(key[j]).wrapping_add(j as u32); + i += 1; + j += 1; + if i >= 624 { + state[0] = state[623]; + i = 1; + } + if j >= key.len() { + j = 0; + } + } + for _ in 0..623 { + state[i] = (state[i] ^ (state[i - 1] ^ (state[i - 1] >> 30)).wrapping_mul(1_566_083_941)).wrapping_sub(i as u32); + i += 1; + if i >= 624 { + state[0] = state[623]; + i = 1; + } + } + state[0] = 0x8000_0000; + Self { state, index: 624 } + } + + fn next_u32(&mut self) -> u32 { + if self.index >= 624 { + for index in 0..624 { + let value = (self.state[index] & 0x8000_0000) | (self.state[(index + 1) % 624] & 0x7fff_ffff); + self.state[index] = self.state[(index + 397) % 624] ^ (value >> 1) ^ if value & 1 == 0 { 0 } else { 0x9908_b0df }; + } + self.index = 0; + } + let mut value = self.state[self.index]; + self.index += 1; + value ^= value >> 11; + value ^= (value << 7) & 0x9d2c_5680; + value ^= (value << 15) & 0xefc6_0000; + value ^= value >> 18; + value + } + + fn below(&mut self, upper: usize) -> usize { + let bits = usize::BITS as usize - upper.leading_zeros() as usize; + loop { + let value = (self.next_u32() >> (32 - bits)) as usize; + if value < upper { + return value; + } + } + } + + fn choice(&mut self, upper: usize) -> usize { + self.below(upper) + } + + fn shuffle(&mut self, values: &mut [T]) { + for index in (1..values.len()).rev() { + let selected = self.below(index + 1); + values.swap(index, selected); + } + } +} + +fn module_source(module_name: &str, body: &str) -> String { + let base = format!( + "module cellscript::audit::{module_name}\n\nresource Coin has store, create, consume, replace, burn, relock {{\n amount: u64,\n nonce: u64,\n}}\n\nreceipt Voucher -> Coin has create, consume, burn {{\n amount: u64,\n nonce: u64,\n holder: Address,\n}}\n\nresource Wallet has store, create, consume, replace, burn, relock {{\n owner: Address,\n}}\n" + ); + format!("{base}\n{}\n", body.trim()) +} + +fn seeded_deep_cases(seed: u64) -> Vec { + let mut rng = StableMt19937::new(seed); + let suffix = format!("{:x}", seed & 0xffff_ffff); + let mut fields = vec!["amount", "nonce"]; + rng.shuffle(&mut fields); + let transfer_fields = fields.iter().map(|field| format!(" {field}")).collect::>().join("\n"); + let helpers = ["std::cell::preserve_type", "std::cell::same_lock", "std::cell::preserve_lock", "std::cell::preserve_capacity"]; + let helper = helpers[rng.choice(helpers.len())]; + let rejects = [ + ( + "require_block_lifecycle", + format!( + "action seeded_reject_lifecycle_{suffix}(coin: Coin, to: Address) -> next_coin: Coin {{\n verification\n require {{\n std::lifecycle::transfer(coin, next_coin, to) {{\n amount\n nonce\n }}\n }}\n}}" + ), + vec!["require block".to_owned(), "verifier-boundary syntax".to_owned()], + ), + ( + "unknown_stdlib", + format!( + "action seeded_reject_unknown_{suffix}(coin_before: Coin) -> coin_after: Coin {{\n verification\n std::cell::teleport(coin_after, coin_before)\n}}" + ), + vec!["unknown stdlib pattern".to_owned()], + ), + ( + "transfer_missing_field", + format!( + "action seeded_reject_missing_{suffix}(coin: Coin, to: Address) -> next_coin: Coin {{\n verification\n std::lifecycle::transfer(coin, next_coin, to) {{\n amount\n }}\n}}" + ), + vec!["missing nonce".to_owned()], + ), + ]; + let reject = &rejects[rng.choice(rejects.len())]; + vec![ + AuditCase { + name: format!("seeded-deep-transfer-{suffix}"), + source: module_source( + &format!("seeded_deep_transfer_{suffix}"), + &format!( + "action seeded_transfer_{suffix}(coin: Coin, to: Address) -> next_coin: Coin {{\n verification\n std::lifecycle::transfer(coin, next_coin, to) {{\n{transfer_fields}\n }}\n}}" + ), + ), + expected: Expected { phase: "accept".into(), contains: Vec::new() }, + oracle: Oracle { + action: Some(format!("seeded_transfer_{suffix}")), + consume_bindings: vec!["coin".into()], + create_bindings: vec!["next_coin".into()], + locked_outputs: vec!["next_coin".into()], + create_fields: BTreeMap::from([("next_coin".into(), fields.iter().map(ToString::to_string).collect())]), + obligation_contains: vec!["create-output-lock".into(), "consume-input:Coin:coin".into()], + ..Oracle::default() + }, + origin: "seeded:deep/stdlib-lifecycle".into(), + }, + AuditCase { + name: format!("seeded-deep-cell-helper-{suffix}"), + source: module_source( + &format!("seeded_deep_cell_helper_{suffix}"), + &format!( + "action seeded_helper_{suffix}(coin_before: Coin) -> coin_after: Coin {{\n verification\n {helper}(coin_after, coin_before)\n}}" + ), + ), + expected: Expected { phase: "accept".into(), contains: Vec::new() }, + oracle: Oracle { action: Some(format!("seeded_helper_{suffix}")), ..Oracle::default() }, + origin: "seeded:deep/cell-helper".into(), + }, + AuditCase { + name: format!("seeded-deep-reject-{}-{suffix}", reject.0), + source: module_source(&format!("seeded_deep_reject_{}_{suffix}", reject.0), &reject.1), + expected: Expected { phase: "reject_compile".into(), contains: reject.2.clone() }, + oracle: Oracle::default(), + origin: "seeded:deep/reject".into(), + }, + ] +} + +fn load_manifest(root: &Path) -> Result { + let path = root.join("tests/syntax_combo/cases.json"); + serde_json::from_slice(&fs::read(&path).with_context(|| format!("failed to read {}", path.display()))?) + .with_context(|| format!("failed to decode {}", path.display())) +} + +fn mode_table<'a>(matrix: &'a toml::Value, mode: &str) -> Option<&'a toml::value::Table> { + matrix.get("mode")?.get(mode)?.as_table() +} + +fn load_cases( + root: &Path, + manifest: &Manifest, + matrix: &toml::Value, + mode: &str, + budget: Option, + seed: u64, +) -> Result> { + // The manifest preserves the established declaration order: 24 generated cases, + // followed by 22 CI matrix cases and 3 deep-only matrix cases. Some of the + // generated edge cases intentionally carry a `matrix:edge/*` provenance, + // so origin filtering would incorrectly remove them from quick mode. + let static_count = match mode { + "quick" => 24, + "ci" => 46, + _ => manifest.cases.len(), + }; + let mut cases: Vec<_> = manifest.cases.iter().take(static_count).cloned().collect(); + if matches!(mode, "deep" | "repro") { + cases.extend(seeded_deep_cases(seed)); + } + let default_budget = mode_table(matrix, if matches!(mode, "quick" | "ci") { mode } else { "deep" }) + .and_then(|table| table.get("budget")) + .and_then(toml::Value::as_integer) + .map(|value| value as usize) + .unwrap_or(cases.len()); + let limit = budget.unwrap_or(default_budget); + cases.truncate(limit.min(cases.len())); + + let seeds = root.join("tests/syntax_combo/seeds"); + if seeds.is_dir() { + let mut paths = fs::read_dir(&seeds)?.filter_map(std::result::Result::ok).map(|entry| entry.path()).collect::>(); + paths.sort(); + let mut existing: BTreeSet = cases.iter().map(|case| case.name.clone()).collect(); + for path in paths { + if path.extension().and_then(|value| value.to_str()) != Some("cell") || !path.is_file() { + continue; + } + let case = parse_seed(root, &path)?; + if existing.insert(case.name.clone()) { + cases.push(case); + } + } + } + Ok(cases) +} + +fn output_matches(text: &str, needles: &[String]) -> bool { + let lowered = text.to_lowercase(); + needles.iter().all(|needle| lowered.contains(&needle.to_lowercase())) +} + +fn failure( + root: &Path, + case: &AuditCase, + phase: &str, + code: &str, + summary: impl Into, + run_dir: &Path, + output: &str, +) -> Result { + let shrink_dir = run_dir.join("shrink"); + fs::create_dir_all(&shrink_dir)?; + let shrink_path = shrink_dir.join(format!("{}.cell", case.case_id())); + let compact_source = + case.source.lines().filter(|line| !line.trim().is_empty() && !line.trim().starts_with("//")).collect::>().join("\n"); + fs::write(&shrink_path, format!("{compact_source}\n"))?; + Ok(json!({ + "case": case.case_id(), + "name": case.name, + "origin": case.origin, + "phase": phase, + "code": code, + "summary": summary.into(), + "shrunk": shrink_path.strip_prefix(run_dir).unwrap_or(&shrink_path).to_string_lossy().replace('\\', "/"), + "output": compact(root, output, 1_200), + })) +} + +fn find_action<'a>(metadata: &'a Value, name: &str) -> Option<&'a Value> { + metadata.get("actions")?.as_array()?.iter().find(|action| action.get("name").and_then(Value::as_str) == Some(name)) +} + +fn push_failure( + failures: &mut Vec, + root: &Path, + case: &AuditCase, + run_dir: &Path, + code: &str, + summary: impl Into, +) -> Result<()> { + failures.push(failure(root, case, "metadata", code, summary, run_dir, "")?); + Ok(()) +} + +fn validate_metadata(root: &Path, case: &AuditCase, metadata_path: &Path, run_dir: &Path) -> Result> { + let metadata: Value = match fs::read(metadata_path).ok().and_then(|bytes| serde_json::from_slice(&bytes).ok()) { + Some(metadata) => metadata, + None => { + return Ok(vec![failure(root, case, "metadata", "SCA-META-JSON", "metadata JSON decode failed", run_dir, "")?]); + } + }; + let mut failures = Vec::new(); + let required = ["actions", "compiler_version", "constraints", "lowering", "runtime", "target_profile"]; + let missing = required.iter().filter(|key| metadata.get(**key).is_none()).copied().collect::>(); + if !missing.is_empty() { + push_failure(&mut failures, root, case, run_dir, "SCA-META-KEYS", format!("metadata missing keys: {}", missing.join(", ")))?; + } + if metadata.pointer("/target_profile/name").and_then(Value::as_str) != Some("ckb") { + push_failure(&mut failures, root, case, run_dir, "SCA-META-PROFILE", "metadata target_profile.name is not ckb")?; + } + + let oracle = &case.oracle; + if let Some(operation) = &oracle.capability_operation { + let registry = metadata.get("capability_registry").unwrap_or(&Value::Null); + if registry.get("capability_set_version").and_then(Value::as_u64) != Some(1) + || registry.get("entailment_version").and_then(Value::as_u64) != Some(1) + { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-CAPABILITY-VERSION", + "capability registry versions are not set to v1", + )?; + } + let canonical = json!(["store", "create", "consume", "destroy", "replace", "burn", "relock", "retarget_type", "read_ref"]); + if registry.get("capabilities") != Some(&canonical) { + push_failure(&mut failures, root, case, run_dir, "SCA-META-CAPABILITY-REGISTRY", "capability registry is not canonical")?; + } + let proofs = metadata + .pointer("/runtime/capability_proofs") + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter(|proof| { + proof.get("operation").and_then(Value::as_str) == Some(operation) + && oracle + .capability_type + .as_deref() + .is_none_or(|kind| proof.get("type_name").and_then(Value::as_str) == Some(kind)) + }) + .collect::>(); + if proofs.is_empty() { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-CAPABILITY-PROOF", + format!("missing capability proof for {operation}"), + )?; + } else { + let proof = proofs[0]; + let fields = ["required", "provided", "entailed", "missing", "capability_set_version", "entailment_version"]; + if fields.iter().any(|field| proof.get(*field).is_none()) || proof.get("missing") != Some(&json!([])) { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-CAPABILITY-EVIDENCE", + "capability proof is missing required/provided/entailed/missing/version evidence", + )?; + } + } + } + + if let Some(enum_name) = &oracle.payload_enum { + let layouts = metadata + .get("enum_layouts") + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter(|layout| layout.get("name").and_then(Value::as_str) == Some(enum_name)) + .collect::>(); + if layouts.is_empty() { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-PAYLOAD-ENUM", + format!("missing payload enum layout for {enum_name}"), + )?; + } else { + let layout = layouts[0]; + let has_payload = layout + .get("variants") + .and_then(Value::as_array) + .into_iter() + .flatten() + .flat_map(|variant| variant.get("fields").and_then(Value::as_array).into_iter().flatten()) + .next() + .is_some(); + if layout.get("generic").and_then(Value::as_bool) != Some(false) + || layout.get("layout").and_then(Value::as_str) != Some("packed-tagged-union-v1") + || layout.get("tag_width_bytes").and_then(Value::as_u64) != Some(1) + || layout.get("encoded_size_bytes").and_then(Value::as_u64).unwrap_or(0) <= 1 + || !has_payload + { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-PAYLOAD-ENUM-LAYOUT", + "payload enum metadata is missing its concrete fixed-width tagged-union contract", + )?; + } + } + } + + if let Some(action_name) = &oracle.protocol_role_action { + if let Some(action) = find_action(&metadata, action_name) { + let candidates = action.get("protocol_role_candidates").and_then(Value::as_array).cloned().unwrap_or_default(); + if candidates.is_empty() { + push_failure(&mut failures, root, case, run_dir, "SCA-META-PROTOCOL-ROLE", "missing attributed role candidates")?; + } else { + let selected = &candidates[0]; + if selected.get("role").and_then(Value::as_str) != oracle.protocol_role.as_deref() + || selected.get("source").and_then(Value::as_str) != oracle.protocol_role_source.as_deref() + { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-PROTOCOL-ROLE-PRECEDENCE", + "selected role/source does not match the audit oracle", + )?; + } + if candidates.iter().any(|candidate| { + candidate.get("evidence_tier").and_then(Value::as_str) != Some("metadata-only") + || candidate.get("authorization_proven").and_then(Value::as_bool) != Some(false) + }) { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-PROTOCOL-ROLE-OVERCLAIM", + "role candidates must remain metadata-only with authorization_proven=false", + )?; + } + let roles = + candidates.iter().filter_map(|candidate| candidate.get("role").and_then(Value::as_str)).collect::>(); + let conflict = roles.len() > 1; + if oracle.protocol_role_conflict.is_some_and(|expected| expected != conflict) { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-PROTOCOL-ROLE-CONFLICT", + format!("role conflict={conflict} does not match expected {:?}", oracle.protocol_role_conflict), + )?; + } + if action.get("proof_plan").and_then(Value::as_array).is_some_and(|plans| { + plans.iter().any(|plan| plan.get("category").and_then(Value::as_str) == Some("protocol-role")) + }) { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-PROTOCOL-ROLE-PROOFPLAN", + "ProtocolGraph roles must not appear as ProofPlan authorization evidence", + )?; + } + } + } else { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-PROTOCOL-ROLE-ACTION", + format!("missing ProtocolGraph role action {action_name}"), + )?; + } + } + + if let Some(scope) = &oracle.borrow_scope { + let region = metadata + .pointer("/runtime/borrow_regions") + .and_then(Value::as_array) + .into_iter() + .flatten() + .find(|region| region.get("scope_name").and_then(Value::as_str) == Some(scope)); + if let Some(region) = region { + if oracle.borrow_view_type.as_deref().is_some_and(|view| region.get("view_type").and_then(Value::as_str) != Some(view)) { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-BORROW-VIEW", + "borrow view type does not match audit oracle", + )?; + } + if region.get("storage").and_then(Value::as_str) != Some("none") + || region.get("abi").and_then(Value::as_str) != Some("none") + || region.get("evidence_tier").and_then(Value::as_str) != Some("checked-static") + { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-BORROW-EVIDENCE", + "borrow region must declare storage=none, abi=none, and checked-static evidence", + )?; + } + let prefix = format!("action:{scope}#borrow-region:"); + let plan = metadata + .pointer("/runtime/proof_plan") + .and_then(Value::as_array) + .into_iter() + .flatten() + .find(|plan| plan.get("origin").and_then(Value::as_str).is_some_and(|origin| origin.starts_with(&prefix))); + if plan.and_then(|plan| plan.get("evidence_tier")).and_then(Value::as_str) != Some("checked-static") { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-BORROW-PROOFPLAN", + "borrow region is missing a checked-static ProofPlan record", + )?; + } + } else { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-BORROW-REGION", + format!("missing borrow metadata for {scope}"), + )?; + } + } + + if let Some(type_name) = &oracle.validity_type { + let type_metadata = metadata + .get("types") + .and_then(Value::as_array) + .into_iter() + .flatten() + .find(|item| item.get("name").and_then(Value::as_str) == Some(type_name)); + if let Some(type_metadata) = type_metadata { + let predicates = type_metadata.get("validity_predicates").and_then(Value::as_array).cloned().unwrap_or_default(); + if predicates.is_empty() { + push_failure(&mut failures, root, case, run_dir, "SCA-META-VALIDITY", "validity metadata has no predicate records")?; + } + let canonical = [ + "checked-static", + "checked-runtime", + "runtime-helper-required", + "builder-evidence-required", + "metadata-only", + "chain-evidence-required", + ]; + let tiers = + predicates.iter().filter_map(|predicate| predicate.get("evidence_tier").and_then(Value::as_str)).collect::>(); + if tiers.iter().any(|tier| !canonical.contains(tier)) { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-VALIDITY-TIER", + "validity metadata contains non-canonical evidence tiers", + )?; + } + for tier in &oracle.validity_tiers { + if !tiers.contains(&tier.as_str()) { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-VALIDITY-TIER", + format!("validity metadata is missing evidence tier '{tier}'"), + )?; + } + } + let prefix = format!("validity:{type_name}#"); + let plan_count = metadata + .pointer("/runtime/proof_plan") + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter(|plan| plan.get("origin").and_then(Value::as_str).is_some_and(|origin| origin.starts_with(&prefix))) + .count(); + if plan_count < predicates.len() { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-VALIDITY-PROOFPLAN", + format!("validity ProofPlan count {plan_count} is smaller than predicate count {}", predicates.len()), + )?; + } + } else { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-VALIDITY-TYPE", + format!("missing type metadata for {type_name}"), + )?; + } + } + + if let Some(action_name) = &oracle.action { + let Some(action) = find_action(&metadata, action_name) else { + push_failure(&mut failures, root, case, run_dir, "SCA-META-ACTION", format!("missing action metadata for {action_name}"))?; + return Ok(failures); + }; + let consume_bindings = action + .get("consume_set") + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter_map(|item| item.get("binding").and_then(Value::as_str)) + .collect::>(); + let expected_consume = oracle.consume_bindings.iter().map(String::as_str).collect::>(); + if !oracle.consume_bindings.is_empty() && consume_bindings != expected_consume { + push_failure(&mut failures, root, case, run_dir, "SCA-META-CONSUME", "consume bindings do not match audit oracle")?; + } + if consume_bindings.iter().copied().collect::>().len() != consume_bindings.len() { + push_failure(&mut failures, root, case, run_dir, "SCA-META-DUP-CONSUME", "duplicate consume binding")?; + } + let create_by_binding = action + .get("create_set") + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter_map(|item| Some((item.get("binding")?.as_str()?, item))) + .collect::>(); + for binding in &oracle.create_bindings { + if !create_by_binding.contains_key(binding.as_str()) { + push_failure(&mut failures, root, case, run_dir, "SCA-META-CREATE", format!("missing create binding {binding}"))?; + } + } + for binding in &oracle.locked_outputs { + if create_by_binding.get(binding.as_str()).and_then(|item| item.get("has_lock")).and_then(Value::as_bool) != Some(true) { + push_failure(&mut failures, root, case, run_dir, "SCA-META-LOCK", format!("create binding {binding} is not locked"))?; + } + } + for (binding, fields) in &oracle.create_fields { + let actual = create_by_binding + .get(binding.as_str()) + .and_then(|item| item.get("fields")) + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter_map(Value::as_str) + .collect::>(); + if actual != fields.iter().map(String::as_str).collect::>() { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-FIELDS", + format!("create fields for {binding} do not match audit oracle"), + )?; + } + } + let obligations = stable_json_compact(action.get("verifier_obligations").unwrap_or(&Value::Null))?; + for needle in &oracle.obligation_contains { + if !obligations.contains(needle) { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-OBLIGATION", + format!("missing obligation containing '{needle}'"), + )?; + } + } + if action + .get("fail_closed_runtime_features") + .is_some_and(|value| !value.as_array().is_some_and(Vec::is_empty) && !value.is_null()) + { + push_failure( + &mut failures, + root, + case, + run_dir, + "SCA-META-FAIL-CLOSED", + "accepted audit case contains fail_closed_runtime_features", + )?; + } + } + Ok(failures) +} + +fn audit_case(root: &Path, case: &AuditCase, run_dir: &Path, cellc: &Path) -> Result<(String, Vec)> { + let case_id = case.case_id(); + let case_path = if case.expected.phase == "reject_parse" { + run_dir.join("parse_reject").join(format!("{case_id}.cell")) + } else { + run_dir.join("cases").join(format!("{case_id}.cell")) + }; + let fmt_path = run_dir.join("fmt").join(format!("{case_id}.cell")); + let asm_path = run_dir.join("asm").join(format!("{case_id}.s")); + let meta_path = run_dir.join("meta").join(format!("{case_id}.json")); + for parent in [case_path.parent(), fmt_path.parent(), asm_path.parent(), meta_path.parent()].into_iter().flatten() { + fs::create_dir_all(parent)?; + } + fs::write(&case_path, &case.source)?; + let cellc = cellc.display().to_string(); + let parse = run_cmd(root, &[cellc.clone(), "--parse".into(), case_path.display().to_string()], Duration::from_secs(20))?; + if case.expected.phase == "reject_parse" { + if parse.success { + return Ok(( + "failed".into(), + vec![failure( + root, + case, + "parse", + "SCA-PARSE-ACCEPTED", + "expected parse rejection, got success", + run_dir, + &parse.output, + )?], + )); + } + if !output_matches(&parse.output, &case.expected.contains) { + return Ok(( + "failed".into(), + vec![failure( + root, + case, + "parse", + "SCA-PARSE-DIAGNOSTIC", + "parse diagnostic missing expected tokens", + run_dir, + &parse.output, + )?], + )); + } + return Ok(("rejected".into(), Vec::new())); + } + if !parse.success { + return Ok(( + "failed".into(), + vec![failure(root, case, "parse", "SCA-PARSE-FAILED", "unexpected parse failure", run_dir, &parse.output)?], + )); + } + + if case.expected.phase == "accept" { + fs::write(&fmt_path, &case.source)?; + let formatted = + run_cmd(root, &[cellc.clone(), "fmt".into(), "--json".into(), fmt_path.display().to_string()], Duration::from_secs(20))?; + if !formatted.success { + return Ok(( + "failed".into(), + vec![failure(root, case, "fmt", "SCA-FMT-FAILED", "formatter failed", run_dir, &formatted.output)?], + )); + } + let checked = run_cmd( + root, + &[cellc.clone(), "fmt".into(), "--check".into(), "--json".into(), fmt_path.display().to_string()], + Duration::from_secs(20), + )?; + if !checked.success { + return Ok(( + "failed".into(), + vec![failure( + root, + case, + "fmt", + "SCA-FMT-NON-IDEMPOTENT", + "formatted source is not idempotent", + run_dir, + &checked.output, + )?], + )); + } + let reparsed = run_cmd(root, &[cellc.clone(), "--parse".into(), fmt_path.display().to_string()], Duration::from_secs(20))?; + if !reparsed.success { + return Ok(( + "failed".into(), + vec![failure(root, case, "fmt", "SCA-FMT-PARSE", "formatted source does not parse", run_dir, &reparsed.output)?], + )); + } + } + + let compiled = run_cmd( + root, + &[ + cellc.clone(), + case_path.display().to_string(), + "--target".into(), + "riscv64-asm".into(), + "--target-profile".into(), + "ckb".into(), + "--primitive-strict".into(), + "0.15".into(), + "-o".into(), + asm_path.display().to_string(), + ], + Duration::from_secs(30), + )?; + if case.expected.phase == "reject_compile" { + if compiled.success { + return Ok(( + "failed".into(), + vec![failure( + root, + case, + "compile", + "SCA-COMPILE-ACCEPTED", + "expected compile rejection, got success", + run_dir, + &compiled.output, + )?], + )); + } + if !output_matches(&compiled.output, &case.expected.contains) { + return Ok(( + "failed".into(), + vec![failure( + root, + case, + "compile", + "SCA-COMPILE-DIAGNOSTIC", + "compile diagnostic missing expected tokens", + run_dir, + &compiled.output, + )?], + )); + } + return Ok(("rejected".into(), Vec::new())); + } + if !compiled.success { + return Ok(( + "failed".into(), + vec![failure(root, case, "compile", "SCA-COMPILE-FAILED", "unexpected compile failure", run_dir, &compiled.output)?], + )); + } + if fs::metadata(&asm_path).map_or(true, |metadata| metadata.len() == 0) { + return Ok(( + "failed".into(), + vec![failure( + root, + case, + "codegen", + "SCA-CODEGEN-EMPTY", + "assembly output is missing or empty", + run_dir, + &compiled.output, + )?], + )); + } + let asm = fs::read_to_string(&asm_path) + .unwrap_or_else(|_| String::from_utf8_lossy(&fs::read(&asm_path).unwrap_or_default()).into_owned()); + for obsolete in ["IrTransfer", "IrClaim", "IrSettle"] { + if asm.contains(obsolete) { + return Ok(( + "failed".into(), + vec![failure( + root, + case, + "codegen", + "SCA-CODEGEN-OBSOLETE", + format!("assembly contains obsolete token {obsolete}"), + run_dir, + "", + )?], + )); + } + } + let metadata = run_cmd( + root, + &[ + cellc, + "metadata".into(), + case_path.display().to_string(), + "--target".into(), + "riscv64-asm".into(), + "--target-profile".into(), + "ckb".into(), + "-o".into(), + meta_path.display().to_string(), + ], + Duration::from_secs(30), + )?; + if !metadata.success { + return Ok(( + "failed".into(), + vec![failure(root, case, "metadata", "SCA-META-FAILED", "metadata command failed", run_dir, &metadata.output)?], + )); + } + let failures = validate_metadata(root, case, &meta_path, run_dir)?; + if failures.is_empty() { + Ok(("accepted".into(), failures)) + } else { + Ok(("failed".into(), failures)) + } +} + +fn rank(mode: &str) -> usize { + match mode { + "quick" => 0, + "ci" => 1, + "deep" => 2, + "repro" => 3, + _ => 0, + } +} + +fn string_array(value: Option<&Value>) -> Vec { + value.and_then(Value::as_array).into_iter().flatten().filter_map(Value::as_str).map(ToOwned::to_owned).collect() +} + +fn evaluate_bug_class_coverage(mode: &str, cases: &[AuditCase], contracts: &[Value]) -> Value { + let names: BTreeSet<_> = cases.iter().map(|case| case.name.as_str()).collect(); + let origins: BTreeSet<_> = cases.iter().map(|case| case.origin.as_str()).collect(); + Value::Array( + contracts + .iter() + .map(|contract| { + let min_mode = contract.get("min_mode").and_then(Value::as_str).unwrap_or("quick"); + let required = rank(mode) >= rank(min_mode); + let required_cases = string_array(contract.get("required_cases")); + let required_origins = string_array(contract.get("required_origins")); + let missing_cases = required_cases.iter().filter(|name| !names.contains(name.as_str())).cloned().collect::>(); + let missing_origins = + required_origins.iter().filter(|origin| !origins.contains(origin.as_str())).cloned().collect::>(); + let covered = missing_cases.is_empty() && missing_origins.is_empty(); + json!({ + "id": contract.get("id").cloned().unwrap_or(Value::Null), + "name": contract.get("name").cloned().unwrap_or(Value::Null), + "status": if required { if covered { "covered" } else { "missing" } } else { "not_required_for_mode" }, + "required": required, + "min_mode": min_mode, + "required_cases": required_cases, + "required_origins": required_origins, + "missing_cases": if required { missing_cases } else { Vec::new() }, + "missing_origins": if required { missing_origins } else { Vec::new() }, + "release_boundary": contract.get("release_boundary").cloned().unwrap_or(Value::Null), + }) + }) + .collect(), + ) +} + +fn governance_oracles(matrix: &toml::Value) -> Value { + let configured = matrix.get("required_oracles"); + let flag = |name: &str| configured.and_then(|value| value.get(name)).and_then(toml::Value::as_bool).unwrap_or(false); + json!({ + "parser": flag("parse"), + "formatter_roundtrip": flag("formatter_roundtrip"), + "type_effect": flag("type_effect"), + "ir_metadata": flag("ir_metadata"), + "codegen_assembly": flag("codegen_assembly"), + "compact_report": flag("compact_report"), + }) +} + +fn contract_failure(code: &str, summary: impl Into) -> Value { + json!({ + "case": "-", + "name": "mode-contract", + "origin": "tests/syntax_combo/matrix.toml", + "phase": "contract", + "code": code, + "summary": summary.into(), + "shrunk": "", + "output": "", + }) +} + +fn validate_mode_contract(mode: &str, matrix: &toml::Value, report: &Value) -> Vec { + if mode == "repro" { + return Vec::new(); + } + let Some(config) = mode_table(matrix, mode) else { + return Vec::new(); + }; + let mut failures = Vec::new(); + for (config_key, report_key, code) in [ + ("min_cases", "generated", "SCA-CONTRACT-CASES"), + ("min_accept", "accepted", "SCA-CONTRACT-ACCEPT"), + ("min_reject", "rejected", "SCA-CONTRACT-REJECT"), + ] { + let Some(expected) = config.get(config_key).and_then(toml::Value::as_integer) else { + continue; + }; + let actual = report.get(report_key).and_then(Value::as_i64).unwrap_or(0); + if actual < expected { + failures.push(contract_failure(code, format!("{mode} {report_key} floor {expected} not met; got {actual}"))); + } + } + let origins = report.get("origins").and_then(Value::as_object); + let required_origins = + config.get("required_origins").and_then(toml::Value::as_array).into_iter().flatten().filter_map(toml::Value::as_str); + let missing_origins = required_origins.filter(|origin| origins.is_none_or(|map| !map.contains_key(*origin))).collect::>(); + if !missing_origins.is_empty() { + failures + .push(contract_failure("SCA-CONTRACT-ORIGIN", format!("{mode} missing required origins: {}", missing_origins.join(", ")))); + } + for item in report.get("known_bug_classes").and_then(Value::as_array).into_iter().flatten() { + if item.get("required").and_then(Value::as_bool) != Some(true) || item.get("status").and_then(Value::as_str) == Some("covered") + { + continue; + } + let mut details = Vec::new(); + let missing_cases = string_array(item.get("missing_cases")); + let missing_origins = string_array(item.get("missing_origins")); + if !missing_cases.is_empty() { + details.push(format!("missing cases: {}", missing_cases.join(", "))); + } + if !missing_origins.is_empty() { + details.push(format!("missing origins: {}", missing_origins.join(", "))); + } + failures.push(contract_failure( + item.get("id").and_then(Value::as_str).unwrap_or("SCA-CONTRACT-BUG"), + format!( + "{mode} bug-class coverage missing for {}: {}", + item.get("name").and_then(Value::as_str).unwrap_or("unknown"), + details.join("; ") + ), + )); + } + failures +} + +fn write_reports(run_dir: &Path, report: &Value, failures: &[Value]) -> Result<()> { + fs::write(run_dir.join("report.json"), format!("{}\n", stable_json_pretty(report)?))?; + let mut jsonl = String::new(); + for item in failures { + jsonl.push_str(&stable_json_compact(item)?); + jsonl.push('\n'); + } + fs::write(run_dir.join("report.jsonl"), jsonl)?; + Ok(()) +} + +pub fn run(root: &Path, mode: &str, seed: u64, budget: Option, case_name: Option<&str>) -> Result { + let _ = DEFAULT_SEED; + let manifest = load_manifest(root)?; + let matrix_path = root.join("tests/syntax_combo/matrix.toml"); + let matrix: toml::Value = + fs::read_to_string(&matrix_path)?.parse().with_context(|| format!("failed to parse {}", matrix_path.display()))?; + let cellc = cellc_bin(root)?; + let timestamp_format = format_description::parse("[year][month][day]-[hour][minute][second]")?; + let timestamp = OffsetDateTime::now_utc().format(×tamp_format)?; + let run_dir = root.join("target/syntax-combo-audit").join(format!("{timestamp}-{mode}-{seed}")); + fs::create_dir_all(&run_dir)?; + let mut cases = load_cases(root, &manifest, &matrix, mode, budget, seed)?; + if mode == "repro" { + let selected = case_name.context("repro mode requires --case ")?; + cases.retain(|case| case.name == selected || case.case_id() == selected); + if cases.is_empty() { + bail!("unknown repro case: {selected}"); + } + } + + let mut failures = Vec::new(); + let mut accepted = 0_usize; + let mut rejected = 0_usize; + let mut phases: BTreeMap> = BTreeMap::new(); + let mut origins: BTreeMap = BTreeMap::new(); + for case in &cases { + *origins.entry(case.origin.clone()).or_default() += 1; + let (status, case_failures) = audit_case(root, case, &run_dir, &cellc)?; + let phase = + phases.entry(case.expected.phase.clone()).or_insert_with(|| BTreeMap::from([("failed".into(), 0), ("passed".into(), 0)])); + if case_failures.is_empty() { + *phase.entry("passed".into()).or_default() += 1; + } else { + *phase.entry("failed".into()).or_default() += 1; + failures.extend(case_failures); + } + match status.as_str() { + "accepted" => accepted += 1, + "rejected" => rejected += 1, + _ => {} + } + } + let known_bug_classes = evaluate_bug_class_coverage(mode, &cases, &manifest.bug_class_contracts); + let mut report = json!({ + "status": if failures.is_empty() { "passed" } else { "failed" }, + "mode": mode, + "seed": seed, + "generated": cases.len(), + "accepted": accepted, + "rejected": rejected, + "failures_count": failures.len(), + "governance_release_matrix": manifest.governance_release_matrix, + "governance_oracles": governance_oracles(&matrix), + "known_bug_classes": known_bug_classes, + "phases": phases, + "origins": origins, + "failures": failures.iter().take(10).cloned().collect::>(), + }); + let contract_failures = validate_mode_contract(mode, &matrix, &report); + if !contract_failures.is_empty() { + failures.extend(contract_failures); + report["status"] = Value::String("failed".into()); + report["failures_count"] = Value::from(failures.len()); + report["failures"] = Value::Array(failures.iter().take(10).cloned().collect()); + } + write_reports(&run_dir, &report, &failures)?; + println!( + "syntax-combo-audit: {} seed={seed} mode={mode} generated={} accepted={accepted} rejected={rejected} failures={}", + report.get("status").and_then(Value::as_str).unwrap_or("failed"), + cases.len(), + failures.len() + ); + println!("report={}", run_dir.join("report.json").display()); + if !failures.is_empty() { + println!("top:"); + for item in failures.iter().take(5) { + println!( + " {} {} case={} phase={}", + item.get("code").and_then(Value::as_str).unwrap_or("-"), + item.get("summary").and_then(Value::as_str).unwrap_or("-"), + item.get("case").and_then(Value::as_str).unwrap_or("-"), + item.get("phase").and_then(Value::as_str).unwrap_or("-") + ); + } + Ok(1) + } else { + Ok(0) + } +} diff --git a/crates/cellscript-tools/src/tooling_release.rs b/crates/cellscript-tools/src/tooling_release.rs new file mode 100644 index 00000000..bdf2be34 --- /dev/null +++ b/crates/cellscript-tools/src/tooling_release.rs @@ -0,0 +1,637 @@ +//! Tooling-release boundary validator used by the repository gate. +//! +//! Asserts that the CellScript release boundary is consistent across +//! `Cargo.toml`, `Cargo.lock`, the VS Code extension, the changelogs, the +//! wiki, the gate script, the website, and the source pin points. +//! +//! Stable behavioural contract: +//! - success: prints exactly `valid CellScript tooling release boundary` to +//! stdout and returns exit code 0; +//! - assertion failure: prints +//! `invalid CellScript tooling release boundary: ` to stderr and +//! returns exit code 1; +//! - structural failure (missing file / malformed JSON or TOML / missing gate +//! marker): returns exit code 1 with a clean `anyhow` diagnostic. + +use std::path::Path; +use std::sync::OnceLock; + +use anyhow::{anyhow, Result}; +use regex::Regex; + +use crate::shared::{contains, read_text, slice_between}; + +/// A small helper for the substring-check idiom `token in text`. +/// +/// Re-reads the file once per call and retains the stable per-token error +/// format ` is missing ''`. +fn require_contains(root: &Path, path: &str, tokens: &[impl AsRef]) -> Result<()> { + let text = read_text(root, path)?; + for token in tokens { + let token = token.as_ref(); + if !contains(&text, token) { + return Err(anyhow!("{path} is missing '{token}'")); + } + } + Ok(()) +} + +/// The message is the inner text only; the wrapping +/// `invalid CellScript tooling release boundary: ` prefix is added here so +/// callers can use the bare inner message. +fn require(condition: bool, message: impl Into) -> Result<()> { + if condition { + Ok(()) + } else { + Err(anyhow!("invalid CellScript tooling release boundary: {}", message.into())) + } +} + +/// Same as `require`, but constructs the message only on failure. +fn require_with String>(condition: bool, msg: F) -> Result<()> { + if condition { + Ok(()) + } else { + Err(anyhow!("invalid CellScript tooling release boundary: {}", msg())) + } +} + +fn require_ordered_script_steps(script_name: &str, command: &str, required_steps: &[&str]) -> Result<()> { + let steps = command.split(" && ").map(str::trim).collect::>(); + let mut next_index = 0; + for required_step in required_steps { + let Some(relative_index) = steps[next_index..].iter().position(|step| step == required_step) else { + return Err(anyhow!( + "invalid CellScript tooling release boundary: website package script '{script_name}' must run '{required_step}' in order" + )); + }; + next_index += relative_index + 1; + } + Ok(()) +} + +/// Capture semver from the first `## - ` heading. `(?m)` lets `^` +/// match every line start. +fn changelog_head() -> &'static Regex { + static RE: OnceLock = OnceLock::new(); + RE.get_or_init(|| { + Regex::new(r"(?m)^## ([0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?) - ").expect("changelog heading regex must compile") + }) +} + +/// Compute `release_surface`: first two dotted components of the version with +/// any `-pre-release` stripped. Mirrors +/// `".".join(crate_version.split("-", 1)[0].split(".")[:2])`. +fn release_surface(crate_version: &str) -> String { + let base = crate_version.split('-').next().unwrap_or(crate_version); + base.split('.').take(2).collect::>().join(".") +} + +/// Entry point. Returns `Ok(())` on a valid boundary and a stable diagnostic on +/// failure. +pub fn run(root: &Path) -> Result<()> { + // --- Stage A: load inputs and derive version-dependent values --------- + let cargo_toml = read_text(root, "Cargo.toml")?; + let cargo: toml::Value = cargo_toml.parse().map_err(|e| anyhow!("Cargo.toml is not valid TOML: {e}"))?; + let cargo_lock: toml::Value = read_text(root, "Cargo.lock")?.parse().map_err(|e| anyhow!("Cargo.lock is not valid TOML: {e}"))?; + let package_json: serde_json::Value = serde_json::from_str(&read_text(root, "editors/vscode-cellscript/package.json")?) + .map_err(|e| anyhow!("VS Code package.json is not valid JSON: {e}"))?; + let website_package_json: serde_json::Value = serde_json::from_str(&read_text(root, "website/package.json")?) + .map_err(|e| anyhow!("website/package.json is not valid JSON: {e}"))?; + let changelog = read_text(root, "CHANGELOG.md")?; + let extension_changelog = read_text(root, "editors/vscode-cellscript/CHANGELOG.md")?; + let extension_readme = read_text(root, "editors/vscode-cellscript/README.md")?; + + let crate_version = cargo + .get("package") + .and_then(|p| p.get("version")) + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow!("Cargo.toml package.version is missing"))? + .to_string(); + + let lock_versions: Vec = cargo_lock + .get("package") + .and_then(|p| p.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|entry| { + let name = entry.get("name").and_then(|n| n.as_str())?; + let version = entry.get("version").and_then(|v| v.as_str())?; + (name == "cellscript").then(|| version.to_string()) + }) + .collect() + }) + .unwrap_or_default(); + + let surface = release_surface(&crate_version); + let changelog_match = changelog_head().captures(&changelog); + + // --- Stage B: version-consistency checks ------------------------------ + require_with(lock_versions.as_slice() == [crate_version.as_str()], || { + "Cargo.lock cellscript version must match Cargo.toml package.version".to_string() + })?; + require_with(package_json.get("version").and_then(|v| v.as_str()) == Some(crate_version.as_str()), || { + "VS Code extension version must match Cargo.toml package.version".to_string() + })?; + require(changelog_match.is_some(), "CHANGELOG.md must start with a semver release heading")?; + require_with(changelog_match.as_ref().and_then(|c| c.get(1)).map(|m| m.as_str()) == Some(crate_version.as_str()), || { + "CHANGELOG.md current release heading must match Cargo.toml package.version".to_string() + })?; + require( + extension_changelog.contains(&format!("## {crate_version}")), + "VS Code extension changelog must include the current package version", + )?; + require( + extension_readme.contains(&format!("current {surface} authoring surface")), + "VS Code extension README must name the current authoring surface", + )?; + require( + !extension_readme.contains("current 0.15 authoring surface"), + "VS Code extension README must not describe the current surface as 0.15", + )?; + + // --- Stage C: source-pin contains checks ------------------------------ + require_contains(root, "src/lib.rs", &[r#"pub const VERSION: &str = env!("CARGO_PKG_VERSION");"#])?; + require_contains(root, "src/main.rs", &["#[command(version = cellscript::VERSION)]"])?; + require_contains(root, "README.md", &[format!("version = \"{crate_version}\"")])?; + + // --- Stage D: wiki gate-version loop ----------------------------------- + for wiki_path in &[ + "docs/wiki/Tutorial-01-Getting-Started.md", + "docs/wiki/Cookbook-Recipes.md", + "docs/wiki/Tutorial-03-Resources-and-Cell-Effects.md", + "docs/wiki/Tutorial-08-Bundled-Example-Contracts.md", + "docs/wiki/Tutorial-11-Scoped-Invariants-and-ProofPlan.md", + ] { + let text = read_text(root, wiki_path)?; + require( + !text.contains("--primitive-strict 0.15"), + format!("{wiki_path} must use the current 0.16 assurance gate in command examples"), + )?; + require( + !text.contains("--primitive-strict=0.15"), + format!("{wiki_path} must use the current 0.16 assurance gate in command examples"), + )?; + } + + // --- Stage E: ckb_acceptance ------------------------------------------ + let ckb_acceptance = read_text(root, "crates/cellscript-tools/src/ckb_acceptance.rs")?; + require( + !ckb_acceptance.contains(r#""--primitive-strict", "0.15""#), + "CKB acceptance runner must not use the retired 0.15 assurance gate", + )?; + require( + ckb_acceptance.contains(r#""--primitive-strict", "0.16""#), + "CKB acceptance runner must use the current 0.16 assurance gate", + )?; + require( + ckb_acceptance.contains(r#""strict_original_ckb_compile_policy_fail_closed":[]"#), + "CKB acceptance runner must keep token/AMM/launch out of strict 0.16 fail-closed coverage", + )?; + let production_evidence = read_text(root, "crates/cellscript-tools/src/production_evidence.rs")?; + require( + production_evidence + .contains(r#"("token_action_runs", "token.cell", &["mint_with_authority", "transfer_token", "burn", "merge"])"#), + "CKB acceptance runner must compile token actions as original strict scoped actions", + )?; + require( + production_evidence + .contains(r#"("amm_action_runs", "amm_pool.cell", &["seed_pool", "swap_a_for_b", "add_liquidity", "remove_liquidity"])"#), + "CKB acceptance runner must compile AMM actions as original strict scoped actions", + )?; + require( + production_evidence.contains(r#"("launch_action_runs", "launch.cell", &["launch_token", "bootstrap_token"])"#), + "CKB acceptance runner must compile launch actions as original strict scoped actions", + )?; + let ckb_acceptance_shell = read_text(root, "scripts/ckb_cellscript_acceptance.sh")?; + require( + ckb_acceptance_shell.contains("ckb-acceptance") + && !ckb_acceptance_shell.contains("mapfile") + && !ckb_acceptance_shell.contains("readarray"), + "CKB acceptance runner must remain compatible with macOS Bash 3.2", + )?; + let ckb_acceptance_live = read_text(root, "crates/cellscript-tools/src/ckb_acceptance_live.rs")?; + require( + ckb_acceptance_live.contains("ckb_acceptance_pin.json"), + "CKB acceptance runner must validate the pinned CKB source identity", + )?; + + // --- Stage F: Tutorial-08 --------------------------------------------- + let tutorial_08 = read_text(root, "docs/wiki/Tutorial-08-Bundled-Example-Contracts.md")?; + require( + tutorial_08.contains("strict v0.16 ProofPlan gate"), + "bundled example tutorial must document the strict 0.16 ProofPlan gate", + )?; + // The token literal contains embedded newlines and exactly two spaces of + // indent before `echo`. Carry it verbatim. + require( + tutorial_08 + .contains("for f in examples/*.cell; do\n echo \"==> $f\"\n cellc \"$f\" --target riscv64-elf --target-profile ckb -o"), + "bundled example compile-all loop must not claim every example passes strict 0.16", + )?; + + // --- Stage G: package.json structural checks -------------------------- + require(package_json.get("name").and_then(|v| v.as_str()) == Some("cellscript-vscode"), "VS Code extension package name changed")?; + require(package_json.get("main").and_then(|v| v.as_str()) == Some("./dist/extension.js"), "VS Code extension entrypoint changed")?; + require( + package_json.get("devDependencies").and_then(|v| v.as_object()).is_some_and(|o| o.contains_key("vscode-languageclient")), + "VS Code extension must build with vscode-languageclient", + )?; + require( + package_json.get("devDependencies").and_then(|v| v.as_object()).is_some_and(|o| o.contains_key("esbuild")), + "VS Code extension must bundle with esbuild", + )?; + require( + package_json.get("devDependencies").and_then(|v| v.as_object()).is_some_and(|o| o.contains_key("@vscode/vsce")), + "VS Code extension must pin vsce for package dry runs", + )?; + require( + package_json.get("scripts").and_then(|v| v.as_object()).is_some_and(|o| o.contains_key("build")), + "VS Code extension must expose a build script", + )?; + require( + package_json.get("scripts").and_then(|v| v.as_object()).is_some_and(|o| o.contains_key("vscode:prepublish")), + "VS Code extension must build before publish", + )?; + require( + package_json.get("scripts").and_then(|v| v.as_object()).is_some_and(|o| o.contains_key("package")), + "VS Code extension must expose a package script", + )?; + require( + package_json.get("scripts").and_then(|v| v.as_object()).is_some_and(|o| o.contains_key("publish:dry-run")), + "VS Code extension must expose a publish dry-run script", + )?; + let publish_dry_run = package_json + .get("scripts") + .and_then(|v| v.as_object()) + .and_then(|o| o.get("publish:dry-run")) + .and_then(|v| v.as_str()) + .ok_or_else(|| { + anyhow!("invalid CellScript tooling release boundary: VS Code extension must expose a publish dry-run script") + })?; + require( + publish_dry_run.contains("vsce package --no-dependencies --out /tmp/cellscript-vscode-dry-run.vsix"), + "VS Code publish dry-run must package a local VSIX instead of using an unsupported publish --dry-run flag", + )?; + + // --- Stage H: contributed commands + activation events ---------------- + let commands: std::collections::BTreeSet = package_json + .get("contributes") + .and_then(|c| c.get("commands")) + .and_then(|c| c.as_array()) + .map(|arr| arr.iter().filter_map(|c| c.get("command").and_then(|v| v.as_str()).map(String::from)).collect()) + .unwrap_or_default(); + let activation: std::collections::BTreeSet = package_json + .get("activationEvents") + .and_then(|v| v.as_array()) + .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect()) + .unwrap_or_default(); + for command in &[ + "cellscript.compileCurrentFile", + "cellscript.showMetadata", + "cellscript.showConstraints", + "cellscript.showAbi", + "cellscript.showActionBuildPlan", + "cellscript.generateTypescriptBuilder", + "cellscript.verifyPackage", + "cellscript.verifyRegistry", + "cellscript.verifyLiveRegistry", + "cellscript.showProductionReport", + ] { + require(commands.contains(*command), format!("VS Code extension must contribute {command}"))?; + require(activation.contains(&format!("onCommand:{command}")), format!("VS Code extension must activate for {command}"))?; + } + + // --- Stage I: contributed configuration settings ---------------------- + let settings: std::collections::BTreeSet = package_json + .get("contributes") + .and_then(|c| c.get("configuration")) + .and_then(|c| c.get("properties")) + .and_then(|v| v.as_object()) + .map(|o| o.keys().cloned().collect()) + .unwrap_or_default(); + for setting in &[ + "cellscript.compilerPath", + "cellscript.useCargoRunFallback", + "cellscript.commandTimeoutMs", + "cellscript.maxOutputBytes", + "cellscript.target", + "cellscript.builderOutputDir", + "cellscript.ckbRpcUrl", + "cellscript.deploymentNetwork", + "cellscript.registryRequirePublisherSignature", + "cellscript.registryRequireAuditReport", + ] { + require(settings.contains(*setting), format!("VS Code extension must expose {setting}"))?; + } + + // --- Stage J: source/extension require_contains blocks ---------------- + require_contains( + root, + "src/main.rs", + &["Start the language server (JSON-RPC over stdio).", "cellscript::lsp::server::run_lsp_server_blocking();"], + )?; + require_contains( + root, + "src/lsp/server.rs", + &[ + "tower_lsp::LanguageServer", + "JSON-RPC", + "completion_provider", + "hover_provider", + "definition_provider", + "references_provider", + "rename_provider", + "document_formatting_provider", + "signature_help_provider", + "folding_range_provider", + "selection_range_provider", + ], + )?; + require_contains( + root, + "editors/vscode-cellscript/extension.js", + &[ + "LanguageClient", + "TransportKind.stdio", + "--lsp", + "selectMetadataEntry", + "findPackageRootForDocument", + "cellscript.showConstraints", + "cellscript.showAbi", + "cellscript.showActionBuildPlan", + "cellscript.generateTypescriptBuilder", + "cellscript.verifyPackage", + "cellscript.verifyRegistry", + "cellscript.verifyLiveRegistry", + "cellscript.showProductionReport", + "gen-builder", + "package", + "verify", + "registry", + "ckbRpcUrl", + "registryRequirePublisherSignature", + "registryRequireAuditReport", + "--require-publisher-signature", + "--require-audit-report", + ], + )?; + require_contains( + root, + "editors/vscode-cellscript/scripts/validate.mjs", + &[ + "LanguageClient", + "TransportKind.stdio", + "cellscript.generateTypescriptBuilder", + "cellscript.verifyLiveRegistry", + "cellscript.builderOutputDir", + "extension README must describe the production local tooling surface", + ], + )?; + require_contains( + root, + "scripts/cellscript_ckb_release_gate.sh", + &[r#"exec "$ROOT_DIR/scripts/cellscript_gate.sh" release"#, r#"exec "$ROOT_DIR/scripts/cellscript_gate.sh" release-quick"#], + )?; + require_contains( + root, + "README.md", + &["cellc action build", "cellc gen-builder --target typescript", "cellc package verify", "cellc registry verify --live"], + )?; + let website_scripts = website_package_json + .get("scripts") + .and_then(serde_json::Value::as_object) + .ok_or_else(|| anyhow!("website/package.json scripts object is missing"))?; + for (script_name, expected_command) in [ + ("prepare:registry", "node scripts/generate-registry-data.mjs"), + ("check:homepage", "node scripts/check-homepage-regressions.mjs"), + ("check:docs", "node scripts/check-doc-links.mjs"), + ("check:dist", "node scripts/check-dist-regressions.mjs"), + ("check:deploy", "node scripts/check-production-deploy.mjs"), + ] { + require_with(website_scripts.get(script_name).and_then(serde_json::Value::as_str) == Some(expected_command), || { + format!("website package script '{script_name}' must remain '{expected_command}'") + })?; + } + let website_build = website_scripts + .get("build") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| anyhow!("invalid CellScript tooling release boundary: website package script 'build' is missing"))?; + require_ordered_script_steps("build", website_build, &["npm run prepare:registry", "npm run build:ci"])?; + let website_ci_build = website_scripts + .get("build:ci") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| anyhow!("invalid CellScript tooling release boundary: website package script 'build:ci' is missing"))?; + require_ordered_script_steps( + "build:ci", + website_ci_build, + &[ + "npm run test:registry-guidance", + "npm run test:registry-browse", + "npm run test:session-storage", + "npm run test:submit-draft", + "npm run test:playground-focus", + "npm run test:playground-session", + "npm run test:playground-presentation", + "npm run check:visual", + "astro check", + "astro build", + "npm run check:homepage", + "npm run test:site-preferences", + "npm run check:docs", + "npm run check:dist", + "npm run check:deploy", + ], + )?; + require_contains(root, "website/src/pages/index.astro", &[r#"href="/registry""#, r#"data-i18n="nav.registryBrowse""#])?; + require_contains( + root, + "scripts/cellscript_gate.sh", + &[ + "run_in_dir", + "run_website_build_check", + "website registry data is stale", + "run npm --prefix website run build:ci", + "run_in_dir editors/vscode-cellscript npm exec -- vsce package --no-dependencies --out /tmp/cellscript-vscode-dry-run.vsix", + "node editors/vscode-cellscript/scripts/validate.mjs", + ], + )?; + + // --- Stage K: gate-script slice + tx_measure_gate checks -------------- + let gate_script = read_text(root, "scripts/cellscript_gate.sh")?; + let backend_gate = slice_between(&gate_script, "run_backend_gate() {", "run_release_auxiliary_checks() {")?; + require(backend_gate.contains("check_source_policy"), "backend gate must enforce the repository source-language policy")?; + let tx_measure_gate = slice_between(&gate_script, "check_ckb_tx_measure_tool() {", "check_novaseal_rust_tooling() {")?; + require( + tx_measure_gate.contains("cargo test --manifest-path tools/ckb-tx-measure/Cargo.toml --locked"), + "CKB transaction measure tooling must be tested by the release gate", + )?; + require( + !tx_measure_gate.contains("RUSTUP_TOOLCHAIN"), + "CKB transaction measure tooling must use CellScript's pinned Rust toolchain", + )?; + for token in [ + "release_ckb_repo_from_args() {", + "staging_dir=\"$(mktemp -d \"$ROOT_DIR/target/cellscript-ckb-tx-measure.XXXXXX\")\"", + "cp tools/ckb-tx-measure/Cargo.toml tools/ckb-tx-measure/Cargo.lock", + "cp src/bin/ckb_tx_measure.rs", + "ln -s \"$ckb_repo\" \"$staging_dir/ckb\"", + ] { + require(gate_script.contains(token), format!("release gate must preserve isolated CKB checkout handling: `{token}`"))?; + } + require( + gate_script.matches("run_release_auxiliary_checks \"$ckb_repo\"").count() == 2, + "release and release-quick gates must pass the selected CKB checkout to auxiliary checks", + )?; + require( + gate_script.contains("--root \"$ROOT_DIR\" workspace-version"), + "release source identity must read the root package version from Cargo.toml", + )?; + require( + !gate_script.contains("workspace.package.version"), + "release source identity must not assume a virtual workspace package table", + )?; + + // --- Stage L: website workflow ----------------------------------------- + require_contains( + root, + ".github/workflows/website-build.yml", + &[ + "workflow_dispatch:", + "Generate registry website data", + "Check generated registry data is committed", + "npm --prefix website run build:ci", + "Upload website dist", + ], + )?; + let website_build_workflow = read_text(root, ".github/workflows/website-build.yml")?; + require( + !website_build_workflow.contains("pull_request:"), + "website artifact workflow must not duplicate the unified CI gate on pull requests", + )?; + require(!website_build_workflow.contains("push:"), "website artifact workflow must not duplicate the unified CI gate on pushes")?; + require_contains( + root, + ".github/workflows/ci.yml", + &["actions/setup-node@v4", "node-version: \"22\"", "services/registry-api/package-lock.json"], + )?; + + // --- Stage M: CLI wiring ---------------------------------------------- + require_contains(root, "src/main.rs", &["cellc_cli_command().get_subcommands()", "cellscript::cli::run()"])?; + require_contains(root, "src/cli/mod.rs", &["mod novaseal_certification;"])?; + require_contains(root, "src/cli/commands.rs", &["Command::Certify", "novaseal-profile-v0"])?; + + // --- Stage N: docs + Rust source require_contains --------------------- + require_contains( + root, + "docs/wiki/Tutorial-07-LSP-and-Tooling.md", + &[ + "CellScript: Generate TypeScript Action Builder", + "cellscript.builderOutputDir", + "cellc registry verify --live", + "cellscript.registryRequirePublisherSignature", + "cellscript.registryRequireAuditReport", + "npm test", + ], + )?; + require_contains( + root, + "docs/archive/0.20/CELLSCRIPT_0_20_ROADMAP.md", + &["VS Code extension", "check_action_builder_toolchain", "CellFabric is frozen"], + )?; + require_contains( + root, + "src/package/mod.rs", + &[ + "failed to resolve registry dependency '{}/{}@{}': {}", + "registry package '{}/{}@{}' has no source_hash in registry.json", + "public registry package '{}/{}@{}' has no immutable source snapshot", + "source_hash mismatch for '{}/{}@{}': expected '{}', got '{}'", + "allow_unverified: detailed.allow_unverified", + "Git { url: String, revision: String }", + "pub fn consistency_issues(&self, manifest: &PackageManifest) -> Vec", + "pub fn replace_with_resolved(&mut self, resolved: &BTreeMap)", + ], + )?; + require_contains( + root, + "tests/cli.rs", + &[ + "cellc_rejects_registry_dependency_without_namespace", + "cellc_build_resolves_artifact_api_dependency_and_writes_lockfile", + "cellc_auth_namespace_claim_posts_signed_capability_payload_to_registry_api", + "cellc_install_path_updates_lockfile_and_remove_prunes_it", + "cellc_fmt_subcommand_formats_sources", + "cellc_run_subcommand_executes_pure_elf_package", + "cellc_gen_builder_typescript_emits_package_scaffold", + "cellc_gen_builder_lockfile_identity_fails_closed", + ], + )?; + require_contains( + root, + "tests/registry.rs", + &[ + "package_manager_resolves_artifact_api_dependency_with_source_hash", + "package_manager_persists_unverified_registry_policy_in_dependency_manifest", + "package_manager_rejects_registry_source_hash_mismatch", + "lockfile_consistency_accepts_matching_registry_source", + ], + )?; + + // --- Stage O: Cargo.toml exclude array -------------------------------- + // The `excluded` literals include the surrounding double quotes so they + // match the TOML array element verbatim via substring on the raw text. + for excluded in &[r#"".github/""#, r#""docs/""#, r#""docs/wiki/""#, r#""editors/""#, r#""proposals/""#] { + require(cargo_toml.contains(excluded), format!("Cargo.toml package exclude is missing {excluded}"))?; + } + + // --- Stage P: success ------------------------------------------------- + println!("valid CellScript tooling release boundary"); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::{changelog_head, require_ordered_script_steps}; + + #[test] + fn changelog_head_accepts_versioned_unreleased_candidate() { + let captures = changelog_head() + .captures("# Changelog\n\n## 0.24.0 - Unreleased\n") + .expect("a versioned undated candidate heading must be accepted"); + assert_eq!(captures.get(1).map(|capture| capture.as_str()), Some("0.24.0")); + } + + #[test] + fn changelog_head_does_not_treat_unversioned_unreleased_as_release_boundary() { + assert!(changelog_head().captures("# Changelog\n\n## Unreleased\n").is_none()); + } + + #[test] + fn website_build_contract_accepts_additional_ordered_checks() { + require_ordered_script_steps( + "build", + "npm run prepare:registry && npm run test:registry && astro check && astro build && npm run test:ui && npm run check:docs && npm run check:dist && npm run check:deploy", + &[ + "npm run prepare:registry", + "astro check", + "astro build", + "npm run check:docs", + "npm run check:dist", + "npm run check:deploy", + ], + ) + .expect("additional website checks must not invalidate the stable build contract"); + } + + #[test] + fn website_build_contract_rejects_missing_or_reordered_steps() { + let error = require_ordered_script_steps( + "build", + "npm run prepare:registry && astro build && astro check && npm run check:docs && npm run check:dist", + &["npm run prepare:registry", "astro check", "astro build", "npm run check:deploy"], + ) + .expect_err("reordered or missing required steps must fail closed"); + assert!(error.to_string().contains("must run 'astro build' in order")); + } +} diff --git a/crates/cellscript-tools/src/verifier_pinning.rs b/crates/cellscript-tools/src/verifier_pinning.rs new file mode 100644 index 00000000..7a792088 --- /dev/null +++ b/crates/cellscript-tools/src/verifier_pinning.rs @@ -0,0 +1,268 @@ +//! NovaSeal runtime-verifier artifact and source pinning checks. + +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{bail, Context, Result}; +use serde_json::Value; +use sha2::{Digest, Sha256}; + +use crate::crypto::{ckb_blake2b256, hex0x, sha256_hex}; + +fn git_files(cwd: &Path, pattern: &str) -> Result> { + let output = Command::new("git").args(["ls-files", pattern]).current_dir(cwd).output()?; + if !output.status.success() { + bail!("git ls-files failed in {}: {}", cwd.display(), String::from_utf8_lossy(&output.stderr).trim()); + } + Ok(String::from_utf8_lossy(&output.stdout).lines().filter(|line| !line.is_empty()).map(str::to_owned).collect()) +} + +fn collect_tree_files( + root: &Path, + directory: &Path, + allowed_extensions: &[&str], + allowed_names: &[&str], + label: &str, + files: &mut BTreeSet, + failures: &mut Vec, +) -> Result<()> { + let mut entries = fs::read_dir(directory)?.collect::, _>>()?; + entries.sort_by_key(std::fs::DirEntry::path); + for entry in entries { + let path = entry.path(); + let metadata = fs::symlink_metadata(&path)?; + let relative = path.strip_prefix(root).unwrap_or(&path).to_string_lossy().replace('\\', "/"); + if metadata.file_type().is_symlink() { + failures.push(format!("{relative} is a symlink inside the NovaSeal {label} source tree")); + continue; + } + if metadata.is_dir() { + let name = entry.file_name(); + if ["target", "build", ".git"].iter().any(|skip| name == *skip) { + continue; + } + collect_tree_files(root, &path, allowed_extensions, allowed_names, label, files, failures)?; + } else if metadata.is_file() + && (path.extension().and_then(|value| value.to_str()).is_some_and(|extension| allowed_extensions.contains(&extension)) + || entry.file_name().to_str().is_some_and(|name| allowed_names.contains(&name))) + { + files.insert(path); + } + } + Ok(()) +} + +fn hash_files(root: &Path, files: impl IntoIterator) -> Result { + let mut digest = Sha256::new(); + for path in files { + let relative = path.strip_prefix(root).unwrap_or(&path).to_string_lossy().replace('\\', "/"); + digest.update(relative.as_bytes()); + digest.update([0]); + digest.update(Sha256::digest(fs::read(path)?)); + } + Ok(format!("0x{}", hex::encode(digest.finalize()))) +} + +fn verifier_source_tree_hash(root: &Path, core_root: &Path, failures: &mut Vec) -> Result { + let mut files = BTreeSet::new(); + for directory in [ + core_root.join("verifier/novaseal_btc_verifier_core"), + core_root.join("verifier/novaseal_btc_verifier_riscv"), + core_root.join("verifier/novaseal_btc_verifier"), + ] { + collect_tree_files( + root, + &directory, + &["rs", "sh"], + &["Cargo.toml", "Cargo.lock", "README.md"], + "verifier TCB", + &mut files, + failures, + )?; + } + hash_files(root, files) +} + +fn profile_source_tree_hash(root: &Path, paths: &[&str], failures: &mut Vec) -> Result { + let mut files = BTreeSet::new(); + for raw in paths { + let path = root.join(raw); + let metadata = fs::symlink_metadata(&path).with_context(|| format!("failed to inspect {}", path.display()))?; + let relative = path.strip_prefix(root).unwrap_or(&path).to_string_lossy().replace('\\', "/"); + if metadata.file_type().is_symlink() { + failures.push(format!("{relative} is a symlink inside the NovaSeal profile source tree")); + } else if metadata.is_file() { + files.insert(path); + } else if metadata.is_dir() { + collect_tree_files( + root, + &path, + &["cell", "schema", "toml", "py", "json", "rs"], + &["Cargo.lock"], + "profile", + &mut files, + failures, + )?; + } + } + hash_files(root, files) +} + +fn load_json(path: &Path) -> Result { + serde_json::from_slice(&fs::read(path)?).with_context(|| format!("failed to decode {}", path.display())) +} + +fn relative(root: &Path, path: &Path) -> String { + path.strip_prefix(root).unwrap_or(path).to_string_lossy().replace('\\', "/") +} + +pub fn run(root: &Path) -> Result { + let core_root = root.join("proposals/novaseal/v0-mvp-skeleton"); + let release_elf = + core_root.join("verifier/novaseal_btc_verifier_riscv/target/riscv64imac-unknown-none-elf/release/novaseal_btc_verifier_riscv"); + if !release_elf.is_file() { + bail!("missing NovaSeal RISC-V verifier release ELF: {}", release_elf.display()); + } + let artifact = fs::read(&release_elf)?; + let artifact_hash = format!("0x{}", sha256_hex(&artifact)); + let data_hash = hex0x(&ckb_blake2b256(&artifact)?); + let size_bytes = artifact.len(); + let mut failures = Vec::new(); + + let mut manifests = BTreeSet::new(); + for tracked in git_files(root, "proposals/novaseal/**/Cell.toml")? { + manifests.insert(root.join(tracked)); + } + let novaseal_root = root.join("proposals/novaseal"); + if novaseal_root.is_dir() { + for tracked in git_files(&novaseal_root, "**/Cell.toml")? { + manifests.insert(novaseal_root.join(tracked)); + } + } + if manifests.is_empty() { + failures.push("no tracked NovaSeal Cell.toml manifests found".to_owned()); + } + for manifest_path in manifests { + let manifest: toml::Value = toml::from_str(&fs::read_to_string(&manifest_path)?)?; + let dependencies = manifest + .get("deploy") + .and_then(|value| value.get("ckb")) + .and_then(|value| value.get("cell_deps")) + .and_then(toml::Value::as_array) + .map(Vec::as_slice) + .unwrap_or(&[]); + let runtime_dependencies = dependencies + .iter() + .filter(|dependency| { + dependency.get("role").and_then(toml::Value::as_str) == Some("runtime_verifier") + || dependency.get("name").and_then(toml::Value::as_str) == Some("cellscript_btc_bip340_verifier_riscv") + }) + .collect::>(); + if runtime_dependencies.is_empty() { + failures.push(format!("{} has no NovaSeal runtime verifier CellDep", relative(root, &manifest_path))); + continue; + } + for (index, dependency) in runtime_dependencies.iter().enumerate() { + let actual_data = dependency.get("data_hash").and_then(toml::Value::as_str); + if actual_data != Some(&data_hash) { + failures.push(format!( + "{} runtime verifier #{index} data_hash {} != {data_hash}", + relative(root, &manifest_path), + actual_data.unwrap_or("None") + )); + } + let actual_artifact = dependency.get("artifact_hash").and_then(toml::Value::as_str); + if actual_artifact != Some(&artifact_hash) { + failures.push(format!( + "{} runtime verifier #{index} artifact_hash {} != {artifact_hash}", + relative(root, &manifest_path), + actual_artifact.unwrap_or("None") + )); + } + } + } + + let source_tree_hash = verifier_source_tree_hash(root, &core_root, &mut failures)?; + let public_template_path = core_root.join("proofs/public_shared_cell_dep_attestation.template.json"); + let public_template = load_json(&public_template_path)?; + let public_hash = public_template.pointer("/runtime_verifier/artifact_hash").and_then(Value::as_str); + if public_hash != Some(&artifact_hash) { + failures.push(format!( + "{} runtime_verifier.artifact_hash {} != {artifact_hash}", + relative(root, &public_template_path), + public_hash.unwrap_or("None") + )); + } + let external_template_path = core_root.join("proofs/bip340_external_tcb_review_attestation.template.json"); + let external_template = load_json(&external_template_path)?; + if external_template.get("artifact_hash").and_then(Value::as_str) != Some(&artifact_hash) { + failures.push(format!( + "{} artifact_hash {} != {artifact_hash}", + relative(root, &external_template_path), + external_template.get("artifact_hash").and_then(Value::as_str).unwrap_or("None") + )); + } + if external_template.get("source_tree_sha256").and_then(Value::as_str) != Some(&source_tree_hash) { + failures.push(format!( + "{} source_tree_sha256 {} != {source_tree_hash}", + relative(root, &external_template_path), + external_template.get("source_tree_sha256").and_then(Value::as_str).unwrap_or("None") + )); + } + + let rwa_source_tree_hash = profile_source_tree_hash( + root, + &[ + "proposals/novaseal/rwa-receipt-profile-v0/Cell.toml", + "proposals/novaseal/rwa-receipt-profile-v0/src/nova_rwa_receipt_type.cell", + "proposals/novaseal/rwa-receipt-profile-v0/src/nova_rwa_receipt_lifecycle_type.cell", + "proposals/novaseal/rwa-receipt-profile-v0/schemas", + "proposals/novaseal/rwa-receipt-profile-v0/fixtures", + "proposals/novaseal/rwa-receipt-profile-v0/proofs/invariant_matrix.json", + ], + &mut failures, + )?; + let rwa_template_path = root.join("proposals/novaseal/rwa-receipt-profile-v0/proofs/legal_registry_review_evidence.template.json"); + let rwa_template = load_json(&rwa_template_path)?; + if rwa_template.get("profile_source_tree_sha256").and_then(Value::as_str) != Some(&rwa_source_tree_hash) { + failures.push(format!( + "{} profile_source_tree_sha256 {} != {rwa_source_tree_hash}", + relative(root, &rwa_template_path), + rwa_template.get("profile_source_tree_sha256").and_then(Value::as_str).unwrap_or("None") + )); + } + + let mapping_path = core_root.join("proofs/proofplan_mapping.json"); + let mapping = load_json(&mapping_path)?; + let summary = mapping.pointer("/btc_verifier_riscv_shell_artifact/current_summary").unwrap_or(&Value::Null); + if summary.get("staged_release_elf_sha256").and_then(Value::as_str) != artifact_hash.strip_prefix("0x") { + failures.push(format!( + "{} staged_release_elf_sha256 {} != {}", + relative(root, &mapping_path), + summary.get("staged_release_elf_sha256").and_then(Value::as_str).unwrap_or("None"), + artifact_hash.strip_prefix("0x").unwrap_or(&artifact_hash) + )); + } + if summary.get("staged_release_elf_size_bytes").and_then(Value::as_u64) != Some(size_bytes as u64) { + failures.push(format!( + "{} staged_release_elf_size_bytes {:?} != {size_bytes}", + relative(root, &mapping_path), + summary.get("staged_release_elf_size_bytes").unwrap_or(&Value::Null) + )); + } + + if !failures.is_empty() { + eprintln!("NovaSeal verifier pinning check failed:"); + for failure in failures { + eprintln!(" - {failure}"); + } + return Ok(1); + } + println!( + "NovaSeal verifier pinning check passed: artifact_hash={artifact_hash} data_hash={data_hash} \ +source_tree_sha256={source_tree_hash} rwa_profile_source_tree_sha256={rwa_source_tree_hash} size_bytes={size_bytes}" + ); + Ok(0) +} diff --git a/crates/cellscript-tools/src/wallet_vectors.rs b/crates/cellscript-tools/src/wallet_vectors.rs new file mode 100644 index 00000000..eb88e508 --- /dev/null +++ b/crates/cellscript-tools/src/wallet_vectors.rs @@ -0,0 +1,493 @@ +//! NovaSeal wallet-signing vector generator. + +use std::fs; +use std::path::Path; +use std::sync::LazyLock; + +use anyhow::{bail, Context, Result}; +use serde_json::{json, Map, Value}; + +use crate::crypto::{bytes32, ckb_blake2b256, decode_hex0x, hex0x, personalized_blake2b256}; +use crate::shared::{lexical_path, stable_json_pretty}; + +const PACKED_HASH_DOMAIN: &[u8] = b"CellScriptPackedHashV0\0"; +const VECTOR_PERSON: &[u8] = b"NovaSealWalletV0"; +const CKB: u64 = 100_000_000; +const COLLATERAL_AMOUNT: u64 = 1_000 * CKB; +const PRINCIPAL_AMOUNT: u64 = 700 * CKB; +const FIXED_FEE_AMOUNT: u64 = 30 * CKB; +const EXPIRY_TIMEPOINT: u64 = 200; + +static ZERO_HASH: LazyLock = LazyLock::new(|| format!("0x{}", "00".repeat(32))); +static BORROWER_AUTHORITY: LazyLock = LazyLock::new(|| format!("0x{}", "11".repeat(32))); +static LENDER_AUTHORITY: LazyLock = LazyLock::new(|| format!("0x{}", "22".repeat(32))); + +fn stable_hash(label: &str, value: &str) -> Result { + Ok(hex0x(&personalized_blake2b256(VECTOR_PERSON, &[label.as_bytes(), b"\0", value.as_bytes()])?)) +} + +fn uint(value: u64, size: usize) -> Result> { + if size > 8 || (size < 8 && value >= (1_u64 << (size * 8))) { + bail!("{value} does not fit u{}", size * 8); + } + Ok(value.to_le_bytes()[..size].to_vec()) +} + +fn packed_hash(type_name: &str, packed: &[u8]) -> Result<(String, String)> { + let length = u32::try_from(packed.len()).context("wallet packed value exceeds u32")?; + let mut preimage = Vec::with_capacity(PACKED_HASH_DOMAIN.len() + type_name.len() + 1 + 4 + packed.len()); + preimage.extend_from_slice(PACKED_HASH_DOMAIN); + preimage.extend_from_slice(type_name.as_bytes()); + preimage.push(0); + preimage.extend_from_slice(&length.to_le_bytes()); + preimage.extend_from_slice(packed); + Ok((hex0x(&preimage), hex0x(&ckb_blake2b256(&preimage)?))) +} + +fn encoded(type_name: &str, packed: Vec) -> Result { + let (preimage, digest) = packed_hash(type_name, &packed)?; + Ok(json!({ + "type": type_name, + "hex": hex0x(&packed), + "hash_preimage_hex": preimage, + "digest_blake2b_256": digest, + })) +} + +fn field_map(encoded: &Value) -> Map { + let mut result = Map::new(); + let Some(fields) = encoded.get("fields").and_then(Value::as_array) else { + return result; + }; + for field in fields { + let Some(name) = field.get("name").and_then(Value::as_str) else { + continue; + }; + if let Some(value) = field.get("value") { + result.insert(name.to_string(), value.clone()); + } else if matches!(field.get("type").and_then(Value::as_str), Some("Byte32" | "Hash")) { + result.insert(name.to_string(), field.get("hex").cloned().unwrap_or(Value::Null)); + } else if field.get("type").and_then(Value::as_str) == Some("OutPoint") { + let components = field.get("components").and_then(Value::as_array); + let component = |wanted: &str| { + components.and_then(|items| items.iter().find(|item| item.get("name").and_then(Value::as_str) == Some(wanted))) + }; + result.insert( + name.to_string(), + json!({ + "tx_hash": component("tx_hash").and_then(|item| item.get("hex")).cloned().unwrap_or(Value::Null), + "index": component("index").and_then(|item| item.get("value")).cloned().unwrap_or(Value::Null), + }), + ); + } else if let Some(nested) = field.get("nested") { + result.insert(name.to_string(), Value::Object(field_map(nested))); + } + } + result +} + +fn required_str<'value>(value: &'value Value, key: &str) -> Result<&'value str> { + value.get(key).and_then(Value::as_str).with_context(|| format!("wallet value is missing string field {key}")) +} + +fn wallet_record( + suite: &str, + name: &str, + action: &str, + signers: &[&str], + signed_intent: &Value, + display: Value, + expected_receipt_hash: Value, +) -> Result { + let preimage = required_str(signed_intent, "hash_preimage_hex")?; + let message = required_str(signed_intent, "digest_blake2b_256")?; + let recomputed = hex0x(&ckb_blake2b256(&decode_hex0x(preimage)?)?); + Ok(json!({ + "suite": suite, + "name": name, + "action": action, + "signers": signers, + "status": if recomputed == message { "passed" } else { "failed" }, + "bip340_message_hash": message, + "signed_type": required_str(signed_intent, "type")?, + "signed_intent_packed_hex": required_str(signed_intent, "hex")?, + "signed_intent_hash_preimage_hex": preimage, + "molecule_fixed_equivalent_hex": required_str(signed_intent, "hex")?, + "molecule_profile": "fixed-width CellScript schema; equivalent to declared-field concatenation for these v0 structs", + "expected_receipt_hash": expected_receipt_hash, + "wallet_display": display, + })) +} + +fn first_truthy(values: impl IntoIterator) -> Value { + values + .into_iter() + .find(|value| match value { + Value::Null => false, + Value::Bool(value) => *value, + Value::String(value) => !value.is_empty(), + Value::Array(value) => !value.is_empty(), + Value::Object(value) => !value.is_empty(), + Value::Number(value) => value.as_f64().is_some_and(|number| number != 0.0), + }) + .unwrap_or(Value::Null) +} + +fn core_vectors(path: &Path) -> Result> { + let payload: Value = serde_json::from_slice(&fs::read(path).with_context(|| format!("failed to read {}", path.display()))?) + .with_context(|| format!("{} is not valid JSON", path.display()))?; + let mut vectors = Vec::new(); + for vector in payload.get("vectors").and_then(Value::as_array).into_iter().flatten() { + let encoded_value = vector.get("encoded").cloned().unwrap_or_else(|| json!({})); + let Some(resolved) = encoded_value.get("resolved").and_then(Value::as_object) else { + continue; + }; + let signed_candidate = resolved.get("signed_intent").filter(|value| value.is_object()).cloned().or_else(|| { + first_truthy([ + resolved.get("resolved_intent").cloned().unwrap_or(Value::Null), + encoded_value.get("intent").cloned().unwrap_or(Value::Null), + ]) + .is_object() + .then(|| { + first_truthy([ + resolved.get("resolved_intent").cloned().unwrap_or(Value::Null), + encoded_value.get("intent").cloned().unwrap_or(Value::Null), + ]) + }) + }); + let Some(mut signed_intent) = signed_candidate else { + continue; + }; + if signed_intent.get("hash_preimage_hex").is_none_or(Value::is_null) + && let Some(packed_hex) = signed_intent.get("hex").and_then(Value::as_str) + { + let type_name = signed_intent.get("type").and_then(Value::as_str).unwrap_or("NovaSealIntentV0"); + let (preimage, digest) = packed_hash(type_name, &decode_hex0x(packed_hex)?)?; + let object = signed_intent.as_object_mut().context("signed intent is not an object")?; + object.insert("hash_preimage_hex".to_string(), Value::String(preimage)); + object.insert("digest_blake2b_256".to_string(), Value::String(digest)); + } + let signed_fields = signed_intent.get("fields").and_then(Value::as_array); + let core = if signed_fields.and_then(|fields| fields.first()).and_then(|field| field.get("nested")).is_some() { + field_map(&signed_fields.expect("checked above")[0]["nested"]) + } else { + field_map(&signed_intent) + }; + let old_cell = field_map(encoded_value.get("old_cell").unwrap_or(&Value::Null)); + let display = json!({ + "protocol": "NovaSeal Core v0", + "fixture": vector.get("fixture").cloned().unwrap_or(Value::Null), + "action": core.get("action").cloned().unwrap_or(Value::Null), + "terminal_path": core.get("terminal_path").cloned().unwrap_or(Value::Null), + "btc_authority_hash": old_cell.get("btc_authority_hash").cloned().unwrap_or(Value::Null), + "btc_authority_hash_semantics": "legacy field name; for NovaSeal v0 this equals the 32-byte BIP340 x-only public key and is not a CKB recipient lock hash or payout script identifier", + "old_cell": core.get("old_cell").cloned().unwrap_or(Value::Null), + "old_state_hash": core.get("old_state_hash").cloned().unwrap_or(Value::Null), + "new_state_hash": core.get("new_state_hash").cloned().unwrap_or(Value::Null), + "old_nonce": core.get("old_nonce").cloned().unwrap_or(Value::Null), + "new_nonce": core.get("new_nonce").cloned().unwrap_or(Value::Null), + "expiry": core.get("expiry").cloned().unwrap_or(Value::Null), + "policy_hash": core.get("policy_hash").cloned().unwrap_or(Value::Null), + }); + let expected_receipt = first_truthy([ + field_map(&signed_intent).get("expected_receipt_hash").cloned().unwrap_or(Value::Null), + resolved.get("resolved_receipt_hash").cloned().unwrap_or(Value::Null), + vector.pointer("/hashes/resolved_receipt_hash").cloned().unwrap_or(Value::Null), + ]); + let name = + first_truthy([vector.get("name").cloned().unwrap_or(Value::Null), vector.get("fixture").cloned().unwrap_or(Value::Null)]); + vectors.push(wallet_record( + "novaseal-core-v0", + &match name { + Value::String(value) => value, + other => other.to_string(), + }, + "key_auth_transition", + &["btc_authority"], + &signed_intent, + display, + expected_receipt, + )?); + } + Ok(vectors) +} + +fn encode_native_payout( + action: u64, + role: u64, + recipient: &str, + amount: u64, + terms_hash: &str, + agreement_id: &str, + nonce: u64, +) -> Result { + let mut packed = Vec::new(); + packed.extend(uint(action, 1)?); + packed.extend(bytes32(agreement_id)?); + packed.extend(uint(role, 1)?); + packed.extend(bytes32(recipient)?); + packed.extend(uint(0, 1)?); + packed.extend(bytes32(&ZERO_HASH)?); + packed.extend(uint(amount, 8)?); + packed.extend(bytes32(terms_hash)?); + packed.extend(uint(nonce, 8)?); + encoded("NativeCkbPayoutV0", packed) +} + +#[allow(clippy::too_many_arguments)] +fn encode_agreement_intent_core( + action: u64, + agreement_id: &str, + terms_hash: &str, + old_status: u64, + new_status: u64, + old_nonce: u64, + new_nonce: u64, + terminal_amount: u64, + payout_commitment_hash: &str, +) -> Result { + let mut packed = Vec::new(); + packed.extend(uint(action, 1)?); + packed.extend(bytes32(agreement_id)?); + packed.extend(bytes32(terms_hash)?); + packed.extend(bytes32(&BORROWER_AUTHORITY)?); + packed.extend(bytes32(&LENDER_AUTHORITY)?); + packed.extend(uint(old_status, 1)?); + packed.extend(uint(new_status, 1)?); + packed.extend(uint(old_nonce, 8)?); + packed.extend(uint(new_nonce, 8)?); + packed.extend(uint(terminal_amount, 8)?); + packed.extend(bytes32(payout_commitment_hash)?); + packed.extend(uint(EXPIRY_TIMEPOINT, 8)?); + encoded("NovaAgreementIntentCoreV0", packed) +} + +#[allow(clippy::too_many_arguments)] +fn encode_canonical_envelope( + action: u64, + agreement_id: &str, + terms_hash: &str, + old_state_commitment: &str, + new_state_commitment: &str, + old_nonce: u64, + new_nonce: u64, + authority_hash: &str, + profile_body_hash: &str, + payout_commitment_hash: &str, +) -> Result { + let mut packed = Vec::new(); + packed.extend(bytes32(agreement_id)?); + packed.extend(bytes32(terms_hash)?); + packed.extend(uint(action, 1)?); + packed.extend(uint(action, 1)?); + packed.extend(bytes32(agreement_id)?); + packed.extend(bytes32(old_state_commitment)?); + packed.extend(bytes32(new_state_commitment)?); + packed.extend(uint(old_nonce, 8)?); + packed.extend(uint(new_nonce, 8)?); + packed.extend(uint(EXPIRY_TIMEPOINT, 8)?); + packed.extend(bytes32(authority_hash)?); + packed.extend(bytes32(profile_body_hash)?); + packed.extend(bytes32(payout_commitment_hash)?); + encoded("NovaSealCanonicalEnvelopeV0", packed) +} + +#[allow(clippy::too_many_arguments)] +fn encode_agreement_receipt_commitment( + action: u64, + agreement_id: &str, + terms_hash: &str, + old_status: u64, + new_status: u64, + terminal_amount: u64, + old_nonce: u64, + new_nonce: u64, + intent_core_hash: &str, + payout_commitment_hash: &str, +) -> Result { + let mut packed = Vec::new(); + packed.extend(uint(action, 1)?); + packed.extend(bytes32(agreement_id)?); + packed.extend(uint(old_status, 1)?); + packed.extend(uint(new_status, 1)?); + packed.extend(bytes32(terms_hash)?); + packed.extend(bytes32(&BORROWER_AUTHORITY)?); + packed.extend(bytes32(&LENDER_AUTHORITY)?); + packed.extend(uint(terminal_amount, 8)?); + packed.extend(uint(old_nonce, 8)?); + packed.extend(uint(new_nonce, 8)?); + packed.extend(bytes32(intent_core_hash)?); + packed.extend(bytes32(payout_commitment_hash)?); + encoded("NovaAgreementReceiptCommitmentV0", packed) +} + +fn encode_agreement_signed_intent(core: &Value, canonical_envelope_hash: &str, expected_receipt_hash: &str) -> Result { + let mut packed = decode_hex0x(required_str(core, "hex")?)?; + packed.extend(bytes32(canonical_envelope_hash)?); + packed.extend(bytes32(expected_receipt_hash)?); + encoded("NovaAgreementSignedIntentV0", packed) +} + +#[allow(clippy::too_many_arguments)] +fn agreement_case( + name: &str, + action: u64, + old_status: u64, + new_status: u64, + old_nonce: u64, + new_nonce: u64, + terminal_amount: u64, + signers: &[&str], +) -> Result { + let agreement_id = stable_hash("agreement_id", "mvb-starter-v0")?; + let terms_hash = stable_hash("terms_hash", "ckb-ckb-fixed-fee-v0")?; + let payout_hash = if action == 0 { + required_str( + &encode_native_payout(action, 0, &BORROWER_AUTHORITY, PRINCIPAL_AMOUNT, &terms_hash, &agreement_id, 0)?, + "digest_blake2b_256", + )? + .to_string() + } else if action == 1 { + let lender = + encode_native_payout(action, 1, &LENDER_AUTHORITY, PRINCIPAL_AMOUNT + FIXED_FEE_AMOUNT, &terms_hash, &agreement_id, 1)?; + let borrower = encode_native_payout(action, 2, &BORROWER_AUTHORITY, COLLATERAL_AMOUNT, &terms_hash, &agreement_id, 1)?; + let mut packed = Vec::new(); + packed.extend(bytes32(required_str(&lender, "digest_blake2b_256")?)?); + packed.extend(bytes32(required_str(&borrower, "digest_blake2b_256")?)?); + packed_hash("RepayPayoutCommitmentV0", &packed)?.1 + } else { + required_str( + &encode_native_payout(action, 3, &LENDER_AUTHORITY, COLLATERAL_AMOUNT, &terms_hash, &agreement_id, 1)?, + "digest_blake2b_256", + )? + .to_string() + }; + let core = encode_agreement_intent_core( + action, + &agreement_id, + &terms_hash, + old_status, + new_status, + old_nonce, + new_nonce, + terminal_amount, + &payout_hash, + )?; + let receipt = encode_agreement_receipt_commitment( + action, + &agreement_id, + &terms_hash, + old_status, + new_status, + terminal_amount, + old_nonce, + new_nonce, + required_str(&core, "digest_blake2b_256")?, + &payout_hash, + )?; + let authority_hash = if action == 2 { &*LENDER_AUTHORITY } else { &*BORROWER_AUTHORITY }; + let previous = if action == 0 { ZERO_HASH.clone() } else { stable_hash("previous_receipt_hash", "agreement-active-v0")? }; + let canonical = encode_canonical_envelope( + action, + &agreement_id, + &terms_hash, + &previous, + required_str(&receipt, "digest_blake2b_256")?, + old_nonce, + new_nonce, + authority_hash, + required_str(&core, "digest_blake2b_256")?, + &payout_hash, + )?; + let signed = encode_agreement_signed_intent( + &core, + required_str(&canonical, "digest_blake2b_256")?, + required_str(&receipt, "digest_blake2b_256")?, + )?; + let action_name = match action { + 0 => "originate_agreement", + 1 => "repay_before_expiry", + 2 => "claim_after_expiry", + _ => bail!("unsupported agreement action {action}"), + }; + wallet_record( + "novaseal-agreement-profile-v0", + name, + action_name, + signers, + &signed, + json!({ + "protocol": "NovaSeal Agreement Profile v0", + "action": action_name, + "agreement_id": agreement_id, + "terms_hash": terms_hash, + "borrower_authority_hash": &*BORROWER_AUTHORITY, + "lender_authority_hash": &*LENDER_AUTHORITY, + "old_status": old_status, + "new_status": new_status, + "old_nonce": old_nonce, + "new_nonce": new_nonce, + "terminal_amount_shannons": terminal_amount, + "canonical_envelope_hash": required_str(&canonical, "digest_blake2b_256")?, + "payout_commitment_hash": payout_hash, + "expiry_timepoint": EXPIRY_TIMEPOINT, + }), + receipt.get("digest_blake2b_256").cloned().unwrap_or(Value::Null), + ) +} + +fn agreement_vectors() -> Result> { + Ok(vec![ + agreement_case("originate_valid", 0, 0, 1, 0, 0, PRINCIPAL_AMOUNT, &["borrower", "lender"])?, + agreement_case("repay_before_expiry_valid", 1, 1, 2, 0, 1, PRINCIPAL_AMOUNT + FIXED_FEE_AMOUNT, &["borrower"])?, + agreement_case("claim_after_expiry_valid", 2, 1, 3, 0, 1, COLLATERAL_AMOUNT, &["lender"])?, + ]) +} + +pub fn run(root: &Path, core_vectors_path: Option<&Path>, output: Option<&Path>, pretty: bool) -> Result { + let default_core = root.join("proposals/novaseal/v0-mvp-skeleton/target/novaseal-canonical-vectors.json"); + let default_output = root.join("target/novaseal-wallet-signing-vectors.json"); + let core_path = lexical_path(core_vectors_path.unwrap_or(&default_core)); + let output = lexical_path(output.unwrap_or(&default_output)); + let mut vectors = core_vectors(&core_path)?; + vectors.extend(agreement_vectors()?); + let matched = vectors.iter().filter(|vector| vector["status"] == "passed").count(); + let core_count = vectors.iter().filter(|vector| vector["suite"] == "novaseal-core-v0").count(); + let agreement_count = vectors.iter().filter(|vector| vector["suite"] == "novaseal-agreement-profile-v0").count(); + let passed = !vectors.is_empty() && matched == vectors.len(); + let payload = json!({ + "schema": "novaseal-wallet-signing-vectors-v0.1", + "status": if passed { "passed" } else { "failed" }, + "hash_algorithm": "ckb_blake2b_256", + "signature_scheme": "BIP340 Schnorr over 32-byte signed intent hash", + "authority_identifier_semantics": { + "btc_authority_hash": "legacy-named NovaSeal core field; in v0 it equals the 32-byte BIP340 x-only public key", + "not_ckb_recipient_lock_hash": true, + "not_payout_script_identifier": true, + "agreement_payout_mapping": "profile/builder surface; payout recipients must not be inferred from the core BTC authority field", + }, + "molecule_alignment": "fixed-width v0 structs use declared-field little-endian concatenation; no dynamic tables/vectors in these signing objects", + "summary": { + "total": vectors.len(), + "core_vectors": core_count, + "agreement_vectors": agreement_count, + "matched": matched, + }, + "vectors": vectors, + }); + let parent = output.parent().filter(|parent| !parent.as_os_str().is_empty()).unwrap_or_else(|| Path::new(".")); + fs::create_dir_all(parent).with_context(|| format!("failed to create {}", parent.display()))?; + fs::write(&output, format!("{}\n", stable_json_pretty(&payload)?)) + .with_context(|| format!("failed to write {}", output.display()))?; + if pretty { + println!( + "wrote {} status={} total={} core={} agreement={}", + output.display(), + payload["status"].as_str().unwrap_or("failed"), + payload["summary"]["total"].as_u64().unwrap_or(0), + payload["summary"]["core_vectors"].as_u64().unwrap_or(0), + payload["summary"]["agreement_vectors"].as_u64().unwrap_or(0), + ); + } + Ok(if passed { 0 } else { 1 }) +} diff --git a/crates/cellscript-tools/tests/native_tools.rs b/crates/cellscript-tools/tests/native_tools.rs new file mode 100644 index 00000000..b821d421 --- /dev/null +++ b/crates/cellscript-tools/tests/native_tools.rs @@ -0,0 +1,191 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn repo_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().expect("CellScript repository root must exist") +} + +fn run(root: &Path, args: &[&str]) -> Output { + Command::new(env!("CARGO_BIN_EXE_cellscript-tools")) + .args(["--root", root.to_str().expect("UTF-8 repository path")]) + .args(args) + .current_dir(root) + .output() + .expect("cellscript-tools must run") +} + +struct TestDir(PathBuf); + +impl TestDir { + fn new(label: &str) -> Self { + let nonce = SystemTime::now().duration_since(UNIX_EPOCH).expect("clock must follow Unix epoch").as_nanos(); + let path = std::env::temp_dir().join(format!("cellscript-tools-rust-test-{label}-{}-{nonce}", std::process::id())); + fs::create_dir(&path).expect("test directory must be creatable"); + Self(path) + } +} + +impl Drop for TestDir { + fn drop(&mut self) { + if self.0.parent() == Some(std::env::temp_dir().as_path()) + && self.0.file_name().and_then(|name| name.to_str()).is_some_and(|name| name.starts_with("cellscript-tools-rust-test-")) + { + let _ = fs::remove_dir_all(&self.0); + } + } +} + +fn write_json(root: &Path, relative: &str, value: &serde_json::Value) { + let path = root.join(relative); + fs::create_dir_all(path.parent().expect("evidence path must have a parent")).expect("evidence directory must be creatable"); + fs::write(path, serde_json::to_vec(value).expect("test evidence must serialize")).expect("test evidence must be writable"); +} + +fn hex32(byte: u8) -> String { + format!("0x{}", format!("{byte:02x}").repeat(32)) +} + +fn write_operator_evidence(root: &Path) { + let tx_hash = hex32(0x11); + write_json( + root, + "target/novaseal-fungible-xudt-devnet-stateful-live.json", + &serde_json::json!({ + "status": "passed", + "issue": {"commit": {"tx_hash": tx_hash}}, + "transfer": {"commit": {"tx_hash": tx_hash}}, + "settle": {"commit": {"tx_hash": tx_hash}}, + }), + ); + write_json( + root, + "target/novaseal-rwa-receipt-devnet-stateful-live.json", + &serde_json::json!({ + "status": "passed", + "materialize": {"commit": {"tx_hash": tx_hash}}, + "claim": {"commit": {"tx_hash": tx_hash}}, + "settle": {"commit": {"tx_hash": tx_hash}}, + }), + ); + write_json( + root, + "target/novaseal-btc-transaction-commitment-devnet-stateful-live.json", + &serde_json::json!({ + "status": "passed", + "commit_transaction": { + "commit": {"tx_hash": tx_hash}, + "public_btc_anchor": { + "kind": "btc_transaction_commitment", + "anchor_source": "isolated-test-evidence", + "btc_txid": hex32(0x21), + "btc_wtxid": hex32(0x22), + "btc_output_index": 0, + "btc_amount_sats": 1, + "ckb_btc_commitment_hash": hex32(0x23), + }, + }, + }), + ); + for (relative, action, kind) in [ + ("target/novaseal-btc-utxo-seal-devnet-stateful-live.json", "close_utxo_seal", "btc_utxo_spend"), + ("target/novaseal-dual-seal-devnet-stateful-live.json", "finalize_dual_seal", "dual_seal_btc_closure"), + ] { + write_json( + root, + relative, + &serde_json::json!({ + "status": "passed", + (action): { + "commit": {"tx_hash": tx_hash}, + "public_btc_anchor": { + "kind": kind, + "anchor_source": "isolated-test-evidence", + "sealed_btc_txid": hex32(0x31), + "sealed_btc_vout_index": 0, + "sealed_btc_amount_sats": 1, + "script_pubkey_hash": hex32(0x32), + "btc_txid": hex32(0x33), + "btc_wtxid": hex32(0x34), + "spend_input_index": 0, + "ckb_btc_commitment_hash": hex32(0x35), + "sealed_utxo_commitment_hash": hex32(0x36), + }, + }, + }), + ); + } + write_json( + root, + "target/novaseal-fiber-candidate-devnet-stateful-live.json", + &serde_json::json!({ + "status": "passed", + "settle_fiber_candidate": {"commit": {"tx_hash": tx_hash}}, + }), + ); + write_json( + root, + "target/novaseal-fiber-node-experiments.json", + &serde_json::json!({ + "workflow_coverage": {"all_required_workflows_executed_passed": true}, + }), + ); +} + +#[test] +fn repository_policy_commands_pass_without_an_interpreter() { + let root = repo_root(); + for command in ["check-skill-pack", "validate-tooling-release", "check-source-policy"] { + let output = run(&root, &[command]); + assert!( + output.status.success(), + "{command} failed:\nstdout={}\nstderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } +} + +#[test] +fn fixture_generators_emit_complete_rust_reports() { + let root = repo_root(); + let temp = TestDir::new("fixtures"); + let evidence = temp.0.join("evidence"); + let operator = temp.0.join("operator.json"); + let service = temp.0.join("service.json"); + write_operator_evidence(&evidence); + let operator_output = run( + &root, + &["profile-operator-fixtures", "--evidence-root", evidence.to_str().unwrap(), "--output", operator.to_str().unwrap()], + ); + assert!(operator_output.status.success(), "operator generator failed: {}", String::from_utf8_lossy(&operator_output.stderr)); + let service_output = run( + &root, + &["service-builder-fixtures", "--operator-fixtures", operator.to_str().unwrap(), "--output", service.to_str().unwrap()], + ); + assert!(service_output.status.success(), "service generator failed: {}", String::from_utf8_lossy(&service_output.stderr)); + let operator_json: serde_json::Value = serde_json::from_slice(&fs::read(operator).unwrap()).unwrap(); + let service_json: serde_json::Value = serde_json::from_slice(&fs::read(service).unwrap()).unwrap(); + assert_eq!(operator_json["status"], "passed"); + assert_eq!(service_json["status"], "passed"); + assert!(service_json["cases"].as_array().is_some_and(|cases| !cases.is_empty())); +} + +#[test] +fn novaseal_summary_preserves_shell_contract() { + let root = repo_root(); + let temp = TestDir::new("summary"); + let report = temp.0.join("report.json"); + fs::write( + &report, + r#"{"status":"local_devnet_passed_external_endpoint_required","live_devnet_rpc_executed":true,"local_blocker_count":0,"acceptance_blocker_count":1,"blocker_count":1,"external_endpoint_coverage":{"status":"external_required"}}"#, + ) + .unwrap(); + let output = run(&root, &["novaseal-acceptance-summary", report.to_str().unwrap()]); + assert!(output.status.success(), "summary failed: {}", String::from_utf8_lossy(&output.stderr)); + assert_eq!( + String::from_utf8(output.stdout).unwrap(), + "local_devnet_passed_external_endpoint_required\ttrue\t0\t1\t1\texternal_required\n" + ); +} diff --git a/crates/cellscript-wasm/Cargo.toml b/crates/cellscript-wasm/Cargo.toml index 1afa4081..4533776d 100644 --- a/crates/cellscript-wasm/Cargo.toml +++ b/crates/cellscript-wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cellscript-wasm" -version = "0.22.0" +version = "0.24.0" edition = "2024" rust-version = "1.97.1" publish = false diff --git a/crates/cellscript-wasm/src/lib.rs b/crates/cellscript-wasm/src/lib.rs index 703caab8..92559e3e 100644 --- a/crates/cellscript-wasm/src/lib.rs +++ b/crates/cellscript-wasm/src/lib.rs @@ -6,8 +6,9 @@ //! (that would inflate the bundle beyond the 600KB budget and is //! tracked as RFC path B / v2). //! -//! The single exported function `compile_metadata_json` takes source -//! text and an optional target profile, and returns a JSON string. +//! The single exported function `compile_metadata_json` takes source text, a +//! mandatory edition, and an optional target profile, and returns a JSON +//! string. //! On success the string is the serialized `CompileMetadata`; on //! failure it is `{"error": "..."}` so the playground can parse it //! uniformly and render diagnostics. @@ -81,11 +82,15 @@ struct LanguageServiceResult { /// consume_set / create_set / estimated_cycles, etc.). On error it /// is `{"error": ""}`. /// -/// The `target` argument is optional; pass `None` for the default -/// (ckb) target profile. +/// `edition` is mandatory and currently only accepts `"2026"`. +/// The `target` argument is optional; pass `None` for the default target. #[wasm_bindgen] -pub fn compile_metadata_json(source: &str, target: Option) -> String { - match cellscript::compile_metadata(source, target) { +pub fn compile_metadata_json(source: &str, edition: &str, target: Option) -> String { + let edition = match edition.parse::() { + Ok(edition) => edition, + Err(error) => return error_json(&error.to_string()), + }; + match cellscript::compile_metadata(source, edition, target) { Ok(metadata) => serde_json::to_string(&metadata).unwrap_or_else(|e| error_json(&format!("failed to serialize metadata: {e}"))), Err(e) => error_json(&e.to_string()), } @@ -103,8 +108,12 @@ pub fn compile_metadata_json(source: &str, target: Option) -> String { /// span. Offsets are UTF-8 byte offsets from the original source; line and /// column are 1-based. #[wasm_bindgen] -pub fn compile_metadata_json_diagnostics(source: &str, target: Option) -> String { - let report = cellscript::compile_metadata_with_diagnostics(source, target); +pub fn compile_metadata_json_diagnostics(source: &str, edition: &str, target: Option) -> String { + let edition = match edition.parse::() { + Ok(edition) => edition, + Err(error) => return diagnostic_error_json(&error.to_string(), source), + }; + let report = cellscript::compile_metadata_with_diagnostics(source, edition, target); let diagnostics = report.diagnostics.iter().map(|error| diagnostic_from_error(error, source)).collect(); let result = CompileDiagnosticResult::new(report.metadata, diagnostics); serde_json::to_string(&result) @@ -117,7 +126,7 @@ pub fn compile_metadata_json_diagnostics(source: &str, target: Option) - /// `entry_path` selects the source that should produce metadata. This is an /// additive API; the single-source functions remain stable. #[wasm_bindgen] -pub fn compile_metadata_json_sources(sources_json: &str, entry_path: &str, target: Option) -> String { +pub fn compile_metadata_json_sources(sources_json: &str, entry_path: &str, edition: &str, target: Option) -> String { if sources_json.len() > MAX_SOURCE_SET_JSON_BYTES { return diagnostic_error_json(&format!("source set JSON exceeds the {} byte WASM input limit", MAX_SOURCE_SET_JSON_BYTES), ""); } @@ -131,7 +140,11 @@ pub fn compile_metadata_json_sources(sources_json: &str, entry_path: &str, targe .collect::>(); let source_by_path = sources.iter().map(|source| (source.path.clone(), source.source.clone())).collect::>(); let fallback_source = sources.iter().find(|source| source.path == entry_path).map(|source| source.source.as_str()).unwrap_or(""); - let report = cellscript::compile_sources_metadata_with_diagnostics(&sources, entry_path, target); + let edition = match edition.parse::() { + Ok(edition) => edition, + Err(error) => return diagnostic_error_json(&error.to_string(), fallback_source), + }; + let report = cellscript::compile_sources_metadata_with_diagnostics(&sources, entry_path, edition, target); let diagnostics = report.diagnostics.iter().map(|error| diagnostic_from_error_for_sources(error, &source_by_path, fallback_source)).collect(); let result = CompileDiagnosticResult::new(report.metadata, diagnostics); @@ -248,7 +261,7 @@ mod tests { #[test] fn wasm_single_source_entrypoints_reject_oversized_input() { let source = " ".repeat(cellscript::MAX_SOURCE_BYTES + 1); - let compile: serde_json::Value = serde_json::from_str(&compile_metadata_json(&source, None)).unwrap(); + let compile: serde_json::Value = serde_json::from_str(&compile_metadata_json(&source, "2026", None)).unwrap(); assert!(compile["error"].as_str().is_some_and(|message| message.contains("source exceeds"))); let language: serde_json::Value = serde_json::from_str(&language_service_json(&source, 0, 0)).unwrap(); @@ -263,7 +276,8 @@ mod tests { { "path": "b.cell", "source": " ".repeat(half) } ]) .to_string(); - let result: serde_json::Value = serde_json::from_str(&compile_metadata_json_sources(&sources, "a.cell", None)).unwrap(); + let result: serde_json::Value = + serde_json::from_str(&compile_metadata_json_sources(&sources, "a.cell", "2026", None)).unwrap(); assert!(result["diagnostics"][0]["message"].as_str().is_some_and(|message| message.contains("source set exceeds"))); } } diff --git a/docs/0.20/CELLSCRIPT_PROTOCOL_MULTI_FILE_EVIDENCE.md b/docs/0.20/CELLSCRIPT_PROTOCOL_MULTI_FILE_EVIDENCE.md index d4fece6e..248ec546 100644 --- a/docs/0.20/CELLSCRIPT_PROTOCOL_MULTI_FILE_EVIDENCE.md +++ b/docs/0.20/CELLSCRIPT_PROTOCOL_MULTI_FILE_EVIDENCE.md @@ -64,7 +64,8 @@ package: nova_fungible_xudt_type.cell Artifact preparation also includes the shared schema source unit: ```bash -python3 scripts/novaseal_planned_profiles_devnet_stateful_live.py \ +cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root . novaseal-planned-devnet \ --profile fungible-xudt \ --prepare-artifacts \ --pretty @@ -88,7 +89,8 @@ also visible in metadata: `NovaFungibleXudtSignedIntentV0` field offsets are Command: ```bash -python3 scripts/novaseal_planned_profiles_devnet_stateful_live.py \ +cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root . novaseal-planned-devnet \ --profile fungible-xudt \ --ckb-repo ../ckb \ --ckb-bin ../ckb-bin/ckb_v0.207.0_x86_64-unknown-linux-gnu-portable/ckb \ diff --git a/docs/CELLSCRIPT_0_21_ROADMAP.md b/docs/CELLSCRIPT_0_21_ROADMAP.md index b7c08a06..0431b4eb 100644 --- a/docs/CELLSCRIPT_0_21_ROADMAP.md +++ b/docs/CELLSCRIPT_0_21_ROADMAP.md @@ -395,7 +395,7 @@ Current implementation note: - write, signing, publish, deployment submission, registry mutation, and shell/editor configuration tools are intentionally absent by default; - the CellScript skill pack lives under `docs/skills/cellscript-*` and - `scripts/check_cellscript_skill_pack.py` verifies that referenced docs, + `cellscript-tools check-skill-pack` verifies that referenced docs, examples, and command names still exist. ## P1: Derived Cyclic ProtocolGraph View diff --git a/docs/CELLSCRIPT_CKB_ADAPTER.md b/docs/CELLSCRIPT_CKB_ADAPTER.md index 0a4cfab1..9e3d202b 100644 --- a/docs/CELLSCRIPT_CKB_ADAPTER.md +++ b/docs/CELLSCRIPT_CKB_ADAPTER.md @@ -87,13 +87,17 @@ crates/cellscript-ckb-adapter/ It parses compiler `ActionPlan` JSON, materializes `ResolvedActionTx` values with `ckb-sdk-rust` / CKB packed types, rejects under-capacity outputs before RPC, and exposes signer, `estimate_cycles`, `test_tx_pool_accept`, and optional -submission as adapter-owned node calls. It also builds headless deploy -transactions that create TYPE_ID code cells from a `DeployArtifactSpec`, and +submission as adapter-owned node calls. It also builds unsigned deploy +transactions that create either TYPE_ID code Cells or immutable data Cells from +a `DeployArtifactSpec`, and generates `DeploymentManifest` records from the resulting evidence. It also -tests that CellScript entry witness bytes are placed into an explicit -`WitnessArgs` field without overwriting lock signatures, and that TYPE_ID -args are computed from the packed first input plus output index before -adapter submission. +tests that CellScript entry witness bytes use the versioned +`cellscript-witnessargs-input-type-v2` contract and are placed into +`WitnessArgs.input_type` before SDK signing while preserving the lock +placeholder. A signed multisig-v2 CKB-VM regression verifies both the lock and +the CellScript type script, and proves that post-signing witness mutation is +rejected. TYPE_ID args are computed from the packed first input plus output +index before adapter submission. The full transaction lifecycle bridge includes: @@ -143,23 +147,29 @@ CKB code cell deployment transaction deployment manifest + evidence ``` -`build_deploy_transaction()` constructs a headless CKB transaction that -deploys a CellScript artifact as an on-chain code cell with TYPE_ID. It: +`build_deploy_transaction()` constructs an unsigned CKB transaction that +deploys a CellScript artifact as an on-chain code Cell. It: -- computes TYPE_ID args from the first input tx_hash + output index; -- constructs the type script (TYPE_ID) and lock script for the code cell; +- verifies that the supplied artifact hash matches the artifact bytes; +- computes TYPE_ID args for `type` deployments, while `data`, `data1`, and + `data2` deployments omit the Type Script and bind to the artifact data hash; +- constructs the lock script for the code Cell; - calculates occupied capacity for the code cell from artifact size; - constructs a change output with remaining capacity minus fee; - validates that both outputs meet occupied-capacity floors; +- inserts the 65-byte zeroed secp-sighash signing placeholder and enforces the + default 1,000 shannons/KB relay-policy fee floor; - assembles the transaction and returns `ResolvedDeployEvidence`. `build_deployment_manifest_from_evidence()` produces a `DeploymentManifest` from the evidence after a successful commit, recording the on-chain code cell reference. -This is headless: no RPC, no live-cell selection, no signing. The caller -provides a pre-resolved capacity input. Use `CkbSdkAcceptance` for node -interaction after building. +The library builder is headless: no RPC, no live-cell selection, no signing. +The caller provides a pre-resolved capacity input and all required CellDeps. +The CLI adds an RPC boundary: it requires CKB mainnet and verifies that the +selected input is live, owned by the requested secp lock, has no Type Script, +and has empty data before it calls the library builder. The output manifest should bind the CellScript artifact to the on-chain code cell: @@ -418,7 +428,7 @@ load_compile_metadata(path) -> CompileMetadata load_action_plan(path) -> ActionPlan load_deployment_manifest(path) -> DeploymentManifest -deploy_artifact_with_type_id(...) +build_deploy_transaction(spec) build_action_transaction(...) emit_acceptance_report(...) ``` @@ -428,31 +438,26 @@ The currently landed stable subset includes `load_action_plan`, script-ref helpers, WitnessArgs placement helpers, TYPE_ID args helpers, and acceptance report emission. -For convenience, `CellScriptAdapter` provides a high-level facade: +`CellScriptAdapter` provides RPC validation and node-interaction helpers. The +legacy `deploy_artifact` and `build_deploy` convenience methods fail closed +because automatic coin selection and signing are not implemented: ```rust // Connect to a CKB node let adapter = CellScriptAdapter::connect("http://127.0.0.1:8114")?; -// Deploy an artifact (finds capacity, builds, submits, waits for commit) -let (manifest, evidence) = adapter.deploy_artifact( - "my-token", - artifact_bytes, - deployer_lock_script, - 1_000, // fee in shannons -)?; - -// Or build without submitting (for external signing) -let (tx, evidence) = adapter.build_deploy( - "my-token", - artifact_bytes, - deployer_lock_script, - 1_000, -)?; +// Registry deployment tooling rejects non-mainnet nodes. +adapter.require_mainnet()?; + +// Validate a caller-selected live input before constructing DeployArtifactSpec. +let (capacity, data) = + adapter.resolve_pure_capacity_input(&capacity_out_point, &deployer_lock_script)?; + +// Build with build_deploy_transaction(&spec), then send the unsigned +// transaction to an external wallet. Never submit it before signing. // Node interaction helpers -adapter.estimate_cycles(&tx)?; -adapter.test_tx_pool_accept(&tx)?; +adapter.submit_transaction(&signed_tx)?; adapter.submit_transaction(&tx)?; adapter.wait_for_commitment(&tx_hash, 30, 500)?; ``` @@ -506,26 +511,28 @@ reported. ## CLI: `cellscript-deploy` -The adapter crate ships a CLI binary for script-driven deploy and status -querying without writing Rust code. +The adapter crate ships a CLI binary for building mainnet deployment +transactions and querying status without writing Rust code. It does not own +wallet keys. Consequently, `deploy` fails closed and `build-deploy` emits an +unsigned transaction with `can_submit: false`; a CKB wallet must sign and +broadcast it. ```bash # Build the binary cargo build -p cellscript-ckb-adapter --bin cellscript-deploy -# Deploy an artifact +# Build the canonical Registry Type Script deployment for external signing export LOCK_ARG=0x$(cat ~/.ckb/default-lock-arg) # your secp256k1 lock arg -cellscript-deploy deploy \ - --artifact token.s \ +cellscript-deploy --rpc http://127.0.0.1:8114 --json build-deploy \ + --artifact contracts/registry-type-script/artifacts/v0.24.0/cellscript-registry-type-script \ --lock-arg $LOCK_ARG \ - --name token \ - --fee 1000 \ - --capacity-out-point 0x: \ - --manifest-out .cell/deployment-manifest.json + --name cellscript-registry-type-script \ + --hash-type data1 \ + --capacity-out-point 0x: -# Build without submitting (for external signing) +# TYPE_ID remains available for upgradeable deployments cellscript-deploy build-deploy \ - --artifact token.s \ + --artifact contract.elf \ --lock-arg $LOCK_ARG \ --capacity-out-point 0x: @@ -536,8 +543,13 @@ cellscript-deploy status --tx-hash 0x cellscript-deploy info ``` -All commands support `--json` for structured output and `--rpc` to override -the default `http://127.0.0.1:8114` endpoint. +The build command validates the RPC genesis hash against CKB mainnet and +resolves the actual input capacity/data instead of trusting command-line +values. The canonical mainnet secp-sighash dep group +`0x71a7ba8fc96349fea0ed3a5c47992e3b4084b031a42264a018e0072e8172e46c:0` +is the default. All commands support `--json` for +structured output and `--rpc` to override the default +`http://127.0.0.1:8114` endpoint. ## External Positioning diff --git a/docs/CELLSCRIPT_COMPILER_ERROR_CODES.md b/docs/CELLSCRIPT_COMPILER_ERROR_CODES.md index 76587cb3..66990f82 100644 --- a/docs/CELLSCRIPT_COMPILER_ERROR_CODES.md +++ b/docs/CELLSCRIPT_COMPILER_ERROR_CODES.md @@ -18,7 +18,7 @@ non-overlapping `E2xxx` range. | `E2201` | `assembly-layout` | Generated assembly could not form a valid machine layout. | Check sections, labels, branches, and block ordering. | | `E2202` | `instruction-encoding` | A RISC-V instruction or immediate could not be encoded. | Check the mnemonic, operands, registers, and immediate range. | | `E2300` | `elf-emission` | A valid RISC-V ELF artifact could not be constructed. | Check entrypoint, section layout, offsets, and size constraints. | -| `E2400` | `external-toolchain` | An explicitly configured RISC-V toolchain failed. | Check the toolchain environment variables, executable paths, and stderr. | +| `E2400` | `verified-artifact-boundary` | The verified lowering/source-map boundary for an ELF artifact could not be constructed or persisted. | Inspect the verified lowering record, source artifact map, and canonical sidecar diagnostic. | | `E2900` | `backend-invariant` | An internal backend invariant failed after semantic checking. | Retain the source and compiler version and report a compiler defect. | The CLI exposes these codes in human diagnostics and in the `diagnostics[].code` diff --git a/docs/CELLSCRIPT_EDITION_POLICY.md b/docs/CELLSCRIPT_EDITION_POLICY.md new file mode 100644 index 00000000..c5a85327 --- /dev/null +++ b/docs/CELLSCRIPT_EDITION_POLICY.md @@ -0,0 +1,165 @@ +# CellScript Edition Policy + +**Status**: normative for the 0.23 development line. + +CellScript editions are long-lived source-language semantic epochs. An edition +answers one question: how should this CellScript source be understood? The year +in an edition label is an identifier, not an annual release schedule. Edition +2026 may remain current across multiple compiler release years. + +The only supported edition is: + +```toml +[package] +edition = "2026" +``` + +`edition` is mandatory in every package manifest. A missing value or any value +other than `2026` is an error. The 0.23 line does not provide an edition +migration command, an implicit alternate edition, or a compatibility parser +because Edition 2026 is the first CellScript source-semantics contract. + +## What The Edition Owns + +An edition owns rules that can change the meaning of the same source text: + +- keywords, reserved words, and resolution of syntactic ambiguities; +- name resolution, scope behavior, and the default prelude; +- type checking, inference defaults, coercions, and flow/resource rules; +- desugaring and other source-observable semantics; and +- edition-specific deprecation diagnostics and migration lints. + +Edition 2026 currently identifies those rules as +`cellscript-source-semantics-2026`. Because it is the first and only edition, +the frontend has no alternate parser or type-checker branch yet. The edition is +still carried through package loading and emitted identity so a future +semantic break cannot be mistaken for the same source contract. + +Additive syntax, diagnostics, formatter improvements, and optimizer changes do +not require a new edition when existing source keeps its meaning. A new edition +is justified only when an intentional source-semantic break cannot be handled +by an additive feature, a warning/deprecation cycle, or an independently +versioned schema or ABI. + +## Independent Compatibility Axes + +The edition does **not** own the compiler release, target profile, +primitive-assurance mode, metadata schemas, or CKB wire ABIs. The compiler +assembles those independently versioned values with the source edition into a +resolved compatibility profile: + +| Axis | Current 0.23 value | +|---|---| +| Source edition | `2026` | +| Source semantics | `cellscript-source-semantics-2026` | +| Compiler release | workspace SemVer (`0.x.y`), recorded separately | +| Target profile | selected independently, normally `ckb` | +| Primitive assurance | selected independently, or `default` | +| Payload ABI | `cellscript-entry-witness-v1` (`CSARGv1\0`) | +| Placement ABI | `cellscript-witnessargs-input-type-v2` | +| Metadata schemas | metadata 57, source 2, artifact 1, constraints 2 | + +The compiler release is recorded next to the profile but is not part of the +profile itself. A compiler patch may change diagnostics or optimization +without changing compatibility. Conversely, an urgent wire-ABI or metadata +fix can advance its own version immediately without waiting for a new calendar +year or source edition. + +For the current CKB placement profile: + +| Contract | Value | +|---|---| +| Placement field | `WitnessArgs.input_type` | +| Witness source | `GroupInput#0`, then `GroupOutput#0` | +| Raw payload alias | rejected | + +The resolved profile uses schema +`cellscript-resolved-compatibility-profile-v1`. It is emitted in compile +metadata and hashed into package, registry, lockfile, deployment, receipt, and +generated-builder identities. Changing any constituent axis changes the +profile identity even when source text and edition stay the same. + +```mermaid +flowchart LR + E["Source edition
2026"] --> R["ResolvedCompatibilityProfile"] + T["target profile"] --> R + P["primitive assurance"] --> R + W["entry + placement ABI"] --> R + S["metadata schemas"] --> R + R --> M["compile metadata + profile hash"] + M --> I["registry + Cell.lock + Deployed.toml"] + M --> B["receipt + generated builder"] +``` + +Registry records therefore retain both `edition` and +`compatibility_profile_hash`. The former tells source consumers how to read the +package; the latter commits to the complete compile/build contract. A registry +consumer must not infer target, primitive, ABI, or metadata versions from the +edition year. + +## Why `CSARGv1` Still Exists + +The source edition and `CSARGv1` solve different problems. + +- `edition = "2026"` selects source-language meaning before a transaction + exists. +- `CSARGv1\0` identifies CellScript positional-argument bytes while a CKB + Script is executing. +- `cellscript-witnessargs-input-type-v2` identifies where those bytes are + placed and how the script-group witness is selected. + +The current compatibility profile combines all three identities. The old raw +placement form—putting `CSARGv1` directly in the witness instead of inside a +canonical `WitnessArgs.input_type`—is not accepted. It fails closed with +runtime error `25 entry-witness-abi-invalid` because the placement ABI says so, +not because calendar year 2026 intrinsically implies a witness layout. + +## Persisted Format Boundary + +The 0.23 line deliberately starts new persisted identities: + +| Surface | Required identity | +|---|---| +| Compile metadata | metadata 57, source 2, artifact 1, constraints 2 | +| Compatibility profile | `cellscript-resolved-compatibility-profile-v1` with every independent axis | +| `Cell.lock` | version 2 | +| `Deployed.toml` | version 2 and `cellscript-deployed-v0.23-edition-2026` | +| Compile receipt | `cellscript-compile-receipt-v2` | +| Generated action builder | `cellscript-generated-action-builder-v0.23-edition-2026` | + +Readers reject earlier versions. They do not silently fill edition/profile +fields or rewrite old files. + +The 0.24 line advances only the lock carrier to version 3 with schema +`cellscript-lock-v0.24-graph-v1`. This is a dependency-resolution and source +identity change, not a new source edition: Edition 2026, the compatibility +profile, `Deployed.toml`, receipt, and generated-builder identities remain +independently versioned. Build/check/test reject older locks; only explicit +`cellc lock` or `cellc update` may repin them. + +## API Boundary + +Package compilation reads the mandatory edition from `Cell.toml`. APIs without +a package manifest must receive the edition explicitly: + +- native metadata-only Rust APIs take `CellScriptEdition`; +- WASM exports take an edition string and accept only `"2026"`; +- browser workers pass `"2026"` explicitly; and +- LSP package compilation resolves the nearest package manifest. + +`CompileOptions::default()` uses the current edition only for in-memory and +standalone compiler use. It is not a fallback for a package missing `edition`. + +## Release And Evolution Requirements + +Different axes have different closure requirements: + +- source-semantic changes require a new edition plus parser, formatter, type + checking, lowering, metadata, LSP, migration diagnostics, docs, and tests; +- entry or placement ABI changes require a new ABI identity plus codegen, + builder, metadata, CKB-VM, `backend`, `dev`, and `ci` evidence; +- metadata changes require a schema bump plus every reader/validator update; +- target and primitive changes retain their own profile and gate contracts; and +- compiler-only compatible improvements use ordinary SemVer releases. + +No edition is created merely because a year or compiler release changed. diff --git a/docs/CELLSCRIPT_ENTRY_WITNESS_ABI.md b/docs/CELLSCRIPT_ENTRY_WITNESS_ABI.md index ea1ed079..87c14a5d 100644 --- a/docs/CELLSCRIPT_ENTRY_WITNESS_ABI.md +++ b/docs/CELLSCRIPT_ENTRY_WITNESS_ABI.md @@ -4,15 +4,53 @@ tooling. CellScript action and lock entrypoints are normal RISC-V functions at the machine -level. Most public arguments come through the grouped input witness. Lock +level. Most public arguments come through the current script group's witness. Lock parameters declared as `lock_args T` instead come from the executing lock script's `Script.args` bytes. The compiler-generated `_cellscript_entry` wrapper loads the required source(s), validates the envelope or script-args layout, decodes positional arguments, and then tail-calls the selected action or lock. -## Envelope +## Placement ABI v2 -Every parameterized entry witness that has witness-backed arguments starts with: +The current CKB placement contract is +`cellscript-witnessargs-input-type-v2`: + +```text +WitnessArgs { + lock: wallet / lock-script signatures, + input_type: CellScript CSARGv1 entry payload, + output_type: protocol-specific output witness data, +} +``` + +The generated wrapper first loads `GroupInput#0`. If the active script group +has no input, it loads `GroupOutput#0`. It never substitutes transaction-global +`Input#0`, because the first member of one lock/type group may be any global +input index. The selected witness must be a canonical three-field Molecule +`WitnessArgs`; its `input_type` `BytesOpt` must contain the entry payload. + +This split lets canonical lock scripts, including multisig-v2, retain exclusive +ownership of `WitnessArgs.lock`. Builders must preserve an existing lock field +and fail rather than overwrite an existing `input_type` field. + +Builders must place the CellScript payload before lock-script signing. CKB +signers commit to the complete serialized `WitnessArgs` while replacing only +the `lock` signature bytes with their zero placeholder; consequently, +`input_type` and `output_type` are part of the signed message. Any change to +those fields after signing invalidates the signature. The adapter helper is +therefore named `place_entry_witness_payload_before_signing`, accepts a lock +placeholder, validates the `CSARGv1\0` payload magic, and must run before the +SDK unlock/sign step. + +Placement ABI `cellscript-witnessargs-input-type-v2` has no raw-payload +compatibility path. The selected group-relative witness must be a canonical +`WitnessArgs`; a raw `CSARGv1\0` payload, malformed table, absent `input_type`, +or payload placed in `lock`/`output_type` fails closed with runtime error +`25 entry-witness-abi-invalid`. + +## Payload Envelope v1 + +Every parameterized entry payload that has witness-backed arguments starts with: ```text 43 53 41 52 47 76 31 00 @@ -20,8 +58,14 @@ Every parameterized entry witness that has witness-backed arguments starts with: This is the ASCII magic `CSARGv1\0`. -Wrong magic, missing bytes, or unsupported parameter placement fails closed with -runtime error `25 entry-witness-abi-invalid`. +The magic remains necessary even though the resolved compatibility profile +records this ABI: Edition 2026 identifies source semantics, the placement ABI +identifies the witness location, and the magic identifies runtime bytes inside +`input_type`. It prevents unrelated protocol bytes from being decoded as +CellScript positional arguments. + +Wrong magic, missing bytes, malformed Molecule, or unsupported parameter +placement fails closed with runtime error `25 entry-witness-abi-invalid`. Entries whose parameters are entirely runtime-bound or `lock_args`-backed do not require a witness envelope. diff --git a/docs/CELLSCRIPT_EXECUTABLE_TEST_SCENARIOS.md b/docs/CELLSCRIPT_EXECUTABLE_TEST_SCENARIOS.md new file mode 100644 index 00000000..15ffbd83 --- /dev/null +++ b/docs/CELLSCRIPT_EXECUTABLE_TEST_SCENARIOS.md @@ -0,0 +1,89 @@ +# CellScript Executable Test Scenarios + +**Status**: implemented on the 0.24 development line + +**Scenario schema**: `cellscript-test-scenario-v1` + +**Report schema**: `cellscript-test-report-v1` + +## Running Tests + +`cellc test` no longer treats compile-only discovery as executed tests. Unless +`--no-run` is selected, a backend and at least one `*.scenario.json` fixture +are required: + +```bash +cellc test --backend simulator +cellc test --backend ckb-vm +cellc test --backend all --json +cellc test --no-run +``` + +The two execution tiers are deliberately different: + +- `simulator` runs the typed development interpreter and reports + `development-non-consensus` evidence; +- `ckb-vm` loads the emitted ELF into the maintained CKB-VM runner and reports + `authoritative-runtime` evidence. + +Neither tier is an RPC admission, transaction commitment, or confirmation +claim. + +## Scenario Shape + +A scenario sits beside its confined relative `.cell` source and declares: + +- an action or lock entry and typed scalar arguments; +- named initial live Cells with capacity, data, lock, and optional Type Script; +- ordered steps with consumed Cells and named replacement outputs; +- CellDeps, header deps, per-input `since`, and `WitnessArgs` lock, + `input_type`, and `output_type` fields; +- either `pass` or one exact registered runtime error code and name; +- maximum interpreter steps, CKB-VM cycles, serialized fixture bytes, and a + minimum Cell capacity; and +- an optional reference to the separate stateful CKB oracle. + +All security-sensitive structs reject unknown fields. Source and oracle paths +must be relative and path-confined. Hashes, scripts, indexes, witnesses, +duplicate names, stale/dead Cell references, output-name reuse, and limits are +validated before execution. + +See `tests/scenarios/positive.scenario.json` for a two-step replacement and +`tests/scenarios/assertion-failure.scenario.json` for an exact +`assertion-failed` (`5`) expectation. + +## Multi-Step State Boundary + +The v1 runner maintains a local live-Cell set. Consumed names become dead, +outputs become live, and `prior_output` must name a Cell consumed by the same +step. This catches stale references, double consumption, and ambiguous +replacement chains. + +The local state model validates scenario bookkeeping. It does not inject those +Cells into CKB syscalls. The current CKB-VM backend executes no-argument ELF +entries; entries that require transaction syscalls or arguments must reference +the separate stateful oracle and remain outside this v1 runner until a +transaction syscall harness is promoted. + +## Exact Failures And Coverage + +The runner validates both the numeric `CellScriptRuntimeError` and its stable +name. An unregistered code, a name/code mismatch, success where failure was +expected, or a different runtime error fails the scenario. + +Every report binds compiler version, artifact hash, compatibility profile, +checker name/version/policy, lowering-record hash, source-map hash, backend, +and evidence tier. Coverage reports list declared and observed entries, +lowering blocks, ProofPlan links, runtime errors, syscalls, and source-linked +instruction ranges. + +Coverage is conservative: v1 claims only the observed entry and exact runtime +outcome. It does not claim unexecuted branches, ProofPlan obligations, or +syscall sites merely because they were present in compiler metadata. + +## Gate Coverage + +- `dev` runs the simulator scenarios. +- `ci` and `backend` run both simulator and CKB-VM scenarios. +- The existing stateful CKB harness remains the transaction-shaped oracle and + is not replaced by local scenario bookkeeping. diff --git a/docs/CELLSCRIPT_GATE_POLICY.md b/docs/CELLSCRIPT_GATE_POLICY.md index a6fb09b8..b88ed5f1 100644 --- a/docs/CELLSCRIPT_GATE_POLICY.md +++ b/docs/CELLSCRIPT_GATE_POLICY.md @@ -14,22 +14,146 @@ deciding whether a change is ready. | Mode | When to run | Evidence boundary | |---|---|---| -| `dev` | Local development before pushing | Formatting, all workspace-package Rust checks, strict backend quick audit, syntax-combination quick audit, skill-pack freshness, README-linked CellScript doc Status freshness, local markdown link check, whitespace diff check | -| `ci` | Pull requests, pushes, and routine merge readiness | Tests and clippy for the compiler, Fiber adapter, CKB adapter, WASM crate, and CKB SDK builder example; strict backend CI audit; package verification; skill-pack/doc freshness; local-link and script syntax checks | -| `backend` | Changes touching IR, codegen, assembler, ABI, ELF, or RISC-V behavior | Full Rust tests, clippy, and strict backend full audit, including stateful CKB scenarios | +| `dev` | Local development before pushing | Native source-policy enforcement; Rust formatting; canonical CellScript example formatting; all workspace-package Rust checks (including the standalone artifact checker and `cellscript-tools`); checker mutation/Myelin handoff tests; simulator package scenarios; both Registry verifiers and their compiler-dependency boundaries; reproducible Registry Type Script build and CKB-VM tests; strict backend quick audit, syntax-combination quick audit, parity-gated skill-pack freshness, README-linked CellScript doc Status freshness, local markdown link check, whitespace diff check | +| `ci` | Pull requests, pushes, and routine merge readiness | Node 22 and native source-policy enforcement; all compiler/checker/adapter/tool tests and clippy; simulator plus CKB-VM package scenarios; standalone-checker dependency and mutation evidence; reproducible Registry Type Script identity plus CKB-VM tests and clippy; Registry API typecheck/tests with compiler-backed and least-privilege artifact workers, Node bundles, and dry-run Worker build; full website behavior/build regression suite; strict backend CI audit; package verification; parity-gated skill-pack/doc freshness; local-link and script syntax checks | +| `backend` | Changes touching IR, codegen, assembler, ABI, ELF, or RISC-V behavior | Compiler, artifact-checker, and Fiber checks/tests/clippy; checker dependency boundary; simulator plus CKB-VM package scenarios; native source-policy enforcement; and strict backend full audit, including stateful CKB scenarios | | `release` | Nightly/stable release candidates and any production CKB claim | Clean tagged source plus `ci`, a fresh size-gated website WASM rebuild, tooling/docs and VS Code checks, pinned-CKB acceptance harnesses, public builder-contract generation, and mandatory stateful scenario/action coverage | | `release-quick` | Wrapper compatibility and local compile-only preflight | `ci` plus compile-only production acceptance; not external live/devnet evidence | `release-quick` is kept for `scripts/cellscript_ckb_release_gate.sh quick`. Use `release` for any production or external live/devnet claim. +CI packages the independently publishable `cellscript-artifact-checker` first, +then verifies the `cellscript` package offline with an exact local crates.io +patch. A real crates.io release must preserve that dependency order: publish +and confirm the checker version before publishing the compiler version. + +Package-manager changes must preserve the lock-authority regression matrix: +standard SemVer edge cases; missing/stale manifest digests; direct/transitive +source drift; alias and graph-edge identity; optional/default/all feature and +test-only roots; environment override plus CKB genesis binding; moving Git +branches remaining pinned until explicit update; exact offline/frozen cache +use; and bounded external resolvers normalizing to immutable sources without +running during locked builds. Registry API checks also validate the complete +`cellscript-registry-profile-catalog-v1` and prove that only CellScript source +profiles are dependency-resolving. + +The same Registry matrix covers +`cellscript-registry-ls-idl-interface-v1`: raw ABI schema and size budgets, +SHA-256 binding, executable suffix placement, publish-time rejection cases, +SQL/in-memory Script lookup, byte-preserving canonical and compatibility +responses, ambiguous type-hash rejection, CLI validate/bind/fetch/bundle, and +both compiler-backed and least-privilege verifier outputs. Passing this matrix +does not assert that a Lock Script semantically implements its IDL. + +`dev` and `ci` run `cellc fmt --check` against +`examples/language/canonical_style.cell`. The formatter's comma-terminated +field form is the canonical checked-in surface; the parser may continue to +accept comma-free fields as compatibility input. The same modes reject raw +`u64` maximum and `MAX - delta` magic literals in the checked NFT, timelock, +atomic-swap, and multi-phase-DAO example pairs; boundary arithmetic must use +their local `U64_MAX` constants. + Both release modes fail before doing expensive work unless the CellScript tree -is completely clean, including untracked files. CI additionally requires the -exact `v` tag at `HEAD`; a manual release dispatch must name +is completely clean, including untracked files. GitHub release CI additionally +requires the exact `v` tag at `HEAD`; a manual release dispatch must name the same version as the root `[package].version`. The GitHub Release workflow runs the full `release` gate first, and binary builds plus publication depend on that job succeeding. +Production CKB acceptance rebuilds the pinned CKB `0.207.0` checkout in a +fresh dedicated Cargo target. That pin resolves `ckb-librocksdb-sys 8.5.4`, +whose `trace_record.h` uses fixed-width integer types without directly +including ``. The acceptance builder therefore sets the exact +`CXXFLAGS=-include cstdint` compatibility contract instead of patching the +clean CKB checkout. The production evidence validator requires both that flag +and `ckb-librocksdb-sys-8.5.4-explicit-cstdint-v1` in +`ckb_runtime_provenance`; changing either is a release-boundary change. + +The 0.23 tooling migration is complete. `cellscript-tools` owns the backend, +syntax-combination, skill-pack, tooling-release, CKB production-evidence, +NovaSeal, and Evolving-DOB gate logic. Website data generation is implemented +by Node scripts in `website/scripts/`. Dev, CI, backend, and release gates have +no Python runtime dependency and reject tracked Python source files. +Node-backed CI uses Node 22. After one checked Registry-data generation pass, +the unified gate and manual website workflow both run +`npm --prefix website run build:ci`; that target owns the complete Registry, +playground, visual, homepage, preference, documentation, dist, deploy, Astro +check, and Astro build regression contract. It builds both production and +Pudge Testnet Registry outputs, checks the six shared routes in each, and +requires their generated CSS and JavaScript assets to be byte-identical while +allowing only explicit network authority and admitted-data differences. + +The 0.23 line also has one edition contract: every package declares +`edition = "2026"`, and all emitted evidence binds the resolved compatibility +profile. The edition owns source semantics only; target, primitive assurance, +metadata schemas, and entry/witness ABIs remain independent profile axes. +Missing/non-2026 editions and superseded lock, deployment, receipt, builder, or +raw-witness placement identities are rejected rather than migrated. See +[`CELLSCRIPT_EDITION_POLICY.md`](CELLSCRIPT_EDITION_POLICY.md). Edition-owned +source changes require complete frontend closure. Independently versioned ABI +changes require the `backend` gate in addition to ordinary `dev` and `ci` +coverage. + +The `ci` gate also typechecks/tests `services/registry-api`, builds both Node +entrypoints, performs its Wrangler dry-run build, and runs tests and clippy for +the independent real-compiler Registry verifier crate. `dev` at least checks +that verifier crate. This pins the single `/v1/artifacts` contract, orthogonal +verification/deployment/availability states, generic artifact bundles, +mainnet deployment evidence, additive migrations, worker boundary, and +database/static-object shape to the CLI-generated Registry entry. It is local +service coverage, not evidence +that Cloudflare, R2, Hyperdrive, Neon, DNS, or a production deployment works. +The CLI coverage includes both first-publish admission paths: the explicit +`cellc auth capability submit`, `cellc auth namespace claim`, then +`cellc publish` sequence, and the short-lived `cellc publish --authorise` +browser session in which the private publishing key remains in the local OS +keychain as pending while the CLI polls with a one-time secret, becomes active +only after the server returns the matching key ID, and is removed only after +the server confirms terminal cancellation or pending-session expiry. A local +polling deadline performs a final authoritative read and preserves the pending +key if the result is still pending or unreachable. Completed sessions remain +poll-readable for a bounded 24-hour recovery window. The browser token survives +a same-tab refresh but is cleared after completion or expiry; the website build +runs the fragment-store-refresh-clear lifecycle regression. Browser-session +completion is one atomic admission boundary across +nonce consumption, publishing-key registration, namespace claim/review, +session state, and audit events. API tests cover expiry, wrong browser/poll/ +challenge tokens, challenge replay, concurrent completion, conflicting +namespace ownership, review-pending admission, post-expiry terminal reads, and +injected mid-transaction failure. Publisher maintenance additionally uses the capability-signed +`cellc artifact set-availability` path, and `cellc artifact cell-dep` performs a +fresh mainnet liveness check before producing a transaction-builder descriptor. +Independent reproducibility builders use `cellc auth reproducer create`; CLI +coverage verifies that its public enrollment contains an importable P-256 SPKI, +that private PKCS#8 material never appears in JSON output, and that explicit CI +secret files are mode 0600 on Unix and no-overwrite. +Capability registration does not silently claim a namespace; +the claim response must be `active` before the write API accepts a version. +Registry API tests pin both accepted publisher roots: JoyID signatures under +`principal_type = joyid_ckb` and recoverable CKB message signatures under +`principal_type = ckb_secp256k1`. CLI fixtures use the generic +`--wallet-signature` surface; the former `--joyid-signature` spelling remains a +visible compatibility alias and does not define a second request shape. +Explicit `--allow-unverified` and `--allow-quarantined` install choices are +persisted per dependency so the lock refresh and later builds exercise the +same auditable resolver policy. + +Both `dev` and `ci` also build the independent +`contracts/registry-type-script` crate for +`riscv64imac-unknown-none-elf`, strip it with the pinned toolchain, verify the +tracked canonical ELF's SHA-256 and CKB data hash, and execute that ELF's +positive and negative lifecycle matrix in CKB-VM through `ckb-testtool`. +The reproducible builder accepts either GNU `sha256sum` or Perl `shasum` and +fails closed when neither SHA-256 implementation is available. +Linux x86_64 additionally requires the fresh build to match the tracked ELF +byte-for-byte. Other build hosts record their host artifact hash and make no +cross-host reproduction claim; the pinned container builder provides that +canonical check there. +Passing this local boundary proves the deployed bytes' behavior and identity; +it does not prove that the code Cell or custody Lock CellDep is live on +mainnet. Production readiness still performs live RPC and confirmation checks. + The full gate reads `scripts/ckb_acceptance_pin.json` and rejects a CKB checkout whose revision or worktree differs from the pin. Its report binds the CKB version string, executable SHA-256, source-template hashes, effective devnet @@ -41,13 +165,14 @@ and validates every step's commit, spent-input liveness, live outputs, cycles, serialized size, and occupied capacity. `--stateful-scenarios` remains only as an explicit option for bounded runs. -The transaction matrix is intentionally described as a Python acceptance -harness. It is not relabelled as generated-builder output. Separately, the gate -runs the public `cellc action build` and `cellc gen-builder` surfaces for every -production action and hashes their generated contracts. Resource Type Scripts -in these local transactions remain `always_success` fixtures; the report -records that this proves verifier behaviour and transaction shape, not a -production passive-resource-identity deployment. +The transaction matrix is produced by the native Rust acceptance harness and +is intentionally labelled as recipe-replayer evidence, not generated-builder +output. Separately, the gate runs the public `cellc action build` and +`cellc gen-builder` surfaces for every production action and hashes their +generated contracts. Resource Type Scripts in these local transactions remain +`always_success` fixtures; the report records that this proves verifier +behaviour and transaction shape, not a production passive-resource-identity +deployment. ### Fiber integration evidence @@ -89,6 +214,32 @@ and Fiber source/build were observed only in a bounded local fixture, no signed announcement report was captured, and the complete declared matrix was not produced. +### 0.24 verified-artifact and scenario evidence + +The 0.24 development line advances compile metadata to schema 58 and makes a +CKB ELF build a four-file bundle: ELF, compile metadata, canonical verified +lowering record, and canonical source map. Every build validates the bundle, +and the gates separately build, test, lint, and dependency-audit +`cellscript-artifact-checker`. The checker does not depend on the parser, +resolver, IR, optimizer, assembler, or code generator. Its mutation and +malformed-input corpora pin bounded `V2400` through `V2418` rejection classes, +including reachability, stack, ELF, instruction, control-flow, syscall, digest, +and source-map failures. + +`dev` runs executable package scenarios with the simulator. `ci` and `backend` +run both simulator and CKB-VM backends and require exact registered runtime +errors for negative fixtures. The v1 runner's multi-step Cell replacement is a +local bookkeeping contract; it does not inject scenario Cells into CKB +syscalls. The existing stateful CKB harness remains the transaction-shaped +oracle. + +Registry API coverage keeps generic source/executable/ABI CKB bundles +`hash_bound`. Supplying any verified sidecar requires the complete +metadata/lowering-record/source-map set and dispatches to the least-privilege +artifact worker. A `structurally_verified` checker level records checker +version, policy, and report hash, but remains distinct from source equivalence, +CKB-VM execution, deployment, and chain evidence. + ### Nightly 0.22 compiler evidence The `nightly-0.22` line adds compile-time callable-effect contracts and @@ -272,13 +423,19 @@ release-evidence boundary. The following ecosystem/bridge scripts are standalone manual tools that are **not** wired into any gate mode and are **not** part of the release-evidence -boundary. They require sibling checkouts (`../ckb`, `../CellFabric`) or external -runtimes and are documented in their respective guides for focused, opt-in use: +boundary. They require sibling or explicitly selected external checkouts and +runtimes, and are documented in their respective guides for focused, opt-in +use: - `./scripts/cellscript_ckb_ecosystem_reuse_gate.sh` — CKB-ecosystem reuse checks; see `docs/CELLSCRIPT_CKB_ADAPTER.md`. - `./scripts/cellscript_ckb_adapter_acceptance.sh` — adapter acceptance against a sibling CKB checkout; see `docs/CELLSCRIPT_CKB_STD_COMPAT.md`. +- `./scripts/cellscript_ls_idl_upstream_acceptance.sh` — exact-pinned LS-IDL + derive, client, and example-script compatibility, including the actual + upstream Rust client calling the Registry compatibility handler, unmodified + upstream RISC-V builds, LS-IDL-bound ELFs, and example CKB-VM execution; see + `docs/CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md`. - `./scripts/cellscript_cellfabric_bridge_smoke.sh` — CellFabric bridge smoke test; see `docs/CELLSCRIPT_CELLFABRIC_BRIDGE.md`. diff --git a/docs/CELLSCRIPT_GATE_REDUNDANCY_AUDIT.md b/docs/CELLSCRIPT_GATE_REDUNDANCY_AUDIT.md index 2de8e5aa..4beb724b 100644 --- a/docs/CELLSCRIPT_GATE_REDUNDANCY_AUDIT.md +++ b/docs/CELLSCRIPT_GATE_REDUNDANCY_AUDIT.md @@ -1,6 +1,6 @@ # CellScript Gate Redundancy Audit -Status: 2026-07-04 +Status: 2026-08-09 This report audits redundant or overly repetitive work in the CellScript gate stack. It covers the unified gate entry point, lower-level audit runners, @@ -23,13 +23,14 @@ acceptance coverage itself. | Area | Previous behaviour | Updated behaviour | Risk | | --- | --- | --- | --- | -| Release auxiliary checks | `release` and `release-quick` run `run_ci_gate`, then repeated `check_cellscript_skill_pack.py`, `check_script_syntax`, and `check_trailing_whitespace` inside `run_release_auxiliary_checks`. | Release modes now inherit those checks from the embedded CI gate and keep release auxiliary checks focused on release-only docs, CKB, NovaSeal, and VS Code evidence. | Low. The checks still run before release-only checks. | -| Website build in the unified gate | `run_website_build_check` ran `npm --prefix website run prepare:registry`, checked generated data, then ran `npm --prefix website run build`; the `build` script ran `prepare:registry` again. | The gate still prepares and checks registry data once, then directly runs `astro check` and `astro build` from `website/`. | Low. The same Astro checks and build still run. | -| Website build workflow | `.github/workflows/website-build.yml` ran automatically on PRs and pushes, duplicating the website build already covered by the unified CI gate. It also ran `npm --prefix website run build`, which generated registry data again. | The workflow is now manual-only via `workflow_dispatch`, keeping the `website/dist` artifact path available on demand. It also generates and checks registry data once, then directly runs `astro check` and `astro build`. | Low. Automatic merge-readiness coverage remains in the unified CI gate. | +| Release auxiliary checks | `release` and `release-quick` run `run_ci_gate`, then repeated `cellscript-tools check-skill-pack`, `check_script_syntax`, and `check_trailing_whitespace` inside `run_release_auxiliary_checks`. | Release modes now inherit those checks from the embedded CI gate and keep release auxiliary checks focused on release-only docs, CKB, NovaSeal, and VS Code evidence. | Low. The checks still run before release-only checks. | +| Website build in the unified gate | `run_website_build_check` ran `npm --prefix website run prepare:registry`, checked generated data, then ran `npm --prefix website run build`; the `build` script ran `prepare:registry` again. An intermediate optimisation called Astro directly but accidentally bypassed website regression suites. | The gate prepares and checks registry data once, then runs `npm --prefix website run build:ci`. That target runs the full Registry, playground, visual, homepage, preference, docs, dist, and deploy regression sequence before Astro output is accepted. | Low. Registry generation remains single-pass and the previously bypassed regression evidence is restored. | +| Website build workflow | `.github/workflows/website-build.yml` ran automatically on PRs and pushes, duplicating the website build already covered by the unified CI gate. It also ran `npm --prefix website run build`, which generated registry data again. | The workflow is manual-only via `workflow_dispatch`, keeping the `website/dist` artifact path available on demand. It generates and checks registry data once, then runs the same `build:ci` regression contract as the unified gate. | Low. Automatic merge-readiness coverage remains in the unified CI gate, and manual artifacts cannot bypass the website regressions. | | VS Code release path | Release auxiliary checks ran `npm run validate`, which built the extension, then `npm run publish:dry-run`, which explicitly built again and then let `vsce package` run `vscode:prepublish`, building again. | The gate directly runs `vsce package --no-dependencies`, letting `vsce` perform the one required prepublish build, then runs `node scripts/validate.mjs` directly against the built output. | Low. The VSIX dry-run and manifest validation still run. | -The release tooling validator was updated to enforce the new direct-call -contract so this optimisation does not drift silently. +The release tooling validator enforces the `build:ci` call and its ordered +regression sequence so the optimisation cannot drift into a direct-Astro +bypass. ## Intentional Overlap Kept @@ -64,10 +65,10 @@ then validates actual package construction. They should remain separate. ## Cross-Workflow Result -The PR/push path now has one automatic website build source: the unified CI -gate. The standalone website workflow remains available for manual artifact -generation only, so it no longer duplicates merge-readiness checks on every PR -or push. +The PR/push path has one automatic website build source: the unified CI gate. +The standalone website workflow remains available for manual artifact +generation only. Both paths use the same Node 22 `build:ci` contract, while +only the unified gate determines automatic merge readiness. ## Validation @@ -75,14 +76,15 @@ The updated paths were checked with: ```bash bash -n scripts/cellscript_gate.sh -python3 scripts/validate_cellscript_tooling_release.py +cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root . validate-tooling-release git diff --check npm --prefix website run prepare:registry -(cd website && npm exec -- astro check && npm exec -- astro build) +npm --prefix website run build:ci (cd editors/vscode-cellscript && npm exec -- vsce package --no-dependencies --out /tmp/cellscript-vscode-dry-run.vsix) node editors/vscode-cellscript/scripts/validate.mjs ``` -Observed website diagnostics were non-fatal existing hints in -`website/public/wasm/cellscript_wasm.js` for unused generated bindings. The -Astro check and build completed successfully. +Node 22 is the supported runtime for both the website and Registry API. CI and +release workflows install it explicitly, and the unified `ci` gate rejects a +different Node major before running Node-backed checks. diff --git a/docs/CELLSCRIPT_GRAMMAR_GOVERNANCE_RFC.md b/docs/CELLSCRIPT_GRAMMAR_GOVERNANCE_RFC.md index 6936d008..c12012bd 100644 --- a/docs/CELLSCRIPT_GRAMMAR_GOVERNANCE_RFC.md +++ b/docs/CELLSCRIPT_GRAMMAR_GOVERNANCE_RFC.md @@ -104,7 +104,7 @@ The 0.21 RC adds governance requirements that build on the baseline matrix: | Compile receipts | `cellc receipt`, `cellc sign-receipt`, `cellc verify-receipt`, and `verify-artifact --receipt` bind metadata/artifact evidence without claiming transaction validity. | `cellscript-compile-receipt-v1`. | | CLI command groups | Public discovery uses nested `explain`, `tx`, `deploy`, `registry`, `package`, and `auth capability` groups; hidden flat aliases are compatibility only. | `cellc --list` and CLI help. | | Diagnostic transport | Global `--json`, `--color=auto|always|never`, and `NO_COLOR` are part of the scripted diagnostics surface; hidden `--message-format=json` is compatibility-only. | CLI command definitions and gate usage. | -| Agent tooling | `cellscript-mcp` and the six `docs/skills/cellscript-*` skills are read-oriented compiler surfaces whose freshness is checked by dev/ci gates. | `scripts/check_cellscript_skill_pack.py`. | +| Agent tooling | `cellscript-mcp` and the six `docs/skills/cellscript-*` skills are read-oriented compiler surfaces whose freshness is checked by dev/ci gates. | `cellscript-tools check-skill-pack`. | ## `verification` diff --git a/docs/CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md b/docs/CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md new file mode 100644 index 00000000..6ff62d18 --- /dev/null +++ b/docs/CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md @@ -0,0 +1,260 @@ +# LS-IDL Registry Profile + +**Status**: CellScript 0.24 protocol and tooling contract + +**Profile schema**: `cellscript-registry-ls-idl-interface-v1` + +**LS-IDL format version**: `0.1` + +This profile lets the CellScript Artifact Registry publish and resolve an +LS-IDL witness interface for a deployed CKB Lock Script. It preserves the +upstream commitment rule exactly: + +```text +code Cell data = executable bytes || SHA-256(raw idl.json bytes) +``` + +The Registry stores the original IDL object bytes in the immutable release +bundle. It never parses and reserialises those bytes on the read path. + +## What The Profile Proves + +An accepted profile proves all of the following: + +- the ABI object is valid JSON within the Registry's size and field budgets; +- the document uses the supported LS-IDL 0.1 schema and witness types; +- the declared digest equals `SHA-256` of the exact ABI object bytes; +- the executable object's final 32 bytes equal that digest; +- the package is a deployable `ckb_executable` with `script_role = "lock"`; +- a lookup result belongs to an active, public, chain-verified deployment on + the requested Registry network. + +It does **not** prove that the Lock Script decodes witness data as described, +that every described field is semantically enforced, that a transaction is +valid, or that the Script is secure. Build verification and security review +remain separate evidence. + +## Artifact Profile Contract + +`profile_contract.interface` is accepted only on deployable CKB executables: + +```json +{ + "schema": "cellscript-registry-ls-idl-interface-v1", + "format": "ls-idl", + "format_version": "0.1", + "object_role": "abi", + "content_type": "application/vnd.ckb.ls-idl+json", + "encoding": "linear-le-v0", + "commitment": { + "algorithm": "sha256", + "placement": "code-cell-data-suffix-32", + "digest": "<64 lowercase hexadecimal characters>" + } +} +``` + +Unknown keys, alternate algorithms, alternate commitment placements, missing +ABI objects, digest mismatches, and executable-suffix mismatches fail closed. +The artifact bundle continues to use CKB Blake2b-256 for its executable +`artifact_hash`; LS-IDL's ABI commitment remains SHA-256. These hashes have +different roles and are not interchangeable. + +## Accepted IDL Document + +The document may contain only these top-level fields: + +- `idl_version` (optional string, matching upstream clients that default it); +- `name` (optional string, matching derive output that may omit it); +- `witness` (required array, at most 256 fields); +- `description` (optional string); +- `script_version` (optional string); and +- `signing` (optional object with non-empty string fields `algorithm`, + `message`, and `hasher`). + +Each witness field contains only `name`, `type`, `required`, and +`description`. Names must be unique. The supported types are: + +| Type | Encoding | +| --- | --- | +| `uint8` | one unsigned byte | +| `uint32` | four-byte little-endian unsigned integer | +| `uint64` | eight-byte little-endian unsigned integer | +| `secp256k1_sig` | 65 bytes | +| `secp256k1_pubkey` | 33 bytes | +| `schnorr_sig` | 64 bytes | +| `bytes` | four-byte little-endian length followed by that many bytes | + +The current linear decoder treats `required` as interface metadata. It does +not introduce a presence bitmap or conditional field skipping, so consumers +must not interpret `required: false` as a wire-level omission rule. + +## Public Read API + +The canonical lookup is: + +```text +GET /v1/ckb/scripts/:code_hash/interfaces/ls-idl + ?network=mainnet|testnet + &hash_type=data|data1|data2|type + [&data_hash=0x...] +``` + +`data_hash` is mandatory for `hash_type=type`, where a type hash alone may not +uniquely identify executable data. More than one matching deployment returns +`409` instead of choosing arbitrarily. + +The compatibility route is: + +```text +GET /idl/:code_hash +``` + +It is retained for existing LS-IDL clients and returns the same original +bytes. New integrations should use the canonical route so network, hash type, +and data-hash identity are explicit. + +Successful responses use +`application/vnd.ckb.ls-idl+json` and expose: + +- `ETag`; +- `x-ls-idl-format-version`; +- `x-ls-idl-sha256`; +- `x-ls-idl-coordinate`; +- `x-ls-idl-commitment`; and +- `x-ls-idl-verification`. + +Clients must hash the response body directly. JSON-equivalent reformatting is +not byte-equivalent and therefore does not preserve the commitment. + +## CLI Workflow + +For a complete publisher-to-consumer walkthrough, see +[Tutorial 15: LS-IDL for CKB Lock Scripts](wiki/Tutorial-15-LS-IDL-for-CKB-Lock-Scripts.md). + +Validate a document and optionally its existing executable binding: + +```bash +cellc artifact ls-idl validate --idl idl.json +cellc artifact ls-idl validate --idl idl.json --executable lock +``` + +Append the raw-byte digest to an executable without silently overwriting it: + +```bash +cellc artifact ls-idl bind \ + --idl idl.json \ + --executable lock \ + --output lock.ls-idl +``` + +Generate a publish-ready artifact bundle and manifest: + +```bash +cellc artifact ls-idl bundle \ + --idl idl.json \ + --executable lock.ls-idl \ + --source lock.rs \ + --namespace example \ + --name example-lock \ + --release 0.1.0 \ + --language rust \ + --hash-type data1 \ + --dep-type code \ + --toolchain rust-1.97.1 \ + --source-revision <40-hex-git-commit> \ + --output artifact.bundle.json \ + --artifact-manifest-output Artifact.toml +cellc publish --artifact-manifest Artifact.toml --dry-run --json +``` + +Fetch exact bytes by deployed Script identity: + +```bash +cellc artifact ls-idl fetch \ + --code-hash 0x<64-hex> \ + --hash-type data1 \ + --network mainnet \ + --output idl.json +``` + +The VS Code extension exposes the validate, bind, and fetch operations through +the command palette. The Registry website exposes package-bound LS-IDL facts +and a direct Script-identity lookup under an explicit `LS-IDL` tab at +`https://cellscript.dev/registry/LS-IDL`. The retired +`/registry/interface` route redirects permanently to that canonical address. + +## Storage And Admission + +Migration `0010_ls_idl_interfaces.sql` adds a partial lookup index over public, +chain-verified deployed evidence. The API narrows candidates in the database, +then rechecks release identity, immutable bundle identity, one-and-only-one ABI +object, raw-byte digest, and profile contract before returning bytes. + +The normal compiler-backed Registry worker and the least-privilege +artifact-only verifier both enforce the same profile. The latter has no +CellScript compiler dependency. A publish that merely labels arbitrary JSON as +LS-IDL, supplies a detached digest, or binds a digest to the wrong executable +is rejected before it can become searchable. + +## Compatibility Evidence + +The deterministic compatibility corpus lives under `tests/compat/ls_idl/` and +pins the current public inputs from all three repositories linked by the +proposal: + +- `ckb-idl-derive` commit + `e7ee35766b9084099e9d840ccd37d2b5d40074a1`; +- `ckb-idl-client` commit + `7d883e0abccba56d423449b673567ee817747936`; +- `ckb_sudt_script` commit + `c20ce3f4813100b78076fd447a0234bb5ad46bbb`; and +- upstream `test-vectors.json` SHA-256 + `a9a6dca4fd0c5fcd2ca7aea6468784be7fdb29d6274049f07090cbab0ce9c1bb`. + +`tests/ls_idl_upstream.rs` pins the complete 17-vector client corpus and all +seven checked-in IDL outputs from the derive and example-script repositories. +It admits every known document and wire type while confirming that the +`molecule_bytes` unknown-type vector fails closed. Files without final newlines +are Base64-wrapped so their decoded bytes and upstream SHA-256 remain exact. + +For an external checkout-level check, run: + +```bash +./scripts/cellscript_ls_idl_upstream_acceptance.sh \ + --derive-repo /path/to/ckb-idl-derive \ + --client-repo /path/to/ckb-idl-client \ + --scripts-repo /path/to/ckb_sudt_script +``` + +The script requires clean checkouts at the pinned commits, checks every raw +fixture hash, runs the derive and client library tests plus the example +scripts' structural witness tests, validates all seven upstream IDLs with +`cellc`, and runs the actual upstream Rust client against the Registry +`/idl/:code_hash` handler. That probe covers fetch, raw-byte SHA-256 +verification, cache use, and linear witness decoding. It then creates a +disposable worktree, builds all three example contracts from the unmodified +merged upstream source with Rust 1.97.1, binds the simple and timelock ELF files +to their exact IDL bytes, and runs all 25 upstream CKB-VM tests against the +bound executables. + +This remains an opt-in compatibility tool rather than release-gate evidence. +At the pinned client commit, the complete vector and library tests pass; the +repository's separate property-test suite still contains Blake2b commitment +fixtures even though production `verify` uses SHA-256. [Upstream PR +#7](https://github.com/OWK50GA/ckb_sudt_script/pull/7) merged CKB's +`passes=lower-atomic` build setting and the explicit timelock `HeaderDep` +loading path, so the acceptance pin now tests the unmodified upstream source +without a CellScript compatibility overlay. + +The upstream mini Registry example parses and reserialises JSON, so it is not +used as the byte-preserving production storage contract. CellScript follows +the production client and proposal commitment and tests SHA-256 end to end. + +## Operational Boundary + +Deploying the website does not deploy the Registry API. Operators must apply +migration `0010_ls_idl_interfaces.sql`, roll the API and verification worker, +and verify the canonical and compatibility routes before advertising live +lookup availability. Existing artifact records without an interface contract +remain valid and are not returned by LS-IDL lookup. diff --git a/docs/CELLSCRIPT_MOLECULE_IFRN_DESIGN_SPACE_IMPROVEMENT_REPORT.md b/docs/CELLSCRIPT_MOLECULE_IFRN_DESIGN_SPACE_IMPROVEMENT_REPORT.md index 1c8d4a8d..d1f9c241 100644 --- a/docs/CELLSCRIPT_MOLECULE_IFRN_DESIGN_SPACE_IMPROVEMENT_REPORT.md +++ b/docs/CELLSCRIPT_MOLECULE_IFRN_DESIGN_SPACE_IMPROVEMENT_REPORT.md @@ -78,7 +78,7 @@ a real six-contract Infern parity matrix. | v0-mvp packed layout is not a production ABI conclusion | `proposals/novaseal/v0-mvp-skeleton/docs/SCHEMA_LAYOUT.md:44-54` | | newer NovaSeal profiles mostly use whole-cell packed hashes | `proposals/novaseal/fungible-xudt-profile-v0/src/nova_fungible_xudt_lifecycle_type.cell:226-227`, `proposals/novaseal/btc-transaction-commitment-profile-v0/src/nova_btc_transaction_commitment_type.cell:361`, `proposals/novaseal/fiber-candidate-profile-v0/src/nova_fiber_candidate_type.cell:378` | | iCKB specs live under the benchmark test surface, not public examples | `tests/benchmarks/ickb_specs/README.md:3-9`, `tests/benchmarks/ickb_diff/claim_manifest.json:5-9`, `roadmap/CELLSCRIPT_ROADMAP.md:343`, `roadmap/CELLSCRIPT_ROADMAP_OVERVIEW.md:330` | -| 0.20 has an ELF entry ABI gate and the build-report linkage | `docs/releases/CELLSCRIPT_0_16_TO_0_20_RELEASE_NOTES.md`, `scripts/ckb_cellscript_acceptance.sh`, `scripts/validate_ckb_cellscript_production_evidence.py`, `docs/CELLSCRIPT_GATE_POLICY.md` | +| 0.20 has an ELF entry ABI gate and the build-report linkage | `docs/releases/CELLSCRIPT_0_16_TO_0_20_RELEASE_NOTES.md`, `scripts/ckb_cellscript_acceptance.sh`, `crates/cellscript-tools/src/production_evidence.rs`, `docs/CELLSCRIPT_GATE_POLICY.md` | | `cell_data_codec_manifest` is emitted and exposed to generated builders | `src/lib.rs`, `src/cli/commands.rs`, `tests/cli.rs`, `docs/releases/CELLSCRIPT_0_16_TO_0_20_RELEASE_NOTES.md` | | DOB-EVO is mainly a lock-hash / production-policy issue, not Molecule-only evidence | Captured in the retired 0.20 audit notes; current release claims must be tied to fresh devnet evidence. | diff --git a/docs/CELLSCRIPT_MYELIN_0_24_HANDOFF.md b/docs/CELLSCRIPT_MYELIN_0_24_HANDOFF.md new file mode 100644 index 00000000..5555bf9f --- /dev/null +++ b/docs/CELLSCRIPT_MYELIN_0_24_HANDOFF.md @@ -0,0 +1,69 @@ +# CellScript 0.24 Myelin Handoff + +**CellScript-side contract**: implemented + +**External Myelin lock adoption**: pending the final clean 0.24 release commit + +## Boundary + +Myelin consumes CellScript as an independently versioned compiler process. It +must not vendor the compiler or add it as a workspace member. Court-facing +compilation uses the `ckb` target and CKB-strict execution. Myelin's finite +session, committee, DA, finality, projection, and `MyelinExtended` semantics +remain Myelin-owned. + +CellScript therefore does not define `myelin`, `myelin_extended`, +`MyelinExtended`, `off-chain-session`, or an equivalent target profile. + +## Versioned Handoff Contract + +`integrations/myelin/cellscript-0.24-handoff-contract.json` freezes the +CellScript side of the transition: + +- Edition 2026 and the `ckb` target; +- metadata schemas `58/2/1/2`; +- `cellscript-entry-witness-v1` inside canonical + `cellscript-witnessargs-input-type-v2` placement; +- no raw-witness compatibility; +- lowering-record, source-map, checker, and checker-policy identities; +- exact compiler binary, source revision/tree, artifact, metadata, profile, + lowering-record, source-map, checker binary, and checker-policy bindings; +- the untrusted scheduler-template boundary; and +- no fallback reader or alias for the prior adapter identity. + +This is a repository-to-repository coordination contract, not a runtime asset +of the published `cellscript` crate. The crates.io package therefore excludes +both this file and its repository-only conformance test. + +The contract is intentionally marked `pending-external-release-pin`. An exact +40-hex source revision cannot truthfully identify uncommitted branch content. +After the 0.24 branch is cleanly committed, Myelin must update its own +toolchain lock, create fresh compiler and checker attestations, regenerate its +fixtures, and pass its production gate. CellScript does not rewrite or silently +adopt a dirty external Myelin worktree. + +## Scheduler Evidence + +CellScript access metadata is an untrusted template. Myelin must resolve final +conflict hashes from authenticated concrete Cells and a validated full Type +Script declaration. Binding names remain diagnostics. Scheduler plans remain +sidecars bound to raw transaction identity. + +The standalone artifact checker proves only that the declared access and +lowering evidence is structurally bound to the artifact. It does not prove +that Myelin resolved a conflict key correctly or that a committee finalized a +session. + +## Adoption Checklist + +1. Commit and identify the exact clean CellScript release revision. +2. Replace the Myelin toolchain lock in one explicit transition; do not add a + compatibility fallback. +3. Build and attest both `cellc` and `cellscript-artifact-checker` from that + revision with Rust 1.97.1. +4. Compile every Myelin fixture with target profile `ckb` and verify metadata, + compatibility profile, lowering record, source map, and artifact digests. +5. Confirm the lock rejects the former raw-witness-compatible identity and all + digest mismatches. +6. Run the Myelin adapter, static-committee, Tendermint, court, and production + gates. Skipped external workloads remain skipped, not passed. diff --git a/docs/CELLSCRIPT_PACKAGE_PROVENANCE_AND_DEPLOYMENT_IDENTITY.md b/docs/CELLSCRIPT_PACKAGE_PROVENANCE_AND_DEPLOYMENT_IDENTITY.md index 1f0dfc39..5bc7ad92 100644 --- a/docs/CELLSCRIPT_PACKAGE_PROVENANCE_AND_DEPLOYMENT_IDENTITY.md +++ b/docs/CELLSCRIPT_PACKAGE_PROVENANCE_AND_DEPLOYMENT_IDENTITY.md @@ -3,7 +3,10 @@ **Status**: implementation contract for the current CellScript CKB profile. Phase 1 landed in the 0.19 line; Phase 2 source-package, generated-builder, deployment identity, and trust-metadata checks extend through 0.20 and the -0.21 RC. +0.21 RC. The 0.23 line deploys the public read/write service and makes its +accepted package status the default CLI resolution authority. The 0.24 +development line adds compiler-independent structural admission for complete +CellScript CKB artifact bundles without changing source-package resolution. **Scope**: Source package registry, deployment registry, lockfile binding, and builder verification for CellScript on CKB @@ -16,6 +19,13 @@ protocol semantics, and v0.18 first-class ScriptRef / ScriptArgs work. **Production boundary ADR**: [`CELLSCRIPT_REGISTRY_PRODUCTION_BOUNDARY_ADR.md`](CELLSCRIPT_REGISTRY_PRODUCTION_BOUNDARY_ADR.md) +**0.23 production authority**: `https://api.registry.cellscript.dev` owns public +discovery and accepted status. The source repository and its `registry.json` +remain mandatory verification inputs after selection. References below to the +`cellscript-registry` Git discovery index describe the explicit +`CELLSCRIPT_REGISTRY_URL` offline/private-mirror path unless a historical phase +is being discussed; they are not an automatic production fallback. + ## Motivation For ordinary development, a package registry can look like crates.io or npm: @@ -76,51 +86,61 @@ Resolution is profile-specific. No resolver may coerce one profile into another. ``` -For backward compatibility, a registry record without an explicit future -profile is interpreted by current `cellc` commands as a CellScript source -package candidate and must still satisfy the existing `Cell.toml`, -`registry.json`, source-hash, build-identity, and deployment-identity checks. -A future registry proxy or discovery index may expose multiple profiles for the -same `namespace/name`, but the lockfile must record which profile was selected. +The 0.24 verified-artifact boundary is an additional admission contract, not a +new dependency profile. A CKB ELF build binds `artifact`, `metadata`, +`lowering_record`, and `source_map`. Registry bundles that opt into this +boundary must provide the complete verified sidecar set; the least-privilege +artifact worker runs the standalone checker and records +`structurally_verified` evidence. Generic source/executable/ABI bundles remain +`hash_bound`, and neither result proves deployment, chain acceptance, or a +security audit. + +Edition 2026 does not infer a missing compatibility profile. It identifies +source semantics only. Current CellScript source packages must declare +`edition = "2026"`, while registry, lockfile, deployment, and builder records +bind the resolved profile hash across the independent target, primitive, +metadata-schema, and entry/witness ABI axes. A future registry proxy or +discovery index may expose multiple profiles for the same `namespace/name`, +but the selected profile must remain explicit. ## Publisher Identity Model -CellScript Registry uses a **JoyID-rooted publisher identity** without a -separate registry account system. The current accepted publisher principal type -is `joyid_ckb`; ordinary publish operations use a delegated local credential: +CellScript Registry uses a **wallet-rooted publisher identity** without a +separate registry account system. It accepts JoyID and standard recoverable CKB +secp256k1 message-signing principals; ordinary publish operations use a +delegated local credential: ```text -principal_type = joyid_ckb -principal_id = +principal_type = joyid_ckb | ckb_secp256k1 +principal_id = -JoyID identity +CKB wallet identity -> root publisher principal -> authorises local publisher credential -> credential signs scoped registry requests ``` -The data model stays principal-typed instead of hard-coding product policy into -every record. The current registry policy accepts only `joyid_ckb`, while the -record shape still names the principal type and concrete principal id. - -The preferred `principal_id` is derived from the JoyID signer key as a -normalized JoyID-CKB identity binding. The registry verifies that the JoyID -signature's key type and public key match the `principal_id` in capability and -revocation payloads; display addresses are presentation data only. +The data model stays principal-typed instead of hard-coding wallet-product +policy into every record. `principal_id` is derived from the signer key, and +the Registry verifies that signature scheme, key type, recovered or supplied +public key, and principal binding agree. Display addresses are presentation +data only. -The intended interactive flow is: +The preferred interactive flow is: ```text -cellc auth capability create --principal-id --scope publish:namespace/package --expires 90d --json > capability-payload.json - -> CLI creates a registry signing key and stores it in the OS keychain - -> CLI prints an authorize_capability payload with capability_pubkey and requested scopes - -> browser/CCC/JoyID signs that exact payload -cellc auth capability submit --payload capability-payload.json --joyid-signature joyid-signature.json - -> signed payload is submitted to the registry write API - -> registry records principal_type, principal_id, scopes, expiry, and revocation status +cellc publish --authorise + -> CLI creates a P-256 publishing key as pending in the OS keychain + -> CLI opens a 15-minute exact-coordinate browser session + -> browser/CCC wallet signs the Registry-built challenge + -> Registry atomically registers the key, claims/reviews the namespace, + completes the session, and records audit events + -> CLI activates only the matching returned key ID and resumes publishing ``` -Daily publishing then avoids JoyID signing prompts and never exposes root +The explicit `auth capability create/submit` plus `auth namespace claim` +commands remain the CI, recovery, and external-wallet path. Daily publishing +then avoids wallet signing prompts and never exposes root publisher authority to CI: ```text @@ -130,16 +150,19 @@ cellc publish -> registry accepts the entry and returns its canonical URL ``` -The JoyID authorisation payload must bind the local capability key: +The wallet authorisation payload must bind the local capability key: ```text protocol: cellscript-registry-auth-v1 action: authorize_capability registry_origin: https://api.registry.cellscript.dev -principal_type: joyid_ckb -principal_id: +principal_type: joyid_ckb | ckb_secp256k1 +principal_id: capability_pubkey: ... -requested_scopes: [publish:cellscript/amm_pool] +requested_scopes: + - publish:cellscript/amm_pool + - deployment:cellscript/amm_pool + - availability:cellscript/amm_pool capability_expires_at: ... nonce: ... issued_at: ... @@ -169,18 +192,23 @@ package -> maintainer principals credential -> scoped permissions ``` -Example scopes: +Current write scopes: ```text publish:cellscript/amm_pool -yank:cellscript/amm_pool -attest:cellscript/amm_pool -manage-maintainers:cellscript/* +deployment:cellscript/amm_pool +availability:cellscript/amm_pool +publish:cellscript/* ``` -This keeps the user-facing identity simple — "my JoyID is my CellScript -publisher identity" — while the engineering surface remains revocable, scoped, -CI-safe, and auditable. +The actions are independent. `publish` admits an immutable release, +`deployment` attaches chain-checked CKB deployment evidence, and +`availability` deprecates, yanks, or restores a release. Namespace wildcards +are accepted, but granting one action never grants another. + +This keeps the user-facing identity simple — "my connected CKB wallet is my +CellScript publisher identity" — while the engineering surface remains +revocable, scoped, CI-safe, and auditable. ## Three-Layer Identity Model @@ -197,7 +225,7 @@ verification scope: │ Build Identity │ │ compiler_version / metadata_schema / schema_hash / │ │ abi_hash / artifact_hash / constraints_hash │ -│ Carrier: Cell.lock [package.build] │ +│ Carrier: Cell.lock [package_build] │ │ Verified: build time │ ├─────────────────────────────────────────────────────────────┤ │ Deployment Identity │ @@ -235,10 +263,11 @@ compiler version, and possibly type-id lineage. A deployment-bound package is what wallets and production builders should rely on when constructing real transactions. -**On-chain-attested package.** A deployment claim has an explicit JoyID-rooted -attestation or chain-indexed record. This is a stronger statement about who made -the deployment claim, but it still does not replace source, build, deployment, -and live-chain verification. +**On-chain-committed package.** A sufficiently confirmed live mainnet Cell +commits the exact Registry release/deployment tuple under the configured +Registry Type Script and custody Lock. This is a current discoverability and +integrity statement, not an attestation of source quality or authorship. It does +not replace source, build, deployment, and live-chain verification. **Deprecated, yanked, or quarantined package.** Historical entries remain addressable for reproducibility, but default search and recommendation surfaces @@ -246,10 +275,13 @@ may suppress them. Quarantine is for abuse or high-risk packages; yanking is a maintainer action that preserves exact-pin warning metadata. The same source package version may have zero, one, or many deployment -bindings. For example, `amm@1.2.0` may start as a source-only package, later -gain a CKB testnet deployment, then eventually a CKB mainnet deployment. These -are separate deployment records attached to the same source/package identity, -not separate source packages. +bindings. For example, `amm@1.2.0` may start as a source-only package and later +gain one or more CKB mainnet deployment bindings. The production Registry +accepts only mainnet deployment evidence. The isolated Pudge Registry accepts +only testnet evidence under separate origins, storage, signing state, wallet +state, RPC identity, and retention policy. These are separate deployment +records attached to the same source/package identity, not separate source +packages. ``` amm@1.2.0 @@ -380,8 +412,10 @@ through the `namespace` field: |---|---| | `token = "0.3.0"` | Auto-resolve: search discovery index for `token`; if ambiguous, default to the consuming package's namespace | | `token = { version = "0.3.0", namespace = "cellscript" }` | Explicit: look up `cellscript/token` in discovery index | +| `local_token = { package = "token", version = "0.3.0", namespace = "cellscript" }` | Resolve declared package `token` under local alias `local_token` | | `token = { version = "0.3.0", path = "../token" }` | Local path, bypasses registry | | `token = { version = "0.3.0", git = "...", tag = "v0.3.0" }` | Git clone, bypasses registry | +| `token = { package = "token", version = "^0.3.0", resolver = "vendor" }` | Invoke a declared bounded resolver only during explicit lock/update, then normalize to Registry or exact Git source | The resolution priority is: `path` > `git` > `registry`. If `path` or `git` is specified, the dependency is resolved locally and the `namespace` field @@ -397,25 +431,30 @@ Source code references types via their full module path (e.g., should be), not deployment *facts* (which specific out_point was deployed to). Intents are determined at compile time; facts are determined after deployment. -### Cell.lock — Build Identity Lock (Extended) +### Cell.lock — Graph And Build Identity Lock -The existing `Cell.lock` records dependency versions and sources. The registry -extension adds build identity hashes, deployment references, and enriches the -registry source type with git provenance. +`Cell.lock` v3 separates mutable resolution from compilation. It records the +root manifest digest, canonical dependency nodes and outgoing alias edges, +runtime/test and environment roots, exact source/content identity, build +identity hashes, and deployment references. **Lockfile schema**: ```toml -version = 1 +version = 3 +schema = "cellscript-lock-v0.24-graph-v1" [package] +edition = "2026" name = "amm_pool" version = "1.2.0" namespace = "cellscript" source_hash = "blake2b:0xabcd..." -[package.build] -compiler_version = "0.21.0" +[package_build] +edition = "2026" +compatibility_profile_hash = "blake2b:0xprofile..." +compiler_version = "0.24.0" target_profile = "ckb" artifact_hash = "blake2b:0x1234..." metadata_hash = "blake2b:0x5678..." @@ -423,25 +462,25 @@ schema_hash = "blake2b:0x9abc..." abi_hash = "blake2b:0xdef0..." constraints_hash = "blake2b:0x1111..." -# Registry dependency — resolved from discovery index -[dependencies.token] -version = "0.3.0" -namespace = "cellscript" -source = { registry = "cellscript/token", url = "https://github.com/cellscript/token", revision = "a1b2c3d4..." } -source_hash = "blake2b:0x2222..." -build = { artifact_hash = "blake2b:0x3333...", abi_hash = "blake2b:0x4444..." } +[root] +manifest_digest = "sha256:..." -# Path dependency (unchanged) -[dependencies.helper] -version = "0.1.0" -source = { path = "../helper" } -source_hash = "blake2b:0x5555..." +[root.dependencies] +token = "token@0.3.0|registry:...|env=default|features=default" + +[root.dev_dependencies] +test_helper = "test_helper@0.1.0|path:...|env=default|features=default" + +# Each entry under [dependencies] is keyed by canonical node ID and records: +# name, namespace, version, exact Path/Git/Registry source, source_hash, +# manifest_digest, outgoing alias-to-node dependencies, and optional build facts. + +[environments.mainnet] +chain_id = "ckb" +genesis_hash = "0x..." -# Git dependency (unchanged) -[dependencies.legacy] -version = "1.0.0" -source = { git = "https://github.com/other/legacy", revision = "e5f6g7h8..." } -source_hash = "blake2b:0x6666..." +[environments.mainnet.dependencies] +token = "token@0.3.0|registry:...|env=mainnet|features=default" [deployment.ckb.aggron4] status = "deployed" @@ -466,11 +505,11 @@ discovery index: | `revision` | Exact git commit hash | Phase 1 | | `version` | Package version string | Phase 1 (existing) | -The `url` and `revision` fields make the lockfile self-sufficient for -re-verification: `cellc package verify` can clone the exact source commit -without re-querying the discovery index. This is analogous to how `go.sum` -records the exact module version and hash, making the `go.mod` file -independently verifiable. +The `url` and `revision` fields make the lockfile self-sufficient for exact +materialization without re-querying discovery or selecting versions. Public +Registry revisions are snapshot SHA-256 identities; Git revisions are full +40-hex commits. Whole-tree and manifest digests are verified after +materialization. The existing `LockedSource::Path { path }` and `LockedSource::Git { url, revision }` are unchanged. @@ -493,9 +532,12 @@ checks that it matches the actual `Deployed.toml` entry; if absent, the verification step is skipped with a warning. Future phases may require `record_hash` for production packages. -**Backward compatibility**: The lockfile uses a single version 1 schema. -The `[package.build]` and `[deployment.*]` sections are optional; their absence -simply means the package has not been built or deployed yet. +**No implicit backward compatibility**: readers accept only lockfile version 3 +and schema `cellscript-lock-v0.24-graph-v1`. Explicit `cellc lock`/`update` may +replace a version 1 or 2 lock; build/check/test never migrate or repin it. +`[package]` is required. When `[package_build]` exists, both `edition` and +`compatibility_profile_hash` are required fields; readers do not infer them. +The `[deployment.*]` sections may remain absent until a deployment exists. **Key invariants**: @@ -510,22 +552,23 @@ simply means the package has not been built or deployed yet. ### Deployed.toml — Deployment Fact Record (New) `Deployed.toml` is the CKB analogue of Move/Sui's `Published.toml`. It is -automatically generated by the deployment tool after the on-chain transaction is -confirmed, and records immutable deployment facts derived from the chain. +generated from locally verified deployment evidence after the externally signed +transaction is confirmed, and records immutable deployment facts derived from +the chain. #### Who Generates and Manages Deployed.toml -`Deployed.toml` is generated by the CellScript deployment tool (`cellscript-deploy` -or the adapter crate's `CellScriptAdapter::deploy_artifact()` API). It is not -hand-authored. +`Deployed.toml` must be generated by deployment orchestration after wallet +signing, broadcast, commitment, and live-output verification. The current +`cellscript-deploy build-deploy` command only builds an unsigned transaction; +it does not claim to generate a committed deployment record. -The generation path is trust-free by construction: the existing adapter crate -architecture is headless-first, meaning all deployment facts are computed -locally before the transaction is submitted, and the only chain-derived value -needed after submission is the `tx_hash`. +The adapter architecture is headless-first: artifact and transaction facts are +computed locally before signing. Chain identity, input liveness, the committed +transaction, and the resulting live output still have to be verified against +RPC; a returned `tx_hash` alone is not sufficient chain evidence. -**Generation flow** (matches existing `deploy_artifact` → `build_deploy_transaction` -→ `build_deployment_manifest_from_evidence` pipeline): +**Generation flow**: ``` 1. cellc build @@ -533,59 +576,63 @@ needed after submission is the `tx_hash`. → all build hashes computed locally (artifact_hash, metadata_hash, schema_hash, abi_hash, constraints_hash) -2. build_deploy_transaction(spec) - → headless: computes TYPE_ID args, data_hash, code_hash, +2. resolve live input + build_deploy_transaction(spec) + → verifies mainnet genesis and a live pure-capacity input + → headless builder computes TYPE_ID args or immutable data1 hash, code_hash, occupied capacity, change output locally → returns (TransactionView, ResolvedDeployEvidence) → evidence already contains: code_hash, hash_type, type_id_args, artifact_hash, occupied_capacity, tx_size -3. submit + wait_for_commitment - → sends transaction through full node RPC +3. external wallet signing + submit + wait_for_commitment + → wallet replaces the standard zeroed secp witness placeholder + → sends the signed transaction through full node RPC → waits for committed status → receives tx_hash from the node response -4. build_deployment_manifest_from_evidence(evidence, tx_hash, output_index) - → constructs DeploymentManifest from locally-computed evidence + tx_hash - → no get_transaction call needed: all hash fields already known +4. verify committed transaction + live output + → re-reads the transaction and code Cell from mainnet RPC + → checks output index, lock, optional Type Script, artifact bytes, and data hash + +5. build_deployment_manifest_from_evidence(evidence, tx_hash, output_index) + → constructs DeploymentManifest only after the chain checks succeed → extends to Deployed.toml by adding network, chain_id, build section, and Cell.lock record_hash ``` -**Why no `get_transaction` / on-chain re-derivation is needed**: The existing -adapter crate's `build_deploy_transaction` already computes `data_hash = -blake2b(artifact_binary)` locally (line 447 of `lib.rs`). The -`ResolvedDeployEvidence` already carries `code_hash`, `hash_type`, and -`type_id_args`. The only chain-derived value is `tx_hash`, which is returned -by `send_transaction`. The full node RPC is used for submission and commitment -waiting, not for re-deriving fields that the tool already knows. - -**Verification path**: 0.19 Phase 1 verification is off-chain and checks that -`Deployed.toml` matches the package/build identity recorded in `Cell.lock`. -0.20 adds live-chain verification where `cellc registry verify --live` (or an -equivalent mode) calls `get_live_cell` to confirm that the on-chain code cell's -data matches `data_hash` in `Deployed.toml`. This separation keeps the trust -model clean: Phase 1 generation/verification is self-contained, while live -chain proof is independently reproducible when RPC is available. - -**Data source requirement**: 0.19 Phase 1 registry acceptance does not require -a CKB full node RPC endpoint. Transaction submission, commitment waiting, and -`get_live_cell` verification are 0.20 live-chain concerns. Light client support -is a possible later enhancement. +**Why committed-output verification is required**: local construction proves +what the tool intended to build, not what a wallet ultimately signed or what +the chain committed. `get_transaction` and `get_live_cell` close that gap and +make the deployment record independently checkable. + +**Verification path**: `cellc registry verify` checks that `Deployed.toml` +matches the package/build identity recorded in `Cell.lock`; `cellc registry +verify --live --rpc-url ` additionally calls `get_live_cell` and verifies +the referenced live code Cell. Deployment orchestration must run the live mode +before treating a newly generated record as chain evidence. + +**Data source requirement**: off-chain registry verification does not require a +CKB RPC endpoint. Mainnet deployment construction, commitment evidence, and +live-chain verification do require one. Light-client support remains a possible +later enhancement. **Immutability**: Once generated, `Deployed.toml` must not be modified. Any re-deployment or upgrade produces a new `[[deployments]]` entry with a distinct set of chain facts, not an edit to an existing entry. ```toml -version = 1 +version = 2 +schema = "cellscript-deployed-v0.23-edition-2026" [package] +edition = "2026" name = "amm_pool" version = "1.2.0" source_hash = "blake2b:0xabcd..." [build] +edition = "2026" +compatibility_profile_hash = "blake2b:0xprofile..." compiler_version = "0.21.0" artifact_hash = "blake2b:0x1234..." metadata_hash = "blake2b:0x5678..." @@ -594,8 +641,10 @@ abi_hash = "blake2b:0xdef0..." constraints_hash = "blake2b:0x1111..." [[deployments]] -network = "aggron4" -chain_id = "ckb-testnet" +edition = "2026" +compatibility_profile_hash = "blake2b:0xprofile..." +network = "mainnet" +chain_id = "ckb-mainnet" script_role = "type" tx_hash = "0xaaaa..." output_index = 0 @@ -604,7 +653,6 @@ hash_type = "data1" dep_type = "code" out_point = "0xaaaa...:0" data_hash = "0xcccc..." -type_id = "0xdddd..." [[deployments.cell_deps]] name = "secp256k1" @@ -614,6 +662,8 @@ dep_type = "dep_group" hash_type = "type" [[deployments]] +edition = "2026" +compatibility_profile_hash = "blake2b:0xprofile..." network = "ckb-mainnet" chain_id = "ckb-mainnet" script_role = "type" @@ -632,10 +682,11 @@ status = "candidate" - `status` — deployment lifecycle state - The full `[build]` section — binding the deployment to build identity -The adapter crate's `load_deployment_manifest` / -`parse_deployment_manifest` functions should be extended to support the new -schema while maintaining backward compatibility with the existing -`cellscript-ckb-deployment-manifest-v0.19` schema. +The adapter crate's `DeploymentManifest` is a separate transaction-adapter +configuration format. Package `Deployed.toml` readers accept only version 2 +with schema `cellscript-deployed-v0.23-edition-2026`; they do not reinterpret +the adapter's historical `cellscript-ckb-deployment-manifest-v0.19` identity as +a package deployment record. ## End-to-End Package Lifecycle @@ -687,26 +738,35 @@ token = { version = "0.3.0", namespace = "cellscript" } Running `cellc build` triggers dependency resolution: 1. Read `Cell.toml` `[dependencies]` → find `token` with `namespace = "cellscript"`. -2. Query the discovery index (`cellscript-registry` Git repo) → - `cellscript/token.json` → - `source = "https://github.com/cellscript/token"`. -3. Clone the source repo, find the latest `0.3.x` tag (e.g., `v0.3.2`). -4. Read `registry.json` from the cloned repo → verify `source_hash` matches. -5. Parse the dependency's `Cell.toml` → resolve transitive dependencies. -6. Write `Cell.lock` with resolved versions and git provenance. +2. Query `https://api.registry.cellscript.dev/v1/artifacts/cellscript/token`. +3. Require the `cellscript_source` profile and `dependency` consumption mode, + then select an eligible verified release. +4. Download its immutable source snapshot from the static Registry origin. +5. Verify the snapshot object identity, package coordinate, file hashes, + Edition, compatibility-profile identity, and whole-tree source hash. +6. Materialize the verified source into the dependency cache. +7. Parse the dependency's `Cell.toml` → resolve transitive dependencies. +8. Write `Cell.lock` with resolved versions and git provenance. + +`CELLSCRIPT_REGISTRY_URL` deliberately selects the legacy Git/offline discovery +authority for private mirrors, tests, and audits. It is not an automatic +fallback when the production API is unavailable. Generated `Cell.lock`: ```toml -version = 1 +version = 2 [package] +edition = "2026" name = "amm_pool" version = "0.1.0" namespace = "cellscript" source_hash = "blake2b:0xabcd..." -[package.build] +[package_build] +edition = "2026" +compatibility_profile_hash = "blake2b:0xprofile..." compiler_version = "0.21.0" target_profile = "ckb" artifact_hash = "blake2b:0x1234..." @@ -718,28 +778,30 @@ constraints_hash = "blake2b:0x1111..." [dependencies.token] version = "0.3.2" namespace = "cellscript" -source = { registry = "cellscript/token", url = "https://github.com/cellscript/token", revision = "f7e8d9c0..." } +source = { registry = "cellscript/token", url = "https://registry.cellscript.dev/source-snapshots/cellscript/token/0.3.2/.json", revision = "sha256:" } source_hash = "blake2b:0x2222..." build = { artifact_hash = "blake2b:0x3333...", abi_hash = "blake2b:0x4444..." } ``` -Key property: `Cell.lock` is **self-sufficient** for re-verification. The `url` -and `revision` fields allow `cellc package verify` to re-clone the exact -source commit without re-querying the discovery index. +Key property: `Cell.lock` is **self-sufficient** for re-verification. For the +public Registry, `url` names the immutable source snapshot and `revision` is its +`sha256:` identity. Explicit Git/offline resolution retains a Git URL and +commit revision. Neither path needs to re-query a mutable discovery index to +identify the already locked bytes. ### Stage 3: Publishing The developer publishes a new version: ```bash -cellc auth capability create --principal-id --scope publish:cellscript/amm_pool --expires 90d --json > capability-payload.json -cellc auth capability submit --payload capability-payload.json --joyid-signature joyid-signature.json -cellc publish +cellc publish --authorise # first interactive publish +cellc publish # later publishes with the active delegated key ``` This automatically: -1. Registers a JoyID-authorised delegated capability key with the write API. +1. For `--authorise`, registers a wallet-authorised delegated capability key + and claims or reviews the namespace through the short-lived browser session. 2. Reads `Cell.toml` -> gets `name`, `namespace`, `version`. 3. Computes `source_hash` from the current source tree. 4. Reads build artifacts for `artifact_hash`, `abi_hash`, `schema_hash`, etc. @@ -751,11 +813,11 @@ This automatically: 8. Creates a canonical registry entry in `source_published` or `indexed_pending` state. -Capability revocation is also JoyID-bound: +Capability revocation is also wallet-bound: ```bash cellc auth capability revoke --principal-id --capability-key-id --json > revoke-payload.json -cellc auth capability revoke --payload revoke-payload.json --joyid-signature joyid-signature.json --reason "rotate delegated key" +cellc auth capability revoke --payload revoke-payload.json --wallet-signature wallet-signature.json --reason "rotate delegated key" ``` The explicit signing flow is: @@ -778,7 +840,7 @@ for audit, offline fixtures, and direct-Git fallback: "version": "1.2.0", "tag": "v1.2.0", "source_hash": "blake2b:0xabcd...", - "cellscript_version": "0.19.0", + "cellscript_version": "0.24.0", "dependencies": { "token": { "namespace": "cellscript", "version": "0.3.0" } }, @@ -802,7 +864,7 @@ git tag v1.2.0 git push --tags ``` -No separate registry account is needed. The JoyID-rooted publisher identity +No separate registry account is needed. The wallet-rooted publisher identity authorises the local credential, and the registry ACL decides whether that credential may publish to the namespace/package. No PR to the `cellscript-registry` discovery index is needed for ordinary version updates; @@ -811,33 +873,45 @@ ownership metadata. ### Stage 4: Deploying -The developer deploys to CKB testnet: +The current adapter CLI builds a mainnet transaction candidate for external +wallet signing: ```bash -cellc deploy --network aggron4 +cellscript-deploy --rpc --json build-deploy \ + --artifact \ + --lock-arg \ + --hash-type data1 \ + --capacity-out-point 0x: ``` -This triggers the existing headless deployment pipeline: +This triggers the implemented construction boundary: 1. `cellc build` → produces artifact, metadata, constraints, schema, ABI. -2. `build_deploy_transaction(spec)` → computes all deployment facts locally - (data_hash, code_hash, TYPE_ID args, capacity). -3. Submit + wait for commitment → receives `tx_hash`. -4. `build_deployment_manifest_from_evidence(evidence, tx_hash, output_index)` → - generates `Deployed.toml`. -5. Update `Cell.lock` `[deployment.ckb.aggron4]` section. +2. The CLI verifies mainnet genesis and the selected live pure-capacity Cell. +3. `build_deploy_transaction(spec)` computes deployment facts locally and emits + `can_submit: false` with the unsigned transaction. +4. A wallet signs and broadcasts the transaction. +5. Deployment orchestration waits for commitment, verifies the live output, + then calls `build_deployment_manifest_from_evidence` and updates `Cell.lock`. + +Steps 4–5 are external orchestration today; the CLI does not claim that an +unsigned build is a deployment or automatically write `Deployed.toml`. Generated `Deployed.toml`: ```toml -version = 1 +version = 2 +schema = "cellscript-deployed-v0.23-edition-2026" [package] +edition = "2026" name = "amm_pool" version = "1.2.0" source_hash = "blake2b:0xabcd..." [build] +edition = "2026" +compatibility_profile_hash = "blake2b:0xprofile..." compiler_version = "0.21.0" artifact_hash = "blake2b:0x1234..." metadata_hash = "blake2b:0x5678..." @@ -846,6 +920,8 @@ abi_hash = "blake2b:0xdef0..." constraints_hash = "blake2b:0x1111..." [[deployments]] +edition = "2026" +compatibility_profile_hash = "blake2b:0xprofile..." network = "aggron4" chain_id = "ckb-testnet" script_role = "type" @@ -862,9 +938,9 @@ type_id = "0xdddd..." Updated `Cell.lock` deployment section: ```toml -[deployment.ckb.aggron4] +[deployment.ckb.mainnet] status = "deployed" -record = "ckb-testnet:0xaaaa..." +record = "ckb-mainnet:0xaaaa..." record_hash = "blake2b:0x9a9a..." ``` @@ -880,11 +956,13 @@ amm = { version = "1.2.0", namespace = "cellscript" } Resolution flow: -1. Query discovery index → `cellscript/amm_pool.json` → - `source = "https://github.com/cellscript/amm_pool"`. -2. Clone at tag `v1.2.0` → read `registry.json` → verify `source_hash`. +1. Query the public Registry API and require an accepted `cellscript/amm_pool` + version with a source repository, tag, source hash, Edition, and profile + identity. +2. Clone at the accepted tag `v1.2.0` → read `registry.json` → match the + accepted identity → verify `source_hash`. 3. Read the dependency's `Cell.lock` (if present) → - find deployment record for `aggron4` → + find deployment record for `mainnet` → `code_hash`, `out_point`, `data_hash` available for builder verification. 4. Write the consumer's `Cell.lock` with resolved versions and git provenance. @@ -921,18 +999,20 @@ source → build → deployment, all bound by cryptographic hashes in └────────────────────────┘ - Discovery Index Source Repository - (cellscript-registry) (github.com/cellscript/amm_pool) + Public Registry API Source Repository + (accepted status) (github.com/cellscript/amm_pool) ┌─────────────────┐ ┌──────────────────────────────────┐ - │ cellscript/ │ │ Cell.toml │ - │ amm_pool.json │──────►│ registry.json ← cellc publish --offline mirror │ - │ token.json │ │ src/ │ + │ /v1/artifacts/ │ │ Cell.toml │ + │ cellscript/ │──────►│ registry.json ← offline mirror │ + │ amm_pool │ │ src/ │ └─────────────────┘ │ Cell.lock ← cellc build │ │ Deployed.toml ← cellc deploy │ └──────────────────────────────────┘ ``` -The discovery index maps `namespace/name` → source repository URL. +The public Registry maps `namespace/name` → accepted version/status and source +repository identity. The legacy Git discovery index can supply the equivalent +source map only when explicitly selected for offline/private-mirror use. The source repository contains everything else: source code, version index (`registry.json`), build identity (`Cell.lock`), and deployment facts (`Deployed.toml`). The public registry service is the write authority for @@ -1014,9 +1094,10 @@ cache-friendly read surface. The data model remains inspired by Go's approach (source lives in its own repo, metadata can travel with the source), but the public write authority is the registry service, not Git push access. -1. **Discovery index** — a lightweight map from `namespace/name` to the source - repository URL and ownership metadata. Updated when a package is claimed, - transferred, or its source location changes. +1. **Public package index** — the deployed API maps `namespace/name` to public + versions, accepted/suppressive status, source repository identity, Edition, + profile hash, and evidence. It is updated by authenticated namespace, + publish, governance, and promotion operations. 2. **Per-package version index** — a canonical registry entry mirrored as `registry.json` for audit, offline fixtures, and direct-Git fallback. The public entry is updated by authenticated `cellc publish`; the local mirror is @@ -1034,10 +1115,12 @@ Rationale: - The CKB ecosystem can start with a small write service because expensive verification work is asynchronous and bounded. -### Discovery Index Repository +### Legacy/Offline Discovery Index Repository -A single Git repository (e.g., `github.com/cellscript/cellscript-registry`) -serves as the discovery index. It is organized by namespace: +A Git repository (e.g., `github.com/cellscript/cellscript-registry`) can serve +as the explicit `CELLSCRIPT_REGISTRY_URL` private/offline discovery authority. +It is not consulted automatically after a failed production API lookup. It is +organized by namespace: ``` cellscript-registry/ @@ -1081,6 +1164,8 @@ alongside `Cell.toml`, for audit and offline use: "tag": "v1.2.0", "source_hash": "blake2b:0xabcd...", "cellscript_version": "0.19.0", + "edition": "2026", + "compatibility_profile_hash": "42d297cd7879917ade58c89cdc5dcbbb38a5d39b720788387db80e918a3f7fd9", "dependencies": { "token": { "namespace": "cellscript", "version": "0.3.0" } }, @@ -1088,6 +1173,7 @@ alongside `Cell.toml`, for audit and offline use: "schema_hash": "blake2b:0x9abc...", "license": "MIT", "released_at": "2026-04-24T00:00:00Z", + "status": "source_published", "yanked": false, "audit": { "report_hash": "blake2b:0x5555...", @@ -1098,6 +1184,14 @@ alongside `Cell.toml`, for audit and offline use: } ``` +This is the registry's initial source-edition/profile shape. `edition` must not +be used to infer a target or ABI; `compatibility_profile_hash` binds those +independent choices. The production Registry deployed this initial schema on +2026-07-31. `migrations/0001_initial.sql` is therefore frozen; later database +changes use additive numbered migrations rather than rewriting the deployed +baseline. Every non-optional field shown above is required; readers do not fill +in omitted `dependencies`, `status`, or `yanked` values. + The `tag` field maps each version to a git tag in the source repository. This allows `cellc install` to clone the exact commit without needing a separate archive storage layer. @@ -1105,11 +1199,10 @@ a separate archive storage layer. ### Publishing Flow ```bash -# First use, or after credential expiry/revocation -cellc auth capability create --principal-id --scope publish:cellscript/amm_pool --expires 90d --json > capability-payload.json -cellc auth capability submit --payload capability-payload.json --joyid-signature joyid-signature.json +# Interactive first use, or after credential expiry/revocation +cellc publish --authorise -# Publish a new version to the registry +# Later publish with an active delegated key cellc publish # → reads Cell.toml # → computes source_hash from current source tree @@ -1127,12 +1220,11 @@ git push --tags ``` No PR to an external registry repository is required for ordinary version -updates. The registry entry is authoritative for public discovery, while the -source repository mirror helps consumers audit and reproduce the same metadata -when `cellc install` clones a tagged version. - -The discovery index only changes when claiming a brand-new package, changing -source location, or changing ownership metadata. +updates. The production Registry entry is authoritative for public discovery +and status, while the source repository mirror lets consumers audit the same +identity when `cellc install` clones the accepted tag. The legacy Git discovery +index remains an explicit offline/private-mirror override rather than an +ordinary production dependency. Initial entry visibility is staged: @@ -1141,7 +1233,7 @@ source_published -> direct URL and author dashboard visible indexed_pending -> waiting for asynchronous verifier/indexer workers verified_build -> build evidence accepted deployed -> deployment facts attached and verified locally -on_chain_attested -> feature-gated JoyID/chain-backed deployment attestation +on_chain_committed -> sufficiently confirmed live Registry commitment Cell deprecated/yanked -> historical entry retained, default resolution suppressed quarantined -> direct URL retained, default search suppressed ``` @@ -1153,9 +1245,9 @@ requires a stronger explicit flag such as `--allow-quarantined`. Default search, recommendations, and production-visible package lists only include entries that passed the required baseline checks. -A mirrored `registry.json` version entry with no `status` is treated as -`source_published`, not as verified. Public registry writes must emit an -explicit status; legacy mirrors need explicit risk flags before direct install. +A mirrored `registry.json` version entry with no `status`, `dependencies`, or +`yanked` field is malformed. Public registry writes and offline mirrors emit +the same complete entry shape. ### Installation Flow @@ -1166,12 +1258,18 @@ cellc install cellscript/amm@1.2.0 Internally: -1. Clone or update the `cellscript-registry` discovery index (cached locally). -2. Look up `cellscript/amm.json` → get source repository URL. -3. Clone the source repository at tag `v1.2.0`. -4. Read `registry.json` from the cloned repository. -5. Verify `source_hash` matches the current source tree. -6. Parse `Cell.toml` and resolve transitive dependencies. +1. Query the production public API for `cellscript/amm`. +2. Select version `1.2.0` only if its public status is accepted for ordinary + resolution; suppressive and pre-verification states fail closed. +3. Read the immutable source-snapshot descriptor, source hash, Edition, and + profile hash from the accepted record. +4. Download the snapshot without redirects and enforce its declared size. +5. Verify the object SHA-256, safe/unique file paths, and every file's BLAKE2b. +6. Atomically materialize the tree and verify the complete `source_hash`. +7. Parse `Cell.toml`, check package identity, and resolve transitive + dependencies. Repository URL, tag, and mirrored `registry.json` remain audit + material; they are used as the resolver authority only under the explicit + Git/offline override. ### Write Path DDoS and Spam Boundary @@ -1185,10 +1283,9 @@ registry.cellscript.dev -> immutable mirrored metadata / artifact URLs api.registry.cellscript.dev - -> WAF / edge limits + -> TLS proxy body limits -> schema fail-fast - -> auth and ACL checks - -> quota and deduplication + -> auth, ACL, application quota and deduplication -> object storage -> bounded verification queues ``` @@ -1201,7 +1298,7 @@ Synchronous publish checks must remain cheap: - manifest/schema validation; - `source_hash` / `manifest_hash` sanity and duplicate-hash rejection; - idempotency keys for retry-safe publishes; -- per IP, ASN, JoyID principal, credential, namespace, and package quotas. +- per IP, ASN, wallet principal, credential, namespace, and package quotas. Expensive work is asynchronous: @@ -1212,7 +1309,7 @@ Expensive work is asynchronous: - chain RPC reads; - search indexing and ranking. -JoyID signatures are identity evidence, not an anti-spam mechanism by +Wallet signatures are identity evidence, not an anti-spam mechanism by themselves. New namespace claims, high-volume publishing, typosquatting-risk names, and on-chain deployment attestations may require cooldown, review, or community challenge. The first production source-package write path does not @@ -1224,12 +1321,17 @@ deleted, so exact pins and incident reviews remain reproducible. ### CLI Integration ```bash -# Authorise a local publisher credential with JoyID-rooted identity -cellc auth capability create --principal-id --scope publish:cellscript/amm --expires 90d --json > capability-payload.json -cellc auth capability submit --payload capability-payload.json --joyid-signature joyid-signature.json - -# Publish a new version to the registry -cellc publish +# Manual/CI authorisation path for either supported principal type +cellc auth capability create --principal-type --principal-id \ + --scope publish:cellscript/amm \ + --scope deployment:cellscript/amm \ + --scope availability:cellscript/amm \ + --expires 90d --json > capability-payload.json +cellc auth capability submit --payload capability-payload.json --wallet-signature wallet-signature.json +cellc auth namespace claim --namespace cellscript --payload capability-payload.json --wallet-signature wallet-signature.json + +# Or use the short interactive path, which resumes the publish automatically +cellc publish --authorise # Optional local/offline discovery mirror cellc registry add --namespace cellscript --name amm --source https://github.com/cellscript/amm @@ -1247,12 +1349,15 @@ cellc package verify cellc registry verify ``` -The `resolve_from_registry` path in `src/package/mod.rs` now implements the -two-tier source-package resolver: discovery index lookup, source repo clone, -tag checkout, `registry.json` identity and schema checks, `source_hash` -verification, `Cell.toml` parsing, and transitive dependency resolution. A -discovery failure reports the namespace, package, requested version, and -registry URL instead of falling through to a local-path placeholder. +The `resolve_from_registry` path in `src/package/mod.rs` implements two explicit +source-package authorities. By default, the production public API supplies the +accepted status, signed identity, and immutable snapshot descriptor; the client +verifies and materializes that snapshot. An explicitly configured +`CELLSCRIPT_REGISTRY_URL` instead supplies the legacy Git/offline index, tag, +and mirrored `registry.json`. Both paths finish with `source_hash`, `Cell.toml`, +and transitive-dependency verification. A lookup failure reports the namespace, +package, requested version, and authority instead of silently downgrading to +Git discovery. ## Deployment Registry (Chain-Indexed) @@ -1279,10 +1384,10 @@ production transaction: ``` cellc build → generates artifact, metadata, schema, abi, constraints - → writes Cell.lock [package.build] + → writes Cell.lock [package_build] cellc deploy plan - → reads Cell.lock [package.build] + → reads Cell.lock [package_build] → reads Cell.toml [deploy.ckb] intent → produces deployment plan JSON @@ -1342,12 +1447,12 @@ transaction. | `PackageInfo` | In `src/package/mod.rs`, no `namespace` field | Add `namespace: String` with `#[serde(default)]`. Required for `cellc publish`; absent means local-only package. | | `DetailedDependency` | In `src/package/mod.rs`, no `namespace` field | Add `namespace: Option` with `#[serde(default, skip_serializing_if = "Option::is_none")]`. Used for explicit registry resolution. | | `PackageManifest` | `Cell.toml` schema | Unchanged structure. `[deploy.ckb]` already supported. `namespace` flows through `PackageInfo`. | -| `Lockfile` | `version/dependencies` only | Extend with `[package.build]`, `[deployment.*]`, `namespace`, `source_hash` on dependencies. | +| `Lockfile` | `version/dependencies` only | Extend with `[package_build]`, `[deployment.*]`, `namespace`, `source_hash` on dependencies. | | `LockedDependency` | `version` + `source` only | Add `namespace: Option`, `source_hash: Option`, `build: Option`. All with `#[serde(default)]`. | -| `LockedSource::Registry` | `{ name, version }` only | Extend to `{ namespace, name, version, url, revision }`. The `url` and `revision` fields carry git provenance from the discovery index. | +| `LockedSource::Registry` | `{ name, version }` only | Extend to `{ namespace, name, version, url, revision }`. Public resolution records the immutable snapshot URL and SHA-256 revision; explicit Git/offline resolution records Git provenance. | | `DeploymentManifest` | In `crates/cellscript-ckb-adapter/src/lib.rs` | Extend to `Deployed.toml` schema: add `network`, `chain_id`, `script_role`, `data_hash`, `status`, `[build]` section. | | `DeploymentRef` | In adapter crate | Add `network`, `chain_id`, `script_role`, `data_hash`, `status` fields as `Option`. | -| `PackageManager::resolve_from_registry` | Implemented two-tier source-package resolver: discovery lookup → source repo clone → tag checkout → `registry.json` verification → source hash check → `Cell.toml` parsing. | Keep non-CellScript artifact profiles fail-closed until profile-specific resolver contracts exist. | +| `PackageManager::resolve_from_registry` | Implemented public-API accepted-status lookup → immutable snapshot size/object/file/path/source verification → atomic cache materialisation → Edition/profile and `Cell.toml` checks. The explicit Git/offline override retains tag + `registry.json` verification. | Keep non-CellScript artifact profiles fail-closed until profile-specific resolver contracts exist. | | `build_deployment_manifest_from_evidence` | In adapter crate | Extend to populate new fields. | | `ManifestCellDepResolver` | In adapter crate | Unchanged. Still resolves CellDeps from manifest. | @@ -1393,61 +1498,22 @@ verifying that two independent builds of the same source produce the same - Replace any `HashMap` with `BTreeMap` for key ordering - Pin the `serde_json` serialization to compact output with sorted keys -These changes are backward-compatible: they only affect the hash computation, -not the schema. A Phase 2 migration can compute both the old and new hashes -to bridge the transition. - -### Backward Compatibility - -- `Cell.lock` uses a single version 1 schema from the start. The `[package.build]` - and `[deployment.*]` sections are optional; their absence simply means the - package has not been built or deployed yet. -- The `Deployed.toml` format uses a distinct schema identifier - (`cellscript-deployed-v0.19`) to avoid confusion with the existing deployment - manifest schema. -- The `LockedDependency` type gains `source_hash` and `build` fields with - `#[serde(default)]` to maintain deserialization compatibility. -- All new fields on `DeploymentRef` use `Option` type (not typed - structs like `H256` or enums), consistent with the existing `DeploymentRef` - which stores `code_hash`, `hash_type`, `args`, `dep_type`, and `out_point` as - plain `String` values. Each new field uses `#[serde(default, - skip_serializing_if = "Option::is_none")]` so that existing - `DeploymentManifest` JSON files with the - `cellscript-ckb-deployment-manifest-v0.19` schema continue to parse without - error. Typed field wrappers (e.g., `H256`, `ScriptRole`, `DeploymentStatus`) - are a Phase 2 concern; Phase 1 keeps everything as `Option` for - maximum serialization compatibility. -- The validation logic in `parse_deployment_manifest` is extended to check - for the new schema identifier. Old-format manifests (without the new fields) - parse successfully with `None` for all new fields. New-format manifests must - have the required fields populated; missing required fields in the new format - are rejected, but missing optional fields are accepted. - -### Non-Breaking Approach - -The implementation should follow this ordering: - -1. Add `Deployed.toml` parsing as a new capability alongside existing - `DeploymentManifest` parsing. New fields on `DeploymentRef` use - `Option` with `#[serde(default, skip_serializing_if = "Option::is_none")]` - so existing manifests continue to parse. -2. Extend `Lockfile` with optional `[package.build]` and `[deployment.*]` fields. - New `record_hash` field on `[deployment.*]` entries is optional in Phase 1; - computed via canonical JSON serialization (not canonical TOML) to match - the existing `metadata_hash` convention. -3. Add `constraints_hash` to `cellc build` output using the same method as - `metadata_hash`: `ckb_blake2b256(serde_json::to_vec(&constraints))`. Same-version - determinism is sufficient for Phase 1; Phase 2 adds Vec sorting for - cross-build determinism. -4. Extend `build_deployment_manifest_from_evidence` to populate the new - `DeploymentRef` fields (`network`, `chain_id`, `data_hash`, `type_id`, - `status`, and the `[build]` section) from the existing `ResolvedDeployEvidence` - and adapter configuration. -5. Implement `resolve_from_registry` without changing existing path/git - resolution. -6. Add `cellc package verify` and `cellc registry verify` as new subcommands. -7. Defer wiring the `registry-client` module into the generated Action Builder - pipeline to 0.20; 0.19 consumes it from package/build verification. +These hashes are deterministic within their explicitly versioned schemas and +the resolved compatibility profile; they are not derived from the edition year. + +### Edition 2026 Breaking Boundary + +- `Cell.lock` version 3 records the package edition and manifest-bound source + graph. A present + `[package_build]` must use the same edition and a non-empty compatibility + profile hash. +- `Deployed.toml` version 2 uses + `cellscript-deployed-v0.23-edition-2026`. Package, build, and every deployment + record must agree on edition and compatibility profile. +- Readers reject version 1 and the old deployment schema. They do not migrate, + fill defaults, or compute both old and new hashes. +- Registry versions and generated builders bind the same edition/profile + identity, so a partial upgrade fails closed before transaction construction. ## Version Control Audit @@ -1491,11 +1557,12 @@ this. No code change needed; the document should reference this convention. **Gap**: `version = 1` and `lock_schema = "cellscript-lock-v1"` are redundant. No migration path is defined between lockfile schema generations. -**Resolution**: Remove `lock_schema`. The `version` field is sufficient — -it is an integer that increments on breaking schema changes. Migration -strategy: when `cellc` reads a lockfile with an older version, it writes -a new lockfile preserving all compatible fields. The `version` field alone -is the schema identifier. +**Resolution**: `Cell.lock` version 3 with +`cellscript-lock-v0.24-graph-v1` is the sole accepted build-time lock +generation. Readers reject older versions and never rewrite them implicitly; +explicit lock/update may repin them. Edition and compatibility profile remain +part of build identity, while root/dependency manifest digests and graph edges +form dependency identity. #### 3. Deployed.toml Schema — Dual Version Identifier @@ -1504,11 +1571,11 @@ overlapping purposes. The `schema` string ties the format to a specific cellscript version, but format evolution is independent of compiler version. -**Resolution**: Keep `version = 1` as the schema identifier (integer, -stable). Remove `schema = "cellscript-deployed-v0.19"`. The relationship -to the existing `cellscript-ckb-deployment-manifest-v0.19` schema is: -`Deployed.toml` version 1 is a superset of the existing manifest schema. -The parser accepts both; the `version` field distinguishes them. +**Resolution**: Package deployment records require both `version = 2` and +`schema = "cellscript-deployed-v0.23-edition-2026"`. The redundancy is +intentional fail-closed evidence: one identifies the structural generation and +the other the semantic edition boundary. The adapter's historical deployment +manifest is a different format and is not accepted as `Deployed.toml`. #### 4. registry.json Dependencies Missing Namespace @@ -1706,11 +1773,11 @@ registry admission authority. | Policy | Evidence | |---|---| -| JoyID-rooted publisher identity | `cellc auth capability create --principal-id --scope publish:ns/pkg --expires 90d --json > capability-payload.json` plus `cellc auth capability submit --payload capability-payload.json --joyid-signature joyid-signature.json` uses the CCC-backed JoyID flow, records `principal_type = joyid_ckb`, binds `principal_id` to a local publisher credential, and stores that credential in the OS keychain | +| Wallet-rooted publisher identity | `cellc auth capability create --principal-type --principal-id --scope publish:ns/pkg --scope deployment:ns/pkg --scope availability:ns/pkg --expires 90d --json > capability-payload.json` plus `cellc auth capability submit --payload capability-payload.json --wallet-signature wallet-signature.json` uses the CCC-backed wallet flow, records the typed principal binding, and stores the delegated private key in the OS keychain | | Scoped publisher credentials | Capability-style signing key with namespace/package/action scopes, expiry, revocation, nonce/origin checks, and CI-safe delegation | -| Namespace/package ACL | Namespace owners, package maintainers, yanking authority, attestation authority, maintainer rotation, and source-location update permissions | +| Namespace/package ACL | Namespace owners, package maintainers, yanking authority, commitment authority, maintainer rotation, and source-location update permissions | | Abuse controls | Separate static read path from write API; WAF/rate limits/body caps/hash dedup/bounded queues/quarantine/cooldown; fee/bond rules remain later policy hooks | -| Entry visibility state machine | `source_published` -> `indexed_pending` -> `verified_build` -> `deployed` -> `on_chain_attested`; `deprecated`/`yanked`/`quarantined` suppress default search without deleting history | +| Entry visibility state machine | `source_published` -> `indexed_pending` -> `verified_build` -> `deployed` -> `on_chain_committed`; `deprecated`/`yanked`/`quarantined` suppress default search without deleting history | ### Phase 0 — No Block on v0.12 @@ -1727,16 +1794,16 @@ implications from the audit above. | # | Work | Evidence | Audit Ref | |---|---|---|---| | 1 | Add `namespace` to `PackageInfo` and `DetailedDependency` | `Cell.toml` with `namespace` parses correctly; `cellc init --namespace` sets it | — | -| 2 | Extend `LockedSource::Registry` with `namespace`, `url`, `revision` | `Cell.lock` writes registry deps with git provenance; re-verification works without discovery index | #2 | +| 2 | Extend `LockedSource::Registry` with `namespace`, `url`, `revision` | Historical 0.19 Git resolver records provenance; 0.23 public resolution reuses the fields for immutable snapshot URL + SHA-256 | #2 | | 3 | Remove `lock_schema` from Cell.lock; keep `version = 1` | Single version identifier; no dual version confusion | #2 | | 4 | Add `schema_version: 1` to `registry.json` format | `cellc publish --offline` writes `schema_version`; `cellc install` rejects unknown versions | #5 | | 5 | Fix `registry.json` dependencies to include namespace | `dependencies: { "token": { "namespace": "cellscript", "version": "0.3.0" } }` | #4 | | 6 | Remove `schema` string from Deployed.toml; keep `version = 1` | Single version identifier; parser accepts both old manifest and new Deployed.toml | #3 | | 7 | Define canonical network table (mainnet/aggron4/devnet) | `cellc deploy --network aggron4` writes correct `network` + `chain_id` | #12 | | 8 | Add `_schema.json` to discovery index repository | `{ "schema_version": 1 }` at repo root | #11 | -| 9 | `Cell.lock` with `[package.build]` hash section | `cellc build` writes artifact/metadata/schema/abi/constraints hashes to lockfile | — | +| 9 | `Cell.lock` with `[package_build]` hash section | `cellc build` writes artifact/metadata/schema/abi/constraints hashes to lockfile | — | | 10 | `Deployed.toml` format definition and parsing | Adapter crate can load and validate `Deployed.toml` records | — | -| 11 | Implement `resolve_from_registry` with two-tier resolution | Discovery index lookup → source repo clone → `registry.json` verification → `Cell.toml` parsing | — | +| 11 | Implement the initial `resolve_from_registry` with two-tier resolution | Historical 0.19 evidence: discovery lookup → source clone → `registry.json` → `Cell.toml`; 0.23 replaces the default transport with verified Registry snapshots | — | | 12 | Define semver compatibility rules and unified version resolution | `cellc build` fails on unsatisfiable version constraints; `"0.3.0"` means `^0.3.0` | #1, #10 | | 13 | Define compiler major.minor compatibility window for `constraints_hash` | `cellc registry verify` rejects cross-version hash comparison; same `0.19.x` → same hash | #6 | | 14 | Define git tag convention `v{version}` with validation | `cellc publish` validates tag matches version; `cellc install` validates tag exists | #8 | @@ -1826,19 +1893,20 @@ Any failure in this chain causes fail-closed rejection. Namespace ownership is the core registry ACL. A namespace has owner principals; packages have maintainer principals; publisher credentials are scoped to -actions such as `publish`, `yank`, `attest`, and `manage-maintainers`. The root -publisher principal is `joyid_ckb`, while daily operations use delegated +actions such as `publish`, `yank`, `commit`, and `manage-maintainers`. The root +publisher principal is `joyid_ckb` or `ckb_secp256k1`, while daily operations use delegated publisher credentials that can expire and be revoked. The exact bootstrap policy for first namespace claim (review, cooldown, reserved namespaces, or later fee/bond hooks) is an ecosystem decision. ### Should reproducible build proofs or audit signatures be required before a package is considered production-ready? -Phase 1 requires hash matching but not build attestations or audit signatures. -Phase 2 adds optional publisher signatures and audit report hashes. Whether -audit signatures become mandatory for production readiness is an ecosystem -policy decision, not a toolchain enforcement decision. The toolchain should -support the mechanism; the policy should be set by the community. +Hash matching remains the baseline for generic artifacts. A release declaring +a reproducible build additionally requires policy-approved, P-256-signed +reproduction reports from independent trust domains before it becomes +`verified` or can acquire deployment evidence. Security audit signatures remain +policy-specific; when a release declares `security.status = audited`, the +referenced audit report must at least be present and hash-bound. ### How should yanking, supersession, and maintainer rotation work? @@ -1860,10 +1928,10 @@ package history, audit record, actor identity, reason, and timestamps. - Do not replace CCC. The Action Builder consumes deployment records; it does not become a wallet, indexer, or chain submission layer. -- Do not introduce a separate registry account system alongside JoyID-rooted +- Do not introduce a separate registry account system alongside wallet-rooted publisher identity. -- Do not require an interactive JoyID signature for every `cellc publish`; - JoyID authorises scoped publisher credentials, and credentials sign daily +- Do not require an interactive wallet signature for every `cellc publish`; + the wallet authorises scoped publisher credentials, and credentials sign daily publish payloads. - Do not introduce hidden signer authority or hidden sighash defaults. - Do not infer transaction semantics from protocol/action names. diff --git a/docs/CELLSCRIPT_REGISTRY_PHASE1.md b/docs/CELLSCRIPT_REGISTRY_PHASE1.md index 2e65aee7..f2bb2a93 100644 --- a/docs/CELLSCRIPT_REGISTRY_PHASE1.md +++ b/docs/CELLSCRIPT_REGISTRY_PHASE1.md @@ -1,636 +1,505 @@ -# CellScript Registry Phase 1: JoyID-Rooted Package Publishing for CKB Smart Contracts - -**Status**: public walkthrough of the Phase 1 registry contract for the current -CellScript CKB profile. Policy decisions defer to +# CellScript Registry: Artifact and Deployment Contract + +**Status**: implemented public contract for the CellScript Registry. The +admission, verification, discovery, deployment-evidence, CLI, and website +surfaces described here are checked in on the current release line. + +The source-package production slice is deployed. Generic artifact, +reproduction, deployment, and chain-index code is implemented, but a public +`on_chain_committed` claim additionally requires operators to deploy and pin +the canonical mainnet Registry Type Script, commitment custody Lock, and both +code CellDeps. Until all four configuration values are present and their Cells +are live with the required confirmation depth, commitment construction fails +closed and scheduled chain reconciliation remains disabled. + +The Pudge Testnet Sandbox is a separate environment, not a network switch in +production. It has its own API, Postgres database, object volume, signing +origin, website build, wallet state, RPC identity, and testnet evidence. +Sandbox releases leave discovery after 72 hours and source objects are removed +after a further 24-hour grace period; Pudge chain history is unaffected. + +The canonical `no_std` Script source, exact deployable ELF, CKB-VM tests, +reproducible Linux build recipe, builder image digest, and release identity are +tracked under `contracts/registry-type-script`. Only a Linux x86_64 rebuild is +treated as a byte reproduction; another host's Rust/LLVM output is reported but +never silently substituted for the deployable artifact. Its args bind the full +custody Lock Script hash and every lifecycle transition must consume a Cell +under that Lock; an unrelated sender cannot create a trusted commitment merely +by locking an output to the Registry address. + +The Registry indexes CKB ecosystem artifacts. A coordinate is +`namespace/name`; a release adds an immutable version. The coordinate does not +imply that the object is a CellScript dependency, executable, deployed Script, +or reusable source library. Those meanings are explicit in the artifact +descriptor and in three independent state axes. + +The production boundary and operator controls remain in [`CELLSCRIPT_REGISTRY_PRODUCTION_BOUNDARY_ADR.md`](CELLSCRIPT_REGISTRY_PRODUCTION_BOUNDARY_ADR.md). -Publishing and consuming smart contract libraries should feel like a normal -package workflow: `cellc publish` publishes a package, and the registry shows -the new entry. The CellScript public registry policy therefore treats publish -as a real registry write, while keeping the trust model hash-first and the read -path static, cacheable, and independently verifiable. The chain only records -what actually matters at runtime. - -This post walks through the design, explains why we chose this model, and shows how to use it end to end. - -The production boundary for the public registry is recorded in -[`CELLSCRIPT_REGISTRY_PRODUCTION_BOUNDARY_ADR.md`](CELLSCRIPT_REGISTRY_PRODUCTION_BOUNDARY_ADR.md). +## One Public Model -## The Problem - -Most package registries you've used — crates.io, npm, PyPI — follow a central-server model. You publish to a server, the server stores your package, and consumers download from the server. That works great for application development. Smart contracts are different. - -A CKB smart contract dependency isn't just source code you download and compile. In production, a builder or wallet needs to know concrete on-chain facts: which CellDep to reference, what the data_hash is, which OutPoint to point to, whether the deployment is active or deprecated. Source packages answer "what code was written." Production deployment answers "which cell on which chain should you actually use." Both layers matter, and they're bound together by cryptographic hashes, not by naming conventions. - -At the same time, a smart-contract registry must not confuse package publishing -with deployment trust. A new package entry can appear quickly, but it should not -become a recommended or production-trusted dependency until source, build, -deployment, and optional chain attestation checks pass. - -## The Core Idea: Publish Once, Verify in Layers - -The public registry policy has two operational paths: - -1. **Write path** — `cellc publish` authenticates the publisher, checks - namespace/package permissions, validates metadata and hashes, admits the - package into the registry, and returns a canonical registry URL. The entry is - immediately addressable, usually as `source_published` or `indexed_pending`. -2. **Read path** — the public website, JSON index, source mirrors, and package - metadata are served through static/CDN-friendly files. Consumers still verify - source hashes, build hashes, and deployment facts instead of trusting the - transport. - -The registry data model still has two tiers: - -The first tier is a **discovery index** — a lightweight map from -`namespace/name` to a source repository URL. Think of it as a phone book with -overrides. It only changes when a package is first claimed or when ownership / -source-location metadata changes. - -The second tier is a **per-package version index** called `registry.json`. The -registry service stores and mirrors the canonical entry, and the same shape can -be checked into the source repository for auditability, local mirrors, and -offline fixtures. When you run `cellc publish`, it computes a source hash, -reads build artifacts, signs the publish payload with a delegated publisher -credential, and submits the version entry to the registry write API. - -The Go-style convention still matters for resolution: if no explicit discovery -entry exists, `cellscript/amm` may resolve to the conventional source location. -But the public registry's write authority is not "who can push to Git"; it is -the namespace/package ACL enforced by registry credentials. - -Offline and bootstrap environments may still use the Git-only fixture path: -generate `registry.json`, commit/tag/push the source, and resolve directly from -Git. That path is a mirror and fallback, not the authority for the public -registry service. - -Compatibility note: Phase 1 is the **CellScript source-package profile** of a -broader registry architecture. The naming convention `namespace/name/version` -can later be reused for other CKB artifacts, but `cellc install` and -`Cell.toml [dependencies]` currently mean "resolve a CellScript package that -has `Cell.toml`, `.cell` source, `registry.json`, and CellScript build -identity". A CKB binary, verifier artifact, deployment record, or -`ckb-bootstrapper` reproducible build output must use a future artifact profile -with its own hash and build-recipe contract. Discovery may become broad; -dependency resolution stays profile-specific and fail-closed. - -```mermaid -graph TB - subgraph "Resolution: Convention First" - Q["cellc install cellscript/amm"] --> C{"Discovery index\nhas entry?"} - C -->|Yes| E["Use explicit URL"] - C -->|No| F["Fallback: github.com/cellscript/amm"] - E --> S["Clone source repo"] - F --> S - end -``` +Every artifact has this descriptor: -```mermaid -graph TB - subgraph "Tier 1: Discovery Index (optional)" - DI["cellscript-registry repo"] - DI --> CS["cellscript/token.json"] - DI --> CA["cellscript/amm.json"] - end - - subgraph "Tier 2: Source Repositories" - SR1["github.com/cellscript/token"] - SR2["github.com/cellscript/amm"] - SR1 --> RJ1["registry.json"] - SR1 --> CT1["Cell.toml"] - SR1 --> SRC1["src/"] - SR2 --> RJ2["registry.json"] - SR2 --> CT2["Cell.toml"] - SR2 --> SRC2["src/"] - end - - CS -->|"explicit map"| SR1 - CA -->|"explicit map"| SR2 - CONV["Convention fallback:\ngithub.com//"] -.->|"auto-resolve"| SR2 +```json +{ + "kind": "deployable_contract", + "profile": "ckb_executable", + "consumption_mode": "deployment", + "language": "rust" +} ``` -## Why This Works for Smart Contracts - -There's a subtlety here that's easy to miss. In a traditional package registry, the package *is* the unit of identity. You install `lodash@4.17.21`, and that's the end of the story. For smart contracts, the package is only the first layer. - -CellScript uses what we call a **three-layer identity model**. A package exists in three distinct identity scopes, and each one answers a different question: - -**Package Identity** answers "what source code was written?" It's carried by `Cell.toml` and the registry index, verified at compile time. The key fields are namespace, name, version, and source_hash. - -**Build Identity** answers "what did the compiler produce?" It's carried by `Cell.lock`, verified at build time. The key fields are compiler_version, artifact_hash, metadata_hash, schema_hash, abi_hash, and constraints_hash. - -**Deployment Identity** answers "which cell on which chain?" It's carried by `Deployed.toml`, verified at runtime. The key fields are network, chain_id, tx_hash, output_index, code_hash, hash_type, data_hash, out_point, dep_type, type_id, and script_role. - -```mermaid -graph TB - subgraph "Package Identity — compile time" - P["Cell.toml + registry.json"] - P -->|"source_hash"| B - end - - subgraph "Build Identity — build time" - B["Cell.lock"] - B -->|"artifact_hash, data_hash"| D - end - - subgraph "Deployment Identity — runtime" - D["Deployed.toml"] - D -->|"on-chain verification"| CKB - end - - CKB["CKB Network"] -``` - -Each layer is independently meaningful but cryptographically bound to the layers above and below through the lockfile. If someone tampers with the source code after publishing, the source_hash won't match. If someone swaps the artifact, the artifact_hash won't match. If someone points to the wrong on-chain cell, the data_hash won't match the on-chain reality. The system fails closed. - -This is why the registry service does not need to become a trust oracle. It is -the publication and discovery authority, while the trust anchors remain the -cryptographic hashes and deployment facts verified independently at each layer. -Once you've found a package, you still verify it. - -## The Three Files - -CellScript uses three files to separate concerns. This is inspired by Move/Sui's `Move.toml` / `Move.lock` / `Published.toml` split, but adapted for CKB's CellDep and OutPoint model instead of Sui's native package-object model. - -### Cell.toml — Deployment Intents - -`Cell.toml` is the source package declaration. It describes what the developer *intends* to deploy, not what was actually deployed. The key addition for the registry is the `namespace` field: +The Registry accepts these kinds: -```toml -[package] -name = "amm_pool" -version = "1.2.0" -namespace = "cellscript" +| Kind | Profile | Consumption | Required immutable objects | +|---|---|---|---| +| `source_library` | `cellscript_source` | `dependency` | CellScript source snapshot | +| `profile_library` | `cellscript_source` | `dependency` | CellScript source snapshot | +| `runtime_verifier` | `ckb_executable` | `tcb` | source, executable, ABI | +| `deployable_contract` | `ckb_executable` | `deployment` | source, executable, ABI | +| `reproducible_binary` | `reproducible_build` | `tcb` | source, executable, build recipe | +| `template` | `copy_material` | `copy` | source material | -[dependencies] -token = { version = "0.3.0", namespace = "cellscript" } +`cellc install` deliberately accepts only the `cellscript_source` + +`dependency` contract. An executable, verifier, reproducible tool, or template +can be discovered and audited through the same Registry, but it cannot be +silently interpreted as a CellScript dependency. -[build] -target_profile = "ckb" -``` +There is one public route family: `/v1/artifacts`. The Registry does not expose +a second package route with a competing data shape. -Dependencies can be resolved from the registry (by namespace and version), from a local path, or from a git URL. Resolution priority is path > git > registry, which means you can always override a registry dependency with a local checkout for development without changing any configuration. +## Independent States -### Cell.lock — Build Identity +Each release exposes three orthogonal states: -`Cell.lock` is the cryptographic bind point between source and deployment. It records exact dependency versions, git revisions, source hashes, and build hashes. It's self-sufficient for re-verification — the `url` and `revision` fields let you re-clone the exact source commit without re-querying the discovery index. +- `verification_status`: `pending`, `hash_bound`, `verified`, `evidence_required`, or + `rejected`; +- `deployment_status`: `not_applicable`, `undeployed`, `deployed`, or + `chain_verified`; +- `availability_status`: `active`, `deprecated`, `yanked`, or `quarantined`. -> **Hash format note**: the `blake2b:0x...` prefix shown in the examples below is -> illustrative naming. The actual `source_hash`, `artifact_hash`, and other -> hash fields are emitted as bare lowercase hex blake2b-256 digests (no prefix), -> so the lockfile compares like-for-like. The `cellc publish` command writes the -> same bare-hex `source_hash` into `registry.json`. +These states must not be collapsed into one lifecycle label. A reproducible +binary may be verified but have no deployment concept. A CKB executable may be +verified and still undeployed. A previously chain-verified release may later be +deprecated without rewriting its evidence. -```toml -version = 1 - -[package] -name = "amm_pool" -version = "1.2.0" -namespace = "cellscript" -source_hash = "blake2b:0xabcd..." - -[package.build] -compiler_version = "0.21.0" -target_profile = "ckb" -artifact_hash = "blake2b:0x1234..." - -[dependencies.token] -version = "0.3.2" -namespace = "cellscript" -source = { registry = "cellscript/token", url = "https://github.com/cellscript/token", revision = "f7e8d9c0..." } -source_hash = "blake2b:0x2222..." - -[deployment.ckb.aggron4] -status = "deployed" -record = "ckb-testnet:0xaaaa..." -``` +`on_chain_committed` is a current-state claim, not a permanent badge. Scheduled +maintenance returns a spent commitment to `deployed` and a stale deployment to +`verification_status = verified` plus `deployment_status = undeployed` +(projected as `verified_build`), while retaining every accepted evidence record +for audit. Disabling the Registry Script configuration also clears current +commitment pointers because the service can no longer re-observe them. -This is analogous to `go.sum` — it pins exact versions with their hashes, making the build independently reproducible. +## Artifact Identity -### Deployed.toml — Deployment Facts +The Registry separates four questions: -`Deployed.toml` records immutable deployment facts derived from the chain. It's generated automatically after a deployment transaction is confirmed, and it must not be edited by hand. +1. **Coordinate identity**: which publisher-controlled name and release? +2. **Source identity**: which immutable source or input bytes? +3. **Build identity**: which executable, ABI, recipe, compiler, and metadata? +4. **Deployment identity**: which live mainnet Cell contains the executable? -```toml -version = 1 - -[package] -name = "amm_pool" -version = "1.2.0" -source_hash = "blake2b:0xabcd..." - -[build] -compiler_version = "0.21.0" -artifact_hash = "blake2b:0x1234..." - -[[deployments]] -network = "aggron4" -chain_id = "ckb-testnet" -script_role = "type" -tx_hash = "0xaaaa..." -output_index = 0 -code_hash = "0xbbbb..." -hash_type = "data1" -dep_type = "code" -out_point = "0xaaaa...:0" -data_hash = "0xcccc..." -type_id = "0xdddd..." -``` +Source and build identity come from immutable, hash-bound bundle objects. +Deployment identity is an additional signed evidence record; publishing an +executable never claims that it is already deployed. -The separation matters. `Cell.toml` says "I want hash_type = data1." `Deployed.toml` says "the cell at 0xaaaa...:0 actually has hash_type = data1, and here's the on-chain proof." One is intent, the other is fact. Confusing the two leads to exactly the kind of supply-chain vulnerabilities that smart contract systems should avoid. +For CKB executables, `artifact_hash` is the CKB Blake2b-256 hash of the +executable bytes. A deployment record must bind the same value as `data_hash`. +The Registry calls `get_live_cell` on the environment's configured CKB network +and verifies: -## Compatibility With Non-CellScript Artifacts +- the OutPoint is live; +- the returned Cell data hash equals the published executable hash; +- for `hash_type = type`, the returned Type Script hash equals `code_hash`; +- for data-hash variants, `code_hash` equals the executable data hash. -The registry service is deliberately shaped so it can grow beyond CellScript -packages without changing the core trust model. The safe extension point is an -explicit profile, not a looser interpretation of the current package format. +It also reads `get_transaction.tx_status` for the creation transaction, +requires `status = committed`, and uses that standard response's block hash for +minimum-confirmation checks. It does not depend on a proxy-specific +`get_live_cell.block_hash` field. -| Object | Current Phase 1 handling | Future-compatible handling | -|---|---|---| -| CellScript library package | Resolved through `Cell.toml [dependencies]` | Remains `cellscript_source_package_v1` | -| Deployed CellScript contract | Verified through `Cell.lock` + `Deployed.toml` | May also be indexed by a deployment artifact profile | -| Runtime verifier or helper script Cell | Not a source dependency unless packaged as CellScript source | Verifier/deployable artifact profile with ABI, CellDep, status, and artifact hashes | -| Reproducible CKB binary or `ckb-bootstrapper` output | Not accepted by `cellc install` as a package | Reproducible-binary profile with source hash, build recipe hash, pinned inputs, and output binary hashes | -| Template, skeleton, cookbook example | Copy by hand or through a scaffold command | Still copy/scaffold only; not dependency-safe by default | +For `dep_type = dep_group`, the Registry decodes the live DepGroup Cell as the +canonical Molecule `OutPointVec`, loads its members, and requires a live member +whose code/data identity matches the published executable. The DepGroup +container bytes are never treated as executable code. -Mixed-use rules: +The production Registry accepts only CKB mainnet deployment records and exposes +no network selector. The isolated Pudge environment accepts only testnet +records and cannot promote them into production state. -- a `namespace/name` may have more than one profile, but a lockfile must record - the selected profile; -- a registry proxy may cache multiple profiles, but it must not rewrite - profile identity or turn one profile into another; -- a CellScript package may reference a generic artifact as deployment evidence - or a declared TCB input only after that artifact profile defines the fields - needed for fail-closed verification; -- current `cellc` commands must keep rejecting non-CellScript package shapes - until profile-specific resolver support exists. +## Publishing CellScript Dependencies -## Publisher Identity and Abuse Boundary +A normal CellScript package uses `Cell.toml` and the native publish path: -CellScript Registry does not need a separate Web2 registry account. It uses a -**JoyID-rooted publisher identity**: - -```text -principal_type = joyid_ckb -principal_id = - -JoyID - -> root publisher principal - -> authorises scoped publisher credentials - -> credentials sign daily publish payloads +```bash +cellc package verify --json +cellc publish --dry-run +cellc publish --authorise # interactive first publish +cellc publish # later publishes with an active delegated key ``` -The preferred `principal_id` is derived from the JoyID signer key as a -normalized JoyID-CKB identity binding, not from the display address. The -registry verifies that every JoyID-signed capability or revocation payload uses -a `principal_id` that matches the signing key, so namespace ACLs, audit records, -and capability revocation all point at the same principal. - -JoyID is not a separate registry account, and ordinary `cellc publish` should not -require an interactive JoyID signing prompt every time. The intended flow is: +Profile libraries use the same compiler-backed snapshot contract and declare +their distinct kind explicitly: -```text -cellc auth capability create --principal-id --scope publish:namespace/package --expires 90d --json > capability-payload.json - -> local registry signing key is generated and stored in the OS keychain - -> CLI prints an authorize_capability payload with capability_pubkey and requested scopes - -> browser/CCC/JoyID signs that exact payload -cellc auth capability submit --payload capability-payload.json --joyid-signature joyid-signature.json - -> signed payload is submitted to the registry write API - -> registry records the key's scope, expiry, principal_type, and principal_id - -cellc publish - -> CLI signs the publish payload with the local publisher credential - -> registry verifies the signature, nonce, expiry, and ACL scope - -> registry accepts the package entry into source_published / indexed_pending +```bash +cellc publish --artifact-kind profile_library --dry-run +cellc publish --artifact-kind profile_library --authorise # first publish ``` -The JoyID signature must bind the capability, not a vague login message: +The verifier compiles the snapshot with the real CellScript compiler and +checks its canonical manifest, source hash, build identity, metadata, and +compatibility-profile identity. Publisher-supplied state is never treated as +verification evidence. -```text -protocol: cellscript-registry-auth-v1 -action: authorize_capability -registry_origin: https://api.registry.cellscript.dev -principal_type: joyid_ckb -principal_id: -capability_pubkey: ... -requested_scopes: [publish:cellscript/amm_pool] -capability_expires_at: ... -nonce: ... -issued_at: ... -expires_at: ... -cli_version: ... -``` +## Publishing Other Artifacts -A daily publish signature must bind the concrete publish action: +Non-CellScript artifacts use `Artifact.toml` plus a bounded JSON bundle: -```text -action: publish -namespace: cellscript -package: amm_pool -version: 1.2.0 -source_hash: ... -manifest_hash: ... -registry_origin: https://api.registry.cellscript.dev -nonce: ... -expires_at: ... +```toml +schema = "cellscript-registry-artifact" +namespace = "acme" +name = "vault-lock" +release = "1.0.0" +kind = "deployable_contract" +language = "rust" +bundle = "vault-lock.bundle.json" +description = "Mainnet vault lock Script" +repository = "https://github.com/acme/vault-lock" +keywords = ["lock", "vault"] ``` -The ACL core is namespace/package ownership: - -```text -namespace -> owner principals -package -> maintainer principals -credential -> scoped permissions +The referenced bundle carries a closed, typed profile contract. For a +deployable contract, canonicalize this object recursively by key and encode the +resulting JSON as the bundle's `manifest_json` string: + +```json +{ + "schema": "cellscript-registry-profile-contract-v1", + "artifact_kind": "deployable_contract", + "profile": "ckb_executable", + "build": { + "target": "riscv64imac-unknown-none-elf", + "toolchain": "rustc 1.97.1", + "profile": "release", + "source_revision": "", + "reproducible": false + }, + "security": { "status": "review_required" }, + "ckb": { + "vm_version": "2", + "script_role": "lock", + "hash_type": "data1", + "dep_type": "code", + "abi_hash": "" + } +} ``` -Example scopes: - -```text -publish:cellscript/amm_pool -yank:cellscript/amm_pool -attest:cellscript/amm_pool -manage-maintainers:cellscript/* +The bundle has this shape: + +```json +{ + "schema": "cellscript-registry-bundle", + "namespace": "acme", + "name": "vault-lock", + "release": "1.0.0", + "profile": "ckb_executable", + "manifest_json": "", + "objects": [ + { "role": "source", "content_base64": "..." }, + { "role": "executable", "content_base64": "..." }, + { "role": "abi", "content_base64": "..." } + ] +} ``` -JoyID signatures prove who authorised a publish credential; they do not prove -the package is useful, safe, or non-spam. Abuse resistance belongs to the -registry service: - -- read traffic is static/CDN-backed and separated from the authenticated write - API; -- write requests pass WAF/rate-limit checks before any expensive work; -- synchronous publish checks are limited to authentication, ACL, schema, - request-size caps, metadata length caps, hash/manifest sanity, - idempotency, quota, and deduplication; -- signed nonces are one-time use for publish, and replayed publish payloads - fail before source snapshot or static registry object writes; -- `Idempotency-Key` is the supported retry mechanism for `cellc publish`; a - matching completed request may replay its response, but the same key with - different content is rejected; -- `cellc publish` sends an idempotency key by default, and CI can pin it with - `--idempotency-key` or `CELLSCRIPT_REGISTRY_IDEMPOTENCY_KEY` when retrying the - same signed publish request; -- if publish admission fails after reserving the retry key but before accepting - the package version, the registry releases that `processing` reservation; the - consumed signed nonce still cannot be reused, so retry with a fresh publish - payload/signature and the same CI retry key; -- build verification, artifact checks, deployment checks, chain RPC reads, and - search indexing run asynchronously in bounded queues; -- rate limits apply per IP, ASN, JoyID principal, credential, namespace, and - package; -- principal-scoped quota and namespace-claim cooldown are counted only after - JoyID signature verification, so forged payloads cannot spend someone else's - principal budget; -- new namespace claims may require review or cooldown, while fee/bond rules - remain later policy hooks; -- new or high-risk packages can be direct-URL visible while excluded from - default search until basic checks pass; -- mirrored `registry.json` entries without an explicit status are treated as - `source_published`, not as verified; -- suspected typosquatting, repeated source/manifest hashes, and reported - packages move to quarantine rather than disappearing from history; -- the first production source-package write path does not require an on-chain - fee or bond, but the schema and policy hooks must allow later fee, - refundable-deposit, or challengeable-record rules for higher-risk actions. - -The capability authorisation endpoint itself must be cheap to serve. Prefer -short-lived stateless signed nonces, small request bodies, and fail-fast parsing -so attackers cannot exhaust Redis, database, or chain RPC resources by hitting -login. - -## Tutorial: End to End - -Let's walk through the complete lifecycle of a package, from authoring to verified on-chain deployment. - -### Step 1: Create a Package +For `reproducible_binary`, use profile `reproducible_build` and replace `abi` +with `build_recipe`; its contract binds the environment, deterministic command, +recipe hash, and expected artifact hash. `runtime_verifier` additionally +requires `verifier_id`, `ipc_abi`, and the IPC ABI hash. For `template`, use +profile `copy_material`, include only `source`, and encode it as a +`cellscript-template-file-map-v1` whose relative paths, contents, and hashes are +authenticated. The CLI rejects unknown contract fields, missing or duplicate +roles, malformed values, unsafe copy paths, and hashes that do not bind the +immutable objects. + +A `ckb_executable` may also set `build.reproducible = true`, include a +`build_recipe` object, and use the same `reproduction` contract. Deployment and +reproducibility are independent axes: the former is proven by a live mainnet +Cell, while the latter still needs reproducible-build evidence beyond a recipe +declaration. + +When `security.status = "audited"`, the contract must include +`security.audit_report_hash` and the bundle must contain exactly one non-empty +`audit_report` object with that CKB Blake2b-256 hash. The status is still a +publisher declaration; the binding prevents the referenced report from being +swapped or omitted. ```bash -cellc init amm_pool --namespace cellscript +cellc publish --artifact-manifest Artifact.toml --dry-run +cellc publish --artifact-manifest Artifact.toml ``` -This generates a `Cell.toml` with `namespace = "cellscript"` and a starter source file. At this point, there's no `Cell.lock`, no `registry.json`, no `Deployed.toml`. The package is purely local. - -### Step 2: Add Dependencies - -Edit `Cell.toml` to add a registry dependency: - -```toml -[dependencies] -token = { version = "0.3.0", namespace = "cellscript" } -``` - -When you build, the resolver kicks in: - -```mermaid -graph LR - A["cellc build"] --> B["Read Cell.toml"] - B --> C["Query discovery index"] - C --> D["cellscript/token.json"] - D --> E["Clone source repo"] - E --> F["Read registry.json"] - F --> G["Verify source_hash"] - G --> H["Write Cell.lock"] +The independent verifier checks the profile-specific object set and recomputes +the published hashes. Generic executable and copy bundles are `hash_bound`; this +does not claim executable semantics, reproducibility, or a security review. A +CellScript CKB bundle may opt into the 0.24 structural boundary by providing +all of `metadata`, `lowering_record`, and `source_map` in addition to source, +executable, and ABI. Partial sidecar sets fail closed. The separate +least-privilege artifact worker runs the compiler-independent checker and emits +`structurally_verified` evidence with checker version, policy schema, and +report hash. That evidence maps to accepted `verification_status = verified`, +but remains neither source equivalence nor deployment evidence. A reproducible +build is marked `evidence_required` until +appropriate build evidence exists; merely uploading output bytes does not prove +reproducibility. + +### LS-IDL Lock Script interface + +A deployable `ckb_executable` with `ckb.script_role = "lock"` may add a +`cellscript-registry-ls-idl-interface-v1` contract. The ABI object is the exact +LS-IDL JSON byte sequence; its SHA-256 must match the contract and the +executable's final 32 bytes. Use `cellc artifact ls-idl validate`, `bind`, and +`bundle` to construct this relationship. The normal and least-privilege +verifiers independently enforce it. + +This adds a discoverable interface identity, not a semantic implementation or +security claim. The full schema, supported field encodings, compatibility +vectors, and operator boundary are in +[`CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md`](CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md). + +## Accepting Reproduction Evidence + +The Registry never executes an arbitrary publisher build recipe in its API +process. Independent builders execute the signed recipe in the declared +environment and emit bounded reports: + +```json +{ + "schema": "cellscript-reproduction-report-v2", + "builder_id": "builder-a", + "trust_domain": "independent-org-a", + "builder_public_key": "p256-spki:", + "environment": "", + "source_hash": "", + "build_recipe_hash": "", + "artifact_hash": "", + "build_log_hash": "", + "generated_at": "2026-08-02T00:00:00Z", + "signature": { + "algorithm": "p256-sha256", + "signature": "" + } +} ``` -The discovery index tells the resolver where to find the source. The `registry.json` inside the source repo provides version metadata. The `source_hash` in that metadata is verified against the actual source tree. If anything has been tampered with, the build fails. - -### Step 3: Publish +Generate each report next to the reproduced artifact and bounded build log: ```bash -cellc auth capability create --principal-id --scope publish:cellscript/amm_pool --expires 90d --json > capability-payload.json -cellc auth capability submit --payload capability-payload.json --joyid-signature joyid-signature.json -cellc publish +# Run once inside each independent builder's own administrative domain. +cellc auth reproducer create \ + --builder-id builder-a \ + --trust-domain independent-org-a \ + --json > reports/builder-a-enrollment.json + +cellc artifact reproduction-report acme/vault-lock@1.0.0 \ + --artifact target/vault-lock \ + --build-log reports/builder-a.log \ + --builder-id builder-a \ + --trust-domain independent-org-a \ + --builder-key-id cap_ \ + --builder-public-key 'p256-spki:' \ + --output reports/builder-a.json ``` -If `--capability-pubkey` is omitted, `cellc auth capability create` generates a -local P-256 capability key and stores the private key in the OS keychain. The -printed payload is the exact JoyID challenge to sign and submit to the registry -write API through `cellc auth capability submit`. Once the capability is -registered, `cellc publish` computes a source hash from the current source tree, -reads build artifacts for their hashes, signs the concrete publish payload with -the capability key, uploads an immutable source snapshot, and submits the -version entry to the registry. A successful publish returns the canonical -package URL and creates an entry that is immediately addressable, usually with -`source_published` or `indexed_pending` visibility. - -Revocation uses the same challenge/submit boundary: - -```bash -cellc auth capability revoke --principal-id --capability-key-id --json > revoke-payload.json -cellc auth capability revoke --payload revoke-payload.json --joyid-signature joyid-signature.json --reason "rotate delegated key" -``` +The create command emits a public `policy_builder` record and stores the +corresponding private key in that builder's OS keychain. For CI enrollment, +on Unix, pass `--private-key-output ` to write PKCS#8 base64 into a +new mode-0600 file, move its value into that builder's secret manager as +`CELLSCRIPT_REPRODUCER_PRIVATE_KEY_PKCS8_B64`, and do not send the file to the +Registry operator. Only the public `policy_builder` record crosses the trust +boundary. -For CI or external signers, the publish payload can be made explicit: +Create the operator promotion payload locally: ```bash -cellc publish --print-payload --json > publish-payload.json -# sign .canonical_payload with the authorised capability private key -cellc publish --payload publish-payload.json --capability-signature +cellc artifact reproduction-evidence acme/vault-lock@1.0.0 \ + --report reports/builder-a.json \ + --report reports/builder-b.json \ + --output reproduced-build-promotion.json ``` -For CI retry safety, pin the publish retry key: +The CLI verifies every report signature and requires distinct builder IDs, +public keys, and trust domains. The API additionally requires each builder to +match `REGISTRY_REPRODUCER_POLICY_JSON` and enforces its configured minimum +trust-domain count. Both layers require exact matches for the signed environment, +source, recipe, executable, and build log. The promotion also references the +accepted `verified_build` evidence. Accepted evidence records the canonical +policy SHA-256 and the threshold used for that decision, so later policy +rotation cannot rewrite the historical trust boundary. A reproducible artifact +stays `evidence_required`, and deployment admission fails, until +`reproduced_build` evidence is accepted. + +Distinct policy labels are necessary but cannot prove organizational +independence. The production operator must obtain each public key from a builder +under separate administrative control and private-key custody. Creating two +keys inside the Registry operator's own infrastructure and assigning different +`trust_domain` strings does not satisfy this model. Readiness proves that the +policy is well-formed and that its P-256 keys are importable; it does not attest +who controls those keys. + +## Consuming Other Artifacts + +Generic artifacts never pass through `cellc install`. Use the explicit +consumer commands: ```bash -cellc publish --payload publish-payload.json \ - --capability-signature \ - --idempotency-key ci-cellscript-amm-pool-1.2.0 +cellc artifact fetch acme/vault-lock@1.0.0 --output vault-lock.bundle.json +cellc artifact verify --bundle vault-lock.bundle.json --receipt vault-lock.bundle.json.receipt.json +cellc artifact pin acme/vault-lock@1.0.0 --output Artifacts.lock --accept-hash-bound +cellc artifact copy acme/starter@1.0.0 --destination ./new-project --accept-hash-bound +cellc artifact reproduction-evidence acme/vault-lock@1.0.0 --report builder-a.json --report builder-b.json --output reproduced-build-promotion.json +cellc artifact record-deployment acme/vault-lock@1.0.0 --code-hash --hash-type data1 --dep-type code --tx-hash --index 0 --capability-key-id +cellc artifact cell-dep acme/vault-lock@1.0.0 --output CellDep.json --accept-hash-bound --rpc-url https://mainnet.ckb.dev/rpc +cellc artifact set-availability acme/vault-lock@1.0.0 --status yanked --reason "security advisory" --capability-key-id +cellc artifact commitment acme/vault-lock@1.0.0 --output RegistryCommitment.json ``` -For auditability and offline mirrors, the same version entry can also be written -to `registry.json` and checked into the source repository: +`fetch` checks the immutable object's SHA-256 identity and every CKB object +hash. `verify` repeats those checks offline from the receipt. `pin` records the +exact Registry identity and requires an explicit trust decision for +integrity-only evidence. `copy` is no-overwrite and rejects traversal, +platform-specific, duplicate, or unauthenticated paths. `cell-dep` requires an +attached RPC-verified mainnet deployment and preserves the DepGroup container +and resolved code-member identities. Before writing `CellDep.json`, it queries +mainnet again, rejects a spent deployment or resolved code member, checks the +RPC chain identity, and rebinds `hash_type` / `dep_type` to the signed profile +contract. It never turns an `undeployed` release into a CellDep. + +`record-deployment` derives the artifact/data identity from the signed Registry +release, signs a payload for the network fixed by the selected Registry +environment, and sends it to the API for live-Cell verification. Production is +mainnet-only; Pudge is testnet-only. Both publisher and recovery paths +reject deployment modes that differ from `profile_contract.ckb`. + +`set-availability` is the publisher control-plane path used by the Manage UI. +It signs a short-lived, nonce-protected capability payload; publishers may set +`active`, `deprecated`, or `yanked`, while administrative quarantine remains a +separate privileged action. + +`commitment` verifies the Registry response against the locally fetched signed +release, then writes the canonical `cellscript-registry-commitment-v1` payload, +CKB Blake2b commitment, compact `CSREGv1 || hash` Cell data, fixed Registry +Type/Lock hashes, and a wallet-ready mainnet transaction intent. The wallet, +not the Registry or CLI, completes capacity, inputs, change, fee, witnesses, +signatures, and broadcast. + +The Registry accepts an on-chain commitment only after reading a sufficiently +confirmed live mainnet Cell and matching its exact data, configured commitment +Lock, and configured Registry Type Script. Readiness separately resolves and +checks the Type and Lock code CellDeps. Scheduled maintenance uses an exact Type +Script indexer query plus the `CSREGv1` prefix to discover commitments and +reconcile their live lifecycle. + +This contract indexes code/artifact evidence; it does not take ownership of +application business Cells. Business state remains governed by the +application's own Lock/Type Scripts, schemas, and replacement transactions. + +## Publisher Authorisation + +The preferred interactive path is `cellc publish --authorise`. The CLI creates +the delegated P-256 key, stores it as pending in the OS keychain, and opens a +15-minute exact-coordinate browser session. The browser receives only a +fragment token and the public capability request. After a supported wallet +signs the Registry-built challenge, one transaction consumes the nonce, +registers the publishing key, claims or reviews the namespace, completes the +session, and appends audit events. The polling CLI activates the key only when +the Registry returns the matching key ID, then resumes the original publish. + +Completed or review-pending sessions remain readable for 24 hours. A same-tab +refresh preserves the browser token, while completion or expiry removes it. +Only Registry-confirmed cancellation or pending-session expiry removes a +pending local key; a local polling timeout preserves it for recovery. + +The explicit capability-create/submit and namespace-claim commands remain the +auditable manual path for CI and external wallet handoff. CCC-detected signers +connect directly; directory-only wallets link out and require a compatible +`wallet-signature.json`. Neither path accepts mnemonic words. Production has no +network selector; the Pudge site and API are separate testnet-only origins. + +## Public Reads -```bash -cellc publish --offline -git add registry.json -git commit -m "publish v1.2.0" -git tag v1.2.0 -git push --tags -``` - -Notice the distinction: public registry publication is authenticated by -namespace/package permission and publisher credentials; Git metadata is the -audit/mirror path. A new package may still need a namespace/package claim or -discovery entry before the first publish. Version updates do not require a PR to -someone else's source repository, but they do require the publisher credential -to carry the correct scope. - -### Step 4: Build and Record Deployment Identity - -0.19 Phase 1 closes the local identity loop before live-chain verification. The -build writes artifact and metadata identity into `Cell.lock`; deployment facts -are recorded in `Deployed.toml`; `cellc registry verify` checks that the -off-chain deployment record matches the locked build/package identity. - -```mermaid -graph TB - B["cellc build
→ RISC-V ELF artifact"] --> DP - DP["Cell.lock
→ build identity"] --> DEP - DEP["Deployed.toml
→ off-chain deployment facts"] --> VFY - VFY["cellc package verify
+ cellc registry verify"] - VFY -. "0.20 live gate" .-> LIVE["get_live_cell
data_hash / CellDep proof"] +```text +GET /health +GET /ready +GET /v1/artifacts +GET /v1/artifacts/:namespace/:name +GET /v1/artifacts/:namespace/:name/releases/:release/evidence +GET /v1/artifacts/:namespace/:name/releases/:release/commitment +GET /v1/ckb/scripts/:code_hash/interfaces/ls-idl?network=:network&hash_type=:hash_type[&data_hash=:data_hash] +GET /idl/:code_hash +GET /artifacts/:namespace/:name/releases/:release.json +POST /v1/artifacts/:namespace/:name/releases +POST /v1/artifacts/:namespace/:name/releases/:release/deployments +POST /v1/artifacts/:namespace/:name/releases/:release/availability +POST /v1/authorisation-sessions +GET /v1/authorisation-sessions/:session_id +POST /v1/authorisation-sessions/:session_id/challenge +POST /v1/authorisation-sessions/:session_id/complete ``` -Headless deploy planning and adapter transaction construction can exist as -supporting evidence, but 0.19 does not require live RPC reads or committed -chain cells for the registry acceptance gate. Live `get_live_cell` verification -is the 0.20 handoff. - -### Step 5: Cross-Verify All Three Layers +The list endpoint accepts `q`, `namespace`, `kind`, `verification`, +`deployment`, `availability`, `limit`, and `offset`. Without an explicit +`verification` filter, public discovery includes only accepted verification +states and excludes `pending` / `rejected`. Pagination offsets count package +coordinates, not version rows. Static release objects and +immutable bundles are served separately from the write database so consumers +can hash-verify and cache them independently. -After build/deployment recording, you can verify the Phase 1 identity chain: +Example discovery request: ```bash -cellc package verify # source_hash matches -cellc registry verify # build/deployment facts match Cell.lock -cellc registry edit --yank 1.2.0 --replaced-by 1.2.1 +curl --fail 'https://api.registry.cellscript.dev/v1/artifacts?kind=deployable_contract&deployment=chain_verified' ``` -Or programmatically: +The website exposes Registry, Submit, and API as peer tabs. Detail pages show +artifact kind, consumption mode, all three state axes, release hashes, +verification evidence, and mainnet deployment evidence without pretending +that every artifact is installable. + +## Fail-Closed Rules + +- Unknown kinds, profiles, languages, object roles, and state values fail. +- LS-IDL lookup returns only an active, public, chain-verified deployable Lock + Script with a schema-valid, raw-byte SHA-256/suffix-bound interface; type-hash + lookup requires `data_hash`, and ambiguous candidates fail. +- Identifiers are 1–64 lowercase letters or digits; `_` and `-` are allowed + only between characters. +- A source dependency resolver rejects every non-CellScript profile. +- A CKB deployment requires prior verified-build evidence. +- A reproducible CKB deployment additionally requires accepted + `reproduced_build` evidence. +- Deployment evidence must match the published executable hash and a live + mainnet Cell. +- Quarantined releases are not returned by public detail or evidence routes. +- The database admits positive identity/state atomically before publishing its + mutable static mirror. Suppressive states are mirrored first to fail closed; + other mirror failures are audited and retried by verification sync, so an + uncommitted release or deployment is never advertised as current. +- State transitions append evidence; they do not mutate hash identity. +- An unconfigured, partially configured, spent, or insufficiently confirmed + Registry Type/Lock Script and CellDep set cannot produce a wallet transaction + intent or current commitment. + +## Validation + +Registry changes are covered by the repository gates: -```rust -// Package Identity: source_hash -let computed = compute_source_hash(&pkg_dir).unwrap(); -assert_eq!(computed, read_lock.package.source_hash.as_deref().unwrap()); - -// Build Identity: artifact_hash -let lock_artifact = read_lock.package_build.as_ref().unwrap().artifact_hash.as_ref().unwrap(); -let deployed_artifact = read_deployed.build.as_ref().unwrap().artifact_hash.as_ref().unwrap(); -assert_eq!(lock_artifact, deployed_artifact); +```bash +./scripts/cellscript_gate.sh dev +./scripts/cellscript_gate.sh ci ``` -These assertions verify that the source has not changed since publishing and -that the deployment record still names the build artifact that was compiled. -0.20 adds the live-chain assertion that the on-chain cell contains the exact -binary named by the deployment record. - -## Design Rationale: Why Git, Why GitHub, Why Now - -A few design decisions deserve more explanation. - -**Why a registry write API at all?** Because `cellc publish` must mean -"publish to the registry". If publish only writes a local file and asks the user -to push Git manually, package authors cannot tell whether the package exists in -the public registry. The write API gives us one authoritative admission point -for namespace ownership, scoped credentials, quotas, yanking, quarantine, and -abuse handling. - -**Why keep Git/static metadata?** Because Git still solves distribution, -auditing, mirroring, offline resolution, and historical inspection well. The -public registry service is the write authority; static indexes, `registry.json`, -source tags, and mirrors are the read/audit surface that clients can cache and -verify. A monorepo index should not become a bottleneck for every version -publish. - -**Why GitHub examples?** We're not locked into GitHub. Discovery maps to source -URLs, and those URLs can point to any Git host. GitHub appears in examples -because much of the CKB ecosystem already develops there. Self-hosted sources -remain valid when the registry entry carries a cloneable URL and verifiable -hashes. - -**Why off-chain deployment records instead of on-chain?** CKB capacity costs make on-chain source-package storage unattractive. A 5KB RISC-V ELF binary requires about 541 CKB of capacity just for the code cell. Storing version metadata, schema manifests, and ABI indices on-chain would multiply that cost for no consensus benefit — these are developer artifacts, not runtime state. The chain should record compact deployment facts (CellDep, OutPoint, data_hash), not replace the entire source distribution system. - -**What about the proxy?** The public registry read path should already behave -like a proxy: static JSON, immutable artifact URLs, CDN caching, and fallbacks to -source Git when cache entries are unavailable. The proxy/cache must not rewrite -identity or bypass source/build/deployment verification. - -## The Test Suite - -Phase 1 acceptance is covered by always-on CLI and registry tests: - -**Offline Git registry**: local publish/resolve, namespace isolation, tag-pinned -source resolution, registry dependency loading, source-root hashing, and -source-hash mismatch rejection. - -**Package/build identity**: namespace initialization, build lockfile identity, -package verification, artifact/metadata/schema/ABI/constraints hash recording, -and fail-closed mismatch cases. - -**Off-chain deployment identity**: `cellc registry verify` compares deployment -facts with `Cell.lock` and fails closed in both text and JSON modes. - -`tests/e2e_registry_devnet.rs` also contains broader headless and ignored live -devnet scenarios. Those are valuable 0.20 candidates, but live RPC / -`get_live_cell` proof is not required for the closed 0.19 Phase 1 gate. - -## What Comes Next - -Phase 1 is deliberately minimal. The public registry policy has a JoyID-rooted -publish write path, a static/cacheable source metadata read path, the -three-file separation, and the three-layer identity model. The current -local/offline fixture exercises the same metadata shape through `registry.json` -and Git tags as a mirror, audit trail, and fallback. - -The write service is the public admission authority for `cellc publish`, -namespace/package claims, yanking, maintainer management, and entry quarantine. -It stays separated from the static/CDN read path and is protected by scoped -publisher credentials, queues, quotas, and fail-fast validation. - -Publisher identity is JoyID-rooted: CCC is the connection layer for interactive -login, JoyID is the accepted publisher root identity, and daily publish -operations use delegated publisher credentials stored in the OS keychain. Audit -signatures and deployment attestations remain separate trust layers; a JoyID -signature says who published or attested, not that the contract is safe. - -Here's what still remains optional or policy-driven, and why: - -**On-chain type script index** (0.20+): An on-chain script that indexes deployments by code_hash or TYPE_ID. Useful for wallets and builders that want to discover deployments without reading off-chain files. But the CKB ecosystem hasn't demonstrated demand for this yet, and the capacity costs are real. We'll build it when it's needed. - -**Yanking and supersession**: The resolver skips `registry.json` versions marked -`yanked` when satisfying a normal version requirement, and version entries carry -`yanked_at` / `yanked_reason` / `replaced_by` metadata. When a yanked version is -reached through an exact `=x.y.z` pin, the resolver warns and suggests the -declared replacement. Remaining future work is policy and UX: who may yank, how -caches retain already-locked versions for reproducible builds, and allowing -yanked versions to resolve from a `Cell.lock` pin without re-resolution. - -The important thing is that none of these additions change the hash-bound trust -model. Adding a write service does not make transport trusted. Adding a proxy -does not change package identity. Adding on-chain indexing does not change how -`Deployed.toml` is generated. The registry's authority is admission and -discovery; verification remains hash-first and fail-closed. - ---- - -*CellScript is a domain-specific language for Nervos CKB smart contracts. The registry implementation lives in `src/package/registry.rs` and the deployment adapter in `crates/cellscript-ckb-adapter/`. The full design document is at `docs/CELLSCRIPT_PACKAGE_PROVENANCE_AND_DEPLOYMENT_IDENTITY.md`.* +The `ci` gate typechecks and tests the API, builds Node API/verifier bundles, +runs the independent Rust verifier, checks the website build, and validates the +compiler and CLI surfaces that create and consume Registry records. diff --git a/docs/CELLSCRIPT_REGISTRY_PRODUCTION_BOUNDARY_ADR.md b/docs/CELLSCRIPT_REGISTRY_PRODUCTION_BOUNDARY_ADR.md index 672db8f1..04fd73f7 100644 --- a/docs/CELLSCRIPT_REGISTRY_PRODUCTION_BOUNDARY_ADR.md +++ b/docs/CELLSCRIPT_REGISTRY_PRODUCTION_BOUNDARY_ADR.md @@ -1,422 +1,330 @@ # ADR: CellScript Registry Production Boundary -**Status**: Accepted design note +**Status**: accepted and implemented; amended 2026-08-09 for browser-session +authorisation, the isolated Pudge Sandbox, and standard CKB confirmation RPCs. -**Date**: 2026-06-23 +**Decision date**: 2026-06-23 +**Current amendment**: 2026-08-09 -**Scope**: Public CellScript source-package registry, publisher identity, -capability authorisation, write/read separation, abuse controls, resolver -visibility, and first production deployment boundary. +## Context -**Out of scope**: Code implementation, dependency selection inside the -repository, and on-chain deployment record submission. +CKB ecosystem discovery spans objects with materially different trust and use +contracts: CellScript dependency source, profile libraries, CKB-VM executables, +deployed Script Cells, reproducible tooling, and copy-only starters. Treating +all of them as “packages” hides whether an object can be installed, executed, +deployed, or only copied. Treating publication, build verification, deployment, +and availability as one status creates false assurance. -## Decision - -The production registry uses a JoyID-rooted publisher identity, a -capability-based daily publish flow, an authenticated write API, and a -static/CDN read path. `cellc publish` creates a real registry entry. Git tags, -Git URLs, and `registry.json` remain audit, mirror, fixture, and offline -fallback material; they are not the public write authority. - -The first production slice is source package publish, registry entry creation, -immutable source snapshot/mirror storage, and verification pipeline admission. -On-chain deployment attestation uses the same identity model, but it is a -separate feature-gated production slice and must not be mixed into the first -write API. - -## Product Entry - -The production frontend exposes JoyID as the publisher identity entry. The -connection layer still goes through CCC adapters so the implementation is not -coupled to JoyID SDK internals. - -Product policy: - -- users see JoyID as the only publisher root identity; -- the frontend uses CCC as the connection layer; -- the production submit page can sign `authorize_capability` payloads through - the CCC JoyID CKB signer and submit them to the registry write API; -- the backend data model stores typed principals instead of `owner = joyid`; -- no separate registry account, email account, or GitHub account is introduced. - -## Publisher Principal - -The current accepted publisher principal is: - -```text -principal_type = joyid_ckb -principal_id = -``` - -Display addresses may be stored for UI and support workflows, but they are not -unique primary keys and must not be used as the registry authority. - -The production submit flow derives the preferred `principal_id` from the JoyID -signer key as `sha256("cellscript-registry-joyid-ckb-principal-v1\n" || -key_type || "\n" || normalized_pubkey)`, encoded as `0x` + lowercase hex. The -registry verifies that the `principal_id` inside an `authorize_capability` or -revocation payload matches the JoyID signer that produced the signature. A -display address may help users recognise the account, but it is not accepted as -the ACL key. - -The principal model is intentionally typed: +The service also needs a wallet-rooted publisher identity without taking +custody of seed material, a static hash-verifiable read path, and an auditable +operator boundary. -```text -principal_type -principal_id -display_address -created_at -last_seen_at -status -``` - -Current production policy accepts only `joyid_ckb`. +## Decision -## Capability Authorisation +The production Registry uses: -`cellc auth capability create --principal-id --scope -publish:namespace/package --expires 90d` is not a generic login. It authorises -a local capability key. JoyID signs a structured capability authorisation -payload that binds the local capability public key, requested scopes, expiry, -and the normalized JoyID-CKB principal binding. +1. one public `/v1/artifacts` resource family; +2. a closed artifact descriptor for kind, verification profile, language, and + consumption mode; +3. independent verification, deployment, and availability states; +4. typed CKB wallet principals that authorise scoped delegated capabilities; +5. Postgres as the write authority and immutable static objects as the normal + content transport; +6. an isolated, profile-aware verification worker; +7. signed, live-RPC-verified CKB mainnet deployment evidence; +8. an isolated Pudge Testnet Sandbox with separate origins, storage, signing, + wallet state, RPC identity, expiry, and evidence; +9. fail-closed CellScript dependency resolution that accepts only the + `cellscript_source` + `dependency` contract. -Required authorisation payload: +There is no account-style Registry identity, no Git convention as public +resolver authority, no testnet option inside the production Registry, and no +second public package route. Pudge is a separate environment and cannot create +production deployment state. -```text -protocol: cellscript-registry-auth-v1 -action: authorize_capability -registry_origin: https://api.registry.cellscript.dev -principal_type: joyid_ckb -principal_id: -capability_pubkey: -requested_scopes: - - publish:namespace/package -capability_expires_at: -nonce: -issued_at: -expires_at: -cli_version: -``` +## Artifact Profiles -The registry verifies the JoyID signature and records the capability public key, -scope set, expiry, revocation state, principal type, and principal id. The -capability private key is generated locally by the CLI and stored in the OS -keychain. +The accepted contracts are: -The command flow is intentionally two-step: +| Kinds | Profile | Consumption | Verification boundary | +|---|---|---|---| +| source/profile library | `cellscript_source` | dependency | compile authenticated snapshot | +| runtime verifier | `ckb_executable` | TCB | source + executable + ABI + optional recipe hashes | +| deployable contract | `ckb_executable` | deployment | source + executable + ABI + optional recipe hashes | +| reproducible binary | `reproducible_build` | TCB | source + output + recipe hashes and evidence | +| template | `copy_material` | copy | authenticated file-map hash | -```bash -cellc auth capability create --principal-id --scope publish:namespace/package --expires 90d --json > capability-payload.json -# Sign capability-payload.json through the production JoyID flow exposed by CCC. -cellc auth capability submit --payload capability-payload.json --joyid-signature joyid-signature.json -``` +The coordinate is shared discovery vocabulary, not shared consumption +semantics. A caller must select on profile and consumption mode, not infer them +from a name or file extension. -Renewal repeats the same authorisation shape with a new expiry. Revocation is -also JoyID-bound and does not depend on a registry password: +## State Model -```bash -cellc auth capability revoke --principal-id --capability-key-id --json > revoke-payload.json -# Sign revoke-payload.json through JoyID. -cellc auth capability revoke --payload revoke-payload.json --joyid-signature joyid-signature.json --reason "rotate delegated key" -``` - -Daily publish uses the capability key: +Every release records: ```text -cellc publish - -> sign publish payload with capability private key - -> registry checks signature, nonce, expiry, origin, revocation, ACL, quota - -> registry admits the entry as source_published / indexed_pending -``` - -JoyID only participates when creating, renewing, or revoking a capability. It -does not sign every `cellc publish`. - -The CLI must also expose the exact publish payload for CI and external signing: - -```bash -cellc publish --print-payload --json > publish-payload.json -# sign the canonical_payload field with the authorised capability private key -cellc publish --payload publish-payload.json --capability-signature -# optional for CI retries of the same signed request -cellc publish --payload publish-payload.json --capability-signature --idempotency-key ci-ns-pkg-1.2.3 -``` - -CI may provide `CELLSCRIPT_CAPABILITY_PRIVATE_KEY_PKCS8_B64` instead of using -the OS keychain, but it still signs only the delegated capability payload. CI -must never receive the JoyID passkey or wallet secret. - -The CLI sends an `Idempotency-Key` on publish; it can derive the key from the -exact request or accept `--idempotency-key` / -`CELLSCRIPT_REGISTRY_IDEMPOTENCY_KEY`. If admission fails after reserving the -key but before the package version is accepted, the registry releases that -`processing` reservation. Because the signed publish nonce may already be -consumed, retrying with the same CI retry key requires a new publish payload and -capability signature. - -## CI Publishing - -CI publishing is part of the first production boundary. CI must not access the -JoyID private key/passkey. A maintainer creates a scoped capability: - -```bash -cellc auth capability create --principal-id --scope publish:ns/pkg --expires 90d --json > capability-payload.json -cellc auth capability submit --payload capability-payload.json --joyid-signature joyid-signature.json +verification_status = pending | hash_bound | verified | evidence_required | rejected +deployment_status = not_applicable | undeployed | deployed | chain_verified +availability_status = active | deprecated | yanked | quarantined ``` -The resulting CI credential is scoped, expiring, revocable, and limited to its -declared package/action set. A leaked CI credential must not compromise the -namespace root principal. - -## Namespace Claims And Governance - -Ordinary namespace claims are first-come-first-served with cooldown. The -registry must ship with a reserved namespace list and review hooks. - -Claims enter manual review when they match any of these categories: - -- short names; -- known brands or protocol names; -- core ecosystem names; -- obvious typosquatting or confusables; -- repeated failed claims from the same principal, IP range, ASN, or credential; -- names reported by maintainers or admins. +The axes are independent. Publication sets initial values only. Verification +and deployment claims require accepted evidence. Operator actions modify only +availability. Evidence and immutable identities are append-only. -Registry admins may reserve, approve, reject, quarantine, or override namespace -claims. The production write API exposes these operations through a -`REGISTRY_ADMIN_TOKEN`-gated admin role, and every manual operation writes an -audit record with an admin actor. +The implementation may retain a derived internal status column while migrating +the deployed schema, but public responses and frontend decisions use the three +orthogonal fields. -## Abuse And DDoS Boundary +## Publisher Principal and Capability -JoyID provides accountability. It is not the only anti-spam mechanism. - -The first production slice does not require on-chain fees and does not require a -bond. However, the schema and policy layer leaves hooks for later bond or -refundable deposit rules through `policy_hooks` and `bond_policy_hooks` tables. - -Current production abuse controls: - -- read and write paths are separated; -- write API sits behind WAF and edge rate limits; -- quotas apply per IP, ASN, JoyID principal, capability, namespace, package, and - source hash; -- principal-scoped quota and namespace-claim cooldown are counted only after - the JoyID signature has been verified, so forged payloads cannot burn another - publisher's principal quota; -- signed publish nonces are consumed before object storage writes, so replayed - publish payloads fail before expensive work; -- `Idempotency-Key` is supported for publish retries: the same logical request - replays the stored response, while the same key with different payload content - is rejected; failed pre-admission writes release a matching `processing` - reservation so CI can retry with the same retry key and a newly signed publish - payload; -- request body, metadata field, source snapshot, and artifact sizes are capped; -- duplicate source/manifest hashes are deduplicated or throttled; -- existing package versions are rejected before source snapshot writes; -- namespace claims have cooldown and review hooks; -- high-risk publishes can enter quarantine; -- expensive work goes through bounded queues; -- manual review can suppress search visibility without deleting history. - -The write path must fail fast before object storage, database writes, chain RPC, -or build workers are invoked. - -## Write API And Storage - -The preferred production stack is: +Accepted principals are: ```text -Cloudflare Pages / Workers -R2 for immutable source snapshots, mirrors, and exported static indexes -Neon Postgres for ACL, audit log, namespace ownership, capabilities, quota, -publish state, and revocation records +joyid_ckb = normalized JoyID CKB public-key binding +ckb_secp256k1 = normalized compressed secp256k1 public-key binding ``` -D1 is not the default production database. The registry needs relational -constraints, audit queries, revocation checks, namespace ownership, quota -accounting, and a publish state machine; those are better suited to Postgres for -the first production implementation. - -The first write API implementation is `services/registry-api`: - -- Cloudflare Worker entrypoint; -- Hyperdrive-bound Neon Postgres store; -- R2 source snapshot writer; -- R2 static package-version JSON writer before package-version admission; -- JoyID `verifySignature` authorisation check; -- canonical challenge binding for capability creation; -- one-time nonce consumption for capability creation, capability revocation, and - package publish; -- publish idempotency records with `processing` and `completed` states; -- P-256 capability-signature verification for daily publish; -- namespace ownership check before publish admission; -- scheduled cleanup for expired nonce, idempotency, and quota records; -- audit/event log and quota hook tables. +Display addresses may be retained for support but are not authority keys. The +API verifies scheme, canonical challenge, public-key recovery/binding, and +principal identity before storing a capability. + +The wallet root authorises a P-256 capability scoped to a namespace/artifact, +with expiry and revocation. Daily publish and deployment requests use the +delegated key. Seed phrases and private wallet keys never cross the wallet +boundary. + +For interactive first publish, `cellc publish --authorise` creates the key as a +pending keychain entry and opens a 15-minute exact-coordinate browser session. +The browser holds only a fragment token and signs a server-built challenge. +Session completion atomically consumes the nonce, registers the public key, +claims or reviews the namespace, records the terminal session state, and writes +audit events. The polling CLI activates only the matching returned key ID and +then resumes the original publish. The explicit capability and namespace +commands remain the manual/CI path. + +Capabilities do not claim namespaces implicitly. The namespace must be active +and owned by the capability principal. Reserved names may require attributed +operator review. + +## Wallet Product Boundary + +The website exposes one CKB wallet entry that opens a compact chooser. The +chooser contains the supported CKB wallet directory; CCC-discovered signers can +connect directly, and unavailable connectors link to the official wallet or +use the external signature handoff. + +The Registry does not pretend that catalog presence means runtime support. +Backend signature verification is identical for browser and external handoff +flows. Recovery phrases are never accepted by the frontend or API. + +The production network is fixed to CKB mainnet and is not shown as a selectable +control. The Pudge site is a separate testnet-only origin with separate wallet +state, not a selector value. + +## Write Path + +Release admission verifies the active capability, scope, namespace ownership, +route/payload equality, closed artifact descriptor, immutable coordinate, +manifest/source hashes, signature, nonce, idempotency record, snapshot/bundle, +and initial state claims. + +Immutable bundle and static release writes happen before database admission. +The release, verifier job, capability use, audit event, nonce, and completed +idempotency response commit transactionally. Admission reports verification as +queued; it is not verification evidence. + +Non-CellScript artifacts use an explicit `Artifact.toml` and JSON bundle. The +closed `cellscript-registry-profile-contract-v1` binds build, declared security, +CKB/ABI, verifier IPC, reproducibility, or copy semantics to the immutable +objects. The bundle is bounded to 5 MiB. Unknown fields and unknown or duplicate +roles fail closed in admission, publisher CLI, and isolated verifier. + +Deployable Lock Scripts may attach +`cellscript-registry-ls-idl-interface-v1`. It binds SHA-256 of the exact ABI +object bytes to the executable's final 32 bytes. The read API resolves those +bytes only from active chain-verified deployment evidence and preserves them +without JSON reserialisation. This is interface identity evidence, not proof of +implementation correctness or a security audit. + +## Verification Boundary + +The worker leases jobs with `FOR UPDATE SKIP LOCKED`, bounded retry, dead-letter +handling, and crash recovery. The verifier runs under resource and filesystem +bounds. + +For CellScript source it authenticates and compiles the real snapshot. For +other profiles it validates bundle identity, required roles, and published +hashes. Reproducible output remains `evidence_required` until appropriate +evidence exists. A copied template is never promoted into dependency or TCB +semantics. + +Evidence insertion and the worker publishing checkpoint commit together. +Static-object refresh happens afterward; reclaiming a crashed publishing job +repeats only the static write. + +## Mainnet Deployment Boundary + +A CKB executable begins as `undeployed`. Deployment evidence uses a separate +signed protocol and requires prior verified-build evidence. + +The production API accepts only `network = mainnet`. It calls `get_live_cell` +for the declared OutPoint and requires a live Cell whose data hash equals the published +executable hash. For Type-hash references it computes the returned Type Script +hash from canonical Molecule serialization; for data-hash references it +requires code hash and data hash equality. + +Confirmation depth comes from the standard creation-transaction path: +`get_transaction.tx_status` must report `committed` and supplies the block hash +used with the current tip. The service does not rely on a proxy-specific +`get_live_cell.block_hash` extension. + +For DepGroups, the API decodes the live container data as canonical Molecule +`OutPointVec`, loads the members, and verifies the matching live code Cell. The +container hash is not substituted for the member executable identity. + +Success appends hash-addressed evidence and sets `deployment_status` to +`chain_verified`. It does not alter verification or availability. + +The Registry may additionally commit the release/deployment tuple in a live +mainnet Cell. Canonical `cellscript-registry-commitment-v1` JSON is CKB +Blake2b-hashed into `CSREGv1 || hash` Cell data. Acceptance checks that exact +data, the commitment custody Lock hash, a Registry Type Script hash used for +chain indexing, minimum confirmation depth, and the live Type/Lock code +CellDeps. A public commitment-proof route returns the preimage, expected Cell +data, and accepted commitment evidence. The full source, ABI, build recipe, +compiler metadata, audit corpus, and publisher history remain off-chain and +content-addressed. + +The canonical Registry Type Script binds its 32-byte args to the complete +custody Lock Script hash, requires every group Cell to use that Lock, and +requires every creation, replacement, or destruction transaction to consume a +Cell under that Lock. This closes the CKB creation-authority gap: sending a new +Cell to the Registry Lock is not sufficient to manufacture an official +commitment without exercising the Registry signer. Production API readiness +also pins the Type code data hash and standard mainnet secp Lock/DepGroup. ## Read Path Production domains: ```text -registry.cellscript.dev -> static/CDN read path backed by R2 registry objects -api.registry.cellscript.dev -> authenticated write API -``` - -Staging uses `staging-registry.cellscript.dev` or an equivalent staging subdomain. - -The read path serves website pages, package metadata, cached indexes, source -mirrors, immutable snapshot URLs, and package status. It must not perform -ordinary registry reads by calling chain RPC or write API internals. -The `registry.cellscript.dev/packages/*` route is served from R2 registry -objects with CDN cache headers; it does not require Hyperdrive or write-store -access. The broader website can still be hosted as static Pages content. - -DNS availability is operational state, not an architecture requirement. The -registry design assumes the domains above once their records are live. - -## Source Snapshot Requirement - -Production must store an immutable source snapshot or mirror object for each -accepted package version. Git URL and tag are audit and fallback fields only. -They are not availability guarantees. - -The source snapshot and the static package-version JSON object must both be -persisted before the version is accepted into the registry store. If the direct -read object cannot be written, the publish must fail without recording an -accepted package version. - -Minimum source metadata: - -```text -source_url -source_tag -source_revision -source_hash -manifest_hash -snapshot_object_key -snapshot_hash -snapshot_size -created_at -``` - -The resolver and verifier still check hashes. The snapshot exists to prevent -source availability from depending on a third-party Git host. - -## Entry State And Resolver Policy - -Publish success creates an immediately addressable package-version JSON entry: - -```text -https://registry.cellscript.dev/packages/:namespace/:name/versions/:version.json +api.registry.cellscript.dev -> authenticated writes and dynamic artifact reads +registry.cellscript.dev -> immutable bundles and static release JSON +cellscript.dev/registry -> static Astro discovery and publishing UI ``` -It does not automatically make the package eligible for default resolver -selection, default search, recommendations, or production-visible lists. +The testnet sandbox uses `api.testnet.registry.cellscript.dev` and +`testnet.registry.cellscript.dev` with independent storage and signing state. +Its records leave discovery after 72 hours and source objects are deleted after +a 24-hour grace period; this does not erase Pudge chain history. -State boundary: +Static release objects use: ```text -source_published -> direct URL available, author dashboard visible -indexed_pending -> async validation/indexing pending -verified_build -> basic source/build checks passed -deployed -> off-chain deployment evidence attached and verified -on_chain_attested -> feature-gated later slice, not first production write API -deprecated -> retained, default selection suppressed -yanked -> retained, default selection suppressed -quarantined -> retained, public visibility restricted +https://registry.cellscript.dev/artifacts/:namespace/:name/releases/:release.json ``` -Default resolver policy: - -- default resolution must not auto-select `source_published`, - `indexed_pending`, or `quarantined` entries; -- missing status in a mirrored `registry.json` is treated as unverified - (`source_published`), not as `verified_build`; -- direct install may allow unverified entries only with an explicit flag such as - `--allow-unverified`; -- quarantined entries require a stronger explicit flag such as - `--allow-quarantined`; -- default search, recommendations, and production-visible package lists show - only entries that passed the required baseline checks; -- exact pins keep reproducibility, but warning and explicit-allow policy must - make risk visible to the caller. - -## Yank, Quarantine, Deprecation, And Deletion - -Package versions are not hard-deleted from registry history. - -Allowed state changes: - -- `yanked`: maintainer/admin action that suppresses default selection while - preserving exact-pin history; -- `deprecated`: maintainer/admin action that points users to a replacement or - successor; -- `quarantined`: admin/review action for abuse, malware, typosquatting, legal, - or high-risk content. - -Suppressive admin transitions (`deprecated`, `yanked`, `quarantined`) must make -the static read object conservative before committing the write-store status -change. This prevents an incident response where the database says -`quarantined` but `registry.cellscript.dev` still serves an older accepted -status. - -If content is illegal, security-sensitive, or clearly malicious, the registry -may hide public access to the artifact or source snapshot. Even then it must -retain: - -- tombstone record; -- package/version history; -- audit log; -- action reason; -- actor identity; -- timestamps; -- replacement or incident reference when available. - -## Deployment Attestation Boundary - -The first production slice does not submit on-chain deployment records and does -not make on-chain attestation part of the initial write API. - -Deployment evidence may appear in schemas as optional metadata, and local -verification can continue to use `Deployed.toml` and `Cell.lock`. On-chain -attestation uses the same JoyID/capability identity model, but it is -feature-gated as a second production slice. - -## Observability And Audit - -Production must include: - -- request id on write API responses; -- audit log for publish, namespace claim, capability create/revoke, quarantine, - review, yank, deprecation, and admin override; -- token-gated audit event read path for review, incident response, and - production debugging; -- publish event log; -- admin action log; -- auth failure log; -- capability usage log, including `last_used_at` updates and - `capability.used` audit events for accepted publish operations; -- nonce replay rejection log; -- namespace claim event log; -- quarantine transition log; -- rate-limit metrics; -- quota metrics; -- maintenance cleanup event log; -- verification queue metrics; -- source snapshot/mirror metrics. - -Logs must be queryable by request id, package coordinate, namespace, principal -id, capability key id, and admin actor. - -## Non-Goals - -- Do not introduce a separate registry account. -- Do not expose generic wallet selection in the production publisher UI. -- Do not bind the backend data model to JoyID SDK-specific fields. -- Do not make JoyID a standalone anti-spam system. -- Do not depend on Git availability for production package availability. -- Do not make on-chain deployment attestation part of the first production write - API. -- Do not use hard delete as ordinary package lifecycle management. +LS-IDL adds a dynamic, chain-identity read at +`/v1/ckb/scripts/:code_hash/interfaces/ls-idl` and the upstream-compatible +`/idl/:code_hash` alias. `hash_type=type` requires a data hash, and ambiguity +fails closed rather than selecting one deployment. + +The static origin does not require Postgres. Objects include immutable bundle +identity, artifact descriptor, all state axes, and accepted evidence. Consumers +verify object, file, source, build, and deployment hashes independently. + +Public list/detail/evidence routes suppress quarantined releases. The API list +supports explicit kind, verification, deployment, availability, namespace, +query, and pagination filters. + +Generic consumers use explicit `cellc artifact` operations. Fetch/verify check +the receipt and all immutable identities; pin records TCB/deployment inputs; +copy safely materializes only an authenticated file map; record-deployment +submits evidence to the network fixed by the selected Registry environment; +CellDep generation requires attached RPC evidence; +commitment generation produces the canonical chain payload. Generic artifacts +never flow through dependency installation. + +## Resolver Boundary + +`cellc install` resolves through the public artifact API and rejects profiles +other than `cellscript_source` or consumption modes other than `dependency`. +The resolver downloads the immutable source snapshot, verifies its object and +file identities, and only then materializes it. + +Unverified releases require an explicit `--allow-unverified`; quarantined +releases are absent from public reads and require operator remediation rather +than accidental fallback. Resolver failure never falls back to a conventional +Git URL. Path and explicit Git dependencies remain independent user-selected +dependency sources. + +## Abuse and Operations + +The service enforces bounded bodies, per-IP/ASN/principal/capability/artifact +quota hooks, namespace claim cooldown, reserved-name policy, signed one-use +nonces, and idempotency conflict detection. Successful and rejected sensitive +actions are attributable through the audit log. + +Admin availability changes accept only `active`, `deprecated`, `yanked`, and +`quarantined`. Generic admin mutation cannot manufacture build or deployment +assurance. Evidence-specific recovery paths validate identity and predecessor +evidence. + +API and static responses use HSTS, no-sniff, anti-framing, no-referrer, +restrictive browser permissions, and deny-all JSON CSP. Postgres is internal; +static serving is read-only. + +## Deployment Choice + +The live self-hosted slice uses Postgres 17, Node 22, an isolated verifier, +persistent object storage, and read-only nginx behind the production TLS proxy. +Cloudflare Worker, Hyperdrive, Neon, and R2 remain a supported equivalent +deployment shape. + +Migrations are additive after the frozen `0001` baseline. The artifact-model +migration intentionally refuses to transform non-empty legacy release data +because no released public contract exists that would justify a lossy mapping. + +Migration `0010_ls_idl_interfaces.sql` adds only a partial functional index for +eligible deployment evidence; exact release, bundle, ABI, digest, and suffix +checks still run on every response. + +Readiness covers database/object access, admin configuration, and the verifier +heartbeat. Backups contain a Postgres custom dump, object archive, image +identity, and checksum manifest; restores are rehearsed into empty volumes +before traffic cut-over. + +## Consequences + +Benefits: + +- broad CKB discovery without weakening CellScript dependency safety; +- deployed and undeployed executables are distinguishable; +- publication cannot masquerade as verification; +- mainnet deployment claims are independently checked against live Cells; +- static content survives write-database incidents; +- wallet authority stays outside the Registry. + +Costs: + +- publishers of generic artifacts must construct an explicit bundle; +- reproducibility needs evidence beyond uploaded bytes; +- deployment recording requires live mainnet RPC availability; +- internal storage still carries derived compatibility fields during the + additive migration. + +## Rejected Alternatives + +- **One `status` field**: conflates assurance, deployment, and availability. +- **Infer artifact type from content**: ambiguous and unsafe for resolution. +- **Treat every entry as installable**: permits executable/template confusion. +- **Git convention as resolver authority**: conflates naming with ownership and + availability. +- **Store the full evidence corpus on chain**: expensive and unnecessary; the + chain should carry runtime commitments while full evidence remains + content-addressed off chain. +- **Accept testnet deployment records in production**: creates a misleading + production state whose deployed status is used for mainnet discovery. Pudge + is instead isolated by origin, storage, signer, wallet state, RPC identity, + expiry policy, and build. diff --git a/docs/CELLSCRIPT_RUNTIME_ERROR_CODES.md b/docs/CELLSCRIPT_RUNTIME_ERROR_CODES.md index d088c040..469714b2 100644 --- a/docs/CELLSCRIPT_RUNTIME_ERROR_CODES.md +++ b/docs/CELLSCRIPT_RUNTIME_ERROR_CODES.md @@ -14,9 +14,12 @@ Use the error name first when debugging. Numeric codes are retained for VM, wallet, explorer, and acceptance-script compatibility. The table was introduced in compile metadata schema 30 and is emitted by the -current schema 55 under +current metadata schema 58 under `constraints.runtime_errors`, so `cellc constraints`, `cellc check --json`, and sidecar metadata all expose the same machine-readable registry. +The verified lowering record also identifies mapped runtime-error exits, and +`cellc test` negative scenarios must match both the numeric code and stable +name under the selected execution backend. When a CLI failure can be tied to this registry, stderr uses the same `error[E####]` code and points to `cellc explain E####`. diff --git a/docs/CELLSCRIPT_SYNTAX_COMBO_AUDIT_METHODOLOGY.md b/docs/CELLSCRIPT_SYNTAX_COMBO_AUDIT_METHODOLOGY.md index 2ce6ef10..615dbf99 100644 --- a/docs/CELLSCRIPT_SYNTAX_COMBO_AUDIT_METHODOLOGY.md +++ b/docs/CELLSCRIPT_SYNTAX_COMBO_AUDIT_METHODOLOGY.md @@ -83,8 +83,9 @@ The repository includes the first executable runner: ```text scripts/cellscript_syntax_combo_audit.sh -scripts/cellscript_syntax_combo_audit.py +crates/cellscript-tools/src/syntax_combo.rs tests/syntax_combo/matrix.toml +tests/syntax_combo/cases.json tests/syntax_combo/seeds/*.cell ``` diff --git a/docs/CELLSCRIPT_VERIFIED_ARTIFACT_BOUNDARY.md b/docs/CELLSCRIPT_VERIFIED_ARTIFACT_BOUNDARY.md new file mode 100644 index 00000000..dc6b8fa3 --- /dev/null +++ b/docs/CELLSCRIPT_VERIFIED_ARTIFACT_BOUNDARY.md @@ -0,0 +1,152 @@ +# CellScript Verified Artifact Boundary + +**Status**: implemented on the 0.24 development line + +**Schemas**: `cellscript-verified-lowering-record-v1`, +`cellscript-source-artifact-map-v1`, and +`cellscript-artifact-checker-policy-v1` + +**Metadata schema**: 58 + +## Purpose + +Every CKB RISC-V ELF build now emits two canonical sidecars in addition to the +artifact and compile metadata: + +```text +build/main.elf +build/main.elf.meta.json +build/main.elf.lowering.json +build/main.elf.sourcemap.json +``` + +The lowering record is a stable audit boundary between typed compilation and +final machine layout. The source map binds source spans and lowering block IDs +to final instruction ranges. Both are hash-bound into compile metadata and are +validated immediately after compilation. + +The sidecars do not claim complete source-to-machine semantic equivalence. +Their explicit claims are `binding-verified` for the lowering record and +`structurally-verified` for machine code. + +## Independent Checker + +`crates/cellscript-artifact-checker` has no production dependency on the +CellScript parser, resolver, type checker, IR, optimizer, assembler, or code +generator. It accepts artifact bytes, compile metadata, one canonical lowering +record, one canonical source map, and explicit policy budgets. + +The checker is an independently publishable crate because the published +`cellscript` crate uses it as a production dependency. Release tooling must +publish the exact checker version before the matching compiler version; CI +verifies the same graph offline through an exact local crates.io patch. + +The checker independently recomputes and validates: + +- schema versions, unknown-field rejection, canonical JSON, counts, ordering, + uniqueness, and domain-separated hashes; +- entry, block, CFG, reachability, call-depth, recursion, frame, stack-slot, + typed ABI, capability, and ProofPlan relationships; +- ELF64 little-endian RISC-V identity, exact static sections, read/execute + segment policy, entry and text/rodata bounds, and absence of dynamic or + relocation state; +- the bounded RV64 instruction set emitted by CellScript, canonical direct + calls, aligned branch/call targets, machine terminators, stack-pointer + adjustments, return-path stack restoration, and declared syscalls; +- every mapped block digest and every source-map range against final ELF bytes; + and +- compiler, source, profile, artifact, lowering-record, and source-map identity + agreement. + +Declared unreachable machine blocks are not silently treated as reachable. +The record carries a `reachable` bit and the checker recomputes it from every +declared entry. + +## Default Budgets + +The default v1 policy caps each artifact, lowering record, and source map at +4 MiB; entries at 2,048; blocks and proof records at 65,536; edges at 262,144; +instructions at 1,048,576; call depth at 256; declared stack frames at 1 MiB; +source-map intervals at 65,536; and one diagnostic at 16 KiB. A consumer may +apply a stricter compatible policy. + +Budget exhaustion is `V2400`. Input-derived counts are checked before graph +traversal, diagnostic text is bounded, and invalid input must return an error +instead of panicking. + +## Stable Rejection Codes + +| Code | Boundary | +| --- | --- | +| `V2400` | policy budget exceeded | +| `V2401` | malformed JSON | +| `V2402` | non-canonical JSON | +| `V2403` | unsupported schema or overclaimed verification state | +| `V2404` | non-canonical ordering or duplicate identity | +| `V2405` | referential-integrity failure | +| `V2406` | CFG, reachability, runtime-exit, or terminator failure | +| `V2407` | ABI, frame, stack-slot, or stack-pointer failure | +| `V2408` | ProofPlan coverage failure | +| `V2409` | artifact identity mismatch | +| `V2410` | compile-metadata or compatibility-profile mismatch | +| `V2411` | invalid ELF format | +| `V2412` | invalid or prohibited ELF section/link state | +| `V2413` | instruction outside the checker policy | +| `V2414` | decoded control-flow target or machine terminator mismatch | +| `V2415` | mapped block digest mismatch | +| `V2416` | source-map identity, range, path, or coverage failure | +| `V2417` | syscall declaration or bounded-call contract failure | +| `V2418` | recursion or call-depth policy failure | + +The deterministic mutation corpus in `tests/artifact_checker.rs` exercises all +stable rejection codes. It is a regression corpus, not a proof of complete +semantic equivalence. + +## CLI Verification + +For an ELF build, `verify-artifact` loads the default sidecars automatically: + +```bash +cellc verify-artifact build/main.elf --json +``` + +Use `--lowering-record` and `--source-map` only when the sidecars use custom +paths. The JSON report keeps these states separate: + +- `binding_verification`; +- `structural_verification`; +- `lowering_record_verification`; +- `ckb_vm_evidence`; +- `chain_evidence`; and +- `semantic_equivalence_claimed`. + +The checker does not execute CKB-VM and does not query a chain. A successful +structural report therefore leaves CKB-VM as `not-executed`, chain evidence as +`not-provided`, and semantic equivalence as `false`. + +## Registry Boundary + +The Registry preserves generic Rust/C/JavaScript CKB bundles as `hash_bound` +when they provide only `source`, `executable`, and `abi`. A bundle that opts +into CellScript structural verification by including any verified sidecar must +provide all of `metadata`, `lowering_record`, and `source_map`; partial sets +fail closed. Artifact-only admission runs +`cellscript-registry-artifact-verify`, whose normal dependency graph contains +the standalone checker but not the CellScript compiler. A +`structurally_verified` result records checker version, policy schema, and +checker-report hash. + +Compiler-backed source-package verification remains a separate worker and a +separate trust state. Structural verification is not a security audit and is +not deployment or chain evidence. + +## Compatibility Rules + +- Unknown fields and future schema versions fail closed. +- Absolute and parent-traversing source paths are rejected. +- Raw `CSARGv1` witness compatibility is rejected; the compatibility profile + must use canonical `WitnessArgs.input_type` placement. +- Assembly output has no verified-artifact sidecars and reports the boundary as + not applicable. +- Consumers must bind all four files from the same build. Mixing a valid ELF, + metadata file, lowering record, or source map from different builds fails. diff --git a/docs/CELLSCRIPT_WEBSITE_PARADIGM_UPGRADE_RFC.md b/docs/CELLSCRIPT_WEBSITE_PARADIGM_UPGRADE_RFC.md index b962dbbf..2eacc89b 100644 --- a/docs/CELLSCRIPT_WEBSITE_PARADIGM_UPGRADE_RFC.md +++ b/docs/CELLSCRIPT_WEBSITE_PARADIGM_UPGRADE_RFC.md @@ -2,12 +2,15 @@ ## Status -Implemented across the 0.20-0.21 line for the website playground, WASM -metadata-only compile path, multi-file browser workspace, and agent-facing -documentation surface. Path B, full ELF generation inside the browser WASM -bundle, remains deferred. +Implemented across the 0.20-0.23 line for the website playground, WASM +metadata-only compile path, multi-file browser workspace, agent-facing +documentation surface, and recoverable browser workbench. The 0.23 workbench +persists workspace/panel state, retains explicitly stale last-valid output, +restarts a failed compiler Worker, and derives Cell Flow plus Inspector views +from metadata. Path B, full ELF generation inside the browser WASM bundle, +remains deferred. -Updated: 2026-07-11 for CellScript 0.21.0. +Updated: 2026-08-09 for the CellScript 0.23 development line. ## Goal @@ -66,13 +69,11 @@ The only WASM blocker is dependency shape, not architecture: `codegen`, `proof_plan`, `ast`, `error`) and the hash crate (`blake2b_simd`, pure Rust) are WASM-safe. -The ELF backend is also viable: `assemble_elf_internal` is a -self-contained pure-Rust RISC-V-to-ELF encoder, and -`try_external_elf_toolchain` returns `Ok(None)` in the browser -because `env::var` is empty, so the compiler falls back to the -internal assembler with no filesystem or shell access. Path B (full -ELF) is therefore possible but deferred to v2 to respect the bundle -size budget; v1 ships the metadata-only path. +The ELF backend is also viable: `assemble_elf_internal` is the sole, +self-contained pure-Rust RISC-V-to-ELF encoder and does not require +filesystem or shell access. Path B (full ELF) is therefore possible but +deferred to v2 to respect the bundle size budget; v1 ships the metadata-only +path. See §10 for the size control plan and §11 for the two-path split. diff --git a/docs/README.md b/docs/README.md index 8f4696ee..ccc157bf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -33,8 +33,16 @@ drafts. Released versions should use non-draft filenames. and live-registry line. - `docs/releases/CELLSCRIPT_0_21_RELEASE_NOTES.md` records semantic closure, authenticated evidence, the canonical CLI tree, MCP, and skill-pack scope. -- `docs/releases/CELLSCRIPT_0_22_RELEASE_NOTES.md` records the current typed - language, diagnostics, metadata schema 55, and bounded Fiber boundary. +- `docs/releases/CELLSCRIPT_0_22_RELEASE_NOTES.md` is the final stable 0.22 + record for its typed language, diagnostics, metadata schema 55, and bounded + Fiber boundary. +- `docs/releases/CELLSCRIPT_0_23_RELEASE_NOTES.md` is the final stable 0.23 + record for Edition 2026, resolved compatibility profiles, metadata schema + 57, recoverable browser tooling, and the Registry publisher-session flow. +- `docs/releases/CELLSCRIPT_0_24_RELEASE_NOTES.md` is the stable 0.24 record + for metadata schema 58, independently checked ELF/lowering evidence, + executable package scenarios, and the least-privilege Registry artifact + worker. Release candidates and planning notes should not live here unless they are the final release record. @@ -62,7 +70,11 @@ High-value active references include: - `CELLSCRIPT_CELLFABRIC_BRIDGE.md` - `CELLSCRIPT_PACKAGE_PROVENANCE_AND_DEPLOYMENT_IDENTITY.md` - `CELLSCRIPT_REGISTRY_PRODUCTION_BOUNDARY_ADR.md` for the accepted production - boundary of the JoyID-rooted public registry write/read architecture + boundary of the wallet-rooted public registry write/read architecture and + isolated Pudge testnet sandbox +- `CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md` for byte-exact LS-IDL admission, + executable suffix commitment, Script-identity lookup, tooling, and operator + boundaries - `../services/registry-api/README.md` for the Cloudflare Workers + R2 + Neon write API implementation and deployment checklist - `CELLSCRIPT_COLLECTIONS_SUPPORT_MATRIX.md` @@ -71,6 +83,9 @@ High-value active references include: - `CELLSCRIPT_LINEAR_OWNERSHIP.md` - `CELLSCRIPT_OUTPUT_BINDINGS.md` - `CELLSCRIPT_RUNTIME_ERROR_CODES.md` +- `CELLSCRIPT_VERIFIED_ARTIFACT_BOUNDARY.md` +- `CELLSCRIPT_EXECUTABLE_TEST_SCENARIOS.md` +- `CELLSCRIPT_MYELIN_0_24_HANDOFF.md` - `CELLSCRIPT_COMPILER_ERROR_CODES.md` - `CELLSCRIPT_SCHEDULER_HINTS.md` - `../examples/fiber/README.md` for the bounded 0.22 Fiber interoperability @@ -117,8 +132,10 @@ to current branch-specific evidence or forward design: - `CELLSCRIPT_GRAMMAR_GOVERNANCE_RFC.md` and `CELLSCRIPT_SYNTAX_COMBO_AUDIT_METHODOLOGY.md` for 0.19 grammar/syntax governance scope -- `CELLSCRIPT_REGISTRY_PHASE1.md` for the 0.19 package/deployment identity - registry closure and 0.20 handoff boundary +- `CELLSCRIPT_REGISTRY_PHASE1.md` for the current artifact, verification, + deployment-evidence, and public API contract +- `CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md` for the 0.24 Lock Script interface + profile and compatibility evidence - `archive/0.20/CELLSCRIPT_0_20_ROADMAP.md` for generated TypeScript action builders, live-chain registry verification, stateful flow evidence, and the bounded CellFabric JSON bridge @@ -130,6 +147,11 @@ to current branch-specific evidence or forward design: capability, and payload-enum design/implementation record - `../roadmap/CELLSCRIPT_0_22_FIBER_NATIVE_SUPPORT_PLAN.md` for the implemented bounded Fiber path and its still-pending production evidence +- `../roadmap/CELLSCRIPT_0_23_ROADMAP.md` for the frozen Edition/ABI, Registry, + native-tooling, and bounded ecosystem-evidence implementation scope +- `../roadmap/CELLSCRIPT_0_24_ROADMAP.md` for the implemented core independent + artifact checker, executable package tests, and source maps, plus the + explicitly pending external Myelin/Fiber/RGB++ evidence checkpoints ## Archive diff --git a/docs/examples/token_amm_bootstrap.md b/docs/examples/token_amm_bootstrap.md index 67210872..79fac848 100644 --- a/docs/examples/token_amm_bootstrap.md +++ b/docs/examples/token_amm_bootstrap.md @@ -183,9 +183,13 @@ which is the fillable skeleton a Rust builder can attach to the candidate transaction after replacing placeholders with concrete cell, capacity, and dry-run facts. -`cellc entry-witness` emits the raw `_cellscript_entry` payload. Do not wrap it -in `WitnessArgs.input_type` unless the CellScript source explicitly reads that -separate CKB witness surface. +`cellc entry-witness` emits the raw `_cellscript_entry` payload, not a complete +transaction witness. For the canonical +`cellscript-witnessargs-input-type-v2` placement ABI, parse or create the +selected script-group `WitnessArgs`, preserve its `lock` and `output_type` +fields, and place the payload in `input_type` before any lock-script signer +runs. Never submit the raw `CSARGv1` payload as a transaction witness and never +mutate `input_type` after signing. ## ProofPlan And Builder Assumptions diff --git a/docs/releases/CELLSCRIPT_0_13_RELEASE_SCOPE.md b/docs/releases/CELLSCRIPT_0_13_RELEASE_SCOPE.md index 85bb6269..c0ef03d0 100644 --- a/docs/releases/CELLSCRIPT_0_13_RELEASE_SCOPE.md +++ b/docs/releases/CELLSCRIPT_0_13_RELEASE_SCOPE.md @@ -280,8 +280,10 @@ cargo test --locked -p cellscript -- --test-threads=1 git diff --check ./scripts/ckb_cellscript_acceptance.sh --production --stateful-scenarios ./scripts/cellscript_ckb_stateful_scenarios.sh -python3 scripts/validate_ckb_cellscript_production_evidence.py \ - target/ckb-cellscript-acceptance//ckb-cellscript-acceptance-report.json +cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root . validate-production-evidence \ + target/ckb-cellscript-acceptance//ckb-cellscript-acceptance-report.json \ + --repo-root . ``` The stateful section is intentionally stricter than a few happy-path flows: diff --git a/docs/releases/CELLSCRIPT_0_16_1_RELEASE_NOTES.md b/docs/releases/CELLSCRIPT_0_16_1_RELEASE_NOTES.md index dabadf3a..3becb848 100644 --- a/docs/releases/CELLSCRIPT_0_16_1_RELEASE_NOTES.md +++ b/docs/releases/CELLSCRIPT_0_16_1_RELEASE_NOTES.md @@ -34,7 +34,8 @@ transactions: ```bash ./scripts/ckb_cellscript_acceptance.sh --production --stateful-scenarios -python3 scripts/validate_ckb_cellscript_production_evidence.py +cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root . validate-production-evidence --repo-root . ``` The validated evidence covers all bundled strict original scoped actions, lock diff --git a/docs/releases/CELLSCRIPT_0_16_TO_0_20_RELEASE_NOTES.md b/docs/releases/CELLSCRIPT_0_16_TO_0_20_RELEASE_NOTES.md index 3ad85a6c..cf1240c6 100644 --- a/docs/releases/CELLSCRIPT_0_16_TO_0_20_RELEASE_NOTES.md +++ b/docs/releases/CELLSCRIPT_0_16_TO_0_20_RELEASE_NOTES.md @@ -324,7 +324,8 @@ CKB production acceptance: ```bash ./scripts/ckb_cellscript_acceptance.sh --production --stateful-scenarios -python3 scripts/validate_ckb_cellscript_production_evidence.py +cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root . validate-production-evidence --repo-root . ``` Bounded local preflight without a CKB node: diff --git a/docs/releases/CELLSCRIPT_0_20_RELEASE_NOTES.md b/docs/releases/CELLSCRIPT_0_20_RELEASE_NOTES.md index 781e7aae..4be32921 100644 --- a/docs/releases/CELLSCRIPT_0_20_RELEASE_NOTES.md +++ b/docs/releases/CELLSCRIPT_0_20_RELEASE_NOTES.md @@ -208,7 +208,8 @@ For 0.20 release readiness, run: ```bash ./scripts/ckb_cellscript_acceptance.sh --production --stateful-scenarios -python3 scripts/validate_ckb_cellscript_production_evidence.py +cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root . validate-production-evidence --repo-root . ``` For a bounded local preflight without a CKB node: diff --git a/docs/releases/CELLSCRIPT_0_21_RELEASE_NOTES.md b/docs/releases/CELLSCRIPT_0_21_RELEASE_NOTES.md index 36014220..d08e8879 100644 --- a/docs/releases/CELLSCRIPT_0_21_RELEASE_NOTES.md +++ b/docs/releases/CELLSCRIPT_0_21_RELEASE_NOTES.md @@ -116,8 +116,8 @@ documentation instead of becoming a second compiler or deployment client. The repository also ships six CellScript programming skills under `docs/skills/`. The unified dev, CI, and release-auxiliary gates run -`scripts/check_cellscript_skill_pack.py` to ensure the skill pack still points -at current docs and command names. +`cellscript-tools check-skill-pack` to ensure the skill pack still points at +current docs and command names. ## Release-Candidate Validation Hardening diff --git a/docs/releases/CELLSCRIPT_0_23_RELEASE_NOTES.md b/docs/releases/CELLSCRIPT_0_23_RELEASE_NOTES.md new file mode 100644 index 00000000..570116cb --- /dev/null +++ b/docs/releases/CELLSCRIPT_0_23_RELEASE_NOTES.md @@ -0,0 +1,486 @@ +# CellScript 0.23 Release Notes + +**Status**: Implementation-complete development release notes for +`nightly-0.23`; not a stable release certificate or production CKB evidence. + +**Updated**: 2026-08-09. + +CellScript 0.23 makes its source semantics and compatibility axes explicit. +Edition 2026 is the first and only CellScript source-semantics epoch. The +independently versioned placement ABI gives CellScript entry arguments one +canonical location: +`WitnessArgs.input_type` on the selected script-group witness. + +This document records completed 0.23 work. The public Registry infrastructure, +read/write domains, website, CLI read authority, and automatic compiler-backed +source-package evidence chain are deployed. General artifact, reproduction, +deployment, and commitment support is implemented in-tree, while canonical +Registry Script deployment, the first real non-CellScript mainnet commitment, +and publisher-owned clean-machine production adoption remain external +operational checkpoints. The short-lived browser authorisation flow, isolated +Pudge Testnet Sandbox, and recoverable Playground workbench are implemented and +regression-tested. The complete Fiber/RGB++ external matrices move to the +conditional 0.24 evidence track. The formerly proposed Off-Chain Session +Runtime compiler profile is retired: current Myelin uses an attested external +compiler process, compiles production requests under `ckb`, and keeps +`MyelinExtended` semantics outside CellScript. + +## 0.23 Scope Closure + +The in-repository 0.23 implementation scope is closed around: + +- Edition 2026, resolved compatibility profiles, metadata schema 57, and + canonical `WitnessArgs.input_type` placement; +- the deployed public Registry source-package path, generalized artifact and + chain-evidence implementation, publisher-session flow, and Pudge sandbox; +- the native Rust/shell/Node gate and repository source policy; +- refreshed audited timelock transaction recipes for the three artifacts + changed by the canonical `U64_MAX` source form, plus a provenance-checked + `` compatibility flag for fresh builds of the pinned CKB/RocksDB + source; +- the recoverable website workbench and current Wiki/docs/tooling contracts; + and +- the bounded Fiber adapter/evidence work actually covered by tests and + recorded reports. + +This closure does not convert external operations into local evidence. Mainnet +Registry Script deployment, a real non-CellScript commitment, a +publisher-owned wallet run, and clean-machine adoption require their real +operator, wallet, transaction, confirmation, and readback evidence. Likewise, +the incomplete pinned Fiber/RGB++ matrices remain pending. + +Myelin no longer has the vendored-compiler architecture assumed by the early +0.23 proposal. Adding an off-chain target profile would now duplicate +Myelin-owned VM/session semantics and weaken the `CkbStrict` court boundary. +The [0.24 roadmap](../../roadmap/CELLSCRIPT_0_24_ROADMAP.md) instead specifies +an independent artifact checker, executable package tests, source maps, and an +explicit Myelin adapter-lock handoff. + +## At A Glance + +| Area | What changes | +| --- | --- | +| Edition | Every package declares the long-lived source-semantics epoch `edition = "2026"`; no other edition, inference, or migration path is accepted. | +| Entry witness | `CSARGv1` is decoded only from canonical Molecule `WitnessArgs.input_type`. | +| Failure mode | Raw payloads, malformed tables, absent `input_type`, wrong placement, and mismatched identities fail closed. | +| Build identity | The resolved profile independently combines edition, target, primitive assurance, metadata schemas, and entry/witness ABIs, then binds them into metadata, registry, lock, deployment, receipt, and builder records. | +| Metadata | Current metadata schema 57 is composed with source schema 2, artifact schema 1, and constraints schema 2 in the resolved profile. | +| Registry contract | The deployed publish contract requires Edition 2026 plus its compatibility-profile hash from CLI signature through API, Postgres, version-addressed JSON, and website; assurance states require ordered evidence. | +| Registry operations | `api.registry.cellscript.dev` and `registry.cellscript.dev` run as an isolated self-hosted Postgres/Node/object-volume/read-only-nginx stack behind trusted TLS. | +| Registry retry safety | Pre-admission failures release only the failed request's nonce and retry reservation; accepted metadata commits transactionally, and readiness covers the actual managed object prefixes. | +| Registry verification | Publish transactionally queues a leased, bounded real-compiler verification job; verified evidence/status commit atomically before crash-safe static-index convergence, and default search stays hidden until the baseline passes. | +| Registry artifact profiles | CellScript dependencies, CKB executables, runtime verifiers, reproducible binaries, and copy-only templates share discovery but retain different resolver, TCB, deployment, and copy contracts. | +| Registry reproducibility | Reproducible profiles stay `evidence_required` until independent builder reports bind the signed environment, source, recipe, executable, and build logs. | +| Registry chain evidence | Mainnet deployment records are RPC-checked; configured Registry Type/Lock Scripts produce wallet transaction intents and a bounded Type-Script indexer reconciles live commitments without erasing history. | +| First publish | `cellc publish --authorise` creates a 15-minute exact-coordinate browser session, keeps the private P-256 key pending in the local keychain, and resumes the publish only after Registry-confirmed wallet approval. | +| Testnet sandbox | Pudge uses a separate API, database, object store, signing origin, website build, wallet state, and testnet evidence lifecycle; it never creates a testnet selector in production. | +| Browser workbench | Playground snapshots preserve source, entry, panels, and dirty state; compile failures retain explicitly stale last-valid output, and a failed compiler Worker can restart without a page reload. | +| Production HTTP boundary | API/static JSON responses use HSTS, deny-all content policy, anti-framing, no-sniff, and restrictive browser permissions; the website ships a reproducible read-only nginx deployment with health checks and bounded logs/temp storage. | +| Registry install policy | Explicit unverified/quarantined install acknowledgements persist per dependency, so lock refresh and subsequent builds retain the same auditable risk choice. | +| Tooling | CLI, LSP, WASM, website bindings, examples, and package tooling use the same edition contract. | +| Syntax audit | Canonical type fields use trailing commas, checked examples use named `u64` boundaries, and compatibility plus CKB-VM regressions cover both source and witness placement. | +| Native gate | Active test, fixture, evidence, and release tooling is Rust, shell, or Node; repository policy rejects Python source reintroduction. | + +## Edition 2026 + +`Cell.toml` now requires: + +```toml +[package] +edition = "2026" +``` + +Edition 2026 selects source-language semantics rather than acting as an annual +compiler release or complete ABI bundle. It owns rules that could change the +meaning of the same source: syntax ambiguities, name resolution, typing and +coercion, desugaring, flow/resource semantics, and migration diagnostics. + +The resolved compatibility profile separately composes: + +- source-language semantics; +- target-profile behavior; +- primitive-assurance mode; +- entry-payload encoding; and +- CKB witness placement and script-group source; +- metadata, source, artifact, and constraints schema versions. + +Compiler SemVer remains another independent identity. Compatible diagnostics, +formatter, optimizer, and additive-language work can ship in ordinary compiler +releases. Wire ABIs and metadata schemas can also advance without waiting for a +new calendar year. A new edition is reserved for an intentional break in the +meaning of existing source. + +The resolved profile is emitted in compile metadata. Its hash is required by +registry build records, `Cell.lock` version 2, `Deployed.toml` version 2, +compile receipts, and generated action builders. Verification rejects a +missing or mismatched profile instead of guessing. + +There is intentionally no compatibility or migration layer. Edition 2026 is +the first CellScript edition contract, and there is no published package +ecosystem that requires another interpretation. + +## Canonical WitnessArgs Entry ABI + +CKB transaction witnesses remain raw byte arrays at the transaction layer. +CellScript now requires the selected bytes to encode the standard Molecule +`WitnessArgs` table: + +```text +WitnessArgs { + lock: BytesOpt, + input_type: BytesOpt, // CellScript CSARGv1 entry payload + output_type: BytesOpt, +} +``` + +The generated entry wrapper loads `GroupInput#0`. If the active script group +has no input, it loads `GroupOutput#0`. It validates the `WitnessArgs` table and +its `BytesOpt` offsets, extracts `input_type`, checks the `CSARGv1\0` magic, and +only then decodes positional arguments. + +```mermaid +flowchart LR + TX["Transaction.witnesses: Bytes[]"] --> G["GroupInput#0
fallback GroupOutput#0"] + G --> WA["Molecule WitnessArgs"] + WA --> LOCK["lock
Lock Script/signature data"] + WA --> IN["input_type
CellScript CSARGv1 payload"] + WA --> OUT["output_type
other Type Script data"] + IN --> ENTRY["CellScript entry wrapper"] +``` + +Placement ABI `cellscript-witnessargs-input-type-v2` does not accept `CSARGv1` +as a raw witness alias. A raw payload, malformed Molecule table, missing +`input_type`, or payload in `lock` or `output_type` fails with runtime error +`25 entry-witness-abi-invalid`. + +Generated builders parse or create `WitnessArgs`, preserve `lock` and +`output_type`, and refuse to overwrite an occupied `input_type`. This keeps +CellScript arguments separate from Lock Script signatures and from another +Type Script's output-side data while remaining compatible with CKB's shared +witness convention. + +## Persisted Format Boundary + +The 0.23 identity set is: + +| Surface | Required identity | +| --- | --- | +| Compile metadata | metadata 57, source 2, artifact 1, constraints 2 | +| Compatibility profile | `cellscript-resolved-compatibility-profile-v1` with independent source/target/assurance/ABI/schema axes | +| `Cell.lock` | version 2 | +| `Deployed.toml` | version 2 and `cellscript-deployed-v0.23-edition-2026` | +| Compile receipt | edition and resolved compatibility profile | +| Generated action builder | `cellscript-generated-action-builder-v0.23-edition-2026` | +| Registry build record | edition and compatibility-profile hash | +| `registry.json` / public publish | one required entry shape with explicit edition, profile hash, status, dependencies, and yank state | + +Consumers reject other identities. Rebuild the artifact and regenerate its +metadata, lock/deployment records, receipt, and builder together. + +The production Registry was deployed on 2026-07-31. Its +`0001_initial.sql` is now the frozen deployed baseline; subsequent schema work +requires additive numbered migrations. The write API accepts one complete +signed nested entry instead of an untyped or incomplete JSON object, persists +edition/profile as typed columns, and repeats them in version-addressed static +JSON. Generic admin status changes may quarantine, yank, deprecate, or move an +entry through indexing, but cannot label it `verified_build`, `deployed`, or +`on_chain_committed`. The ordered evidence-promotion endpoint validates +identity-bound evidence and the preceding evidence reference for each of those +states. + +The first additive migration, `0002_verification_jobs.sql`, closes the gap +between the API's `verification: queued` response and actual execution. Publish +admission inserts the job in the same transaction as the version. A separate +least-privilege worker claims jobs with Postgres `SKIP LOCKED` leases, +authenticates the generated snapshot, compiles it with the current CellScript +compiler, verifies canonical manifest and resolved-profile identities, and +atomically records `verified_build` evidence. Static version JSON is refreshed +after that commit; lease recovery resumes only static publication if the +evidence already exists. Three attempts, exponential delay, dead letters, +admin metrics/requeue, bounded process resources/output/time, and a worker +heartbeat in API readiness make the queue operationally fail-closed. Default +public list/search now shows only `verified_build`, `deployed`, and +`on_chain_committed`; direct URLs and explicit status filters preserve admitted +history. + +Manifest hashes are now computed from recursively key-sorted canonical JSON. +This removes the previous cross-process nondeterminism caused by serializing +`HashMap` fields directly and gives the publisher and isolated verifier one +stable identity. + +## General Artifact And Chain Evidence Closure + +The public model no longer equates Registry discovery with `cellc install`. +Each release declares an artifact kind, profile, source language, and +consumption mode. Only `cellscript_source` plus `dependency` enters the package +resolver. A CKB executable is consumed through explicit artifact verification, +pinning, deployment, and CellDep commands; a runtime verifier is a declared TCB +input; and a template is copied without becoming an implicit dependency. + +Reproducibility is now an evidence transition rather than a manifest adjective. +`cellc artifact reproduction-report` creates a P-256-signed builder report, and +`cellc artifact reproduction-evidence` verifies two to sixteen reports with +distinct builder IDs, public keys, and trust domains. Every report must use +`cellscript-reproduction-report-v2` and match the signed environment, source +hash, build-recipe hash, executable hash, build-log hash, and timestamp. The API +additionally binds each builder to `REGISTRY_REPRODUCER_POLICY_JSON` and +requires the configured minimum number of independent trust domains. The +Registry stores the canonical policy SHA-256 and acceptance threshold and binds +the promotion to the accepted `verified_build` evidence. Until +that promotion succeeds, a reproducible executable remains `evidence_required` +and cannot acquire deployment evidence. + +For an RPC-verified mainnet deployment, the commitment endpoint computes the +canonical `cellscript-registry-commitment-v1` payload and compact +`CSREGv1 || commitment_hash` Cell data. When operators configure the canonical +Registry Type Script, commitment custody Lock, and both code CellDeps, the +endpoint also returns a mainnet-only wallet transaction intent. A compatible wallet supplies +capacity, inputs, change, fee, witnesses, signatures, and broadcast. Scheduled +maintenance scans exact Type Script matches through the CKB indexer and +reconciles current state: a matching sufficiently confirmed live Cell promotes +the release to `on_chain_committed`; a spent or immature commitment falls back +to `deployed`; and a stale deployment falls back to +`deployment_status = undeployed` (projected as `verified_build`). Disabling Script configuration +also clears current commitment pointers. Evidence remains append-only. + +Live-Cell identity and confirmation depth use two standard RPC observations: +`get_live_cell` proves that the declared OutPoint is still live, while +`get_transaction.tx_status` proves that its creation transaction is committed +and supplies the block hash used for confirmation counting. The Registry does +not depend on a proxy-specific `get_live_cell.block_hash` extension. + +The canonical Registry Type Script implementation is tracked as an independent +`no_std` crate under `contracts/registry-type-script`, together with the exact +3,352-byte deployable ELF and its pinned Linux x86_64 builder image identity. +The canonical host rebuild must match that artifact byte-for-byte; other hosts +report their host artifact without making a cross-host reproduction claim. +CKB-VM tests always execute the tracked deployable bytes. Its Type args bind +the custody Lock Script hash, all group Cells must use that Lock, and creation +also requires a custody-locked input. Production configuration is rejected if +it drifts from the tracked code data hash or the standard mainnet secp +Lock/DepGroup. + +This is an implementation boundary, not a claim that the canonical mainnet +Registry Scripts have already been deployed. Production chain commitment stays +disabled until all four Script/CellDep values are deployed, confirmed, and configured, and +the first real non-CellScript mainnet artifact is still an adoption/evidence +checkpoint. + +## CLI, LSP, WASM, And Website + +- Package commands read Edition 2026 from `Cell.toml`. +- LSP modules carry the edition through the same compiler path used by `cellc`. +- WASM metadata exports require an explicit edition argument and currently + accept only `"2026"`. +- The playground worker and TypeScript declarations pass that edition into the + WASM boundary and include it in compiler-output provenance. +- Browser-local workspace snapshots preserve source files, selected entry, + active panels, and saved/dirty state. Compile failure keeps the last valid + output with an explicit stale label, and Worker failure exposes a restart + action. Cell Flow and Inspector remain metadata-derived views; raw actions, + types, metadata, and diagnostics stay available, and browser WASM still emits + no ELF. +- Registry list and dynamic detail pages read the live production API, display + evidence plus each version's source edition and separate + compatibility-profile hash, and use the checked-in fixture only as an + explicitly labelled read-only mirror during API failure. The Coming Soon + surface is removed. +- Submit separates artifact kind from source language instead of hard-coding + Rust. Manage exposes isolated reproduction, mainnet deployment, and + commitment command builders alongside publish, inspect, and availability; + task-specific fields disappear when the task changes. +- `cellc publish --authorise` is the interactive first-publish path. It creates + a 15-minute browser session, stores the delegated private key as pending + before opening the browser, and resumes the original publish only after the + Registry returns the matching key ID. `--no-open` supports remote terminals. + The explicit `auth capability submit` plus `auth namespace claim` sequence + remains the manual, CI, and external-wallet path. +- The isolated Pudge Sandbox uses testnet-only origins, storage, signing state, + RPC identity, wallet state, and deployment evidence. Releases leave discovery + after 72 hours and source objects are removed after a 24-hour grace period; + on-chain Pudge history is not deleted. +- Production operations include dependency-aware readiness, bounded proxy and + application request bodies, persistent Postgres/object volumes, and a daily + systemd backup. The first backup passed SHA-256 checks plus non-destructive + `pg_restore --list` and object-archive inspection. +- `cellc install` and `cellc update` use the public API's accepted status as + their default registry authority, then download the immutable source snapshot + and verify its SHA-256 descriptor, safe file paths, per-file BLAKE2b hashes, + source hash, edition, and profile identity. The legacy + `CELLSCRIPT_REGISTRY_URL` path remains an explicit Git/`registry.json` + offline override. +- Entry-witness reports, ABI reports, action plans, and generated builders + expose canonical `WitnessArgs.input_type` placement. +- NovaSeal core, agreement, and planned-profile devnet transaction constructors + serialize their `CSARGv1` payloads as Molecule `WitnessArgs.input_type` + instead of emitting the retired raw form. + +## Native Tooling Closure + +The 0.23 line also completes the removal of Python from active project tooling. +`cellscript-tools` owns gate, evidence, fixture, NovaSeal, Evolving-DOB, and CKB +acceptance logic; website data generation remains in tracked Node modules. +Every gate runs the native source-policy check, which rejects Python sources, +generated interpreter caches, and interpreter references in active tooling +source across the repository and initialized submodules. + +Native fixture generation can read live reports from an explicit isolated +evidence root. Its integration tests therefore pass from a clean checkout and +cannot inherit stale `target/` reports from a developer machine. + +This changes the tooling implementation, not the meaning of production +evidence. iCKB equivalence, NovaSeal pinning, stateful CKB scenarios, and +website/WASM checks retain their separate evidence boundaries. + +## Syntax And Example Audit Closure + +The 0.23 syntax audit found no reason to redesign actions, `verification`, +invariants, destruction policies, parameter sources, or registry namespaces. +It did close two checked-in consistency gaps: + +- type declarations now use the formatter's canonical comma-terminated field + form in `examples/language/canonical_style.cell`; the parser still accepts + comma-free fields as compatibility input; +- syntax-combination quick, CI, and deep modes require both canonical and + compatibility field seeds; +- atomic-swap, NFT, timelock, and multi-phase-DAO examples and their package + mirrors define `U64_MAX` locally and express overflow guards as named + arithmetic; and +- `dev` and `ci` reject formatter drift and reintroduction of the cleaned raw + boundary literals. + +The merge-readiness pass also exposed four crypto-primitive CKB-VM fixtures +that still supplied raw `CSARGv1` witnesses. They now use the adapter's +placement ABI v2 path and keep the runtime's error-25 rejection of raw or +malformed entry witnesses intact. + +## Deliberate Boundaries + +CellScript 0.23 does not claim: + +- that witness bytes are authority without explicit signature and key binding; +- that `input_type` is the input Cell's Type Script; +- that compiler success proves transaction construction, capacity, dry-run, + tx-pool, commitment, or liveness; +- that `CSARGv1` replaces Molecule or CKB `WitnessArgs`; or +- stable-release readiness from `dev` or `ci` alone. + +## Validation Commands + +Routine local validation: + +```bash +./scripts/cellscript_gate.sh dev +``` + +Merge-readiness validation: + +```bash +./scripts/cellscript_gate.sh ci +``` + +The syntax-audit closure is additionally covered by the canonical formatter +check, the syntax-combination matrix, the bundled example tests, and the +`crypto_primitives` CKB-VM integration test included in these unified gates. + +ABI and generated RISC-V validation: + +```bash +./scripts/cellscript_gate.sh backend +``` + +Production release evidence: + +```bash +./scripts/cellscript_gate.sh release +``` + +Both release modes require a clean tree and their documented external +dependencies. The backend gate runs stateful scenarios but does not itself +impose release source-identity cleanliness. A passing lighter gate must not be +reported as release evidence. + +The pinned CKB `0.207.0` build records +`CXXFLAGS=-include cstdint` and +`ckb-librocksdb-sys-8.5.4-explicit-cstdint-v1` in its runtime provenance. This +is a host-toolchain compatibility include for the pinned RocksDB header, not a +CKB source patch. The refreshed timelock recipe identities were accepted only +after two deterministic artifact builds matched and the complete production +stateful matrix passed against the pinned CKB checkout. + +The 2026-08-09 implementation-closure snapshot passed `dev`, the complete +Node-22 `ci` gate, and `backend`. The clean detached backend run rebuilt CKB +revision `f7fa4436737756f97a24e254f22c13a36316ecea` with CKB SDK `v5.1.0`, +then passed 43 action cases, 17 lock cases, and all 26 stateful scenarios / 46 +steps with no missing action or artifact identities. This is local +implementation evidence. The workspace remains version `0.22.0`; no 0.23 +tag, stable release, mainnet deployment, or external adoption claim is made by +this document. + +Deployed Registry liveness and public read verification: + +```bash +curl --fail --silent --show-error https://api.registry.cellscript.dev/ready +curl --fail --silent --show-error 'https://api.registry.cellscript.dev/v1/artifacts?limit=5' +curl --fail --silent --show-error https://registry.cellscript.dev/health +curl --fail --silent --show-error https://cellscript.dev/registry/ > /dev/null +``` + +On 2026-07-31, a disposable cryptographically valid WebAuthn-shaped P-256 +fixture completed capability registration, namespace claim, signed publish, +same-request idempotent replay, static snapshot reads, a fresh-directory +install/check/build, capability revocation, and rejection of a later publish. +Its exact database and live object records were removed after the test; the six +object files remain in the server's isolated recovery directory rather than the +served object volume. + +On 2026-08-01, an isolated production Compose topology completed a real +`cellc publish` through transactional queue admission, leased snapshot +authentication and compilation, evidence persistence, `verified_build`, +default-list visibility, and the version-addressed static object. The exact +containers, volumes, package rows, objects, and test credential were removed +afterward. This is deployment-mechanics evidence, not publisher-owned JoyID +evidence. + +The same automatic pipeline was then deployed to the live production topology +from CellScript commit `4b1fdeec`. An explicitly seeded one-time smoke +principal/capability/namespace completed external `cellc publish`, worker claim, +real compilation, atomic evidence promotion, static convergence, default-list +visibility, and a fresh consumer install/check/build without +`--allow-unverified`. The exact database records were deleted transactionally; +the two test objects were removed from the served volume and retained only in +a checksum-verified recovery directory. All queue counts returned to zero, all +four production containers remained healthy, and a checksum-verified backup +captured the migrated, cleaned state. This proves the live worker boundary but +still does not substitute for publisher-owned JoyID authorisation. + +The final production hardening pass makes the website deployment itself a +tracked artifact instead of server-local configuration. Its nginx container +runs read-only with bounded writable tmpfs mounts, health checks, log rotation, +and `no-new-privileges`; the website, API, and static Registry preserve HSTS, +anti-framing, no-sniff, cross-domain-policy, referrer, and permissions headers +through the shared TLS proxy. JSON-only Registry responses additionally carry a +deny-all content security policy. + +The post-migration backup is also restore-tested, not only checksum-tested. An +isolated Postgres 17 container restored both numbered migrations and all seven +core Registry tables, while an isolated object volume accepted the complete +archive. Neither restore target shared the production database, object volume, +network endpoint, or lifecycle; both temporary targets were removed after the +drill. + +These endpoints prove the deployed service boundary, not a publisher-owned +production adoption. The browser-session flow itself is implemented and +regression-tested; a publisher-owned clean-machine production publish and first +consumer install remain the explicit adoption checkpoint. + +## Detailed Documentation + +- [CellScript Edition Policy](../CELLSCRIPT_EDITION_POLICY.md) +- [Entry Witness ABI](../CELLSCRIPT_ENTRY_WITNESS_ABI.md) +- [Package provenance and deployment identity](../CELLSCRIPT_PACKAGE_PROVENANCE_AND_DEPLOYMENT_IDENTITY.md) +- [CKB target profiles](../wiki/Tutorial-05-CKB-Target-Profiles.md) +- [Metadata verification and production gates](../wiki/Tutorial-06-Metadata-Verification-and-Production-Gates.md) +- [0.23 roadmap](../../roadmap/CELLSCRIPT_0_23_ROADMAP.md) +- [0.24 roadmap](../../roadmap/CELLSCRIPT_0_24_ROADMAP.md) +- [Changelog](../../CHANGELOG.md) diff --git a/docs/releases/CELLSCRIPT_0_24_RELEASE_NOTES.md b/docs/releases/CELLSCRIPT_0_24_RELEASE_NOTES.md new file mode 100644 index 00000000..8e5801e3 --- /dev/null +++ b/docs/releases/CELLSCRIPT_0_24_RELEASE_NOTES.md @@ -0,0 +1,412 @@ +# CellScript 0.24 Release Notes + +**Status**: stable release at `refs/tags/v0.24.0`. Final `dev`, `ci`, `backend`, +`release-quick`, and `release` evidence is recorded in the validation section. +The refreshed iCKB +evidence submodule commit `0e18ccd97bd75cac7de9211dc8d344c0bc08942f` is +published and bound by the parent gitlink. External ecosystem claims remain +limited to the explicit integration status below. + +**Release date**: 2026-08-22 + +**Source edition**: 2026 + +**Metadata schemas**: 58 / 2 / 1 / 2 + +**Rust toolchain**: 1.97.1 + +## Highlights + +The 0.24 line closes four trust gaps without adding a new language edition: + +1. CKB ELF builds emit a canonical verified lowering record and source map, + and a smaller compiler-independent checker recomputes bounded structural + invariants over those sidecars and final machine bytes. +2. `cellc test` requires an execution backend and runs versioned positive, + exact-negative, and multi-step local Cell scenarios under the simulator, + CKB-VM, or both. +3. Package resolution becomes lock-authoritative: `Cell.lock` v3 records a + manifest-bound dependency graph, exact source identities, feature/test and + CKB-environment roots, while ordinary builds never perform mutable version + selection. +4. Deployable CKB Lock Scripts can publish LS-IDL as a byte-exact Registry + interface, with the raw IDL SHA-256 committed in the executable suffix and + resolvable by deployed Script identity. + +The Registry can now admit artifact-only CKB bundles through a least-privilege +worker that depends on the standalone checker, not the CellScript compiler. + +## Verified Artifact Files + +ELF builds add: + +- `ARTIFACT.lowering.json` using + `cellscript-verified-lowering-record-v1`; +- `ARTIFACT.sourcemap.json` using + `cellscript-source-artifact-map-v1`; and +- a `verified_artifact` identity in metadata schema 58. + +`cellc verify-artifact` reports binding, structural, lowering-record, CKB-VM, +and chain evidence independently. Successful checking is not described as +complete source equivalence, VM execution, deployment, or commitment. + +## Checker Evidence + +The standalone checker validates canonical JSON, budgets, graph and +reachability policy, typed ABI/frame contracts, ProofPlan links, static ELF +shape, emitted RV64 instructions, canonical call targets, control flow, stack +restoration, syscalls, block digests, and source-map ranges. Stable rejection +codes `V2400` through `V2418` have a deterministic mutation corpus. + +The production dependency graph of both the checker and the Registry +artifact-only verifier excludes the CellScript compiler. The Registry records +the checker version, policy schema, and report hash for structurally verified +admission. + +The checker is packaged as an independent crates.io dependency. Packaging +gates verify it first and then verify the compiler against an exact local +registry patch; an actual release must publish the checker before the compiler. + +## Executable Package Scenarios + +`cellc test` requires `--backend simulator|ckb-vm|all` unless `--no-run` is +used. Scenario JSON rejects unknown fields and validates confined source paths, +named live Cell replacement, script identities, deps, headers, `since`, +witness fields, capacities, limits, and exact runtime errors. + +Simulator output is development/non-consensus evidence. CKB-VM output is local +authoritative runtime evidence, not chain evidence. The v1 CKB-VM runner +supports no-argument entries; transaction-syscall cases remain with the +stateful CKB oracle. + +`examples/scenario_basics` is the checked-in runnable form of this contract. It +executes positive and exact-negative fixtures under both backends and provides +a concrete four-file bundle/checker walkthrough. + +Native `cellc run` now includes the VM runner by default. It executes only a +no-argument standalone ELF and fails closed for parameter or transaction/ +syscall context. Development interpretation requires explicit `--simulate`; +there is no silent evidence-tier fallback. + +## LS-IDL Lock Script Interfaces + +0.24 adds an end-to-end LS-IDL path without inventing a second ABI format. +`cellc artifact ls-idl` can validate the bounded upstream 0.1 document, append +`SHA-256(raw idl.json bytes)` to a CKB executable, generate a publish-ready +artifact bundle, and fetch the original bytes by deployed Script identity. + +Registry admission accepts the interface only for a deployable +`ckb_executable` Lock Script. The compiler-backed worker and least-privilege +artifact verifier independently check the IDL schema, raw ABI digest, and +executable's final 32 bytes. The API returns the stored bytes directly through +the canonical Script-identity route and the existing-client `/idl/:code_hash` +compatibility route; it never parses and reserialises the committed JSON. + +The website now has a standalone Script-identity lookup under the explicit +`LS-IDL` tab and a dedicated LS-IDL document section on matching artifact +pages. The lookup surface is full-width and aligned with Registry browsing +rather than presented as a smaller generic “Interface” utility. The VS Code +extension exposes validate, bind, and fetch commands. + +Compatibility evidence pins all 17 current `ckb-idl-client` vectors and all +seven checked-in IDLs from `ckb-idl-derive` and `ckb_sudt_script`. An opt-in +checkout-level acceptance script validates their raw hashes and runs the actual +upstream Rust client against Registry's `/idl/:code_hash` handler, covering +fetch, SHA-256 suffix verification, cache use, and witness decoding. The +runnable `examples/registry_ls_idl` remains the smaller explanatory fixture. + +This is deliberately a narrow trust claim. Schema and suffix binding prove +which bytes were published and committed. They do not prove that a Lock Script +implements the described decoder correctly, and they are not a security audit. +The full profile and operator boundary are documented in the +[LS-IDL Registry profile](../CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md). + +## Mainnet And Testnet Registry Parity + +The production and Pudge Testnet Registry websites now build from one shared +interface contract. Browse, Publish, LS-IDL, API, Manage, and dynamic artifact +detail are present in both outputs and load the same byte-identical generated +CSS and JavaScript. The website CI build produces both environments and rejects +a missing route, divergent asset, or missing shared workflow hook. + +Network context remains explicit rather than cosmetically erased. Testnet uses +its own API and object origins, `ckt` address prefix, Pudge chain, expiry policy, +and isolated records. Its LS-IDL form and copied API example default to +`testnet`; Manage and artifact details do not preload or fall back to mainnet +records. Production continues to default to mainnet. The environment control +is the intended visible distinction between otherwise matching interfaces. + +## Registry Type Script Release Identity + +The independently versioned Registry Type Script has a reproducible 0.24.0 +artifact at +`contracts/registry-type-script/artifacts/v0.24.0/cellscript-registry-type-script`. +The 3,352-byte ELF has SHA-256 +`0f48a8736360c121f6ae0f04ab4b0496834f6715d47e3284a0a07add609dede9` +and CKB data hash +`0x0dd596ade29e06e5bcc00f56abf36ecbe9afaa09f1b26a64436aa37854da622b`. +The canonical Linux x86_64 rebuild matches the tracked bytes exactly, and the +Registry API's production configuration gate binds this identity. + +This artifact identity is release evidence, not a claim of mainnet +deployment. Production chain commitments remain disabled until the artifact +and all required Script/CellDep values are deployed, confirmed, and explicitly +configured. + +## Playground Experience Upgrade + +The browser Playground is now a recoverable Cell-oriented workbench rather +than a one-shot compiler demo. Local workspace snapshots retain source files, +entry selection, active panels, and saved/dirty state across refreshes. A +failed compile keeps the previous successful output visible but explicitly +marks it stale, and a failed compiler Worker can restart without a page reload. + +The new Cell Flow view derives actions and type transitions from compiler +metadata, with source-linked selection and a contextual Inspector. A short, +optional guide helps first-time users through the workbench while raw actions, +types, diagnostics, and metadata remain directly accessible. Focus mode keeps +the same workbench but expands it to the viewport; mobile retains a compact +panel switcher. The WASM boundary remains metadata-only: the Playground does +not claim to emit or execute a production ELF. + +## Website Release Identity + +The 0.24 website carries forward the corrected release boundary. Its homepage +names `v0.24.0` as the current stable release, while its Playground worker, +compiler sample, and distribution checks use the matching 0.24 compiler +identity. Its 0.24 release branch removes the forward-looking 0.25-only +package-interface, typed-semantics, and syntax-highlighting presentation +fields; those remain outside the 0.24 release. The canonical Playground asset +is +`20260819-v0.24.0-19ce8898`; its WASM SHA-256 is +`19ce8898e8161f100edebf6f982d856f3e59bfac31572642b53f2e01c70a1a17`. +The raw module is 1,485,936 bytes and 567,048 bytes under the gate's gzip +measurement, below the 600 KiB budget. + +The Node 22 website build validates both production and Pudge Testnet outputs, +the six-route byte-identical asset parity boundary, the current `v0.24.0` +release URL and tag, the compiler asset identity, and the exact WASM digest. +The parent repository pins website commit +`9849c0cb051439901bd3d9c01bd6ba58e8e40751` on the published +`codex/nightly-0.24-release` branch. + +The 0.25-only Playground verification surface remains on the separate +`codex/nightly-0.25-release` website branch at commit +`3db5838ef1ba4b93fb0fc0f188c5374696e27377`. That is the exact website gitlink +used by parent branch `nightly-0.25`; neither release line depends on a hidden +or unreachable submodule commit. + +## Compatibility And Migration + +The CKB adapter no longer exports the deprecated, permanently fail-closed +`deploy_artifact` and `build_deploy` methods. Integrations must construct a +verified unsigned transaction with `build_deploy_transaction`, hand signing to +an external wallet, and submit the signed transaction explicitly. + +CKB ELF generation now always uses the audited internal assembler. The +unreachable external RISC-V toolchain fallback and its environment-controlled +paths have been removed. Diagnostic `E2400` now identifies failure at the +verified lowering/source-map boundary; assembler layout, encoding, and ELF +failures retain the `E220x`/`E2300` families. + +The code generator has been split along its documented ABI, assembler, call, +collection, expression, frame, runtime, schema, and Cell-operation boundaries. +This is an internal ownership refactor, not a new source-language or ABI claim. +The Fiber configuration renderer replaces the deprecated `serde_yaml` crate +with the maintained `serde_yaml_ng` continuation without promoting a Fiber +target profile. + +These are repository-local integration changes. The Fiber migration updates +CellScript's Cargo dependency selection and lockfile; it does not patch or +publish upstream Fiber, CKB, CKB SDK, NovaSeal, or third-party crate source. +The website and editor gitlinks are project-owned release components rather +than upstream protocol dependencies. + +The Registry website refresh adds route-specific headers, a shared content +grid, clearer evidence hierarchy, refined substrate material, and the restored +homepage brand animation. These presentation changes do not alter Registry +evidence semantics or the mainnet/testnet isolation boundary. + +Final release hardening resolves dynamic Molecule-backed `u128` pointers once +before limb loads and spills the first wide operand while loading the second, +so schema validation cannot overwrite live arithmetic state. Mixed +`u128 +/- u64` operations now reject overflow and underflow with runtime error +49. Exact CKB-VM vectors cover both boundaries. Registry admission now rejects +non-canonical SemVer forms such as leading-zero core or numeric prerelease +identifiers. + +## Integration Status + +- The CellScript side of the Myelin 0.24 handoff is versioned and tested. The + external Myelin lock update remains pending until the final 0.24 tag supplies + an exact CellScript source revision. No raw-witness alias or Myelin target + profile is added. +- Fiber remains no-profile. Static compiler/CKB-VM evidence is retained, but + the complete external lifecycle and negative matrix has no complete evidence + bundle and remains pending. +- RGB++ remains an ecosystem identity sidecar. Rgbpp Lock, BTC Time Lock, BTC + SPV, witness/commitment, deployment, confirmation, reorg, and paired + CKB/Bitcoin evidence are not complete and are not promoted. + +## Package And Registry Evolution: Lessons From Sui Move + +The package work was informed by Mysten's Sui Move `move-package-alt` design at +commit `5a9f37431c473fa2f6d49abecbcc6a6d7190f533`. CellScript adopts the parts +that reduce ambiguity in an auditable CKB compiler, while retaining a different +source, artifact, and chain model. + +### Lock first; repin explicitly + +The central principle is that dependency selection and compilation are +different authorities. `cellc lock`, `cellc update`, `cellc add`, +`cellc remove`, and `cellc install` may resolve mutable requirements and write +a new graph. `build`, `check`, and `test` consume only that graph. A missing +lock, changed manifest digest, missing graph edge, moved source, or changed +content hash fails closed and tells the user to repin explicitly. + +`--locked` documents the intent to require the existing dependency graph. +That graph is authoritative even without the flag; the flag is useful in CI +and scripts. `--frozen` additionally implies offline operation and suppresses +all `Cell.lock` writes, including refreshed build evidence. `--offline` permits +only already materialized exact Registry/Git sources. + +This follows Move's separation between resolution and pinned compilation, but +CellScript keeps build/deployment evidence in the same file. Therefore an +ordinary non-frozen build may refresh `[package.build]` and deployment facts; +it never changes dependency nodes or root edges. + +### A graph, not a flat version list + +`cellscript-lock-v0.24-graph-v1` records: + +- the exact SHA-256 digest of the root `Cell.toml`; +- canonical dependency nodes with declared package name, SemVer, immutable + source, whole-tree source hash, dependency-manifest digest, and outgoing + alias-to-node edges; +- separate runtime and test root edges; +- feature-qualified node identities; and +- named CKB environment roots bound to both `chain_id` and the 32-byte genesis + hash. + +The graph allows two source/version nodes to coexist in resolution. It does +not pretend that two packages declaring the same CellScript module are safe: +the compiler's existing duplicate-module and type-identity checks still fail +closed. This is deliberately narrower than importing Move's package/type +identity wholesale. + +Git branches and tags are update-time conveniences only. They resolve to a +full 40-hex commit and an immutable local cache. A later branch movement has no +effect on a locked build; only explicit repinning observes it. Registry sources +are likewise materialized from the exact snapshot URL and `sha256:` identity +recorded in the lock, without repeating discovery or version selection. + +### Standard SemVer, aliases, features, tests, and environments + +Version requirements now use standard SemVer matching, including correct +`0.x`, prerelease, build-metadata, range, and lower-bound behavior. A bare +CellScript version retains the existing compatible (`^`) meaning. + +Dependency aliases are separate from declared package identity through +`package = "..."`. `[features]` supports `default`, feature-to-feature +expansion, and `dep:` activation for optional dependencies. Feature +cycles and unknown activation targets are rejected. `[dev_dependencies]` enter +only the `cellc test` graph. `[build.dependencies]` remains reserved and fails +closed because executing build scripts without an isolation contract would +expand the trusted computing base. + +`[environments.]` binds dependency choice to a concrete CKB chain +identity. `[dependency_overrides.]` can replace declared dependencies for +that environment, but there is no implicit mainnet/testnet selection: callers +must pass `--environment ` when overrides exist. This adapts Move's named +environment idea to CKB's genesis-bound Cell Model rather than copying Sui +addresses or published package IDs. + +`examples/package_graph` is the portable runnable form of these package +features. Its checked-in graph covers a declared-package alias, standard SemVer +requirements, optional and transitive feature activation, a test-only +dependency, two genesis-bound environments, and an exact testnet override. +Frozen/offline commands prove that those selections are consumed from the lock +without invoking mutable resolution. + +### Bounded resolver extension, normalized before trust + +`[resolvers.]` is a versioned extension point for package ecosystems that +cannot be expressed directly. The executable path is absolute and SHA-256 +bound. CellScript invokes it without a shell or inherited environment, with a +10-second deadline and 1 MiB stdout/stderr limits, over +`cellscript-dependency-resolver-request-v1`. The response must use +`cellscript-dependency-resolver-response-v1` and normalize to either an exact +Registry version or a Git URL plus full commit. + +The resolver itself is never stored as build authority and is never executed +by a locked build. `Cell.lock` contains only the normalized source and content +identity. This preserves Move's extensibility insight without permitting an +unbounded plugin system inside compilation. + +### Registry profiles are versioned and fail closed + +Registry artifact admission now uses +`cellscript-registry-profile-catalog-v1`. Every supported profile names its +validator, allowed kinds/languages/consumption modes, whether a profile +contract is required, and whether it may participate in dependency resolution. +Only `cellscript_source` has `resolver_capability = dependency`; CKB +executables, reproducible builds, and copy material remain discoverable but +non-resolving. Adding a future profile is therefore an explicit versioned +contract change rather than another scattered conditional. + +What 0.24 does **not** copy from Move is equally important: there is no Move +bytecode/module ID, Sui address or object identity, implicit environment +selection, unrestricted resolver plugin, source-equivalence claim from hashes, +or conversion of executable/copy artifacts into source dependencies. + +## Validation + +The exact source identified by `refs/tags/v0.24.0` passed all five canonical +gates on the release date 2026-08-22. The environment used Rust `1.97.1`, Node +`22.23.2`, CKB revision +`f7fa4436737756f97a24e254f22c13a36316ecea`, CKB SDK `v5.1.0`, the +`riscv64imac-unknown-none-elf` target, and the pinned Docker base +`rust:1.97.1-slim-bookworm@sha256:99e09cb2284e2ddbb73a995deee3e91783fd04d177602ccf6eab326d778ee777`: + +```bash +./scripts/cellscript_gate.sh dev +./scripts/cellscript_gate.sh ci +./scripts/cellscript_gate.sh backend +./scripts/cellscript_gate.sh release-quick --ckb-repo /path/to/pinned/ckb +./scripts/cellscript_gate.sh release --ckb-repo /path/to/pinned/ckb +``` + +The run covered compiler and workspace tests, Clippy, the full strict backend +audit, all 218 iCKB differential cases, simulator and CKB-VM package scenarios, +Registry API and independent-verifier tests, package construction, website +production/testnet parity, VS Code packaging, the canonical Docker WASM build, +NovaSeal RISC-V reproducibility and pinning, and the non-compile-only production +stateful CKB acceptance harness. The Registry Type Script rebuilt byte-for-byte +to the 3,352-byte artifact recorded above. The NovaSeal verifier rebuilt to +100,912 bytes with SHA-256 +`be66f22507b734c8a432c4c85f0079cc7461caaa92ee3277d50ea8f62ce95ff7` +and CKB data hash +`0x988bab12eab02ebfccc2d2a84da46f4119c5343797ada24104683e61cd07d26e`. +Its RWA profile source binding is +`0x779dad4139b1ffbbfa774d9e9d82b9765d4c23b62a86accd395e0b15bef4db47`. + +The refreshed iCKB matrix is versioned in the benchmark submodule rather than +copied into the parent repository. Commit +`0e18ccd97bd75cac7de9211dc8d344c0bc08942f` is published on that submodule's +`main` branch, and the parent repository binds the same gitlink, so a clean +clone can reconstruct the exact evidence tree that passed `backend`. + +The canonical website build produced the 0.24 WASM identity recorded above and +passed the 600 KiB gzip budget. Gates establish source and artifact evidence; +they do not publish either crates.io package or deploy the Registry Type +Script. Publication and deployment remain explicit release-operator actions +and must preserve the validated source and artifact identities. + +## Detailed References + +- [Verified artifact boundary](../CELLSCRIPT_VERIFIED_ARTIFACT_BOUNDARY.md) +- [Executable test scenarios](../CELLSCRIPT_EXECUTABLE_TEST_SCENARIOS.md) +- [LS-IDL Registry profile](../CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md) +- [Myelin handoff](../CELLSCRIPT_MYELIN_0_24_HANDOFF.md) +- [Gate policy](../CELLSCRIPT_GATE_POLICY.md) +- [0.24 roadmap](../../roadmap/CELLSCRIPT_0_24_ROADMAP.md) diff --git a/docs/skills/cellscript-metadata-audit/SKILL.md b/docs/skills/cellscript-metadata-audit/SKILL.md index c439a3f0..684b0046 100644 --- a/docs/skills/cellscript-metadata-audit/SKILL.md +++ b/docs/skills/cellscript-metadata-audit/SKILL.md @@ -4,7 +4,9 @@ description: CompileMetadata, ProofPlan, builder assumptions, constraints, ABI, references: - docs/wiki/Tutorial-06-Metadata-Verification-and-Production-Gates.md - docs/wiki/Tutorial-11-Scoped-Invariants-and-ProofPlan.md + - docs/wiki/Tutorial-14-Verified-Artifacts-and-Executable-Tests.md - docs/CELLSCRIPT_GATE_POLICY.md + - docs/CELLSCRIPT_VERIFIED_ARTIFACT_BOUNDARY.md commands: - cellc metadata - cellc constraints @@ -21,9 +23,12 @@ stream, not consensus truth. ProofPlan rows, TemplateLayout records, receipts, constraints, ABI, and builder assumptions explain what the compiler emitted and what remains to be checked by builders or CKB nodes. -For 0.22, inspect compile metadata schema 55 together with typed transaction -views, bounded quantifiers/collections, capability proofs, enum layouts, -validity predicates, borrow regions, and `fungible-type-group-v1` evidence. +For the current 0.24 development line, inspect current metadata schema 58 under +Edition 2026 and the resolved compatibility profile, together with the canonical +lowering record and source map for CKB ELF builds. Typed transaction views, bounded +quantifiers/collections, capability proofs, enum layouts, validity predicates, +borrow regions, and `fungible-type-group-v1` evidence introduced on the 0.22 +line remain part of that evidence stream. Distinguish evidence states precisely: compile-only, metadata-only, runtime-required, helper-backed, builder-backed, node dry-run, tx-pool accepted, @@ -34,4 +39,7 @@ Validation defaults: - run `cellc metadata . --target-profile ckb` to inspect metadata without writing a file; - run `cellc explain proof . --target-profile ckb --json` for ProofPlan; -- run `cellc verify-artifact` before trusting artifact/metadata identity. +- run `cellc verify-artifact` before trusting the artifact/metadata/lowering/ + source-map identity and structural contract; +- keep the report's binding, structural, lowering-record, CKB-VM, chain, and + semantic-equivalence fields separate. diff --git a/docs/tutorials/phase1-end-to-end.md b/docs/tutorials/phase1-end-to-end.md index 8ec8d39b..a1bb4d0d 100644 --- a/docs/tutorials/phase1-end-to-end.md +++ b/docs/tutorials/phase1-end-to-end.md @@ -16,6 +16,14 @@ By the end of this tutorial you will understand: - how a verifier downstream of you can confirm that what they imported, compiled, and deployed is the same thing you published. +The production surfaces are live at `https://cellscript.dev/registry/`, +`https://api.registry.cellscript.dev`, and +`https://registry.cellscript.dev`. The first publisher-owned JoyID publication +and clean-machine install remain the final interactive adoption checkpoint. +The automatic compiler-backed worker is deployed and has passed a live +publish-to-install smoke; that one-time seeded identity is deployment evidence, +not a substitute for the remaining publisher-owned JoyID flow. + ## Audience You are writing or porting a contract for CKB. You have a working @@ -180,18 +188,20 @@ The architecture splits the paths: | Path | Responsibility | |---|---| | Write API | Authentication, namespace/package ACL, quota, schema checks, hash sanity, yanking, quarantine, and queue admission. | -| Static read path | CDN/cacheable package indexes, source mirrors, direct package URLs, and website browsing. | +| Static read path | Cacheable package indexes, immutable source snapshots, direct package URLs, and website browsing. | | Verifier | Source/build/deployment hash checks, optional live-chain checks, and fail-closed policy. | -The source still lives somewhere content-addressed, usually Git. The build hash -is computed locally from the source and toolchain. The deployment record is a -small text file with chain facts that a verifier can re-check. The registry -service admits and indexes metadata, but consumers still verify the selected -package. +The Registry stores a content-addressed source snapshot for every accepted +version. A Git repository and tag remain useful provenance and offline-mirror +material, but the normal install path does not require that host to be online. +The build hash is computed locally from the verified source and toolchain. The +deployment record is a small text file with chain facts that a verifier can +re-check. The registry service admits and indexes metadata, but consumers still +verify the selected package. -The discovery index maps a `(namespace, name)` pair to a Git URL and ownership -metadata. It changes when a package is claimed, transferred, or moved. It does -not need to change for every version publish. +The public API maps a `(namespace, name)` pair to accepted versions, immutable +snapshot descriptors, provenance, and ownership-governed state. The explicit +Git/offline index remains available for private mirrors and air-gapped use. ## Authoring a package from scratch @@ -225,9 +235,9 @@ later lock against. You do not need to write the lockfile yourself. If your contract imports another contract, declare it in the manifest's dependency section. There are three dependency kinds: -- a **registry** dependency, named by `(namespace, name)` plus a - version range. The toolchain resolves it via the discovery index - when present, or via the default convention when not. +- a **registry** dependency, named by `(namespace, name)` plus a version range. + The toolchain queries the production API, selects an accepted version, and + materializes its verified immutable snapshot. - a **git** dependency, named by a Git URL plus an optional tag, branch, or revision. The toolchain clones the URL into a local cache. @@ -245,18 +255,28 @@ Authorise a local publisher credential the first time you publish, or whenever the credential expires or is revoked: ```bash -cellc auth capability create --principal-id --scope publish:cellscript/amm_pool --expires 90d --json > capability-payload.json +cellc auth capability create --principal-id \ + --scope publish:cellscript/amm_pool \ + --scope deployment:cellscript/amm_pool \ + --scope availability:cellscript/amm_pool \ + --expires 90d --json > capability-payload.json cellc auth capability submit --payload capability-payload.json --joyid-signature joyid-signature.json +cellc auth namespace claim --namespace cellscript --payload capability-payload.json --joyid-signature joyid-signature.json ``` This generates a local capability key, stores the private key in the OS keychain, and prints a capability authorisation payload. The registry submit page derives `` from the connected JoyID signer, and the browser/CCC/JoyID flow signs that exact payload, binding the local capability -public key, requested scopes, expiry, and principal id. It does not create a -separate registry account. +public key, requested scopes, expiry, and principal id. Publishing, deployment +evidence, and availability changes are separate scopes, so CI can receive only +the actions it needs. This does not create a separate registry account. + +Namespace ownership is explicit and must be active before the first publish; +capability registration alone does not claim it. Reserved namespaces may +require operator review. -Then run the toolchain's `publish` command. It does five things: +Then run the toolchain's `publish` command. The request does six things: 1. Recomputes the source hash from the current working tree and the current manifest, so the published hash matches the source you @@ -266,8 +286,17 @@ Then run the toolchain's `publish` command. It does five things: 3. Signs the publish payload with the local publisher credential. 4. Uploads an immutable source snapshot. 5. Submits the version entry to the registry write API. -6. Receives a canonical registry URL and an initial visibility state such as - `source_published` or `indexed_pending`. +6. Receives a canonical registry URL, `source_published`, and + `verification: queued`. + +The version and its verification job commit in one transaction. A separate +leased worker then authenticates the generated source snapshot, compiles it +with the current CellScript compiler, verifies the canonical manifest and +resolved compatibility-profile hashes, atomically records `verified_build` +evidence, and refreshes the version-addressed static object. Attempts are +bounded and operator-visible. Default search and normal resolution include the +package only after this baseline passes; the direct version URL and explicit +unverified policy remain available for audit. You should still commit the mirrored metadata file, tag the commit, and push both. That mirror travels with the source: every clone of your repo at that tag @@ -295,16 +324,22 @@ path dependencies take a filesystem path. Run the toolchain's resolver. It performs three checks per dependency: -1. It fetches the source through the right channel (discovery - index, Git clone, or local read). -2. It computes the source hash and compares it to the recorded - source hash from the metadata. A mismatch is a hard error. +1. It fetches the source through the right channel (Registry snapshot, explicit + Git dependency, or local path). +2. For Registry sources it verifies descriptor size/SHA-256, safe paths, + per-file BLAKE2b, and then the complete source hash. A mismatch is a hard + error. 3. It transitively resolves any dependencies that the dependency itself declares. The resolver writes a snapshot of the resolved graph into the lockfile so subsequent builds do not need network access. +Normal resolution accepts `verified_build`, `deployed`, and +`on_chain_committed`. An exact `source_published` or `indexed_pending` version +requires `--allow-unverified`; a quarantined version requires the stronger +`--allow-quarantined`. These acknowledgements persist in the dependency table. + ### Step 3 — Build Run the build. The build reads the lockfile, recomputes the source @@ -416,11 +451,10 @@ scope. ## Operating without GitHub -Phase 1 has no dependency on GitHub. The discovery index is a tiny -JSON file that lives anywhere you want it to live. The package -metadata lives inside the source repo and travels with it. The -resolver clones from any Git URL it can reach, including self-hosted -Gitea, GitLab, or a bare repo on a file share. +Production Registry installs have no dependency on GitHub: they use the +Registry's immutable source object. Repository metadata still travels with the +entry for audit and may point to GitHub, self-hosted Gitea, GitLab, or another +Git server without changing the installed bytes. For air-gapped environments, declare dependencies as `path` or as `git` URLs pointing at a local mirror. The lockfile pins the @@ -464,9 +498,9 @@ specific command. Substitute your toolchain's CLI for each step. Commit and push. 3. **Consumer pulls.** A second developer adds the package as a - dependency and resolves. The resolver fetches the source through - Git (or the discovery index) and verifies that the recorded - source hash matches the actual source tree. The resolution writes + dependency and resolves. The resolver fetches the immutable Registry + snapshot, verifies its object and file hashes, and confirms that the + recorded source hash matches the reconstructed source tree. Resolution writes a fresh lockfile for the consumer. 4. **Consumer builds.** The consumer's build computes the six @@ -492,7 +526,7 @@ which layer disagrees. Phase 1 gives you: -- content-addressed source identity, with no central server; +- content-addressed source identity served by a separated read-only path; - per-build identity that survives toolchain upgrades being treated as a deliberate action; - per-network deployment identity that can be checked locally or @@ -500,12 +534,12 @@ Phase 1 gives you: - fail-closed verification at every layer, with structured disagreements rather than silent overrides. -In exchange, you give up: +The deliberate constraints are: - mutable channels like `latest` and `stable`; -- a canonical index that resolves a namespace globally; - automatic cross-profile reuse; -- the convenience of "yank" and re-publish. +- mutable version overwrite or re-publish; yanking preserves history and + suppresses normal selection instead of deleting an accepted version. For long-lived, auditable, multi-team contracts, those trade-offs are usually worth it. For toy experiments, the lack of a `latest` diff --git a/docs/wiki/CKB-Glossary.md b/docs/wiki/CKB-Glossary.md index daf5b71e..2b547f9c 100644 --- a/docs/wiki/CKB-Glossary.md +++ b/docs/wiki/CKB-Glossary.md @@ -78,12 +78,18 @@ a standalone compiler proof. ## Witness -Witness data is user-supplied transaction data. It can carry signatures, -parameters, or other bytes, but the data itself is not automatically authority. +At the transaction layer, each Witness is an arbitrary byte string. CKB does +not require every Witness to have one global application schema. `WitnessArgs` +is the standard Molecule convention that lets a Lock Script and input/output +Type Scripts share one witness through the optional `lock`, `input_type`, and +`output_type` fields. In CellScript, `witness T` means typed data decoded from the transaction witness -surface. A `witness Address` is still just data unless a lock verifies a real -signature binding. +surface. Placement ABI `cellscript-witnessargs-input-type-v2` reads the +`CSARGv1` entry payload from `WitnessArgs.input_type` on the selected +script-group witness; Edition 2026 independently identifies source semantics. A +`witness Address` is still just data unless a lock verifies a real signature +binding. ## Script Args diff --git a/docs/wiki/Cookbook-Recipes.md b/docs/wiki/Cookbook-Recipes.md index c69d0e2b..b6237b0a 100644 --- a/docs/wiki/Cookbook-Recipes.md +++ b/docs/wiki/Cookbook-Recipes.md @@ -16,8 +16,9 @@ cellc examples/token.cell --target riscv64-elf --target-profile ckb --primitive- cellc verify-artifact /tmp/token.elf --expect-target-profile ckb ``` -This proves that the artifact and metadata agree under the CKB profile. It does -not prove that a complete CKB transaction has been built or accepted. +This proves that the ELF, metadata, lowering record, and source map agree under +the bounded structural checker and CKB profile. It does not prove complete +source equivalence or that a CKB transaction has been built or accepted. ## Recipe: Create A Linear Resource @@ -320,7 +321,10 @@ cellc entry-witness . --target-profile ckb --action transfer ``` These reports tell builders and reviewers what data the entry expects. They do -not prove that the transaction has been assembled correctly. +not prove that the transaction has been assembled correctly. Under Edition +2026, place the reported `CSARGv1` payload in the selected group witness's +Molecule `WitnessArgs.input_type`. Preserve `lock` and `output_type`, and fail +if `input_type` is already occupied; a raw payload is not a supported alias. ## Recipe: Sign And Verify A Compile Receipt diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index 767b6b37..8535163a 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -6,7 +6,7 @@ and the locks that decide whether a Cell may be spent. The compiler then turns that `.cell` source into ckb-vm compatible RISC-V assembly or ELF artifacts, and writes metadata that explains what was built. -Last updated: 2026-07-21 (CellScript 0.22.0). +Last updated: 2026-08-10 (`nightly-0.24` development line). This wiki is a guided path. It starts with one compiled example, then slowly builds the mental model: source files, Cell effects, packages, the CKB profile, @@ -36,6 +36,21 @@ After that, the wiki continues outward: collections, payload enums, validity predicates, borrow regions, stable `E2xxx` diagnostics, and bounded Fiber interoperability extend that evidence without hiding builder or chain obligations; +- v0.23 makes Edition 2026 the single source-semantics epoch, composes it with + independently versioned target/assurance/ABI/schema axes, and places + CellScript entry payloads only in canonical `WitnessArgs.input_type`; +- v0.23 also makes the browser playground recoverable: snapshots, last-valid + results, worker restart, Cell Flow, and Inspector views keep metadata work + auditable without claiming browser ELF generation; +- v0.24 emits a canonical lowering record and source-to-artifact map alongside + each CKB ELF, then validates the four-file bundle with a bounded standalone + checker that does not load the compiler front end or code generator; +- v0.24 makes `cellc test` run explicit simulator or CKB-VM scenarios with + exact runtime errors, backend-labelled evidence, local multi-step Cell + replacement, and conservative source-linked coverage; +- v0.24 publishes byte-exact LS-IDL for deployed Lock Scripts, binds the raw + IDL SHA-256 to the executable suffix, and resolves it through the Registry + without upgrading that identity check into an implementation or audit claim; - production evidence proves more than compiler success; - editor tooling shortens the local loop; - bundled examples show the style in real contracts. @@ -54,12 +69,18 @@ If you already know what you need, jump directly: - working in an editor: read [LSP and Tooling](Tutorial-07-LSP-and-Tooling.md); - learning by example: finish with [Bundled Example Contracts](Tutorial-08-Bundled-Example-Contracts.md); - driving `cellc` from an agent: read [Agentic Loops and cellscript-mcp](Tutorial-13-Agentic-Loops-and-cellscript-mcp.md). +- checking structural artifacts and executable scenarios: read + [Verified Artifacts and Executable Tests](Tutorial-14-Verified-Artifacts-and-Executable-Tests.md). +- publishing or resolving an LS-IDL Lock Script interface: read + [LS-IDL for CKB Lock Scripts](Tutorial-15-LS-IDL-for-CKB-Lock-Scripts.md). - using CellScript fungible assets with Fiber: read the - [bounded Fiber interoperability guide](https://github.com/CellScript-Labs/CellScript/blob/nightly-0.22/examples/fiber/README.md). + [bounded Fiber interoperability guide](https://github.com/CellScript-Labs/CellScript/blob/nightly-0.24/examples/fiber/README.md). - evaluating Spore or RGB++ integration: read [Spore and RGB++ Interoperability Boundaries](Spore-and-RGBPP-Interop-Boundaries.md). - spawning a pinned BIP340 verifier: read the [verifier CellDep ABI](../CELLSCRIPT_SIGNATURE_VERIFIER_ABI.md). +- publishing or resolving a Lock Script interface: read the + [LS-IDL Registry profile](../CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md). ## Tutorial Path @@ -89,11 +110,18 @@ If you already know what you need, jump directly: 11. [Scoped Invariants and ProofPlan](Tutorial-11-Scoped-Invariants-and-ProofPlan.md): inspect 0.15 invariant trigger/scope/read metadata and understand metadata-only ProofPlan gaps. -12. [Phase 1 Registry: End-to-End](Tutorial-12-Phase1-Registry-End-to-End.md): - follow the registry package flow from init through verification. +12. [Registry Artifacts: End-to-End](Tutorial-12-Phase1-Registry-End-to-End.md): + publish and inspect CellScript and non-CellScript artifacts. 13. [Agentic Loops and cellscript-mcp](Tutorial-13-Agentic-Loops-and-cellscript-mcp.md): drive the read-oriented compiler surface from an automated writer in a write -> check -> explain -> fix loop. +14. [Verified Artifacts and Executable Tests](Tutorial-14-Verified-Artifacts-and-Executable-Tests.md): + independently check a CKB ELF bundle, run simulator and CKB-VM package + scenarios, and keep structural, runtime, and chain evidence separate. +15. [LS-IDL for CKB Lock Scripts](Tutorial-15-LS-IDL-for-CKB-Lock-Scripts.md): + validate exact IDL bytes, bind them to a Lock Script executable, publish + the Registry bundle, record deployment evidence, and resolve the interface + with `cellc`. After the numbered path, use [Cookbook Recipes](Cookbook-Recipes.md) for small patterns and keep [CKB Glossary](CKB-Glossary.md) nearby for terminology. @@ -143,28 +171,35 @@ cargo run --locked --bin cellc -- examples/token.cell --target riscv64-elf --tar cargo run --locked --bin cellc -- verify-artifact /tmp/token.elf --expect-target-profile ckb ``` -The compile step writes two files: +The compile step writes four files: ```text /tmp/token.elf /tmp/token.elf.meta.json +/tmp/token.elf.lowering.json +/tmp/token.elf.sourcemap.json ``` -The ELF is the executable artifact. The metadata sidecar is the explanation: -where the source came from, which profile was used, what schema was produced, -and which obligations still need review. +The ELF is the executable artifact. Metadata explains where the source came +from, which profile was used, what schema was produced, and which obligations +still need review. The lowering record exposes the bounded structural contract, +and the source map binds it to final instruction ranges. ## Before You Call It Production `cellc verify-artifact` is an important first check, but it is not the whole -story. It proves that an artifact and its metadata agree. It does not prove that -a concrete CKB transaction can spend the right inputs, serialize the right -witness, fit capacity rules, pass dry-run, and commit. +story. For an ELF it proves that the four-file bundle agrees and that the +standalone checker accepted the declared structural contract. It does not prove +complete source-to-machine semantic equivalence or that a concrete CKB +transaction can spend the right inputs, serialize the right witness, fit +capacity rules, pass dry-run, and commit. Keep two levels separate: -- compiler evidence: source, artifact, metadata, and selected policy flags - agree; +- structural compiler evidence: source, artifact, metadata, lowering record, + source map, and selected checker policy agree; +- runtime evidence: an explicitly named simulator or CKB-VM backend executed + the scenario, with the evidence tier retained; - CKB chain evidence: builder-generated transactions were checked on a local CKB chain with cycles, transaction size, capacity, and positive/negative behavior evidence. diff --git a/docs/wiki/Tutorial-01-Getting-Started.md b/docs/wiki/Tutorial-01-Getting-Started.md index ce64f4aa..512a5a10 100644 --- a/docs/wiki/Tutorial-01-Getting-Started.md +++ b/docs/wiki/Tutorial-01-Getting-Started.md @@ -103,21 +103,24 @@ Then compile the same source to ELF: cargo run --locked --bin cellc -- examples/token.cell --target riscv64-elf --target-profile ckb --primitive-strict 0.16 -o /tmp/token.elf ``` -After the ELF build, look for the metadata sidecar: +After the ELF build, look for the complete verified-artifact bundle: ```text /tmp/token.elf /tmp/token.elf.meta.json +/tmp/token.elf.lowering.json +/tmp/token.elf.sourcemap.json ``` -Treat the `.meta.json` file as part of the build result. The ELF is what runs. -The metadata explains the source identity, target profile, schema, runtime -requirements, and verification obligations that belong to that ELF. +Treat all four files as one build result. The ELF is what runs. Metadata +explains source identity, target profile, schema, runtime requirements, and +verification obligations. The lowering record and source map expose the +bounded structural contract checked against final machine bytes. ## Verify the Artifact -Now ask a narrow but important question: does this artifact match its metadata -sidecar and the CKB profile you expected? +Now ask a narrow but important question: does this four-file bundle satisfy the +standalone structural checker and the CKB profile you expected? ```bash cargo run --locked --bin cellc -- verify-artifact /tmp/token.elf --expect-target-profile ckb diff --git a/docs/wiki/Tutorial-02-Language-Basics.md b/docs/wiki/Tutorial-02-Language-Basics.md index 28d14530..f62f1d91 100644 --- a/docs/wiki/Tutorial-02-Language-Basics.md +++ b/docs/wiki/Tutorial-02-Language-Basics.md @@ -147,6 +147,24 @@ Compound assignment is a write boundary. `target += rhs` is valid only when arithmetic and ordering remain unsupported except for explicitly implemented `u128` delta or equality paths. +### Named Integer Boundaries + +When an overflow guard needs the maximum `u64`, name it locally and keep the +relationship visible: + +```cellscript +const U64_MAX: u64 = 18446744073709551615 +const MAX_LOCK_PERIOD: u64 = 2628000 + +require current_height <= U64_MAX - MAX_LOCK_PERIOD, + "lock range overflow" +``` + +Do not replace `U64_MAX - delta` with its precomputed decimal value. The named +expression documents the proof obligation and prevents top-level examples from +drifting away from their package `src/main.cell` mirrors. `u64::MAX` is not a +CellScript built-in in this release. + `Signature` is not a built-in scalar. If a contract needs to carry a signature, model it explicitly: @@ -197,7 +215,8 @@ The shorthand is exactly `field: field`; it does not infer or rename fields. ## Concrete Payload Enums -Nightly 0.22 supports concrete, fixed-width payload variants: +Concrete, fixed-width payload variants were introduced on the 0.22 line and +remain supported by the current compiler: ```cellscript enum Limit { @@ -270,7 +289,8 @@ or use an explicit stdlib lifecycle pattern such as ### Type Validity -On the nightly 0.22 line, a type can state pure value predicates in a final +Introduced on the 0.22 line and retained by the current compiler, a type can +state pure value predicates in a final `validity` section: ```cellscript @@ -531,8 +551,11 @@ Cell in the current script group whose spend is guarded by this lock invocation. It is not an output Cell, not a transaction-wide scan, and not all same-type Cells unless the language explicitly adds such multiplicity syntax. -`witness Address` means decoded transaction witness data only. It is not a -signer or ownership proof. +`witness Address` means decoded transaction witness data only. Under Edition +2026 the entry wrapper obtains it from the `CSARGv1` payload inside +`WitnessArgs.input_type` on `GroupInput#0`, or `GroupOutput#0` for an +output-only script group. It does not mean an arbitrary raw witness, and it is +not a signer or ownership proof. ## Lock Boundary Primitives @@ -542,7 +565,7 @@ of hiding it behind account-style authorization language. | Primitive | Meaning in CellScript | CKB-facing interpretation | |---|---|---| | `protected T` | Typed view of the Cell state guarded by this lock invocation. | One selected input Cell in the current script group, not an output Cell and not a transaction-wide scan. | -| `witness T` | Typed value decoded from transaction witness data. | User-supplied witness bytes decoded by the entry ABI. It is not a signer proof. | +| `witness T` | Typed value decoded from transaction witness data. | A value decoded from the `CSARGv1` payload in canonical `WitnessArgs.input_type`. It is not a signer proof. | | `require expr` / `require expr, "message"` | Action or lock verifier guard. | If `expr` is false, the current script validation fails. The optional string message is kept for source readability and tooling. | | `lock_args T` | Typed fixed-width value decoded from the executing script args. | CKB `Script.args` data for this lock invocation. It is not a signer proof. | @@ -597,6 +620,11 @@ example above is still a boundary-classification example. Treat `Address`, `lock_args Address`, and `witness Address` as data unless an explicit verifier result and key-to-authority binding prove otherwise. +These are two distinct witness uses. Entry parameters such as `claimed_owner` +come from `WitnessArgs.input_type`; `witness::lock(input)` explicitly reads the +`lock` field. Sharing one serialized `WitnessArgs` does not make the fields +interchangeable. + `lock_args Address` is already bound to the executing lock script's typed `Script.args` bytes. That makes it a stable script-argument value, but it still does not verify a transaction signature. CellScript 0.22 exposes the explicit diff --git a/docs/wiki/Tutorial-04-Packages-and-CLI-Workflow.md b/docs/wiki/Tutorial-04-Packages-and-CLI-Workflow.md index 915f13de..6af55781 100644 --- a/docs/wiki/Tutorial-04-Packages-and-CLI-Workflow.md +++ b/docs/wiki/Tutorial-04-Packages-and-CLI-Workflow.md @@ -45,9 +45,9 @@ A minimal manifest looks like this: ```toml [package] +edition = "2026" name = "my_contract" version = "0.1.0" -edition = "2021" entry = "src/main.cell" source_roots = ["src"] @@ -62,6 +62,9 @@ my_lib = { path = "../my_lib" } Read the manifest as a build promise: +- `edition = "2026"` selects the source-language semantic epoch. It is + mandatory; CellScript does not infer, migrate, or accept any other edition, + and the year does not imply an annual release cadence; - `entry` tells the compiler where the package starts; - `source_roots` tells the compiler which package directories contain `.cell` modules; @@ -71,11 +74,25 @@ Read the manifest as a build promise: - path, git, and registry source-package dependencies keep package inputs explicit and lockable. -Registry source-package resolution is implemented for packages that provide -`Cell.toml`, `registry.json`, tag-pinned Git provenance, and a verified -`source_hash`. Local path dependencies remain the fastest repeatable -development workflow, and non-CellScript registry artifact profiles still fail -closed until they have their own resolver contracts. +Production Registry source-package resolution selects an accepted version from +the public API, downloads its immutable source snapshot, and verifies object +SHA-256, safe paths, per-file BLAKE2b, `Cell.toml`, Edition/profile identity, +and the whole-tree `source_hash`. `registry.json` plus tag-pinned Git remain the +explicit offline/mirror authority. Local path dependencies remain the fastest +repeatable development workflow, and non-CellScript registry artifact profiles +still fail closed until they have their own resolver contracts. + +The edition is one input to the emitted compatibility profile. Target, +primitive assurance, metadata schemas, and wire ABIs keep independent version +identities, so they can advance without creating a new source edition. The +profile hash commits to the complete combination in every downstream +build/deployment identity. See +[CellScript Edition Policy](../CELLSCRIPT_EDITION_POLICY.md). + +As a rule of thumb, compiler SemVer answers “which implementation produced +this output?”, Edition answers “how is this source understood?”, and the +resolved compatibility profile answers “which complete source/target/ABI/schema +contract was used?”. ## Multi-file Packages @@ -116,12 +133,37 @@ Useful flags: cellc build --target riscv64-asm cellc build --target riscv64-elf cellc build --target-profile ckb +cellc build --locked +cellc build --frozen +cellc build --offline +cellc build --features audit,metrics +cellc build --all-features +cellc build --no-default-features +cellc build --environment mainnet cellc build --production cellc build --json ``` +Dependency builds are lock-authoritative. Run `cellc lock` or `cellc update` +when dependency selection is intended; `build`, `check`, and `test` otherwise +consume only the existing graph. `--locked` makes that assertion explicit, +`--frozen` also disables network access and every lockfile write, and +`--offline` permits only already materialized exact source pins. + `build` reads `Cell.toml`, compiles the current package entry, and writes the -artifact plus metadata sidecar under the configured output directory. +artifact plus metadata sidecar under the configured output directory. A CKB +ELF build also writes canonical verified-artifact sidecars: + +```text +build/main.elf +build/main.elf.meta.json +build/main.elf.lowering.json +build/main.elf.sourcemap.json +``` + +The lowering record and source map are checked against final ELF bytes during +compilation. They are structural/binding evidence, not a complete +source-equivalence or chain-execution claim. For a one-off source file, use the top-level compiler form instead: @@ -132,6 +174,30 @@ cellc path/to/file.cell That form is great for quick experiments. Packages are better when you need repeatability. +## Execute Package Scenarios + +Executable tests are versioned `*.scenario.json` files under `tests/`. Name a +backend explicitly: + +```bash +cellc test --backend simulator +cellc test --backend ckb-vm +cellc test --backend all --json +``` + +`simulator` is fast development evidence. `ckb-vm` executes the emitted ELF and +is local authoritative runtime evidence. Use `cellc test --no-run` only when +compile-only checking is intentional. Without `--no-run`, an omitted backend +or an empty scenario set is an error rather than a false pass. + +The v1 scenario format rejects unknown fields and validates named live Cells, +replacement steps, Scripts, deps, headers, `since`, witnesses, capacity and +size limits, and exact runtime error code/name pairs. Its multi-step Cell set +is a local bookkeeping oracle; the CKB-VM backend currently supports +no-argument entries and does not inject those declared Cells into syscalls. +Transaction-syscall scenarios remain with the repository's stateful CKB +oracle. See [Verified Artifacts and Executable Tests](Tutorial-14-Verified-Artifacts-and-Executable-Tests.md). + ## Check Without Writing Artifacts Use `check` when you want fast feedback: @@ -238,10 +304,18 @@ cellc deploy plan . --target-profile ckb --json cellc deploy verify --plan Deployed.toml --json cellc registry verify --json cellc package verify --json -cellc auth capability create --principal-id joyid:example --scope publish:cellscript/my_contract --expires 90d --json +cellc auth capability create --principal-id \ + --scope publish:cellscript/my_contract \ + --expires 90d --json cellc gen-builder . --target typescript --target-profile ckb --json ``` +`package verify` checks build identity as well as the dependency graph. A +freshly cloned example intentionally carries a graph-only `Cell.lock`; run +`cellc build --locked` first to populate `[package.build]`. A frozen build +cannot add that local evidence because `--frozen` suppresses every lockfile +write. + Legacy flat aliases such as `solve-tx`, `deploy-plan`, and `explain-assumptions` remain executable for compatibility, but they are hidden from public discovery. Prefer `--json` where a command offers it, and reserve @@ -275,7 +349,7 @@ cellc add my_lib --path ../my_lib graph and write `Cell.lock`, run: ```bash -cellc install +cellc lock ``` You can also add and lock a local dependency in one command: @@ -291,11 +365,10 @@ cellc add math --git https://example.com/math.git cellc install math --git https://example.com/math.git ``` -For reviewable package identity, prefer a manifest-level detailed dependency -with `rev = ""`, then run `cellc install` so `Cell.lock` -records the resolved package source. Branch, tag, and default-branch Git -dependencies are easier to move without changing `Cell.toml`, so treat them as -development convenience rather than production evidence. +For reviewable package identity, a manifest may name a branch or tag during +development, but `cellc lock`/`update` immediately normalizes it to a full +40-hex commit and immutable cache. A later branch movement does not affect +builds until the next explicit repin. Remove it: @@ -303,9 +376,84 @@ Remove it: cellc remove my_lib ``` -`install`, `update`, and normal dependency removal refresh the lockfile so +`add`, `install`, `update`, and normal dependency removal refresh the lockfile so direct and transitive local path dependencies stay consistent. +`Cell.lock` v3 is a graph rather than a flat list. It binds the exact root +manifest digest, each dependency manifest and whole source tree, outgoing +alias-to-node edges, feature/test modes, and named CKB environments. Local +projects should commit it to version control: the lockfile is reviewed build +input, not a local cache, and normal build/check/test commands do not silently +repin it. Dependency aliases can differ from declared package names: + +```toml +[dependencies.math] +package = "canonical_math" +version = "^1.2.0" +``` + +Optional dependencies are activated through versioned feature roots: + +```toml +[dependencies.audit] +version = "^1.0.0" +optional = true + +[features] +default = [] +auditing = ["dep:audit"] +``` + +`[dev_dependencies]` are present only in the `cellc test` graph. Feature +cycles, unknown features, alias collisions, and unknown `dep:` targets fail +closed. `[build.dependencies]` is reserved until CellScript has an isolated +build-script execution contract. + +For chain-dependent selection, declare the chain, not an implicit label: + +```toml +[environments.mainnet] +chain_id = "ckb" +genesis_hash = "0x...32-byte-genesis-hash..." + +[dependency_overrides.mainnet.registry_types] +version = "=2.0.0" +namespace = "cellscript" +``` + +When overrides exist, `--environment mainnet` is mandatory. The environment +root in `Cell.lock` binds both `chain_id` and genesis hash. + +The portable checked-in example exercises these inputs together: + +```bash +cd examples/package_graph +cellc check --frozen --offline --environment mainnet +cellc check --frozen --offline --environment testnet --features full +cellc test --no-run --frozen --offline --environment testnet --all-features +``` + +Its local dependency alias is distinct from the declared package name, and its +testnet override resolves a different exact version of the same declared +package. Omitting `--environment` is an intentional fail-closed example. + +Advanced ecosystems may declare a hash-pinned bounded resolver. It runs only +during explicit lock/update, without a shell or inherited environment, and +must normalize its versioned JSON response to an exact Registry version or Git +commit. Locked builds never invoke it: + +```toml +[resolvers.vendor] +command = "/absolute/path/to/vendor-resolver" +sha256 = "sha256:" +args = ["resolve"] + +[dependencies.math] +package = "canonical_math" +version = "^1.2.0" +resolver = "vendor" +``` + ## Registry Resolver Boundaries CellScript's registry design follows the same split as the package identity @@ -316,26 +464,26 @@ model: - deployment identity answers which CKB Cell, CellDep, or runtime artifact is being used. -Registry discovery can be broad. It may index CellScript source packages, +Registry discovery is broad. It indexes CellScript source packages, runtime verifiers, deployed CKB artifacts, reproducible artifacts, and even external CKB tooling artifacts such as bootstrapper outputs. Resolver profiles must stay narrower: an object can be discovered without being installable by `cellc add`. -That means registry resolution is stricter than discovery. Current `cellc` -registry dependencies are CellScript source-package dependencies. Future -profile-aware resolver paths should accept only objects that can be checked -fail-closed: +That means registry resolution is stricter than discovery. The versioned +`cellscript-registry-profile-catalog-v1` marks only the +`cellscript_source` + `dependency` contract as dependency-resolving. `cellc add` +and `cellc install` reject every other profile. +Other profiles use explicit `cellc artifact` commands and fail closed on +unknown fields, identities, roles, or lifecycle state: -| Kind | Current `cellc add` | Future profile boundary | +| Kind | `cellc add` | Current explicit boundary | | --- | --- | --- | -| `source_package` / library | yes | Source and API identity must be pinned and reproducible. | -| `runtime_verifier` / `spawn-verifier` | no, unless wrapped as a CellScript package today | TCB object; requires verifier ID, ABI, artifact identity, build profile, security status, and production deployment pins when used in production. | -| `deployable_contract` | no, unless it is a CellScript source package today | Must expose build/audit/deployment identity, not just source text. | -| `deployed_artifact_record` | no | Must bind network, OutPoint, dep type, code/data hash, and status. | -| `reproducible_artifact` | no | Must bind source hash, build profile hash, artifact hash, and compatibility profile. | -| `protocol_profile_library` | only if it is a real CellScript package today | Must be a real package with checkable source/schema/API semantics. | -| `template`, `cookbook`, `protocol_skeleton`, scaffold | no | Copy-only starting material; after copying, it becomes local project code. | +| `source_library` / `profile_library` | yes | Compiler-backed source and API identity are pinned in `Cell.lock`. | +| `runtime_verifier` | no | `artifact fetch`, `verify`, and `pin`; verifier ID, IPC ABI, artifact, build, security, and production CellDep remain explicit TCB facts. | +| `deployable_contract` | no | `artifact fetch`, `verify`, `pin`, `record-deployment`, and `cell-dep` bind build and live mainnet deployment identity; `artifact ls-idl` validates, binds, bundles, or resolves a Lock Script interface without making it a source dependency. | +| `reproducible_binary` | no | `artifact reproduction-evidence` binds independent builders to source, recipe, environment, executable, and logs before verified use. | +| `template` | no | `artifact copy` authenticates a bounded file map, rejects traversal and overwrite, and then leaves local project source. | The rule is intentionally blunt: @@ -356,9 +504,8 @@ hashes, build profile, TCB/security status, and any production CellDep pins. A NovaSeal starter project, by contrast, is not dependency-safe merely because it contains useful `.cell` code. If users are expected to copy it and edit terms, authorities, manifests, or deployment pins, it belongs in a cookbook or template -flow, not in dependency resolution. The current `cellc` CLI does not ship a -template or cookbook-copy command; copy starter material with repository tooling -or a future scaffold command, then treat the result as local project source. +flow, not in dependency resolution. Use `cellc artifact copy`, then treat the +authenticated result as local project source. It should not be installed with: @@ -386,20 +533,39 @@ cellc info --json Use `info` when you want a quick view of the package boundary before building or debugging dependency resolution. -## Experimental Commands +## Registry Commands Registry source-package installation and registry-backed `update` are supported -for the CellScript source-package profile. The public registry policy is: -`cellc auth capability create --principal-id --scope -publish:namespace/package --expires 90d` authorises a JoyID-rooted publisher -capability, then `cellc publish` writes a real registry entry. The -`principal_id` is derived from the connected JoyID signer, not from a display -address. The same metadata can still be +for the CellScript source-package profile. The preferred interactive first-use +path is `cellc publish --authorise`: it creates a 15-minute browser session, +authorises a wallet-rooted delegated key, and resumes the publish after the +Registry returns the matching key ID. `--no-open` supports remote terminals. +Later `cellc publish` calls use the active scoped key. + +For CI, recovery, or an external-wallet handoff, `cellc auth capability create +--principal-type --principal-id ` creates +the wallet payload; submit the wallet signature and claim the namespace before +publishing. Inside a package directory, omitting `--scope` infers only the exact +`publish` scope. Add `deployment` or `availability` scopes explicitly when that +delegated key genuinely needs those actions; none implies another. +The `principal_id` is cryptographically derived from the signer, not from a +display label. The same metadata can still be mirrored with `cellc publish --offline` to `registry.json` and Git tags for audit, local fixtures, and offline fallback. `cellc registry add` manages discovery/claim metadata rather than -ordinary version publication. `run`, `repl`, cryptographic audit-signature -verification, and non-CellScript artifact profiles remain future-facing or -fail-closed. +ordinary version publication. + +Non-CellScript profiles publish with `Artifact.toml` and +`cellc publish --artifact-manifest Artifact.toml`. Consumers use the explicit +`cellc artifact fetch`, `verify`, `pin`, `copy`, `reproduction-evidence`, +`record-deployment`, `cell-dep`, `commitment`, and `set-availability` commands; +none silently turns an executable, TCB object, or template into a source +dependency. `run`, `repl`, and cryptographic audit-signature verification +retain their separate documented assurance boundaries. + +For LS-IDL Lock Scripts, `cellc artifact ls-idl validate|bind|bundle` prepares +the byte-exact interface contract and `fetch` resolves it by chain-verified +Script identity. The raw IDL SHA-256/executable-suffix relationship is an +identity check, not proof of implementation correctness. ## Next diff --git a/docs/wiki/Tutorial-05-CKB-Target-Profiles.md b/docs/wiki/Tutorial-05-CKB-Target-Profiles.md index 9bd6133b..355c0293 100644 --- a/docs/wiki/Tutorial-05-CKB-Target-Profiles.md +++ b/docs/wiki/Tutorial-05-CKB-Target-Profiles.md @@ -7,9 +7,18 @@ For CKB work, the answer should be explicit. The CKB profile controls syscall choices, source constants, header/runtime rules, artifact packaging, metadata policy, and verification boundaries. +Edition and target profile are related, but they are not duplicate settings. +`edition = "2026"` selects source-language semantics. The independently +versioned `ckb` target profile selects CKB-facing runtime rules. The resolved +compatibility profile combines both identities with primitive assurance, +metadata schemas, and wire ABIs. Changing the target cannot opt out of the +edition, and passing `--target-profile ckb` cannot repair a package with a +missing or non-2026 edition. + ## What You Will Learn - how to use the `ckb` profile consistently; +- how Edition 2026 and the CKB profile combine; - why unsupported CKB assumptions fail closed; - which commands check assembly and ELF-compatible paths; - which CKB details deserve review before deployment. @@ -37,7 +46,8 @@ The profile checks and records: - CKB source constants; - CKB header ABI restrictions; - raw ELF packaging without ABI trailer; -- Molecule-facing schema, entry witness metadata, and typed lock args ABI; +- Molecule-facing schema, canonical `WitnessArgs.input_type` entry placement, + and typed lock args ABI; - CKB Blake2b release/deployment hash helper support; - `args_parts` lock-args partition metadata for typed builders; - manifest-level `hash_type`, CellDep, and DepGroup reporting; @@ -96,6 +106,10 @@ from the beginning: - record CKB `hash_type`, CellDeps, and DepGroups in `Cell.toml`; - inspect `cellc constraints --target-profile ckb --json` before deployment; - inspect witness layout with `cellc abi` or `cellc entry-witness`; +- place the reported `CSARGv1` entry payload in + `WitnessArgs.input_type` on the first witness of the active script group; +- preserve `WitnessArgs.lock` and `output_type` when constructing or signing a + transaction; - avoid scheduler witness ABI unless you are deliberately using that surface; - avoid unsupported signature/hash helper syscalls; - use metadata and `verify-artifact` to confirm target profile and packaging. @@ -105,6 +119,15 @@ The lock-boundary keywords from the previous chapter also matter here. which values come from witness data. `lock_args` tells readers which values come from CKB `Script.args`. None of them silently verifies a signature. +Under placement ABI `cellscript-witnessargs-input-type-v2`, CellScript entry +parameters are not decoded from arbitrary raw witness bytes. The wrapper +selects `GroupInput#0`, or `GroupOutput#0` for an output-only script group, +parses a Molecule `WitnessArgs`, and reads `input_type`. Raw `CSARGv1`, malformed +tables, absent `input_type`, and placement in `lock` or `output_type` fail +closed. Edition 2026 is recorded alongside this independently versioned ABI in +the resolved compatibility profile. See the +[Entry Witness ABI](../CELLSCRIPT_ENTRY_WITNESS_ABI.md). + Capacity has the same boundary discipline. `with_capacity_floor(...)` is a source-level floor, and `occupied_capacity("TypeName")` makes capacity policy visible to reports. The final transaction still needs builder-side occupied @@ -126,7 +149,7 @@ deployment, live asset Script, CellDeps, and operator-controlled Fiber configuration. Use the separate `cellscript-fiber` binary and follow the -[bounded Fiber interoperability guide](https://github.com/CellScript-Labs/CellScript/blob/nightly-0.22/examples/fiber/README.md). A successful +[bounded Fiber interoperability guide](https://github.com/CellScript-Labs/CellScript/blob/nightly-0.24/examples/fiber/README.md). A successful offline compatibility check proves only that the source matches the closed fungible contract. Production readiness still needs live CKB identity, node configuration, restart, announcement, and lifecycle/negative evidence. diff --git a/docs/wiki/Tutorial-06-Metadata-Verification-and-Production-Gates.md b/docs/wiki/Tutorial-06-Metadata-Verification-and-Production-Gates.md index f3be7b9e..9cd94992 100644 --- a/docs/wiki/Tutorial-06-Metadata-Verification-and-Production-Gates.md +++ b/docs/wiki/Tutorial-06-Metadata-Verification-and-Production-Gates.md @@ -1,15 +1,36 @@ # Tutorial 06: Metadata Verification and Production Gates -Every CellScript artifact should be treated as a pair: +Every CellScript CKB ELF build should be treated as one four-file bundle: ```text artifact artifact.meta.json +artifact.lowering.json +artifact.sourcemap.json ``` -The artifact is executable RISC-V assembly or ELF. The metadata sidecar is the -explanation: source identity, target profile, artifact hash, schema layout, -runtime requirements, scheduler information, and verifier obligations. +The artifact is executable RISC-V ELF. The metadata sidecar is the explanation: +source identity, target profile, artifact hash, schema layout, runtime +requirements, scheduler information, and verifier obligations. The canonical +lowering record exposes a bounded CFG, ABI, stack, ProofPlan, syscall, runtime +exit, and final machine-range contract. The canonical source map binds source +spans and lowering blocks to final ELF instruction ranges. Assembly output does +not claim this verified-artifact boundary. + +On the 0.24 development line it also carries mandatory `edition = "2026"` and the fully +resolved compatibility profile. Edition contributes source semantics only. +The profile combines that with independently versioned target, +primitive-assurance, entry payload, witness placement, and metadata-schema +axes. Verification rejects a sidecar whose profile does not resolve from those +inputs; it never guesses another contract. Current outputs use metadata schema +58, source schema 2, artifact schema 1, and constraints schema 2. Registry, +lock, deployment, receipt, and generated-builder readers require the same +resolved-profile identity. + +The distinction matters during review: compiler SemVer can advance for +compatible implementation work, and a wire ABI or metadata schema can advance +for an urgent fix, without forcing a new calendar-year source edition. A new +Edition is reserved for a change to the meaning of existing source. This chapter is about trust boundaries. It teaches you what compiler evidence can prove, and where you still need CKB transaction evidence. @@ -19,9 +40,11 @@ can prove, and where you still need CKB transaction evidence. Compiler verification is necessary, but it is not the same thing as a deployed transaction or chain acceptance report. -If `verify-artifact` passes, you know the artifact and metadata agree. You do -not yet know that a transaction builder can provide the right inputs, serialize -the right witness, satisfy capacity, pass dry-run, and commit. +If `verify-artifact` passes for an ELF, you know all four files agree and that +the standalone checker independently accepted the bounded structural contract. +You do not yet know that a transaction builder can provide the right inputs, +serialize the right witness, satisfy capacity, pass dry-run, and commit. The +checker does not claim complete source-to-machine semantic equivalence. That distinction prevents overclaiming. @@ -51,6 +74,10 @@ Start with the basic check: cellc verify-artifact build/main.elf ``` +The command automatically loads `build/main.elf.meta.json`, +`build/main.elf.lowering.json`, and `build/main.elf.sourcemap.json`. Use +`--metadata`, `--lowering-record`, and `--source-map` only for custom paths. + Pin the target profile: ```bash @@ -71,9 +98,13 @@ cellc verify-artifact build/main.elf --deny-fail-closed cellc verify-artifact build/main.elf --deny-runtime-obligations ``` -Read this gate narrowly: it verifies the artifact, metadata, source hash -expectations, and selected policy flags. It does not prove that a concrete CKB -transaction has been built, deployed, dry-run, indexed, or measured. +Read this gate narrowly: it verifies binding, structural ELF/lowering/source-map +invariants, source hash expectations, and selected policy flags. Its JSON report +keeps `binding_verification`, `structural_verification`, +`lowering_record_verification`, `ckb_vm_evidence`, and `chain_evidence` +separate, and keeps `semantic_equivalence_claimed = false`. It does not prove +that a concrete CKB transaction has been built, deployed, dry-run, indexed, or +measured. ## Check Before Build @@ -163,7 +194,8 @@ the proof's versions must match the registry. `replace_unique` additionally records the exact `identity(...)` condition declared by the same resource. No proof may source authority from a container or another Cell type. -Schema 53 includes top-level `enum_layouts` for concrete payload ADTs. Audit the +Top-level `enum_layouts` for concrete payload ADTs first appeared in schema 53 +and remain in current metadata schema 58. Audit the `packed-tagged-union-v1` layout, one-byte tag, sequential variant tags, packed field offsets, encoded size, ownership, storage, and ABI together. A `linear-cell-handle` field is exactly eight bytes and forces @@ -189,8 +221,8 @@ the `consume_each` runtime-helper tier. For `BoundedList` driving `builder-evidence-required`; it is not proof that a transaction builder supplied the matching outputs or sufficient capacity. -The validity record first appeared during the 0.22 schema sequence and is -emitted by current schema 55 as `types[].validity_predicates`. Review each predicate's +The validity record first appeared in schema 55 during the 0.22 line and is +retained by current metadata schema 58 as `types[].validity_predicates`. Review each predicate's `expression`, `dependencies`, `evidence_tier`, `runtime_checked_on_create`, `create_paths_selected`, `create_paths_checked`, `update_paths_selected`, `create_path_status`, @@ -209,7 +241,8 @@ are compile errors. Pure imported helpers are retained transitively and receive module-qualified dependency names; lifecycle helpers and transaction-view reads are rejected in validity predicates. -Current schema 55 records explicit borrow blocks in +Explicit borrow blocks first appeared in schema 55 and current metadata schema +58 records them in `runtime.borrow_regions`. Review `root`, `binding`, `view_type`, `storage`, `abi`, `allowed_effects`, `evidence_tier`, and `source_span`. A canonical record has `View`, @@ -311,7 +344,8 @@ ProofPlan coverage states are intentionally explicit: | `gap:runtime-helper-required` | The claim maps to a runtime helper, but the selected entry did not emit matching helper coverage. | | `checked-runtime` | Generated runtime access backs the claim for the selected entry. | -On the nightly 0.22 line, invariant read ranges and aggregate operands are +Introduced on the 0.22 line and retained by the current compiler, invariant +read ranges and aggregate operands are parsed once into a closed typed target: a source view (`inputs`, `outputs`, `group_inputs`, `group_outputs`, `cell_deps`, `header_deps`, `witness`, or `lock_args`) plus optional cell type and field. The formatter emits canonical @@ -319,7 +353,8 @@ plural source-view names, while ProofPlan keeps the same readable target text. Unknown generic source views fail in the parser; later compiler phases do not recover their meaning by splitting strings. -Nightly 0.22 also records who must discharge every obligation: +The evidence tiers introduced on the 0.22 line still record who must discharge +every obligation: | Evidence tier | Discharged by | |---|---| @@ -339,9 +374,10 @@ evidence into compiler proof; those tiers remain external obligations. For the review-finding closure matrix, see `docs/archive/0.17/CELLSCRIPT_0_17_REVIEW_FINDINGS_CLOSURE.md`. -## Nightly 0.22 Effect And Terminal Evidence +## Effect And Terminal Evidence -Function helpers can now publish the same stable effect contract as actions: +Introduced on the 0.22 line and retained by the current compiler, function +helpers can publish the same stable effect contract as actions: ```cellscript #[effect(ReadOnly)] @@ -430,6 +466,11 @@ For CellScript releases, `quick` is part of the pre-push gate and `ci` runs before builder-backed CKB acceptance. A direct CKB acceptance run does not replace this preflight because it only proves selected concrete transactions. +The required syntax origins include both comma-terminated canonical type +fields and comma-free compatibility input. The formatter must converge both to +the comma-terminated form; lifecycle field blocks remain newline-separated +field names without commas. + ## Unified Gate Entry Points For repository work, use the unified gate wrapper instead of hand-picking @@ -448,6 +489,13 @@ IR/codegen/RISC-V changes. `release` is the production CKB evidence gate. `release-quick` is a compile-only release preflight, not external live/devnet evidence. See `docs/CELLSCRIPT_GATE_POLICY.md` for the exact command contract. +In `dev` and `ci`, the wrapper also checks that +`examples/language/canonical_style.cell` is already formatter-clean and that +the checked atomic-swap, NFT, timelock, and multi-phase-DAO example pairs use +named `U64_MAX` boundary expressions. CI's CKB-VM integration tests encode +CellScript entry payloads in canonical `WitnessArgs.input_type`; a raw +`CSARGv1` witness is a negative ABI case, not a valid test shortcut. + Fiber's no-profile compatibility harness is deliberately separate from these unified gates: @@ -542,14 +590,48 @@ build or publish until this full gate has passed, and the tag/version must match the workspace version. The report's builder-backed action runs, lock cases, and stateful transactions -come from handwritten Python acceptance harnesses and are labelled that way. -The separate public-builder contract gate proves that every production action -is exposed by `cellc action build` and `cellc gen-builder`; it does not claim -those generated packages constructed the acceptance transactions. Likewise, +come from the native Rust recipe replayer and are labelled that way. The +separate public-builder contract gate proves that every production action is +exposed by `cellc action build` and `cellc gen-builder`; it does not claim those +generated packages constructed the acceptance transactions. Likewise, `always_success` resource Type Scripts are fixture-only. They prove scoped verifier behaviour and transaction shape, not the production resource-identity deployment story. +Registry artifact evidence remains another independent boundary. A +`verified_build` record may carry compiler-backed CellScript verification, the +declared hash-bound generic profile level, or `structurally_verified` evidence +from the least-privilege artifact checker. Generic CKB bundles remain +`hash_bound`; structural admission requires the complete metadata, lowering +record, and source map set, and partial verified sidecars fail closed. None of +those levels is deployment or chain evidence. A reproducible profile is not +`verified` until `reproduced_build` evidence binds at least two independent +builders to the signed source, recipe, environment, executable, and logs. +Likewise, a wallet-ready Registry commitment file is not chain evidence. Only a +sufficiently confirmed live mainnet Cell matching the configured Registry Type +Script, commitment custody Lock, exact commitment data, and both live Script +code CellDeps can produce current `on_chain_committed` state. Scheduled +reconciliation demotes that current state when the commitment or deployment +Cell is spent or no longer sufficiently confirmed. + +LS-IDL introduces another narrow Registry evidence layer. The interface +verifier checks a bounded IDL schema, `SHA-256` of the exact ABI object bytes, +and the executable's final 32-byte commitment. A chain-verified lookup also +binds those bytes to a deployed Script identity. This is still not proof that +the Lock Script implements the decoder correctly and is not a security audit. +Do not promote `schema-and-suffix-bound` into semantic, VM, or chain-execution +evidence. + +Package resolution is an earlier, separate gate. `Cell.lock` v3 binds the +exact `Cell.toml` digest, dependency graph edges, dependency manifests, +whole-tree hashes, exact Git/Registry source pins, feature/test modes, and CKB +environment genesis identity. Build/check/test never perform mutable version +selection. A changed manifest or source requires explicit `cellc lock` or +`cellc update`; `--frozen` additionally forbids network access and lockfile +writes. The Registry's versioned profile catalog allows only +`cellscript_source` to enter this graph. Executable, reproducible, TCB, and copy +artifacts retain their separate evidence and consumption paths. + For the current NovaSeal profile set, production-ready source-package evidence means the live local devnet runners pass for core, Agreement, and the six planned profiles: BTC transaction commitment, BTC UTXO seal, dual seal, Fiber diff --git a/docs/wiki/Tutorial-07-LSP-and-Tooling.md b/docs/wiki/Tutorial-07-LSP-and-Tooling.md index 3cc8c373..d61bbed2 100644 --- a/docs/wiki/Tutorial-07-LSP-and-Tooling.md +++ b/docs/wiki/Tutorial-07-LSP-and-Tooling.md @@ -24,6 +24,7 @@ instead of only the message. The same record is available through ## What You Will Learn - what the LSP server supports; +- how the CLI, LSP, WASM, and playground share Edition 2026; - how the VS Code extension starts the server; - which settings matter for local development; - where editor tooling helps; @@ -60,13 +61,50 @@ cellc --lsp In practice you usually let the editor start it for you. -On `nightly-0.22`, qualified enum completion includes concrete payload -constructors: after `Limit::`, `Some` advertises `Some(u64)` and inserts +## One Edition Across Tooling + +The editor is not an edition compatibility layer. Package-backed LSP documents +take `edition = "2026"` from `Cell.toml` and carry it into the same compiler +path as `cellc`. A missing or non-2026 value is a package error; the LSP does +not infer or migrate it. + +The browser boundary is equally explicit. The WASM metadata exports take an +edition argument: + +```text +compile_metadata_json(source, edition, target?) +compile_metadata_json_diagnostics(source, edition, target?) +compile_metadata_json_sources(sources_json, entry_path, edition, target?) +``` + +The only accepted value is `"2026"`. The playground worker passes that value +and records it in compiler-output provenance, so browser metadata cannot +silently use a different compatibility contract from native builds. + +Introduced on the 0.22 line and retained by the current compiler, qualified +enum completion includes concrete payload constructors: after `Limit::`, +`Some` advertises `Some(u64)` and inserts `Some(value1)`, while `None` remains a bare variant. Enum hover reads the same compiler metadata as `cellc metadata` and shows the tagged-union layout, ABI, storage class, encoded width, and linear-payload flag. Generic or variable-width payload ADTs are intentionally not advertised as supported. +## Recoverable Browser Workbench + +The website playground is a metadata workbench over the WASM compiler path, +not a browser ELF builder. Its workspace snapshot preserves source files, the +selected entry, active panels, and saved/dirty state in browser-local storage. +Compile failure keeps the last valid output visible with an explicit stale +label; if the compiler Worker stops, restart it from the playground without +reloading the page. + +Cell Flow derives an inputs → action → outputs view from compile metadata. The +Inspector connects a selected action or type back to its declaration and shows +effects, estimated cycles, capabilities, runtime features, and layout evidence. +Raw actions, types, diagnostics, and metadata remain available alongside those +views. None of these panels upgrades metadata into consensus proof, and the +browser path still emits no assembly or ELF. + ## VS Code Extension The extension lives in: @@ -109,6 +147,7 @@ Useful settings: | `cellscript.builderOutputDir` | Output directory for generated TypeScript action-builder packages. Relative paths resolve from the nearest package `Cell.toml`. | | `cellscript.ckbRpcUrl` | Optional CKB RPC URL for live registry verification. | | `cellscript.deploymentNetwork` | Optional network filter for live registry verification and generated builder deployment binding. | +| `cellscript.registryApiUrl` | Optional Registry API base URL for LS-IDL fetch. | | `cellscript.registryRequirePublisherSignature` | Add `--require-publisher-signature` to registry verification commands. This is a metadata-presence gate, not cryptographic signature verification. | | `cellscript.registryRequireAuditReport` | Add `--require-audit-report` to registry verification commands. | @@ -131,6 +170,22 @@ The extension contributes commands for the local compiler and builder loop: | `CellScript: Verify Registry` | `cellc registry verify --json` | | `CellScript: Verify Live Registry` | `cellc registry verify --live --json` | | `CellScript: Show Production Report` | compiler version + metadata + constraints + release-audit boundary | +| `CellScript: Validate LS-IDL` | `cellc artifact ls-idl validate --idl ` | +| `CellScript: Bind LS-IDL to CKB Executable` | `cellc artifact ls-idl bind --idl --executable ` | +| `CellScript: Fetch LS-IDL by CKB Script` | `cellc artifact ls-idl fetch --code-hash --output idl.json` | + +The LS-IDL commands preserve the interface's exact byte identity. Validation +checks the supported schema, binding appends the raw IDL SHA-256 to a selected +executable, and fetch writes the Registry response without JSON +reserialisation. This proves schema and commitment identity, not that a Lock +Script implements the interface correctly. + +Entry-witness commands report placement ABI +`cellscript-witnessargs-input-type-v2` within the resolved compatibility profile: +`CSARGv1` is stored in Molecule `WitnessArgs.input_type` on the selected +script-group witness. Tooling must preserve `lock` and `output_type`; it must +not emit the entry payload as raw witness bytes. Edition 2026 independently +identifies how the source was understood. `CellScript: Show Production Report` is useful while editing because it displays compiler version, metadata, constraints, and release-audit boundaries. @@ -176,6 +231,7 @@ cellc check --all-targets --json cellc metadata . --target riscv64-elf --target-profile ckb -o /tmp/metadata.json cellc build --target riscv64-elf --target-profile ckb --json cellc verify-artifact build/main.elf --verify-sources --expect-target-profile ckb +cellc test --backend all --json cellc package verify --json cellc registry verify --json ``` @@ -251,23 +307,31 @@ The package manager supports: - `cellc doc` - `cellc add --path` - `cellc remove` +- `cellc lock` - `cellc info` - `cellc package verify` - `cellc registry verify` -- lockfile consistency checks for local dependencies +- manifest-bound `Cell.lock` v3 graph checks for local, Git, and Registry + dependencies, feature/test modes, and named CKB environments Use the top-level `cellc path/to/file.cell` form for one-off file compilation. Use `cellc build` for package builds. -Local `cellc install --path`, registry source-package `cellc install`, and -`cellc update` are supported lockfile workflows for packages that can be -resolved and source-hash verified. Public `cellc publish` is an authenticated -registry write authorised by a JoyID-rooted capability; `cellc registry add` -remains the local/offline discovery metadata path. Treat `run`, registry proxy -use, cryptographic publisher signature verification, and non-CellScript artifact -profiles as future-facing or fail-closed. +`cellc lock`, local `cellc install --path`, registry source-package +`cellc install`, and `cellc update` are explicit lockfile workflows for +packages that can be resolved and source-hash verified. Normal build/check/test +consume that graph; `--frozen` adds offline, no-write behavior. For an +interactive first Registry write, +`cellc publish --authorise` obtains a wallet-rooted delegated capability and +resumes the publish; later `cellc publish` calls use the active scoped key. +`cellc registry add` remains the local/offline discovery metadata path. +Non-CellScript artifact profiles have explicit fetch, verify, pin, copy, +deployment, and commitment commands and never become source dependencies by +implicit resolver coercion. ## Next With the tooling loop in place, continue with [Bundled Example Contracts](https://github.com/CellScript-Labs/CellScript/wiki/Tutorial-08-Bundled-Example-Contracts). +For the 0.24 checker and scenario boundaries, also read +[Verified Artifacts and Executable Tests](Tutorial-14-Verified-Artifacts-and-Executable-Tests.md). diff --git a/docs/wiki/Tutorial-08-Bundled-Example-Contracts.md b/docs/wiki/Tutorial-08-Bundled-Example-Contracts.md index fc189880..dd07c7ca 100644 --- a/docs/wiki/Tutorial-08-Bundled-Example-Contracts.md +++ b/docs/wiki/Tutorial-08-Bundled-Example-Contracts.md @@ -45,19 +45,37 @@ There are no checked-in `examples/business` or `examples/acceptance` mirrors; acceptance-only profile/effect/scheduler metadata belongs in runner configuration or generated files under `target/`. +Three 0.24 workflow packages sit beside, but are not part of, the business +matrix: + +- `examples/scenario_basics` runs one positive and one exact-negative scenario + through both the simulator and CKB-VM, and builds a four-file verified + artifact bundle; +- `examples/package_graph` demonstrates standard SemVer, a package alias, + optional and transitive features, a test-only dependency, explicit CKB + environments, a testnet dependency override, and frozen/offline consumption + of the tracked graph; +- `examples/registry_ls_idl` demonstrates the supported LS-IDL witness fields, + raw-byte SHA-256/executable-suffix binding, publish scaffolding, and curated + normal and negative compatibility vectors. + +These packages are deliberately small and synthetic. They teach tooling +boundaries without implying that simulator bookkeeping or illustrative chain +identities are production evidence. + Despite its legacy filename, `multisig.cell` is not a signature verifier or a standalone custody Lock Script. Its `Approval` values and `reported_time` arguments are witness data. A surrounding Lock Script must authenticate the approver, and any production time policy must bind a HeaderDep-derived value. -CellScript 0.22 has no implicit signer identity, sighash selection, or witness -layout. Packages that need cryptographic custody may call the explicit BIP340 -CellDep verifier ABI, but the bundled threshold-approval example deliberately -does not do so. +Since CellScript 0.22 there has been no implicit signer identity, sighash +selection, or witness layout. Packages that need cryptographic custody may call +the explicit BIP340 CellDep verifier ABI, but the bundled threshold-approval +example deliberately does not do so. ## Fiber Interoperability Examples -CellScript 0.22 also includes seven bounded interoperability examples under -`examples/fiber/`. They are not additional members of the bundled CKB +The CellScript 0.22 line introduced seven bounded interoperability examples +under `examples/fiber/`. They are not additional members of the bundled CKB production matrix: | Example | Interoperability boundary | @@ -73,7 +91,7 @@ production matrix: The `cellscript-fiber` adapter derives the dedicated artifact and native Fiber configuration; it does not change the `.cell` source into a Fiber-specific language. Follow the -[bounded Fiber interoperability guide](https://github.com/CellScript-Labs/CellScript/blob/nightly-0.22/examples/fiber/README.md) +[bounded Fiber interoperability guide](https://github.com/CellScript-Labs/CellScript/blob/nightly-0.24/examples/fiber/README.md) for the check, deployment, enable, materialization, and doctor workflow. `examples/registry.cell`, `examples/atomic_swap.cell`, @@ -137,6 +155,17 @@ cellc build --package amm_pool --target riscv64-elf --target-profile ckb --json cellc build --package launch --target riscv64-elf --target-profile ckb --json ``` +For the 0.24 workflow examples, follow their tracked locks without repinning: + +```bash +cd examples/scenario_basics +cellc test --frozen --offline --backend all --json + +cd ../package_graph +cellc check --frozen --offline --environment mainnet +cellc check --frozen --offline --environment testnet --features full +``` + Do not treat `cellc build --workspace` as the canonical compile-all command for this checked-in examples tree. Some folders under `examples/` are compiler and tooling fixtures rather than packages with a `src/main.cell` entry. diff --git a/docs/wiki/Tutorial-09-Action-Model-and-Canonical-Syntax.md b/docs/wiki/Tutorial-09-Action-Model-and-Canonical-Syntax.md index 96e1f29b..c13e81c2 100644 --- a/docs/wiki/Tutorial-09-Action-Model-and-Canonical-Syntax.md +++ b/docs/wiki/Tutorial-09-Action-Model-and-Canonical-Syntax.md @@ -36,18 +36,45 @@ as token split/merge, often do not have an identity-bearing state continuation. `destroy` validate transaction shape; they are not VM-side allocation or mutation effects. +## Canonical Field Separators + +Type declarations use a trailing comma after every field: + +```cellscript +resource Vault has store, create, consume { + owner: Address, + balance: u64, +} +``` + +This is the output produced by `cellc fmt` and the form used by the canonical +language example. The parser continues to accept newline-separated fields +without commas as compatibility input, but new source and documentation should +not use that spelling as the canonical style. + +Do not apply this rule to lifecycle field blocks. A `preserve` or +`std::lifecycle` block contains newline-separated field names, not a comma +list: + +```cellscript +preserve vault_after from vault_before { + owner + balance +} +``` + ## State Continuation Use `transition old -> new` for a same-type Cell continuation: ```cellscript shared Pool has store { - token_a_symbol: [u8; 8] - token_b_symbol: [u8; 8] - reserve_a: u64 - reserve_b: u64 - total_lp: u64 - fee_rate_bps: u16 + token_a_symbol: [u8; 8], + token_b_symbol: [u8; 8], + reserve_a: u64, + reserve_b: u64, + total_lp: u64, + fee_rate_bps: u16, } action swap_a_for_b(pool_before: Pool, input: Token, min_output: u64, to: Address) -> (pool_after: Pool, token_out: Token) { @@ -117,11 +144,11 @@ continues: ```cellscript receipt Listing has consume, burn { - nft_hash: Hash - seller: Address - price: u64 - payment_symbol: [u8; 8] - expires_at: u64 + nft_hash: Hash, + seller: Address, + price: u64, + payment_symbol: [u8; 8], + expires_at: u64, } action buy_listing(listing: Listing, nft_before: NFT, payment: Token, buyer: Address) -> (nft_after: NFT, seller_payment: Token) { diff --git a/docs/wiki/Tutorial-12-Phase1-Registry-End-to-End.md b/docs/wiki/Tutorial-12-Phase1-Registry-End-to-End.md index c60aa273..be80f0e7 100644 --- a/docs/wiki/Tutorial-12-Phase1-Registry-End-to-End.md +++ b/docs/wiki/Tutorial-12-Phase1-Registry-End-to-End.md @@ -1,140 +1,419 @@ -# Tutorial 12: Phase 1 Registry: End-to-End +# Tutorial 12: Registry Artifacts End to End -This tutorial walks through the Phase 1 registry loop at the level a package -author or reviewer needs: source identity, build identity, deployment identity, -and the commands that bind them together. +**Status**: current tutorial for publishing and inspecting CellScript and +non-CellScript artifacts in the public Registry. -For the longer repository version, read -[docs/tutorials/phase1-end-to-end.md](https://github.com/CellScript-Labs/CellScript/blob/main/docs/tutorials/phase1-end-to-end.md). +The Registry is not limited to dependency packages. It distinguishes source +libraries, profile libraries, CKB runtime verifiers, deployable contracts, +reproducible binaries, and copy-only templates. This tutorial uses the native +CellScript path first, then the generic artifact path. -## What Phase 1 Proves +## 1. Authorise the first publish -Phase 1 is not a chain acceptance test and not a trust oracle. It answers three -bounded questions: +Start from the package or artifact directory, not from an empty browser form: -| Question | Evidence | -| --- | --- | -| Which source was published? | `Cell.toml`, package source hash, namespace/name/version, registry metadata. | -| Which build came from that source? | Artifact hash, metadata hash, ABI/schema/constraint hashes, compiler version, target profile. | -| Which deployed Cell claims to contain that build? | Network, tx hash, output index, code hash, data hash, CellDep/deployment metadata. | +```bash +cellc publish --authorise +``` -The rule is fail-closed. Missing hashes, stale source, toolchain drift, or a -deployment record that does not match chain facts should be treated as a -verification failure. +`cellc` creates the delegated P-256 key, stores it as pending in the local OS +keychain, opens a 15-minute exact-coordinate Registry session, waits for wallet +approval, and then resumes the original publish. The private key never enters +the browser. The Registry atomically registers the public key, claims or reviews +the namespace, completes the session, and records the audit trail. Use +`--no-open` to print the browser URL for a remote or terminal-only environment. -## Author Flow +The browser token is fragment-only, survives a same-tab refresh, and is removed +on completion or expiry. Completed or review-pending sessions remain readable +to the polling CLI for 24 hours so an approval committed near the deadline can +be recovered. A local polling timeout preserves the pending key unless the +Registry confirms cancellation or pending-session expiry. -Start with a package: +The production site has no network selector and accepts mainnet evidence only. +Pudge testing uses `https://testnet.registry.cellscript.dev/registry`, with a +different API origin, database, object store, signing identity, wallet state, +and testnet-only evidence. Start that flow explicitly with: ```bash -cellc init my_contract -cd my_contract +cellc publish --authorise --api-url https://api.testnet.registry.cellscript.dev ``` -Fill in the package identity in `Cell.toml`: name, namespace, version, -description, repository, license, entry file, and target profile. Then write the -source and build it: +Sandbox records disappear from discovery after 72 hours and their source bytes +are purged after a 24-hour grace period; this does not erase Pudge chain history. +The explicit capability-submit and namespace-claim commands remain available +for CI, external-wallet signing, and recovery. -```bash -cellc check --target-profile ckb --json -cellc build --target riscv64-elf --target-profile ckb --json +## 2. Publish a CellScript source library + +Add the namespace to `Cell.toml`: + +```toml +[package] +name = "math" +version = "1.0.0" +namespace = "acme" ``` -Before publishing, do a local dry run: +Verify and publish: ```bash -cellc publish --dry-run --json +cellc package verify --json +cellc publish --dry-run +cellc publish --authorise # first publish +cellc publish # later publishes with an active delegated key ``` -For an offline mirror or release fixture, write local registry metadata: +Use `--artifact-kind profile_library` when the package is a named CellScript +profile library. Both kinds use compiler-backed verification and remain valid +`Cell.toml` dependencies. + +## 3. Publish a deployable CKB contract + +Create `Artifact.toml`: + +```toml +schema = "cellscript-registry-artifact" +namespace = "acme" +name = "vault-lock" +release = "1.0.0" +kind = "deployable_contract" +language = "rust" +bundle = "vault-lock.bundle.json" +description = "Vault lock Script" +``` + +Create a closed profile contract first. Its ABI hash is the CKB Blake2b-256 of +the immutable ABI object: + +```json +{ + "schema": "cellscript-registry-profile-contract-v1", + "artifact_kind": "deployable_contract", + "profile": "ckb_executable", + "build": { + "target": "riscv64imac-unknown-none-elf", + "toolchain": "rustc 1.97.1", + "profile": "release", + "source_revision": "", + "reproducible": false + }, + "security": { "status": "review_required" }, + "ckb": { + "vm_version": "2", + "script_role": "lock", + "hash_type": "data1", + "dep_type": "code", + "abi_hash": "" + } +} +``` + +Canonicalize it recursively by key and put that JSON string in the immutable +bundle. Each payload is base64-encoded bytes, not a path: + +```json +{ + "schema": "cellscript-registry-bundle", + "namespace": "acme", + "name": "vault-lock", + "release": "1.0.0", + "profile": "ckb_executable", + "manifest_json": "", + "objects": [ + { "role": "source", "content_base64": "..." }, + { "role": "executable", "content_base64": "..." }, + { "role": "abi", "content_base64": "..." } + ] +} +``` + +Validate before sending anything: ```bash -cellc publish --offline --json +cellc publish --artifact-manifest Artifact.toml --dry-run ``` -For public publishing, authorize a local publisher capability through the JoyID -flow, then publish: +The CLI checks the coordinate, release, kind/language pair, bundle profile, +required object roles, size limit, and computed hashes. Publish with: ```bash -cellc auth capability create --principal-id joyid:example --scope publish:cellscript/amm_pool --expires 90d --json > capability-payload.json -cellc auth capability submit --payload capability-payload.json --joyid-signature joyid-signature.json -cellc publish --json +cellc publish --artifact-manifest Artifact.toml ``` -The public write API admits package metadata, but consumers still verify the -source and build identity locally. +The release initially reports: + +```text +verification_status = pending +deployment_status = undeployed +availability_status = active +``` -## Consumer Flow +After the independent verifier binds the source, executable, ABI, and profile +contract hashes, verification becomes `hash_bound`. That is an integrity claim, +not a claim about Script semantics, security review, or deployment. + +## 4. Prove a reproducible build + +Skip this step for the non-reproducible example above. If the signed profile +sets `build.reproducible = true`, or the kind is `reproducible_binary`, the +release remains `evidence_required` until independent builders reproduce the +same executable. + +Each builder writes a bounded report: + +```json +{ + "schema": "cellscript-reproduction-report-v2", + "builder_id": "builder-a", + "trust_domain": "independent-org-a", + "builder_public_key": "p256-spki:", + "environment": "", + "source_hash": "", + "build_recipe_hash": "", + "artifact_hash": "", + "build_log_hash": "", + "generated_at": "2026-08-02T00:00:00Z", + "signature": { + "algorithm": "p256-sha256", + "signature": "" + } +} +``` -Add a dependency, resolve it, and check the resulting package graph: +Generate a signed report on each independent builder: ```bash -cellc add math --git https://example.com/math.git -cellc install -cellc package verify --json +cellc auth reproducer create \ + --builder-id builder-a \ + --trust-domain independent-org-a \ + --json > reports/builder-a-enrollment.json + +cellc artifact reproduction-report acme/vault-lock@1.0.0 \ + --artifact target/vault-lock \ + --build-log reports/builder-a.log \ + --builder-id builder-a \ + --trust-domain independent-org-a \ + --builder-key-id cap_ \ + --builder-public-key 'p256-spki:' \ + --output reports/builder-a.json ``` -Registry packages use the same fail-closed principle as path and Git -dependencies: the selected source must match the recorded identity before the -compiler can treat it as part of the build. +Each builder sends only the generated public `policy_builder` record to the +Registry operator. The private key stays in that builder's OS keychain. A CI +builder on Unix may pass `--private-key-output ` during enrollment, +import the mode-0600 file's PKCS#8 base64 value into its own secret manager as +`CELLSCRIPT_REPRODUCER_PRIVATE_KEY_PKCS8_B64`, and must not share that file. -Then build and verify the artifact: +Validate and combine at least two signed reports with distinct builder IDs, +public keys, and trust domains: ```bash -cellc build --target riscv64-elf --target-profile ckb --json -cellc verify-artifact build/main.elf --expect-target-profile ckb --verify-sources --production +cellc artifact reproduction-evidence acme/vault-lock@1.0.0 \ + --report reports/builder-a.json \ + --report reports/builder-b.json \ + --output reproduced-build-promotion.json +``` + +The command verifies each P-256 report signature and fetches and verifies the +signed release, predecessor build evidence, source, recipe, artifact, +environment, and report identities. It does not execute the publisher's recipe. +A Registry operator reviews and submits the generated `reproduced_build` +promotion payload. The API also requires every builder to match its configured +policy, enforces a minimum number of trust domains, and records that policy's +canonical SHA-256 and threshold in the accepted evidence. Only then does +verification become `verified`; a reproducible executable cannot be recorded +as deployed before this transition. + +## 5. Record a deployment on the Registry's fixed network + +The deployment request is a signed +`cellscript-registry-deployment` / `record_deployment` payload sent to: + +```text +POST /v1/artifacts/acme/vault-lock/releases/1.0.0/deployments ``` -## Deployment Review +It includes the published `artifact_hash`, equal `data_hash`, `code_hash`, +`hash_type`, `dep_type`, and the environment's OutPoint. The API requires a +separately granted `deployment:` capability for the same coordinate and prior +verified-build evidence; a `publish:` scope alone is insufficient. + +The API first verifies the configured RPC chain identity. It calls +`get_live_cell` to prove that the OutPoint remains live and reads +`get_transaction.tx_status` to prove the creation transaction is committed and +obtain the block hash used for confirmation counting. It rejects a dead or +missing Cell, an uncommitted creation transaction, insufficient confirmation +depth, a data-hash mismatch, a Type Script hash mismatch, a network mismatch, or an +OutPoint that is not bound to the published executable. A successful request +appends deployment evidence and changes only `deployment_status` to +`chain_verified`. + +For a DepGroup OutPoint, the API decodes the live Cell data as the canonical +Molecule `OutPointVec` and finds the matching live code member. It does not hash +the DepGroup container as though it were the executable. + +## 6. Publish and resolve an LS-IDL Lock Script interface -After a deployment adapter records chain facts, verify the local deployment -metadata: +For a Lock Script that follows LS-IDL 0.1, start with the original `idl.json` +bytes. Do not pretty-print or reserialise them after computing the commitment: ```bash -cellc registry verify --json +cellc artifact ls-idl validate --idl idl.json +cellc artifact ls-idl bind \ + --idl idl.json \ + --executable target/release/vault-lock \ + --output target/release/vault-lock.ls-idl +cellc artifact ls-idl bundle \ + --idl idl.json \ + --executable target/release/vault-lock.ls-idl \ + --source src/lib.rs \ + --namespace acme \ + --name vault-lock \ + --release 1.0.0 \ + --language rust \ + --hash-type data1 \ + --dep-type code \ + --toolchain rust-1.97.1 \ + --source-revision <40-hex-git-commit> \ + --output artifact.bundle.json \ + --artifact-manifest-output Artifact.toml +cellc publish --artifact-manifest Artifact.toml --dry-run --json ``` -If you have a CKB RPC endpoint and want live chain checks: +After publishing and recording chain-verified deployment evidence, resolve the +same bytes through either the CLI or canonical API: ```bash -cellc registry verify --live --rpc-url "$CELLSCRIPT_CKB_RPC_URL" --json +cellc artifact ls-idl fetch \ + --code-hash 0x<64-hex> \ + --hash-type data1 \ + --network mainnet \ + --output idl.json + +curl --fail \ + 'https://api.registry.cellscript.dev/v1/ckb/scripts/0x<64-hex>/interfaces/ls-idl?network=mainnet&hash_type=data1' \ + --output idl.json ``` -Live checks do not replace source/build verification. They add the chain-facing -question: does the recorded OutPoint still expose the expected deployment -identity? +The compatibility route `/idl/:code_hash` returns the same original bytes. +The Registry proves the document schema, raw-byte digest, executable suffix, +and deployment identity. It does not prove that the Lock Script correctly +implements the interface, and it is not a security audit. See the +[LS-IDL tutorial](Tutorial-15-LS-IDL-for-CKB-Lock-Scripts.md) for the complete +`cellc` workflow and the +[LS-IDL Registry profile](../CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md) for the +closed schema and trust boundary. -## What Not To Put In The Resolver +## 7. Inspect and consume the artifact -The registry may discover more than the resolver can import. Keep these -boundaries separate: +Open the artifact detail page or query the API: -| Object | Correct treatment | -| --- | --- | -| CellScript source package | `Cell.toml` dependency, resolved by `cellc install`. | -| Deployed verifier or helper script | Deployment/verifier evidence with code hash, data hash, OutPoint, ABI, and status. | -| Reproducible CKB binary | Future artifact profile, not a source package. | -| Protocol skeleton or cookbook | Copy into local source; after copying, verify as your own package. | +```bash +curl --fail 'https://api.registry.cellscript.dev/v1/artifacts/acme/vault-lock' +curl --fail 'https://api.registry.cellscript.dev/v1/artifacts/acme/vault-lock/releases/1.0.0/evidence' +``` + +Check these independently: -A useful repository is not automatically an installable dependency. A cookbook -is starting material, not registry-trusted source identity. +- artifact kind, profile, language, and consumption mode; +- source, executable, ABI, or recipe hashes; +- verification, deployment, and availability states; +- evidence producer and evidence hash; +- mainnet OutPoint, code hash, data hash, hash type, and dep type. -## Failure Modes To Expect +Do not use `cellc install` for this executable. `cellc install` accepts only +`cellscript_source` artifacts whose consumption mode is `dependency`. -Phase 1 should reject: +Consume it explicitly: -- source files that no longer hash to the published source identity; -- `Cell.lock` or deployment metadata that names a different build; -- missing compiler, target profile, ABI, schema, or constraints hashes; -- deployment records with mismatched network, tx hash, output index, code hash, - or data hash; -- production verification that still depends on unresolved runtime obligations. +```bash +cellc artifact fetch acme/vault-lock@1.0.0 --output vault-lock.bundle.json +cellc artifact verify --bundle vault-lock.bundle.json --receipt vault-lock.bundle.json.receipt.json +cellc artifact pin acme/vault-lock@1.0.0 --output Artifacts.lock --accept-hash-bound +cellc artifact reproduction-evidence acme/vault-lock@1.0.0 --report builder-a.json --report builder-b.json --output reproduced-build-promotion.json +cellc artifact record-deployment acme/vault-lock@1.0.0 --network mainnet --code-hash --hash-type data1 --dep-type code --tx-hash --index 0 --capability-key-id +cellc artifact cell-dep acme/vault-lock@1.0.0 --output CellDep.json --accept-hash-bound --rpc-url https://mainnet.ckb.dev/rpc +cellc artifact set-availability acme/vault-lock@1.0.0 --status yanked --reason "security advisory" --capability-key-id +cellc artifact commitment acme/vault-lock@1.0.0 --output RegistryCommitment.json +``` -## See Also +`cell-dep` fails until mainnet deployment evidence has been verified, then +rechecks that the deployment (and resolved DepGroup code member) is still live +at consumption time. Deployment mode must equal the immutable profile +contract. The commitment file contains canonical `CSREGv1` Cell data; +current commitment still requires the API to read a sufficiently confirmed +live mainnet Cell and match its configured Type/Lock identities and both live +code CellDeps. When those Scripts and CellDeps are configured, the file +also contains a mainnet-only transaction intent. A compatible wallet completes +capacity, inputs, change, fee, witnesses, signatures, and broadcast. + +Scheduled maintenance discovers exact Registry Type Script matches through the +CKB indexer. A sufficiently confirmed live matching commitment promotes the +current release to `on_chain_committed`; spending that Cell returns it to `deployed`; and spending +or replacing the deployment Cell returns it to `verified_build`. Accepted +evidence remains available for audit. + +The transaction-intent and scanner code is implemented, but production does +not claim a chain commitment until operators deploy and configure the canonical +mainnet Registry Type Script, commitment custody Lock, and both code CellDeps. + +For the isolated Pudge flow, use: + +```bash +cellc publish --api-url https://api.testnet.registry.cellscript.dev +cellc artifact record-deployment acme/vault-lock@1.0.0 \ + --network testnet \ + --api-url https://api.testnet.registry.cellscript.dev \ + --code-hash --hash-type data1 --dep-type code \ + --tx-hash --index 0 --capability-key-id +``` + +`cell-dep` reads the accepted evidence network and defaults to the matching +official RPC; an explicit `--rpc-url` still has to report the same chain. + +## 8. Other artifact kinds + +- `runtime_verifier`: `ckb_executable` bundle with source, executable, and ABI; + consumption mode is `tcb`. +- A generic `ckb_executable` with only `source`, `executable`, and `abi` + remains `hash_bound`. A CellScript release may opt into independent + structural admission by adding the complete `metadata`, `lowering_record`, + and `source_map` role set. Supplying only part of that set fails closed. The + least-privilege artifact worker records checker version, policy, and report + hash as `structurally_verified` evidence; it does not load the compiler and + does not claim source equivalence or deployment. +- A `ckb_executable` that is built reproducibly may additionally include + `build_recipe`, set `build.reproducible = true`, and bind the recipe, + environment, command, and expected executable hash in `reproduction`. +- `reproducible_binary`: `reproducible_build` bundle with source, executable, + and `build_recipe`; the Registry reports `evidence_required` until build + evidence is sufficient. +- `template`: `copy_material` bundle containing a + `cellscript-template-file-map-v1` source object; use `cellc artifact copy`. + It rejects traversal, duplicates, hash drift, and overwrites. + +An artifact declaring `security.status = "audited"` must also carry an +immutable `audit_report` bundle object whose CKB Blake2b-256 hash exactly +matches `security.audit_report_hash`. This authenticates the referenced report; +it does not make the Registry the auditor. + +## 9. Naming rules + +Namespace and artifact names are 1–64 characters. Use lowercase letters and +digits; `_` and `-` may appear only between characters. A one-character name is +valid. The UI and API enforce the same rule. + +## 10. Registry scope and repository validation + +The Registry names code, build recipes, TCB inputs, deployment facts, and +compact commitments. It does not operate application business Cells. Those +Cells remain governed by their own Lock/Type Scripts, schemas, and replacement +transactions; publishing a Script is not equivalent to indexing every state +Cell that uses it. + +```bash +./scripts/cellscript_gate.sh dev +``` -- [Packages and CLI Workflow](Tutorial-04-Packages-and-CLI-Workflow.md) -- [Metadata, Verification, and Production Gates](Tutorial-06-Metadata-Verification-and-Production-Gates.md) -- [CKB Target Profiles](Tutorial-05-CKB-Target-Profiles.md) -- [Agentic Loops and cellscript-mcp](Tutorial-13-Agentic-Loops-and-cellscript-mcp.md) -- `docs/CELLSCRIPT_PACKAGE_PROVENANCE_AND_DEPLOYMENT_IDENTITY.md` -- `docs/CELLSCRIPT_REGISTRY_PHASE1.md` +For the complete model and failure rules, see +[`docs/CELLSCRIPT_REGISTRY_PHASE1.md`](../CELLSCRIPT_REGISTRY_PHASE1.md). diff --git a/docs/wiki/Tutorial-14-Verified-Artifacts-and-Executable-Tests.md b/docs/wiki/Tutorial-14-Verified-Artifacts-and-Executable-Tests.md new file mode 100644 index 00000000..3a60931b --- /dev/null +++ b/docs/wiki/Tutorial-14-Verified-Artifacts-and-Executable-Tests.md @@ -0,0 +1,167 @@ +# Tutorial 14: Verified Artifacts and Executable Tests + +CellScript 0.24 adds two related boundaries: a standalone checker for generated +CKB ELF bundles, and package scenarios that must name and run an execution +backend. Together they make more compiler claims independently inspectable +without calling local execution chain evidence. + +## Build the Four-File Bundle + +The checked-in `scenario_basics` package is the smallest complete example. From +the repository root: + +```bash +cd examples/scenario_basics +cellc build --frozen --offline --json +``` + +The build emits: + +```text +build/main.elf +build/main.elf.meta.json +build/main.elf.lowering.json +build/main.elf.sourcemap.json +``` + +The lowering record is a canonical, versioned boundary over entries, basic +blocks, CFG and call edges, ABI and stack declarations, ProofPlan links, +syscalls, runtime-error exits, and final machine ranges. The source map connects +source spans and lowering blocks to those final instruction ranges. All four +files bind the same source, resolved compatibility profile, and artifact. + +Assembly output does not emit or claim this boundary. + +## Run the Independent Checker + +Verify the default bundle: + +```bash +cellc verify-artifact build/main.elf --json +``` + +For non-default paths: + +```bash +cellc verify-artifact build/main.elf \ + --metadata evidence/main.meta.json \ + --lowering-record evidence/main.lowering.json \ + --source-map evidence/main.sourcemap.json \ + --json +``` + +The standalone checker validates bounded schema, identity, CFG, ABI, frame, +stack, ProofPlan, ELF, RV64 instruction, branch/call, syscall, block-digest, +and source-map invariants. It does not load the CellScript front end or code +generator. Keep these report fields distinct: + +- `binding_verification`: the bundle identities agree; +- `structural_verification`: the independent structural policy passed; +- `lowering_record_verification`: the lowering contract passed; +- `ckb_vm_evidence`: whether CKB-VM was actually executed; +- `chain_evidence`: whether separate chain evidence was supplied; and +- `semantic_equivalence_claimed`: remains `false` for this boundary. + +A successful checker result is not proof that arbitrary source is equivalent +to arbitrary RISC-V. It is also not RPC admission, deployment, commitment, or +confirmation evidence. + +## Add an Executable Scenario + +Place a `*.scenario.json` file under the package's `tests/` directory. The v1 +schema names the confined source file, CKB target profile, entry, initial live +Cells, ordered replacement steps, dependencies, headers, `since`, witnesses, +limits, and an exact expectation. + +See `examples/scenario_basics/tests/pass.scenario.json` and +`assertion-failure.scenario.json` for runnable positive and exact-negative +fixtures. Scenario sources intentionally stay in the same `tests/` directory: +v1 rejects absolute paths and parent traversal instead of letting a fixture +escape its evidence root. + +A minimal positive shape is: + +```json +{ + "schema": "cellscript-test-scenario-v1", + "name": "main-succeeds", + "source": "main.cell", + "target_profile": "ckb", + "entry": { "kind": "action", "name": "main", "args": [] }, + "initial_cells": [], + "steps": [{ + "name": "run-main", + "consumes": [], + "outputs": [], + "cell_deps": [], + "header_deps": [], + "since": {}, + "witnesses": [], + "expectation": { "status": "pass", "result": "()", "runtime_error": null } + }], + "limits": { + "max_steps": 1000, + "max_cycles": 10000000, + "max_transaction_bytes": 65536, + "minimum_cell_capacity": 100000000 + }, + "oracle": null +} +``` + +Negative scenarios use `status = "runtime-error"` and must match both the +registered numeric `CellScriptRuntimeError` and its stable name. Unknown fields, +path escape, duplicate or stale Cell names, ambiguous indexes, invalid scripts, +and unsupported evidence requests fail before execution. + +## Run Both Evidence Tiers + +```bash +cellc test --backend simulator --frozen --offline +cellc test --backend ckb-vm --frozen --offline +cellc test --backend all --frozen --offline --json +``` + +The simulator is deterministic development feedback and is labelled +`development-non-consensus`. CKB-VM execution is labelled +`authoritative-runtime`. `cellc test` cannot report executed success without a +backend and an executable scenario; `--no-run` is the explicit compile-only +escape hatch. + +The v1 runner validates multi-step live-Cell bookkeeping: consumed names become +dead, declared outputs become live, and `prior_output` must name a Cell consumed +by the same step. The current CKB-VM backend executes no-argument ELF entries. +It does not yet inject scenario Cells into CKB syscalls. Transaction-shaped +entries must point at the separate stateful CKB oracle and must not be relabelled +as v1 CKB-VM scenario coverage. + +## Read Coverage Conservatively + +The JSON report binds the compiler, artifact, compatibility profile, checker +policy, lowering record, source map, backend, and evidence tier. Coverage lists +declared and observed entries, lowering blocks, ProofPlan links, runtime errors, +syscalls, and source-linked instruction ranges. + +Only the observed entry and exact runtime outcome are promoted. The presence of +an unexecuted branch, ProofPlan obligation, or syscall in metadata is not test +coverage. + +## Registry and Production Boundaries + +A generic CKB Registry bundle with `source`, `executable`, and `abi` remains +`hash_bound`. CellScript structural admission is opt-in and requires the +complete `metadata`, `lowering_record`, and `source_map` set; partial verified +sidecars fail closed. The least-privilege Registry worker records the checker +version, policy, and report hash as `structurally_verified` evidence without +loading the compiler. + +Neither structural Registry admission nor local scenarios replace builder, +dry-run, deployment, commitment, or confirmation evidence. Use the full release +gate only when making a production CKB claim. + +## Next + +Use [Metadata Verification and Production Gates](Tutorial-06-Metadata-Verification-and-Production-Gates.md) +to place these results in the full evidence ladder, and +[Packages and CLI Workflow](Tutorial-04-Packages-and-CLI-Workflow.md) for the +complete package lifecycle. diff --git a/docs/wiki/Tutorial-15-LS-IDL-for-CKB-Lock-Scripts.md b/docs/wiki/Tutorial-15-LS-IDL-for-CKB-Lock-Scripts.md new file mode 100644 index 00000000..2a65234a --- /dev/null +++ b/docs/wiki/Tutorial-15-LS-IDL-for-CKB-Lock-Scripts.md @@ -0,0 +1,494 @@ +# Tutorial 15: LS-IDL for CKB Lock Scripts + +**Status**: current CellScript 0.24 workflow for LS-IDL 0.1 Lock Script +interfaces. + +LS-IDL describes the witness fields expected by a CKB Lock Script. CellScript +Registry preserves that description as exact bytes and binds it to the +deployed executable with this commitment: + +```text +code Cell data = executable bytes || SHA-256(raw idl.json bytes) +``` + +This tutorial follows the complete implemented workflow: + +```text +raw idl.json + -> cellc validate + -> cellc bind to a clean CKB executable + -> cellc bundle and publish + -> deploy the bound executable + -> record chain evidence + -> cellc fetch the exact IDL by Script identity +``` + +The word **raw** matters. Whitespace, key order, and the final newline are part +of the committed IDL identity. Parsing and reserialising equivalent JSON can +produce different bytes and therefore a different SHA-256 digest. + +## What `cellc` Does + +The LS-IDL command group is: + +```bash +cellc artifact ls-idl +``` + +It provides four bounded operations: + +- `validate` checks the supported LS-IDL 0.1 schema and reports the SHA-256 of + the exact file bytes; +- `bind` appends that 32-byte digest to a CKB executable; +- `bundle` creates the immutable Registry bundle and `Artifact.toml`; and +- `fetch` resolves an IDL by a chain-verified CKB Script identity, validates + the Registry response contract, and writes the exact response bytes. + +`cellc` does not derive `idl.json` from a Rust type, compile an external Rust, +C, or JavaScript Lock Script, deploy a code Cell, or sign a deployment +transaction. Use +[`ckb-idl-derive`](https://github.com/OWK50GA/ckb-idl-derive), +[`ckb-idl-client`](https://github.com/OWK50GA/ckb-idl-client), and the Lock +Script's own build and deployment workflow for those steps. The executable +passed to `cellc` must already be a real CKB RISC-V artifact. + +The checked-in walkthrough inputs are under +[`examples/registry_ls_idl`](../../examples/registry_ls_idl/README.md). +`lock.rs` and `idl.json` show the derive-to-document relationship, but the +example intentionally does not pretend to be an audited or deployable Lock +Script. + +## Prerequisites + +Prepare all of the following before publishing: + +- CellScript 0.24 `cellc`; +- the original LS-IDL 0.1 `idl.json`; +- a clean, unbound CKB Lock Script executable; +- the source file to include in the Registry bundle; +- an immutable 40- or 64-hex source revision; +- the intended CKB `hash_type` and `dep_type`; and +- a compatible CKB wallet for Registry writes and deployment. + +Validation and lookup are public read operations and do not require a wallet. +Wallet authorisation is required when publishing or attaching deployment +evidence. + +## 1. Prepare the IDL Document + +A small document looks like this: + +```json +{ + "idl_version": "0.1", + "name": "demo_lock", + "witness": [ + { + "name": "signature", + "type": "secp256k1_sig", + "required": true, + "description": "Recoverable CKB secp256k1 signature" + }, + { + "name": "nonce", + "type": "uint64", + "required": true + }, + { + "name": "memo", + "type": "bytes", + "required": false + } + ] +} +``` + +The Registry profile accepts a non-empty JSON object no larger than 256 KiB. +`witness` is required and may contain at most 256 fields. Field names must be +unique. + +The implemented field types are: + +| LS-IDL type | Linear encoding | +|---|---| +| `uint8` | one unsigned byte | +| `uint32` | four-byte little-endian unsigned integer | +| `uint64` | eight-byte little-endian unsigned integer | +| `secp256k1_sig` | 65 bytes | +| `secp256k1_pubkey` | 33 bytes | +| `schnorr_sig` | 64 bytes | +| `bytes` | four-byte little-endian length followed by the payload | + +The optional top-level fields are `idl_version`, `name`, `description`, +`script_version`, and `signing`. When present, `signing` contains exactly the +non-empty string fields `algorithm`, `message`, and `hasher`. + +In LS-IDL 0.1, `required: false` is descriptive interface metadata. The current +linear decoder has no presence bitmap and does not skip that field on the +wire. Do not use `required: false` as an optional-field encoding rule. + +## 2. Validate the Exact Bytes + +Run validation before touching the executable: + +```bash +cellc artifact ls-idl validate --idl idl.json --json +``` + +The JSON result includes: + +```json +{ + "status": "valid", + "format": "ls-idl", + "format_version": "0.1", + "sha256": "", + "executable_suffix_bound": false +} +``` + +This step rejects malformed JSON, unknown keys, unsupported types, duplicate +field names, missing required field properties, and profile budget violations. +It does not modify the IDL or executable. + +Treat the reported digest as part of the release identity. If `idl.json` +changes afterward, even only in formatting, repeat validation and binding. + +## 3. Bind the IDL to the Executable + +Always bind from a clean build output: + +```bash +cellc artifact ls-idl bind \ + --idl idl.json \ + --executable build/demo-lock \ + --output build/demo-lock.ls-idl \ + --json +``` + +`bind` validates the IDL, computes `SHA-256(raw idl.json bytes)`, and writes: + +```text +build/demo-lock bytes || 32-byte IDL digest +``` + +It never silently overwrites an existing output. Choose a new output path or +pass `--force` only when replacing that exact intended file. If the input +already ends with the same digest, `bind` does not append it a second time. + +Do not repeatedly bind updated IDLs onto an already bound artifact. Rebuild or +return to the clean executable, then bind the new IDL once. The bound output is +the artifact that must be tested, hashed, deployed, and published; the original +unbound executable has a different CKB data hash and is not the registered +LS-IDL artifact. + +Verify the final pair explicitly: + +```bash +cellc artifact ls-idl validate \ + --idl idl.json \ + --executable build/demo-lock.ls-idl \ + --json +``` + +The result now reports `executable_suffix_bound: true`. A suffix mismatch is a +hard error. + +## 4. Create the Registry Bundle + +Create the immutable bundle only from the bound executable: + +```bash +cellc artifact ls-idl bundle \ + --idl idl.json \ + --executable build/demo-lock.ls-idl \ + --source src/lib.rs \ + --namespace example \ + --name demo-lock \ + --release 0.1.0 \ + --language rust \ + --hash-type data1 \ + --dep-type code \ + --toolchain 'rustc 1.97.1 + ckb-std' \ + --source-revision <40-or-64-hex-immutable-revision> \ + --output artifact.bundle.json \ + --artifact-manifest-output Artifact.toml \ + --json +``` + +The accepted `--language` values are `cellscript`, `rust`, `c`, `javascript`, +and `other`. `--hash-type` defaults to `data1`; `--dep-type` defaults to +`code`. State both explicitly in release automation so deployment evidence +cannot inherit an accidental default. + +The command writes two files: + +- `artifact.bundle.json` contains exactly one `source`, `executable`, and `abi` + object as Base64-encoded bytes; and +- `Artifact.toml` names the `deployable_contract` release and points to that + bundle. + +It also reports four different identities: + +| Output field | Algorithm and object | Purpose | +|---|---|---| +| `source_hash` | CKB Blake2b-256 of the source object | immutable source-object identity | +| `artifact_hash` | CKB Blake2b-256 of the bound executable | CKB executable/data identity | +| `abi_hash` | CKB Blake2b-256 of the raw IDL object | Registry ABI object identity | +| `idl_sha256` | SHA-256 of the raw IDL object | LS-IDL executable-suffix commitment | + +Do not substitute one hash for another. In particular, LS-IDL uses SHA-256 for +the suffix even though the Registry artifact and ABI objects also have CKB +Blake2b-256 identities. + +The generated profile records `script_role = "lock"`, +`encoding = "linear-le-v0"`, `build.reproducible = false`, and +`security.status = "review_required"`. The command does not manufacture a +reproducibility or audit claim. + +## 5. Dry-Run and Publish + +Validate the generated bundle locally before any Registry write: + +```bash +cellc publish \ + --artifact-manifest Artifact.toml \ + --dry-run \ + --json +``` + +The dry-run rechecks the coordinate, object roles, size limits, profile +contract, raw IDL schema and hashes, and executable suffix. It does not upload +anything. + +For a first production publish, let `cellc` create a scoped delegated key and +open the wallet authorisation flow: + +```bash +cellc publish \ + --artifact-manifest Artifact.toml \ + --authorise \ + --json +``` + +`cellc` prints the pending `cap_...` publishing key ID, stores its private key +in the local OS keychain, opens a 15-minute browser session, waits for CKB +wallet approval, and then continues the publish. The private key does not enter +the browser. + +This short interactive path requests only +`publish:example/demo-lock`. It deliberately does not grant permission to +attach deployment evidence or change availability. Those scopes are +independent. + +For the isolated Pudge Testnet Registry, select its API explicitly: + +```bash +cellc publish \ + --artifact-manifest Artifact.toml \ + --authorise \ + --api-url https://api.testnet.registry.cellscript.dev \ + --json +``` + +Production and Testnet have separate API origins, databases, object stores, +wallet state, and chain evidence. Do not publish to one environment and record +the deployment in the other. + +After admission, the release starts with independent states similar to: + +```text +verification_status = pending +deployment_status = undeployed +availability_status = active +``` + +The Registry worker must accept the immutable bundle and promote its integrity +evidence before deployment evidence can be attached. A `hash_bound` result is +an identity/integrity statement, not a security review. + +## 6. Deploy the Bound Artifact and Record Evidence + +Deploy `build/demo-lock.ls-idl`, not `build/demo-lock`. Deployment transaction +construction, capacity, fees, witnesses, signing, and broadcast remain in the +external CKB builder and wallet. + +After the deployment transaction is committed, attach its OutPoint to the +published release. First authorise a delegated key with the exact deployment +scope. The `principal_id` is the normalized identity binding derived from the +connected signer, not the displayed CKB address. Choose `joyid_ckb` or +`ckb_secp256k1` for `--principal-type`: + +```bash +cellc auth capability create \ + --principal-type \ + --principal-id \ + --scope deployment:example/demo-lock \ + --expires 90d \ + --json > deployment-capability.json +``` + +Sign the payload with the matching CKB wallet, save the wallet result as +`deployment-wallet-signature.json`, and submit it: + +```bash +cellc auth capability submit \ + --payload deployment-capability.json \ + --wallet-signature deployment-wallet-signature.json \ + --json +``` + +`create` stores the generated delegated private key in the local OS keychain; +`submit` returns its `cap_...` key ID after the Registry accepts the +wallet-rooted grant. For Testnet, add +`--registry-origin https://api.testnet.registry.cellscript.dev` to `create` +and `--api-url https://api.testnet.registry.cellscript.dev` to `submit`. + +The manual flow may request `publish:` and `deployment:` together when one key +must perform both operations, but neither scope implies the other. This +tutorial keeps them separate so a deployment key cannot publish a new release. + +Then record the Testnet deployment: + +```bash +cellc artifact record-deployment example/demo-lock@0.1.0 \ + --network testnet \ + --api-url https://api.testnet.registry.cellscript.dev \ + --code-hash 0x<64-hex> \ + --hash-type data1 \ + --dep-type code \ + --tx-hash 0x<64-hex-deployment-transaction-hash> \ + --index 0 \ + --capability-key-id cap_ \ + --json +``` + +For production, use `--network mainnet` and the default production Registry +origin. The command signs the deployment record with the delegated key, while +the Registry verifies the configured RPC network, live code Cell, committed +creation transaction, confirmations, artifact data hash, Script identity, and +declared deployment mode. + +The immutable profile and the evidence command must agree on `hash_type` and +`dep_type`: + +- for `data`, `data1`, or `data2`, `code_hash` identifies the bound code Cell + data; and +- for `type`, `code_hash` is the code Cell Type Script hash. Later LS-IDL + lookup also needs the current code Cell `data_hash` to disambiguate the + executable bytes. + +Do not describe a locally generated deployment payload as chain evidence. The +Registry lookup becomes available only after the release is active, public, +and backed by accepted chain-verified deployment evidence. + +## 7. Fetch the Exact IDL by Script Identity + +For a mainnet `data1` Script: + +```bash +cellc artifact ls-idl fetch \ + --code-hash 0x<64-hex> \ + --hash-type data1 \ + --network mainnet \ + --output fetched-idl.json \ + --json +``` + +For Testnet, use the matching Registry API: + +```bash +cellc artifact ls-idl fetch \ + --code-hash 0x<64-hex> \ + --hash-type data1 \ + --network testnet \ + --api-url https://api.testnet.registry.cellscript.dev \ + --output fetched-idl.json \ + --json +``` + +For a Type Hash deployment, add the live code Cell data hash: + +```bash +cellc artifact ls-idl fetch \ + --code-hash 0x<64-hex-type-script-hash> \ + --hash-type type \ + --data-hash 0x<64-hex-code-cell-data-hash> \ + --network mainnet \ + --output fetched-idl.json \ + --json +``` + +`fetch` refuses to overwrite an existing output unless `--force` is explicit. +Before writing, it requires the LS-IDL content type and +`schema-and-suffix-bound` verification header, enforces the 256 KiB limit, +validates the LS-IDL schema, hashes the response body directly, and compares +that digest with the Registry header. + +The public browser flows expose the same lookup for +[Mainnet](https://cellscript.dev/registry/LS-IDL) and the isolated +[Pudge Testnet](https://testnet.registry.cellscript.dev/registry/LS-IDL/). +Lookup and download are read-only; connecting a wallet is needed only for +Registry writes. + +## 8. Understand the Evidence Boundary + +The workflow deliberately separates evidence: + +| Stage | What it establishes | +|---|---| +| `validate --idl` | supported schema and SHA-256 of the exact IDL bytes | +| `validate --executable` | the executable ends with that exact 32-byte digest | +| `bundle` | source, executable, ABI objects, hashes, and closed profile agree | +| Registry verification | admitted immutable objects satisfy the same profile contract | +| `record-deployment` | accepted live-chain deployment evidence matches the published artifact | +| `fetch` | the returned exact bytes match the Registry digest and verification contract | + +None of these stages proves that the Lock Script actually decodes every field +as described, applies the intended signing rules, authorises the correct user, +accepts a complete valid transaction set, rejects every invalid transaction, +or is secure. LS-IDL Registry support is a byte-identity and deployment-binding +contract. Implementation tests, CKB-VM transaction tests, review, and audit +remain separate responsibilities. + +`cellc verify-artifact` is also a different boundary. It independently checks +the four-file lowering/source-map bundle emitted by CellScript CKB builds; it +is not a replacement LS-IDL verifier for an arbitrary Rust or C executable. + +## Common Failures + +### The executable suffix does not match + +The IDL was changed or reformatted after binding, or the unbound executable was +selected. Return to the clean executable and bind the final IDL bytes again. + +### `cellc` refuses to overwrite a file + +This is intentional. Use a new output path, or pass `--force` only after +checking the exact target. + +### Type Hash lookup requires `--data-hash` + +A Type Script hash may identify more than one executable data revision. Supply +the current code Cell data hash; the Registry returns `409` rather than choosing +an ambiguous deployment. + +### Fetch returns not found + +Check the Registry environment, network, `code_hash`, `hash_type`, and optional +`data_hash`. A published bundle is not enough: the release must also be active, +public, and chain-verified on the requested network. + +### Publish succeeds but lookup is not ready + +Publication, verification, and deployment are separate states. Wait for bundle +verification, deploy the bound artifact, and record the committed deployment +OutPoint before expecting Script-identity lookup to succeed. + +## Next + +Read [Registry Artifacts End to End](Tutorial-12-Phase1-Registry-End-to-End.md) +for publisher capabilities, deployment evidence, availability, and artifact +consumption. Use the +[LS-IDL Registry Profile](../CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md) as the +closed schema and API reference. diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md index 78e3b461..e64cffe4 100644 --- a/docs/wiki/_Sidebar.md +++ b/docs/wiki/_Sidebar.md @@ -12,11 +12,16 @@ - [Tutorial 09: Action Model and Canonical Syntax](https://github.com/CellScript-Labs/CellScript/wiki/Tutorial-09-Action-Model-and-Canonical-Syntax) - [Tutorial 10: Standard Library](https://github.com/CellScript-Labs/CellScript/wiki/Tutorial-10-Standard-Library) - [Tutorial 11: Scoped Invariants and ProofPlan](https://github.com/CellScript-Labs/CellScript/wiki/Tutorial-11-Scoped-Invariants-and-ProofPlan) -- [Tutorial 12: Phase 1 Registry: End-to-End](https://github.com/CellScript-Labs/CellScript/wiki/Tutorial-12-Phase1-Registry-End-to-End) +- [Tutorial 12: Registry Artifacts End-to-End](https://github.com/CellScript-Labs/CellScript/wiki/Tutorial-12-Phase1-Registry-End-to-End) - [Tutorial 13: Agentic Loops and cellscript-mcp](https://github.com/CellScript-Labs/CellScript/wiki/Tutorial-13-Agentic-Loops-and-cellscript-mcp) +- [Tutorial 14: Verified Artifacts and Executable Tests](https://github.com/CellScript-Labs/CellScript/wiki/Tutorial-14-Verified-Artifacts-and-Executable-Tests) +- [Tutorial 15: LS-IDL for CKB Lock Scripts](https://github.com/CellScript-Labs/CellScript/wiki/Tutorial-15-LS-IDL-for-CKB-Lock-Scripts) - [Cookbook Recipes](https://github.com/CellScript-Labs/CellScript/wiki/Cookbook-Recipes) - [CKB Glossary](https://github.com/CellScript-Labs/CellScript/wiki/CKB-Glossary) - [Spore and RGB++ Interoperability Boundaries](https://github.com/CellScript-Labs/CellScript/wiki/Spore-and-RGBPP-Interop-Boundaries) -- [BIP340 Verifier CellDep ABI](https://github.com/CellScript-Labs/CellScript/blob/nightly-0.22/docs/CELLSCRIPT_SIGNATURE_VERIFIER_ABI.md) -- [CellScript 0.22 Release Notes](https://github.com/CellScript-Labs/CellScript/blob/nightly-0.22/docs/releases/CELLSCRIPT_0_22_RELEASE_NOTES.md) -- [Bounded Fiber Interoperability Guide](https://github.com/CellScript-Labs/CellScript/blob/nightly-0.22/examples/fiber/README.md) +- [BIP340 Verifier CellDep ABI](https://github.com/CellScript-Labs/CellScript/blob/nightly-0.24/docs/CELLSCRIPT_SIGNATURE_VERIFIER_ABI.md) +- [LS-IDL Registry Profile](https://github.com/CellScript-Labs/CellScript/blob/nightly-0.24/docs/CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md) +- [CellScript 0.22 Release Notes](https://github.com/CellScript-Labs/CellScript/blob/v0.22.0/docs/releases/CELLSCRIPT_0_22_RELEASE_NOTES.md) +- [CellScript 0.23 Release Notes](https://github.com/CellScript-Labs/CellScript/blob/v0.23.0/docs/releases/CELLSCRIPT_0_23_RELEASE_NOTES.md) +- [CellScript 0.24 Release Notes](https://github.com/CellScript-Labs/CellScript/blob/v0.24.0/docs/releases/CELLSCRIPT_0_24_RELEASE_NOTES.md) +- [Bounded Fiber Interoperability Guide](https://github.com/CellScript-Labs/CellScript/blob/nightly-0.24/examples/fiber/README.md) diff --git a/editors/vscode-cellscript b/editors/vscode-cellscript index 61e1f2cf..8d5eba22 160000 --- a/editors/vscode-cellscript +++ b/editors/vscode-cellscript @@ -1 +1 @@ -Subproject commit 61e1f2cf11170fe765e82136a3b7762cff0935c4 +Subproject commit 8d5eba22468d2ba7bcbba20b0711da70a1238f2a diff --git a/examples/Cell.toml b/examples/Cell.toml index 01b7a7b8..da97c6de 100644 --- a/examples/Cell.toml +++ b/examples/Cell.toml @@ -11,4 +11,6 @@ members = [ "token", "vesting", "language", + "package_graph", + "scenario_basics", ] diff --git a/examples/amm_pool/Cell.lock b/examples/amm_pool/Cell.lock new file mode 100644 index 00000000..a206d4e7 --- /dev/null +++ b/examples/amm_pool/Cell.lock @@ -0,0 +1,23 @@ +version = 3 +schema = "cellscript-lock-v0.24-graph-v1" + +[package] +edition = "2026" +name = "amm_pool" +version = "0.1.0" +source_hash = "3931af98c3db3e68e166ddd4f7f367679ddbc1144243d9475a60f62348e760c6" + +[root] +manifest_digest = "sha256:f3899870aed3db9e1fbfe237b0425f90f5fcbd2953d7d929da9833f136191d06" + +[root.dependencies] +token = "token@0.1.0|path:../token|env=default|features=default" + +[dependencies."token@0.1.0|path:../token|env=default|features=default"] +name = "token" +version = "0.1.0" +source_hash = "d5ecb3241eeb97983005b9622cbe7a9540cbdec133ec22105c07955f99c7b889" +manifest_digest = "sha256:3adaae2b8a70a8a49246c28c95a6bd3686fefaed383dfb598c7d35da2a2ad4f9" + +[dependencies."token@0.1.0|path:../token|env=default|features=default".source.Path] +path = "../token" diff --git a/examples/amm_pool/Cell.toml b/examples/amm_pool/Cell.toml index ec67990d..34c6be89 100644 --- a/examples/amm_pool/Cell.toml +++ b/examples/amm_pool/Cell.toml @@ -1,4 +1,5 @@ [package] +edition = "2026" name = "amm_pool" version = "0.1.0" diff --git a/examples/atomic_swap.cell b/examples/atomic_swap.cell index f1d6b31e..682de9f2 100644 --- a/examples/atomic_swap.cell +++ b/examples/atomic_swap.cell @@ -8,6 +8,8 @@ use cellscript::fungible_token::Token // exercises consume/create/destroy, witness binding, hash commitments, and // time-point guards across a multi-action business flow. +const U64_MAX: u64 = 18446744073709551615 + const REFUND_LOCK_PERIOD: u64 = 100 enum SwapState { @@ -60,7 +62,7 @@ action initiate_swap( let now = env::current_timepoint() require token.amount > 0, "zero amount" require timeout_timepoint > now, "timeout is not in the future" - require timeout_timepoint <= 18446744073709551515, "refund timeout overflow" + require timeout_timepoint <= U64_MAX - REFUND_LOCK_PERIOD, "refund timeout overflow" consume token create swap_lock = SwapLock { swap_id, initiator, participant, hashlock, timeout_timepoint, token_symbol: token.symbol, amount: token.amount, state: SwapState::Pending } with_lock(initiator) diff --git a/examples/atomic_swap/Cell.lock b/examples/atomic_swap/Cell.lock new file mode 100644 index 00000000..06b4a459 --- /dev/null +++ b/examples/atomic_swap/Cell.lock @@ -0,0 +1,23 @@ +version = 3 +schema = "cellscript-lock-v0.24-graph-v1" + +[package] +edition = "2026" +name = "atomic_swap" +version = "0.1.0" +source_hash = "02d817d1f3ce4df9dce11809d8398704ca3cc8ada777bf00551f6638b3a9168f" + +[root] +manifest_digest = "sha256:e2a638007970156136b60e89f4e93af2ffbf2eb9dfec16060159e2f687537f12" + +[root.dependencies] +token = "token@0.1.0|path:../token|env=default|features=default" + +[dependencies."token@0.1.0|path:../token|env=default|features=default"] +name = "token" +version = "0.1.0" +source_hash = "d5ecb3241eeb97983005b9622cbe7a9540cbdec133ec22105c07955f99c7b889" +manifest_digest = "sha256:3adaae2b8a70a8a49246c28c95a6bd3686fefaed383dfb598c7d35da2a2ad4f9" + +[dependencies."token@0.1.0|path:../token|env=default|features=default".source.Path] +path = "../token" diff --git a/examples/atomic_swap/Cell.toml b/examples/atomic_swap/Cell.toml index 39157d9e..1bffb3b6 100644 --- a/examples/atomic_swap/Cell.toml +++ b/examples/atomic_swap/Cell.toml @@ -1,4 +1,5 @@ [package] +edition = "2026" name = "atomic_swap" version = "0.1.0" diff --git a/examples/atomic_swap/src/main.cell b/examples/atomic_swap/src/main.cell index f1d6b31e..682de9f2 100644 --- a/examples/atomic_swap/src/main.cell +++ b/examples/atomic_swap/src/main.cell @@ -8,6 +8,8 @@ use cellscript::fungible_token::Token // exercises consume/create/destroy, witness binding, hash commitments, and // time-point guards across a multi-action business flow. +const U64_MAX: u64 = 18446744073709551615 + const REFUND_LOCK_PERIOD: u64 = 100 enum SwapState { @@ -60,7 +62,7 @@ action initiate_swap( let now = env::current_timepoint() require token.amount > 0, "zero amount" require timeout_timepoint > now, "timeout is not in the future" - require timeout_timepoint <= 18446744073709551515, "refund timeout overflow" + require timeout_timepoint <= U64_MAX - REFUND_LOCK_PERIOD, "refund timeout overflow" consume token create swap_lock = SwapLock { swap_id, initiator, participant, hashlock, timeout_timepoint, token_symbol: token.symbol, amount: token.amount, state: SwapState::Pending } with_lock(initiator) diff --git a/examples/ckb-sdk-builder/Cargo.toml b/examples/ckb-sdk-builder/Cargo.toml index e5f78668..516f2c5c 100644 --- a/examples/ckb-sdk-builder/Cargo.toml +++ b/examples/ckb-sdk-builder/Cargo.toml @@ -7,3 +7,6 @@ publish = false [dependencies] cellscript-ckb-adapter = { path = "../../crates/cellscript-ckb-adapter" } + +[dev-dependencies] +ckb-types = "1.0.0" diff --git a/examples/ckb-sdk-builder/README.md b/examples/ckb-sdk-builder/README.md index 26fe54b6..0d5309d5 100644 --- a/examples/ckb-sdk-builder/README.md +++ b/examples/ckb-sdk-builder/README.md @@ -24,8 +24,27 @@ It demonstrates the boundary: outputs, lineage, witnesses, warnings, and estimated fee without rendering UI. - `AcceptedActionReport` records cycles, tx-pool acceptance, optional submitted tx hash, tx size, occupied capacity, fee, and lineage after node checks. +- `place_entry_witness_payload_before_signing` preserves the existing + `WitnessArgs.lock` and `output_type` fields while placing the compiler-emitted + `CSARGv1` payload in canonical `WitnessArgs.input_type`. Call it before any + lock-script signer because the signature commits to the complete witness. - `ckb-sdk-rust` owns transaction building, signer integration, RPC cycle estimation, tx-pool acceptance, and optional submission. +The placement step is explicit: + +```rust +let witness = place_entry_witness_payload_before_signing( + &base_witness_args, + EntryWitnessPlacementAbi::WitnessArgsInputTypeV2, + entry_payload, +)?; +``` + +`entry_payload` is the raw output of `cellc entry-witness`; the resulting +serialized `WitnessArgs` is the transaction witness. A raw `CSARGv1` payload, +`output_type` alias, or post-signing mutation is not compatible with the 0.23 +CKB entry ABI. + The cookbook tests are offline and do not require a running CKB node. Focused local-node evidence lives in `scripts/cellscript_ckb_adapter_acceptance.sh`. diff --git a/examples/ckb-sdk-builder/src/lib.rs b/examples/ckb-sdk-builder/src/lib.rs index 765e8d84..7981c2f2 100644 --- a/examples/ckb-sdk-builder/src/lib.rs +++ b/examples/ckb-sdk-builder/src/lib.rs @@ -9,6 +9,7 @@ pub use cellscript_ckb_adapter::*; #[cfg(test)] mod tests { use super::*; + use ckb_types::{bytes::Bytes, packed::WitnessArgs, prelude::*}; #[test] fn cookbook_uses_formal_adapter_crate() { @@ -20,4 +21,25 @@ mod tests { assert!(!evidence.ckb_vm_execution); assert!(!evidence.tx_pool_acceptance); } + + #[test] + fn cookbook_places_entry_payload_in_witnessargs_input_type_before_signing() { + let base = WitnessArgs::new_builder() + .lock(Some(Bytes::from(vec![0u8; 65])).pack()) + .output_type(Some(Bytes::from_static(b"preserved-output-type")).pack()) + .build(); + let payload = Bytes::from_static(b"CSARGv1\0\x2a\0\0\0\0\0\0\0"); + + let witness = + place_entry_witness_payload_before_signing(&base, EntryWitnessPlacementAbi::WitnessArgsInputTypeV2, payload.clone()) + .expect("canonical entry placement should succeed before signing"); + + assert_eq!(witness.lock().to_opt().expect("lock placeholder preserved").raw_data().len(), 65); + assert_eq!(witness.input_type().to_opt().expect("entry payload placed").raw_data(), payload); + assert_eq!( + witness.output_type().to_opt().expect("output_type preserved").raw_data(), + Bytes::from_static(b"preserved-output-type") + ); + assert_eq!(EntryWitnessPlacementAbi::WitnessArgsInputTypeV2.name(), ENTRY_WITNESS_PLACEMENT_ABI); + } } diff --git a/examples/ecosystem/rgbpp-identity-adapter/Cell.lock b/examples/ecosystem/rgbpp-identity-adapter/Cell.lock new file mode 100644 index 00000000..dd666424 --- /dev/null +++ b/examples/ecosystem/rgbpp-identity-adapter/Cell.lock @@ -0,0 +1,13 @@ +version = 3 +schema = "cellscript-lock-v0.24-graph-v1" + +[package] +edition = "2026" +name = "rgbpp-identity-adapter" +version = "0.1.0" +source_hash = "ccd0b78a969aed2eb9369b6b084e936c0cc424b9e148fe2260d3686316c0cf65" + +[root] +manifest_digest = "sha256:3b780858ff3139137e16a91ad2000b38fb8f1b604a9abc631b3a4388ec96c82f" + +[dependencies] diff --git a/examples/ecosystem/rgbpp-identity-adapter/Cell.toml b/examples/ecosystem/rgbpp-identity-adapter/Cell.toml index 4949d360..41025224 100644 --- a/examples/ecosystem/rgbpp-identity-adapter/Cell.toml +++ b/examples/ecosystem/rgbpp-identity-adapter/Cell.toml @@ -1,3 +1,4 @@ [package] +edition = "2026" name = "rgbpp-identity-adapter" version = "0.1.0" diff --git a/examples/ecosystem/spore-identity-adapter/Cell.lock b/examples/ecosystem/spore-identity-adapter/Cell.lock new file mode 100644 index 00000000..7a100cec --- /dev/null +++ b/examples/ecosystem/spore-identity-adapter/Cell.lock @@ -0,0 +1,13 @@ +version = 3 +schema = "cellscript-lock-v0.24-graph-v1" + +[package] +edition = "2026" +name = "spore-identity-adapter" +version = "0.1.0" +source_hash = "dde328d2a240144d0bf38f17fc2ab7bacf6c7bcd8d153adaace02e11801e97bf" + +[root] +manifest_digest = "sha256:b90a106e1761fcce0e3c62fffe787624635d6cd67e81cbe2a19992cdb496a17d" + +[dependencies] diff --git a/examples/ecosystem/spore-identity-adapter/Cell.toml b/examples/ecosystem/spore-identity-adapter/Cell.toml index 0d0edc96..4b3d3372 100644 --- a/examples/ecosystem/spore-identity-adapter/Cell.toml +++ b/examples/ecosystem/spore-identity-adapter/Cell.toml @@ -1,3 +1,4 @@ [package] +edition = "2026" name = "spore-identity-adapter" version = "0.1.0" diff --git a/examples/language/Cell.lock b/examples/language/Cell.lock new file mode 100644 index 00000000..ba9576a2 --- /dev/null +++ b/examples/language/Cell.lock @@ -0,0 +1,13 @@ +version = 3 +schema = "cellscript-lock-v0.24-graph-v1" + +[package] +edition = "2026" +name = "language" +version = "0.1.0" +source_hash = "1144971351564a15e0b22b0aafb89d1efd7a5455d369d01a7ab65a86433a1920" + +[root] +manifest_digest = "sha256:6933e8e1d59ab812f7d2906a21a2bedf26b2a9d37a4207923f808dc050cdad09" + +[dependencies] diff --git a/examples/language/Cell.toml b/examples/language/Cell.toml index c9c1718c..4e8a89b3 100644 --- a/examples/language/Cell.toml +++ b/examples/language/Cell.toml @@ -1,3 +1,4 @@ [package] +edition = "2026" name = "language" version = "0.1.0" diff --git a/examples/language/canonical_style.cell b/examples/language/canonical_style.cell index c215ee8c..133e6d38 100644 --- a/examples/language/canonical_style.cell +++ b/examples/language/canonical_style.cell @@ -1,56 +1,43 @@ module cellscript::canonical_style resource Vault has store, create, consume, replace, relock { - owner: Address - asset_symbol: [u8; 8] - balance: u64 + owner: Address, + asset_symbol: [u8; 8], + balance: u64, } receipt DepositReceipt has store, create, consume, burn { - amount: u64 - participant: Address - approvals: Vec
+ amount: u64, + participant: Address, + approvals: Vec
, } action open_vault(owner: Address, asset_symbol: [u8; 8], balance: u64) -> vault: Vault { verification require balance > 0, "empty vault" - - create vault = Vault { - owner, - asset_symbol, - balance - } with_lock(owner) - + create vault = Vault { owner, asset_symbol, balance } with_lock(owner) } + action issue_receipt(vault: Vault, amount: u64, participant: Address) -> (next_vault: Vault, receipt: DepositReceipt) { verification require amount > 0 require participant == vault.owner - std::lifecycle::transfer(vault, next_vault, participant) { owner asset_symbol balance } - - create receipt = DepositReceipt { - amount, - participant, - approvals: [] - } with_lock(participant) - + create receipt = DepositReceipt { amount, participant, approvals: [] } with_lock(participant) } + action discard_empty_receipt(receipt: DepositReceipt) { verification require receipt.amount == 0 destroy receipt - } + lock vault_owner(protected vault: Vault, lock_args owner: Address, witness claimed_owner: Address) -> bool { verification - // CKB boundary surfaces: protected input cell, script args, witness field, - // and an explicit sighash digest. This still is not first-class signer auth. let input = source::group_input(0) let witness_lock = witness::lock(input) let digest = env::sighash_all(input) diff --git a/examples/language/v0_14_capacity_time.cell b/examples/language/v0_14_capacity_time.cell index 4abae553..0cbaf7d4 100644 --- a/examples/language/v0_14_capacity_time.cell +++ b/examples/language/v0_14_capacity_time.cell @@ -12,6 +12,7 @@ action mint_after_maturity(amount: u64) -> output: TimedToken { // pretend to emulate CKB epoch or block-number behavior. require_maturity(100) require_time(1714000000) + require_epoch_after(10, 0, 1) require_epoch_relative(10, 0, 1) let floor = occupied_capacity("TimedToken") diff --git a/examples/launch/Cell.lock b/examples/launch/Cell.lock new file mode 100644 index 00000000..2c78050d --- /dev/null +++ b/examples/launch/Cell.lock @@ -0,0 +1,36 @@ +version = 3 +schema = "cellscript-lock-v0.24-graph-v1" + +[package] +edition = "2026" +name = "launch" +version = "0.1.0" +source_hash = "23c016db10e5cd19e5592a78300bb849fac358e3771ca2bd9e664edb0a19194d" + +[root] +manifest_digest = "sha256:9629cf5051cae3a674cd370db56cdc9adbe65014629e1cd2403f93caf5dc7d17" + +[root.dependencies] +amm_pool = "amm_pool@0.1.0|path:../amm_pool|env=default|features=default" +token = "token@0.1.0|path:../token|env=default|features=default" + +[dependencies."amm_pool@0.1.0|path:../amm_pool|env=default|features=default"] +name = "amm_pool" +version = "0.1.0" +source_hash = "3931af98c3db3e68e166ddd4f7f367679ddbc1144243d9475a60f62348e760c6" +manifest_digest = "sha256:f3899870aed3db9e1fbfe237b0425f90f5fcbd2953d7d929da9833f136191d06" + +[dependencies."amm_pool@0.1.0|path:../amm_pool|env=default|features=default".source.Path] +path = "../amm_pool" + +[dependencies."amm_pool@0.1.0|path:../amm_pool|env=default|features=default".dependencies] +token = "token@0.1.0|path:../token|env=default|features=default" + +[dependencies."token@0.1.0|path:../token|env=default|features=default"] +name = "token" +version = "0.1.0" +source_hash = "d5ecb3241eeb97983005b9622cbe7a9540cbdec133ec22105c07955f99c7b889" +manifest_digest = "sha256:3adaae2b8a70a8a49246c28c95a6bd3686fefaed383dfb598c7d35da2a2ad4f9" + +[dependencies."token@0.1.0|path:../token|env=default|features=default".source.Path] +path = "../token" diff --git a/examples/launch/Cell.toml b/examples/launch/Cell.toml index 8f274b81..d9e9c05a 100644 --- a/examples/launch/Cell.toml +++ b/examples/launch/Cell.toml @@ -1,4 +1,5 @@ [package] +edition = "2026" name = "launch" version = "0.1.0" diff --git a/examples/multi_phase_dao.cell b/examples/multi_phase_dao.cell index 1d932352..2fc6519d 100644 --- a/examples/multi_phase_dao.cell +++ b/examples/multi_phase_dao.cell @@ -2,6 +2,8 @@ module cellscript::multi_phase_dao use cellscript::fungible_token::Token +const U64_MAX: u64 = 18446744073709551615 + // Multi-phase governance: a proposal moves through a linear state machine // (Draft -> Active -> Executed or Defeated). This example exercises flow-edge // validation, state transitions, vote tallying, and the full consume/create @@ -75,7 +77,7 @@ action propose( verification require voting_duration > 0, "zero voting duration" let now = env::current_timepoint() - require voting_duration <= 18446744073709551615 - now, "voting end overflow" + require voting_duration <= U64_MAX - now, "voting end overflow" create proposal = Proposal { proposal_id, state: ProposalState::Draft, proposer, for_votes: 0, against_votes: 0, start_timepoint: now, end_timepoint: now + voting_duration, execution_payload_hash } with_lock(proposer) } @@ -116,8 +118,8 @@ action cast_vote( require now < proposal_before.end_timepoint, "voting closed" require voting_token.amount > 0, "zero weight" require voting_token.symbol == config.voting_token_symbol, "wrong voting token" - require !support || voting_token.amount <= 18446744073709551615 - proposal_before.for_votes, "for-vote overflow" - require support || voting_token.amount <= 18446744073709551615 - proposal_before.against_votes, "against-vote overflow" + require !support || voting_token.amount <= U64_MAX - proposal_before.for_votes, "for-vote overflow" + require support || voting_token.amount <= U64_MAX - proposal_before.against_votes, "against-vote overflow" let for_votes = if support { proposal_before.for_votes + voting_token.amount } else { proposal_before.for_votes } let against_votes = if support { proposal_before.against_votes } else { proposal_before.against_votes + voting_token.amount } preserve proposal_after from proposal_before { @@ -156,7 +158,7 @@ action execute_proposal( let now = env::current_timepoint() require proposal_before.state == ProposalState::Active, "not active" require now >= proposal_before.end_timepoint, "voting not closed" - require proposal_before.against_votes <= 18446744073709551615 - proposal_before.for_votes, "vote total overflow" + require proposal_before.against_votes <= U64_MAX - proposal_before.for_votes, "vote total overflow" let total_votes = proposal_before.for_votes + proposal_before.against_votes require total_votes >= config.quorum_votes, "quorum not met" require proposal_before.for_votes > proposal_before.against_votes, "not enough for-votes" diff --git a/examples/multi_phase_dao/Cell.lock b/examples/multi_phase_dao/Cell.lock new file mode 100644 index 00000000..1e3d5e25 --- /dev/null +++ b/examples/multi_phase_dao/Cell.lock @@ -0,0 +1,23 @@ +version = 3 +schema = "cellscript-lock-v0.24-graph-v1" + +[package] +edition = "2026" +name = "multi_phase_dao" +version = "0.1.0" +source_hash = "f7ddc4c23c8bb9de7f6981df3ab2da9d98a964cf1c1388e8238966d5d4bb8dd9" + +[root] +manifest_digest = "sha256:5eb057483ad083caedc98337cea0ffcbd66a540869ea3d78b487acce98fb2847" + +[root.dependencies] +token = "token@0.1.0|path:../token|env=default|features=default" + +[dependencies."token@0.1.0|path:../token|env=default|features=default"] +name = "token" +version = "0.1.0" +source_hash = "d5ecb3241eeb97983005b9622cbe7a9540cbdec133ec22105c07955f99c7b889" +manifest_digest = "sha256:3adaae2b8a70a8a49246c28c95a6bd3686fefaed383dfb598c7d35da2a2ad4f9" + +[dependencies."token@0.1.0|path:../token|env=default|features=default".source.Path] +path = "../token" diff --git a/examples/multi_phase_dao/Cell.toml b/examples/multi_phase_dao/Cell.toml index daff7adf..683eafe4 100644 --- a/examples/multi_phase_dao/Cell.toml +++ b/examples/multi_phase_dao/Cell.toml @@ -1,4 +1,5 @@ [package] +edition = "2026" name = "multi_phase_dao" version = "0.1.0" diff --git a/examples/multi_phase_dao/src/main.cell b/examples/multi_phase_dao/src/main.cell index 1d932352..2fc6519d 100644 --- a/examples/multi_phase_dao/src/main.cell +++ b/examples/multi_phase_dao/src/main.cell @@ -2,6 +2,8 @@ module cellscript::multi_phase_dao use cellscript::fungible_token::Token +const U64_MAX: u64 = 18446744073709551615 + // Multi-phase governance: a proposal moves through a linear state machine // (Draft -> Active -> Executed or Defeated). This example exercises flow-edge // validation, state transitions, vote tallying, and the full consume/create @@ -75,7 +77,7 @@ action propose( verification require voting_duration > 0, "zero voting duration" let now = env::current_timepoint() - require voting_duration <= 18446744073709551615 - now, "voting end overflow" + require voting_duration <= U64_MAX - now, "voting end overflow" create proposal = Proposal { proposal_id, state: ProposalState::Draft, proposer, for_votes: 0, against_votes: 0, start_timepoint: now, end_timepoint: now + voting_duration, execution_payload_hash } with_lock(proposer) } @@ -116,8 +118,8 @@ action cast_vote( require now < proposal_before.end_timepoint, "voting closed" require voting_token.amount > 0, "zero weight" require voting_token.symbol == config.voting_token_symbol, "wrong voting token" - require !support || voting_token.amount <= 18446744073709551615 - proposal_before.for_votes, "for-vote overflow" - require support || voting_token.amount <= 18446744073709551615 - proposal_before.against_votes, "against-vote overflow" + require !support || voting_token.amount <= U64_MAX - proposal_before.for_votes, "for-vote overflow" + require support || voting_token.amount <= U64_MAX - proposal_before.against_votes, "against-vote overflow" let for_votes = if support { proposal_before.for_votes + voting_token.amount } else { proposal_before.for_votes } let against_votes = if support { proposal_before.against_votes } else { proposal_before.against_votes + voting_token.amount } preserve proposal_after from proposal_before { @@ -156,7 +158,7 @@ action execute_proposal( let now = env::current_timepoint() require proposal_before.state == ProposalState::Active, "not active" require now >= proposal_before.end_timepoint, "voting not closed" - require proposal_before.against_votes <= 18446744073709551615 - proposal_before.for_votes, "vote total overflow" + require proposal_before.against_votes <= U64_MAX - proposal_before.for_votes, "vote total overflow" let total_votes = proposal_before.for_votes + proposal_before.against_votes require total_votes >= config.quorum_votes, "quorum not met" require proposal_before.for_votes > proposal_before.against_votes, "not enough for-votes" diff --git a/examples/multisig/Cell.lock b/examples/multisig/Cell.lock new file mode 100644 index 00000000..f081672c --- /dev/null +++ b/examples/multisig/Cell.lock @@ -0,0 +1,13 @@ +version = 3 +schema = "cellscript-lock-v0.24-graph-v1" + +[package] +edition = "2026" +name = "multisig" +version = "0.1.0" +source_hash = "fac1d99dd5873c9f27a6dd4279533d5e41913a3ce1e02e2076ae3a6c68b30c43" + +[root] +manifest_digest = "sha256:d109f09c75f68286d1e15c79ab1bf9ad64b261fc7fc43c2c2835578b4362c3e9" + +[dependencies] diff --git a/examples/multisig/Cell.toml b/examples/multisig/Cell.toml index 53e25d5d..5e4e57de 100644 --- a/examples/multisig/Cell.toml +++ b/examples/multisig/Cell.toml @@ -1,3 +1,4 @@ [package] +edition = "2026" name = "multisig" version = "0.1.0" diff --git a/examples/nft.cell b/examples/nft.cell index d0242568..9c9e539c 100644 --- a/examples/nft.cell +++ b/examples/nft.cell @@ -2,6 +2,8 @@ module cellscript::nft use cellscript::fungible_token::Token +const U64_MAX: u64 = 18446744073709551615 + const MAX_SUPPLY: u64 = 10000 const ROYALTY_BASIS_POINTS: u16 = 250 @@ -108,11 +110,11 @@ action buy_from_listing(nft_before: NFT, listing: Listing, buyer: Address, royal require listing.collection_id == nft_before.collection_id, "Collection mismatch" require listing.token_id == nft_before.token_id, "Token mismatch" require listing.seller == nft_before.owner, "Seller is not current owner" - require seller_payment.amount <= 18446744073709551615 - royalty_payment.amount, "Payment overflow" + require seller_payment.amount <= U64_MAX - royalty_payment.amount, "Payment overflow" require royalty_payment.amount + seller_payment.amount == listing.price, "Payment must equal listing price" require royalty_payment.symbol == seller_payment.symbol, "Payment token mismatch" require nft_before.royalty_bps <= 1000, "Royalty too high" - require nft_before.royalty_bps == 0 || listing.price <= 18446744073709551615 / nft_before.royalty_bps as u64, "Royalty overflow" + require nft_before.royalty_bps == 0 || listing.price <= U64_MAX / nft_before.royalty_bps as u64, "Royalty overflow" let royalty_amount = listing.price * nft_before.royalty_bps as u64 / 10000 require royalty_payment.amount == royalty_amount, "Incorrect royalty payment" require seller_payment.amount == listing.price - royalty_amount, "Incorrect seller payment" @@ -147,12 +149,12 @@ action accept_offer(nft_before: NFT, offer: Offer, royalty_payment: Token, selle require now < offer.expires_at, "Offer expired" require offer.collection_id == nft_before.collection_id, "Collection mismatch" require offer.token_id == nft_before.token_id, "Token mismatch" - require seller_payment.amount <= 18446744073709551615 - royalty_payment.amount, "Payment overflow" + require seller_payment.amount <= U64_MAX - royalty_payment.amount, "Payment overflow" require royalty_payment.amount + seller_payment.amount == offer.price, "Payment must equal offer price" require royalty_payment.symbol == offer.payment_symbol, "Wrong royalty payment token" require seller_payment.symbol == offer.payment_symbol, "Wrong seller payment token" require nft_before.royalty_bps <= 1000, "Royalty too high" - require nft_before.royalty_bps == 0 || offer.price <= 18446744073709551615 / nft_before.royalty_bps as u64, "Royalty overflow" + require nft_before.royalty_bps == 0 || offer.price <= U64_MAX / nft_before.royalty_bps as u64, "Royalty overflow" let royalty_amount = offer.price * nft_before.royalty_bps as u64 / 10000 require royalty_payment.amount == royalty_amount, "Incorrect royalty payment" require seller_payment.amount == offer.price - royalty_amount, "Incorrect seller payment" diff --git a/examples/nft/Cell.lock b/examples/nft/Cell.lock new file mode 100644 index 00000000..612af43e --- /dev/null +++ b/examples/nft/Cell.lock @@ -0,0 +1,23 @@ +version = 3 +schema = "cellscript-lock-v0.24-graph-v1" + +[package] +edition = "2026" +name = "nft" +version = "0.1.0" +source_hash = "5aaef48889f5da8217a6f7d8fdbe06be694ad74fb6937aaf6928645416bb7174" + +[root] +manifest_digest = "sha256:acfa416a9cd8c344337766afead9726f03093aa6000c7ae1b20c39c8b55fdbea" + +[root.dependencies] +token = "token@0.1.0|path:../token|env=default|features=default" + +[dependencies."token@0.1.0|path:../token|env=default|features=default"] +name = "token" +version = "0.1.0" +source_hash = "d5ecb3241eeb97983005b9622cbe7a9540cbdec133ec22105c07955f99c7b889" +manifest_digest = "sha256:3adaae2b8a70a8a49246c28c95a6bd3686fefaed383dfb598c7d35da2a2ad4f9" + +[dependencies."token@0.1.0|path:../token|env=default|features=default".source.Path] +path = "../token" diff --git a/examples/nft/Cell.toml b/examples/nft/Cell.toml index 6d0557b3..61f6355a 100644 --- a/examples/nft/Cell.toml +++ b/examples/nft/Cell.toml @@ -1,4 +1,5 @@ [package] +edition = "2026" name = "nft" version = "0.1.0" diff --git a/examples/nft/src/main.cell b/examples/nft/src/main.cell index d0242568..9c9e539c 100644 --- a/examples/nft/src/main.cell +++ b/examples/nft/src/main.cell @@ -2,6 +2,8 @@ module cellscript::nft use cellscript::fungible_token::Token +const U64_MAX: u64 = 18446744073709551615 + const MAX_SUPPLY: u64 = 10000 const ROYALTY_BASIS_POINTS: u16 = 250 @@ -108,11 +110,11 @@ action buy_from_listing(nft_before: NFT, listing: Listing, buyer: Address, royal require listing.collection_id == nft_before.collection_id, "Collection mismatch" require listing.token_id == nft_before.token_id, "Token mismatch" require listing.seller == nft_before.owner, "Seller is not current owner" - require seller_payment.amount <= 18446744073709551615 - royalty_payment.amount, "Payment overflow" + require seller_payment.amount <= U64_MAX - royalty_payment.amount, "Payment overflow" require royalty_payment.amount + seller_payment.amount == listing.price, "Payment must equal listing price" require royalty_payment.symbol == seller_payment.symbol, "Payment token mismatch" require nft_before.royalty_bps <= 1000, "Royalty too high" - require nft_before.royalty_bps == 0 || listing.price <= 18446744073709551615 / nft_before.royalty_bps as u64, "Royalty overflow" + require nft_before.royalty_bps == 0 || listing.price <= U64_MAX / nft_before.royalty_bps as u64, "Royalty overflow" let royalty_amount = listing.price * nft_before.royalty_bps as u64 / 10000 require royalty_payment.amount == royalty_amount, "Incorrect royalty payment" require seller_payment.amount == listing.price - royalty_amount, "Incorrect seller payment" @@ -147,12 +149,12 @@ action accept_offer(nft_before: NFT, offer: Offer, royalty_payment: Token, selle require now < offer.expires_at, "Offer expired" require offer.collection_id == nft_before.collection_id, "Collection mismatch" require offer.token_id == nft_before.token_id, "Token mismatch" - require seller_payment.amount <= 18446744073709551615 - royalty_payment.amount, "Payment overflow" + require seller_payment.amount <= U64_MAX - royalty_payment.amount, "Payment overflow" require royalty_payment.amount + seller_payment.amount == offer.price, "Payment must equal offer price" require royalty_payment.symbol == offer.payment_symbol, "Wrong royalty payment token" require seller_payment.symbol == offer.payment_symbol, "Wrong seller payment token" require nft_before.royalty_bps <= 1000, "Royalty too high" - require nft_before.royalty_bps == 0 || offer.price <= 18446744073709551615 / nft_before.royalty_bps as u64, "Royalty overflow" + require nft_before.royalty_bps == 0 || offer.price <= U64_MAX / nft_before.royalty_bps as u64, "Royalty overflow" let royalty_amount = offer.price * nft_before.royalty_bps as u64 / 10000 require royalty_payment.amount == royalty_amount, "Incorrect royalty payment" require seller_payment.amount == offer.price - royalty_amount, "Incorrect seller payment" diff --git a/examples/package_graph/Cell.lock b/examples/package_graph/Cell.lock new file mode 100644 index 00000000..89321155 --- /dev/null +++ b/examples/package_graph/Cell.lock @@ -0,0 +1,107 @@ +version = 3 +schema = "cellscript-lock-v0.24-graph-v1" + +[package] +edition = "2026" +name = "package_graph_demo" +version = "0.1.0" +source_hash = "5f146552641c097ec165eeb72cc368f3c2b4f762221417da3960960967ef26cd" + +[root] +manifest_digest = "sha256:57f0f363771fc6ca67b678e3b9a676b1e1ee4021acfa25e0cf6eeb5a7f7edad2" + +[dependencies."audit_helpers@0.4.2|path:deps/audit-helpers|env=mainnet|features=default"] +name = "audit_helpers" +version = "0.4.2" +source_hash = "44b8be2a00aaba5c51b296623598792a95caa48c8d0cbd5036936a92a68038b4" +manifest_digest = "sha256:e24ef32ca50f1e257c7757d6c16d483a07c7e574236267947b16d2aa36cdc8fc" + +[dependencies."audit_helpers@0.4.2|path:deps/audit-helpers|env=mainnet|features=default".source.Path] +path = "deps/audit-helpers" + +[dependencies."audit_helpers@0.4.2|path:deps/audit-helpers|env=testnet|features=default"] +name = "audit_helpers" +version = "0.4.2" +source_hash = "44b8be2a00aaba5c51b296623598792a95caa48c8d0cbd5036936a92a68038b4" +manifest_digest = "sha256:e24ef32ca50f1e257c7757d6c16d483a07c7e574236267947b16d2aa36cdc8fc" + +[dependencies."audit_helpers@0.4.2|path:deps/audit-helpers|env=testnet|features=default".source.Path] +path = "deps/audit-helpers" + +[dependencies."canonical_math@1.2.3|path:deps/canonical-math|env=mainnet|features=default"] +name = "canonical_math" +version = "1.2.3" +source_hash = "fd053efe46302c263148061ecd62e798100d31c38b2308285097462f70331864" +manifest_digest = "sha256:bcb871cd37a39cf52b5b30b3b23c43ada0b90f0c68476b1218a08a0dd223b5ea" + +[dependencies."canonical_math@1.2.3|path:deps/canonical-math|env=mainnet|features=default".source.Path] +path = "deps/canonical-math" + +[dependencies."canonical_math@1.2.3|path:deps/canonical-math|env=testnet|features=default"] +name = "canonical_math" +version = "1.2.3" +source_hash = "fd053efe46302c263148061ecd62e798100d31c38b2308285097462f70331864" +manifest_digest = "sha256:bcb871cd37a39cf52b5b30b3b23c43ada0b90f0c68476b1218a08a0dd223b5ea" + +[dependencies."canonical_math@1.2.3|path:deps/canonical-math|env=testnet|features=default".source.Path] +path = "deps/canonical-math" + +[dependencies."network_contracts@1.0.0|path:deps/contracts-mainnet|env=mainnet|features=default"] +name = "network_contracts" +version = "1.0.0" +source_hash = "509ab43e27f7e1cba59a079b92ed7d0e04128aab27de513817bb5225c71d7274" +manifest_digest = "sha256:2e2fc06b5cf1f269e91356b8c39efc68f182e43592b6384df0629271dcfee6fe" + +[dependencies."network_contracts@1.0.0|path:deps/contracts-mainnet|env=mainnet|features=default".source.Path] +path = "deps/contracts-mainnet" + +[dependencies."network_contracts@2.0.0|path:deps/contracts-testnet|env=testnet|features=default"] +name = "network_contracts" +version = "2.0.0" +source_hash = "148e0ead158a5dfa4f84b6ec4b7c3d7783debe732d894dbf0157c0b8309e9539" +manifest_digest = "sha256:b7269f73fc0802b76cd2dcc357d3927aca7d62c2194449082c02e5b083c5828c" + +[dependencies."network_contracts@2.0.0|path:deps/contracts-testnet|env=testnet|features=default".source.Path] +path = "deps/contracts-testnet" + +[dependencies."scenario_test_support@0.1.0|path:deps/test-support|env=mainnet|features=default"] +name = "scenario_test_support" +version = "0.1.0" +source_hash = "34956e5ec9750c7ba52b41b76396456f4f006952903579b1680863fa23f33d63" +manifest_digest = "sha256:3d736afd4e06f265e1d00489321475a5b89b0c60bf040ed05814f21e7ec86c54" + +[dependencies."scenario_test_support@0.1.0|path:deps/test-support|env=mainnet|features=default".source.Path] +path = "deps/test-support" + +[dependencies."scenario_test_support@0.1.0|path:deps/test-support|env=testnet|features=default"] +name = "scenario_test_support" +version = "0.1.0" +source_hash = "34956e5ec9750c7ba52b41b76396456f4f006952903579b1680863fa23f33d63" +manifest_digest = "sha256:3d736afd4e06f265e1d00489321475a5b89b0c60bf040ed05814f21e7ec86c54" + +[dependencies."scenario_test_support@0.1.0|path:deps/test-support|env=testnet|features=default".source.Path] +path = "deps/test-support" + +[environments.mainnet] +chain_id = "ckb-mainnet" +genesis_hash = "0x1111111111111111111111111111111111111111111111111111111111111111" + +[environments.mainnet.dependencies] +audit = "audit_helpers@0.4.2|path:deps/audit-helpers|env=mainnet|features=default" +contracts = "network_contracts@1.0.0|path:deps/contracts-mainnet|env=mainnet|features=default" +core = "canonical_math@1.2.3|path:deps/canonical-math|env=mainnet|features=default" + +[environments.mainnet.dev_dependencies] +test_support = "scenario_test_support@0.1.0|path:deps/test-support|env=mainnet|features=default" + +[environments.testnet] +chain_id = "ckb-testnet" +genesis_hash = "0x2222222222222222222222222222222222222222222222222222222222222222" + +[environments.testnet.dependencies] +audit = "audit_helpers@0.4.2|path:deps/audit-helpers|env=testnet|features=default" +contracts = "network_contracts@2.0.0|path:deps/contracts-testnet|env=testnet|features=default" +core = "canonical_math@1.2.3|path:deps/canonical-math|env=testnet|features=default" + +[environments.testnet.dev_dependencies] +test_support = "scenario_test_support@0.1.0|path:deps/test-support|env=testnet|features=default" diff --git a/examples/package_graph/Cell.toml b/examples/package_graph/Cell.toml new file mode 100644 index 00000000..9c8f5283 --- /dev/null +++ b/examples/package_graph/Cell.toml @@ -0,0 +1,47 @@ +[package] +edition = "2026" +name = "package_graph_demo" +version = "0.1.0" + +[dependencies.core] +package = "canonical_math" +version = "^1.2.0" +path = "deps/canonical-math" + +[dependencies.audit] +package = "audit_helpers" +version = "~0.4.0" +path = "deps/audit-helpers" +optional = true + +[dependencies.contracts] +package = "network_contracts" +version = ">=1.0.0, <3.0.0" +path = "deps/contracts-mainnet" + +[dev_dependencies.test_support] +package = "scenario_test_support" +version = "=0.1.0" +path = "deps/test-support" + +[features] +default = [] +auditing = ["dep:audit"] +full = ["auditing"] + +[environments.mainnet] +chain_id = "ckb-mainnet" +genesis_hash = "0x1111111111111111111111111111111111111111111111111111111111111111" + +[environments.testnet] +chain_id = "ckb-testnet" +genesis_hash = "0x2222222222222222222222222222222222222222222222222222222222222222" + +[dependency_overrides.testnet.contracts] +package = "network_contracts" +version = "=2.0.0" +path = "deps/contracts-testnet" + +[build] +target = "riscv64-elf" +target_profile = "ckb" diff --git a/examples/package_graph/README.md b/examples/package_graph/README.md new file mode 100644 index 00000000..bbf79a31 --- /dev/null +++ b/examples/package_graph/README.md @@ -0,0 +1,33 @@ +# Lock-Authoritative Package Graph + +This portable 0.24 example concentrates the package features that do not +belong in the business-contract examples: + +- the local alias `core` resolves declared package `canonical_math` through a + standard `^1.2.0` SemVer requirement; +- optional `audit_helpers` is activated through `dep:audit` and the transitive + `full` feature; +- `scenario_test_support` enters only the test graph; +- mainnet and testnet roots bind explicit CKB chain identities; and +- the testnet environment replaces `network_contracts` with an exact `2.0.0` + path source. + +The tracked `Cell.lock` contains every feature, test, and environment root, so +the following commands perform no mutable dependency selection: + +```bash +cd examples/package_graph +cellc check --frozen --offline --environment mainnet +cellc check --frozen --offline --environment testnet --features auditing +cellc test --no-run --frozen --offline --environment testnet --all-features +``` + +Omitting `--environment` fails closed because this manifest has an explicit +environment override. Run `cellc lock` only when intentionally repinning the +graph. + +Git and Registry requirements normalize to immutable commits or snapshots at +repin time. They are not included here because a portable checked-in example +must not depend on mutable network discovery. Hash-pinned external resolvers +remain test/documentation fixtures because their commands must use +machine-specific absolute paths. diff --git a/examples/package_graph/deps/audit-helpers/Cell.toml b/examples/package_graph/deps/audit-helpers/Cell.toml new file mode 100644 index 00000000..4b7c232f --- /dev/null +++ b/examples/package_graph/deps/audit-helpers/Cell.toml @@ -0,0 +1,5 @@ +[package] +edition = "2026" +name = "audit_helpers" +version = "0.4.2" +entry = "src/lib.cell" diff --git a/examples/package_graph/deps/audit-helpers/src/lib.cell b/examples/package_graph/deps/audit-helpers/src/lib.cell new file mode 100644 index 00000000..3418a11a --- /dev/null +++ b/examples/package_graph/deps/audit-helpers/src/lib.cell @@ -0,0 +1,5 @@ +module audit::helpers + +fn audit_marker() -> u64 { + return 24 +} diff --git a/examples/package_graph/deps/canonical-math/Cell.toml b/examples/package_graph/deps/canonical-math/Cell.toml new file mode 100644 index 00000000..d5ed6ebc --- /dev/null +++ b/examples/package_graph/deps/canonical-math/Cell.toml @@ -0,0 +1,5 @@ +[package] +edition = "2026" +name = "canonical_math" +version = "1.2.3" +entry = "src/lib.cell" diff --git a/examples/package_graph/deps/canonical-math/src/lib.cell b/examples/package_graph/deps/canonical-math/src/lib.cell new file mode 100644 index 00000000..7c147b6b --- /dev/null +++ b/examples/package_graph/deps/canonical-math/src/lib.cell @@ -0,0 +1,5 @@ +module canonical::math + +fn increment(value: u64) -> u64 { + return value + 1 +} diff --git a/examples/package_graph/deps/contracts-mainnet/Cell.toml b/examples/package_graph/deps/contracts-mainnet/Cell.toml new file mode 100644 index 00000000..fd2388fd --- /dev/null +++ b/examples/package_graph/deps/contracts-mainnet/Cell.toml @@ -0,0 +1,5 @@ +[package] +edition = "2026" +name = "network_contracts" +version = "1.0.0" +entry = "src/lib.cell" diff --git a/examples/package_graph/deps/contracts-mainnet/src/lib.cell b/examples/package_graph/deps/contracts-mainnet/src/lib.cell new file mode 100644 index 00000000..750df5c1 --- /dev/null +++ b/examples/package_graph/deps/contracts-mainnet/src/lib.cell @@ -0,0 +1,5 @@ +module network::contracts + +fn network_marker() -> u64 { + return 1 +} diff --git a/examples/package_graph/deps/contracts-testnet/Cell.toml b/examples/package_graph/deps/contracts-testnet/Cell.toml new file mode 100644 index 00000000..a0335518 --- /dev/null +++ b/examples/package_graph/deps/contracts-testnet/Cell.toml @@ -0,0 +1,5 @@ +[package] +edition = "2026" +name = "network_contracts" +version = "2.0.0" +entry = "src/lib.cell" diff --git a/examples/package_graph/deps/contracts-testnet/src/lib.cell b/examples/package_graph/deps/contracts-testnet/src/lib.cell new file mode 100644 index 00000000..c98cf72f --- /dev/null +++ b/examples/package_graph/deps/contracts-testnet/src/lib.cell @@ -0,0 +1,5 @@ +module network::contracts + +fn network_marker() -> u64 { + return 2 +} diff --git a/examples/package_graph/deps/test-support/Cell.toml b/examples/package_graph/deps/test-support/Cell.toml new file mode 100644 index 00000000..73958388 --- /dev/null +++ b/examples/package_graph/deps/test-support/Cell.toml @@ -0,0 +1,5 @@ +[package] +edition = "2026" +name = "scenario_test_support" +version = "0.1.0" +entry = "src/lib.cell" diff --git a/examples/package_graph/deps/test-support/src/lib.cell b/examples/package_graph/deps/test-support/src/lib.cell new file mode 100644 index 00000000..fe037a00 --- /dev/null +++ b/examples/package_graph/deps/test-support/src/lib.cell @@ -0,0 +1,5 @@ +module scenario::test_support + +fn expected_value() -> u64 { + return 25 +} diff --git a/examples/package_graph/src/main.cell b/examples/package_graph/src/main.cell new file mode 100644 index 00000000..b20cca65 --- /dev/null +++ b/examples/package_graph/src/main.cell @@ -0,0 +1,6 @@ +module package_graph_demo + +action calculate(value: u64) -> u64 { + verification + return canonical::math::increment(value) + network::contracts::network_marker() +} diff --git a/examples/registry/Cell.lock b/examples/registry/Cell.lock new file mode 100644 index 00000000..ec066fcb --- /dev/null +++ b/examples/registry/Cell.lock @@ -0,0 +1,13 @@ +version = 3 +schema = "cellscript-lock-v0.24-graph-v1" + +[package] +edition = "2026" +name = "registry" +version = "0.1.0" +source_hash = "6c28690e638297ccd4fe50f4fe566e0c83b0aa1bb9d08ea4b810d575bea0d8f5" + +[root] +manifest_digest = "sha256:169bed8b030af587acba63fc57cdc1de6f452a9d0202f3575aad7dc33d6c6299" + +[dependencies] diff --git a/examples/registry/Cell.toml b/examples/registry/Cell.toml index d12fec79..4f3af0e3 100644 --- a/examples/registry/Cell.toml +++ b/examples/registry/Cell.toml @@ -1,3 +1,4 @@ [package] +edition = "2026" name = "registry" version = "0.1.0" diff --git a/examples/registry_ls_idl/README.md b/examples/registry_ls_idl/README.md new file mode 100644 index 00000000..8c2363e4 --- /dev/null +++ b/examples/registry_ls_idl/README.md @@ -0,0 +1,60 @@ +# Registry LS-IDL example + +This fixture demonstrates the 0.24 Registry profile for an LS-IDL 0.1 CKB +Lock Script interface. `idl.json` is intentionally formatted rather than +canonicalised: its exact bytes, including whitespace and final newline, are +the bytes committed by SHA-256 and returned by the Registry. + +`lock.rs` shows the corresponding `ckb-idl-derive` declaration. It is a small +integration example, not an audited or deployable Lock Script. + +Prepare a publishable generic artifact from a real RISC-V Lock Script ELF: + +```bash +cellc artifact ls-idl validate --idl idl.json +cellc artifact ls-idl bind \ + --idl idl.json \ + --executable build/demo-lock \ + --output build/demo-lock.ls-idl +cellc artifact ls-idl bundle \ + --idl idl.json \ + --executable build/demo-lock.ls-idl \ + --source lock.rs \ + --namespace demo \ + --name ls-idl-lock \ + --release 0.1.0 \ + --language rust \ + --toolchain 'rustc 1.97.1 + ckb-std' \ + --source-revision '' \ + --output bundle.json \ + --artifact-manifest-output Artifact.toml +cellc publish --artifact-manifest Artifact.toml --dry-run +``` + +After the Registry has accepted the bundle and chain-verified its deployment, +clients can retrieve the exact IDL bytes: + +```bash +cellc artifact ls-idl fetch \ + --code-hash 0x<64-hex> \ + --hash-type data1 \ + --data-hash 0x<64-hex> \ + --output fetched-idl.json +``` + +The Registry proves bounded schema conformance, immutable object hashes, and +the `SHA-256(idl.json)` executable suffix. It does not prove signature +correctness, authorization semantics, or the security of the Lock Script. +`required = false` remains descriptive in LS-IDL 0.1; it does not make a field +conditionally absent from the current linear decoder. + +`vectors.json` remains a small, readable Registry-facing example. The exact +upstream compatibility corpus is separately pinned and executed under +`tests/compat/ls_idl/` and `tests/ls_idl_upstream.rs`. This example was checked +against `ckb-idl-derive` commit +`e7ee35766b9084099e9d840ccd37d2b5d40074a1` and `ckb-idl-client` commit +`7d883e0abccba56d423449b673567ee817747936`; that client's complete +`test-vectors.json` has SHA-256 +`a9a6dca4fd0c5fcd2ca7aea6468784be7fdb29d6274049f07090cbab0ce9c1bb`. +The opt-in `scripts/cellscript_ls_idl_upstream_acceptance.sh` additionally runs +that actual Rust client against CellScript Registry's compatibility route. diff --git a/examples/registry_ls_idl/idl.json b/examples/registry_ls_idl/idl.json new file mode 100644 index 00000000..167ad6a6 --- /dev/null +++ b/examples/registry_ls_idl/idl.json @@ -0,0 +1,21 @@ +{ + "witness": [ + { + "name": "signature", + "type": "secp256k1_sig", + "required": true, + "description": "Recoverable CKB secp256k1 signature" + }, + { + "name": "nonce", + "type": "uint64", + "required": true + }, + { + "name": "memo", + "type": "bytes", + "required": false, + "description": "Length-prefixed application bytes; required=false is descriptive in 0.1" + } + ] +} diff --git a/examples/registry_ls_idl/lock.rs b/examples/registry_ls_idl/lock.rs new file mode 100644 index 00000000..64ef310d --- /dev/null +++ b/examples/registry_ls_idl/lock.rs @@ -0,0 +1,16 @@ +//! Illustrative `ckb-idl-derive` input for `idl.json`. +//! This is not a complete or audited CKB Lock Script. + +use ckb_idl_derive::CkbWitness; + +#[derive(CkbWitness)] +struct DemoLockWitness { + #[witness(description = "Recoverable CKB secp256k1 signature")] + signature: [u8; 65], + nonce: u64, + #[witness( + required = false, + description = "Length-prefixed application bytes; required=false is descriptive in 0.1" + )] + memo: Vec, +} diff --git a/examples/registry_ls_idl/vectors.json b/examples/registry_ls_idl/vectors.json new file mode 100644 index 00000000..c2d41434 --- /dev/null +++ b/examples/registry_ls_idl/vectors.json @@ -0,0 +1,59 @@ +{ + "schema": "cellscript-ls-idl-registry-vectors-v1", + "format": "ls-idl", + "format_version": "0.1", + "wire_encoding": "linear-le-v0", + "vectors": [ + { + "id": "empty", + "fields": [], + "wire_hex": "", + "expect": "valid" + }, + { + "id": "uint8", + "fields": [{ "name": "difficulty", "type": "uint8", "required": true }], + "wire_hex": "2a", + "expect": "valid" + }, + { + "id": "uint32-le", + "fields": [{ "name": "nonce", "type": "uint32", "required": true }], + "wire_hex": "efbeadde", + "expect": "valid" + }, + { + "id": "uint64-le", + "fields": [{ "name": "height", "type": "uint64", "required": true }], + "wire_hex": "0068e5cf8b010000", + "expect": "valid" + }, + { + "id": "bytes-length-prefix", + "fields": [{ "name": "memo", "type": "bytes", "required": false }], + "wire_hex": "0500000068656c6c6f", + "expect": "valid" + }, + { + "id": "bytes-missing-prefix", + "fields": [{ "name": "memo", "type": "bytes", "required": true }], + "wire_hex": "616263", + "expect": "error", + "error": "FieldTooShort" + }, + { + "id": "trailing-bytes", + "fields": [{ "name": "memo", "type": "bytes", "required": true }], + "wire_hex": "0100000061ff", + "expect": "error", + "error": "TrailingBytes" + }, + { + "id": "unknown-type", + "fields": [{ "name": "digest", "type": "[u8;32]", "required": true }], + "wire_hex": "", + "expect": "error", + "error": "UnknownType" + } + ] +} diff --git a/examples/scenario_basics/Cell.lock b/examples/scenario_basics/Cell.lock new file mode 100644 index 00000000..30949d89 --- /dev/null +++ b/examples/scenario_basics/Cell.lock @@ -0,0 +1,13 @@ +version = 3 +schema = "cellscript-lock-v0.24-graph-v1" + +[package] +edition = "2026" +name = "scenario_basics" +version = "0.1.0" +source_hash = "1ae9e3846d862a5dfcd0f430315d68bc7a415bbf7b9d70afd12edac9b39646c6" + +[root] +manifest_digest = "sha256:27f66dac6971a8727e2d43d4bde373e7c67e32a2f229b1833af512c4e4429d81" + +[dependencies] diff --git a/examples/scenario_basics/Cell.toml b/examples/scenario_basics/Cell.toml new file mode 100644 index 00000000..3a892b79 --- /dev/null +++ b/examples/scenario_basics/Cell.toml @@ -0,0 +1,8 @@ +[package] +edition = "2026" +name = "scenario_basics" +version = "0.1.0" + +[build] +target = "riscv64-elf" +target_profile = "ckb" diff --git a/examples/scenario_basics/README.md b/examples/scenario_basics/README.md new file mode 100644 index 00000000..4eddb08e --- /dev/null +++ b/examples/scenario_basics/README.md @@ -0,0 +1,30 @@ +# Executable Scenario Basics + +This package is the runnable 0.24 companion to the executable-package-test +documentation. It keeps a positive entry and an exact registered runtime error +under `tests/`, then executes each scenario with both evidence backends: + +```bash +cd examples/scenario_basics +cellc test --frozen --offline --backend all --json +``` + +The simulator reports `development-non-consensus`; CKB-VM reports +`authoritative-runtime`. Neither result is chain, deployment, or transaction +syscall evidence. + +The package also demonstrates the four-file verified-artifact bundle: + +```bash +cellc build --frozen --offline +cellc verify-artifact build/main.elf --verify-sources --json +``` + +`--frozen` consumes the tracked dependency graph without adding local build +identity to `Cell.lock`. To exercise package identity verification, run an +ordinary locked build first: + +```bash +cellc build --locked +cellc package verify --json +``` diff --git a/examples/scenario_basics/src/main.cell b/examples/scenario_basics/src/main.cell new file mode 100644 index 00000000..e54c1f65 --- /dev/null +++ b/examples/scenario_basics/src/main.cell @@ -0,0 +1,10 @@ +module scenario_basics + +action main() { + verification +} + +action reject() { + verification + require false, "expected failure" +} diff --git a/examples/scenario_basics/tests/assertion-failure.scenario.json b/examples/scenario_basics/tests/assertion-failure.scenario.json new file mode 100644 index 00000000..ba3aefda --- /dev/null +++ b/examples/scenario_basics/tests/assertion-failure.scenario.json @@ -0,0 +1,38 @@ +{ + "schema": "cellscript-test-scenario-v1", + "name": "bundled-exact-runtime-error", + "source": "scenario_basics.cell", + "target_profile": "ckb", + "entry": { + "kind": "action", + "name": "reject", + "args": [] + }, + "initial_cells": [], + "steps": [ + { + "name": "assertion-fails", + "consumes": [], + "outputs": [], + "cell_deps": [], + "header_deps": [], + "since": {}, + "witnesses": [], + "expectation": { + "status": "runtime-error", + "result": null, + "runtime_error": { + "code": 5, + "name": "assertion-failed" + } + } + } + ], + "limits": { + "max_steps": 1000, + "max_cycles": 10000000, + "max_transaction_bytes": 65536, + "minimum_cell_capacity": 100000000 + }, + "oracle": null +} diff --git a/examples/scenario_basics/tests/pass.scenario.json b/examples/scenario_basics/tests/pass.scenario.json new file mode 100644 index 00000000..a4167bd9 --- /dev/null +++ b/examples/scenario_basics/tests/pass.scenario.json @@ -0,0 +1,35 @@ +{ + "schema": "cellscript-test-scenario-v1", + "name": "bundled-positive-entry", + "source": "scenario_basics.cell", + "target_profile": "ckb", + "entry": { + "kind": "action", + "name": "main", + "args": [] + }, + "initial_cells": [], + "steps": [ + { + "name": "main-succeeds", + "consumes": [], + "outputs": [], + "cell_deps": [], + "header_deps": [], + "since": {}, + "witnesses": [], + "expectation": { + "status": "pass", + "result": "()", + "runtime_error": null + } + } + ], + "limits": { + "max_steps": 1000, + "max_cycles": 10000000, + "max_transaction_bytes": 65536, + "minimum_cell_capacity": 100000000 + }, + "oracle": null +} diff --git a/examples/scenario_basics/tests/scenario_basics.cell b/examples/scenario_basics/tests/scenario_basics.cell new file mode 100644 index 00000000..b114c162 --- /dev/null +++ b/examples/scenario_basics/tests/scenario_basics.cell @@ -0,0 +1,10 @@ +module scenario_basics_tests + +action main() { + verification +} + +action reject() { + verification + require false, "expected failure" +} diff --git a/examples/timelock.cell b/examples/timelock.cell index c2296d65..92eb54fc 100644 --- a/examples/timelock.cell +++ b/examples/timelock.cell @@ -2,6 +2,8 @@ module cellscript::timelock use cellscript::fungible_token::Token +const U64_MAX: u64 = 18446744073709551615 + const MIN_LOCK_PERIOD: u64 = 10 const MAX_LOCK_PERIOD: u64 = 2628000 @@ -52,7 +54,7 @@ receipt EmergencyRelease has create, consume, replace, burn { action create_absolute_lock(lock_id: Hash, owner: Address, unlock_height: u64) -> created_lock: TimeLock { verification let current_height = env::current_timepoint() - require current_height <= 18446744073706923615, "Lock range overflow" + require current_height <= U64_MAX - MAX_LOCK_PERIOD, "Lock range overflow" require unlock_height > current_height + MIN_LOCK_PERIOD, "Unlock height too close" require unlock_height <= current_height + MAX_LOCK_PERIOD, "Unlock height too far" create created_lock = TimeLock { lock_id, owner, lock_type: LockType::Absolute, unlock_height, created_at: current_height } @@ -63,7 +65,7 @@ action create_relative_lock(lock_id: Hash, owner: Address, lock_period: u64) -> let current_height = env::current_timepoint() require lock_period >= MIN_LOCK_PERIOD, "Lock period too short" require lock_period <= MAX_LOCK_PERIOD, "Lock period too long" - require lock_period <= 18446744073709551615 - current_height, "Unlock height overflow" + require lock_period <= U64_MAX - current_height, "Unlock height overflow" create created_lock = TimeLock { lock_id, owner, lock_type: LockType::Relative, unlock_height: current_height + lock_period, created_at: current_height } } @@ -145,9 +147,9 @@ action extend_lock(time_lock_before: TimeLock, additional_period: u64, owner: Ad let current_height = env::current_timepoint() require time_lock_before.owner == owner, "Not the owner" require !can_unlock(&time_lock_before, current_height), "Already unlocked" - require additional_period <= 18446744073709551615 - time_lock_before.unlock_height, "Unlock height overflow" + require additional_period <= U64_MAX - time_lock_before.unlock_height, "Unlock height overflow" let new_unlock_height = time_lock_before.unlock_height + additional_period - require current_height <= 18446744073706923615, "Lock range overflow" + require current_height <= U64_MAX - MAX_LOCK_PERIOD, "Lock range overflow" require new_unlock_height <= current_height + MAX_LOCK_PERIOD, "New unlock height too far" preserve time_lock_after from time_lock_before { lock_id @@ -161,7 +163,7 @@ action extend_lock(time_lock_before: TimeLock, additional_period: u64, owner: Ad action batch_create_locks(lock_ids: [Hash; 4], owners: [Address; 4], unlock_heights: [u64; 4]) -> (lock0: TimeLock, lock1: TimeLock, lock2: TimeLock, lock3: TimeLock) { verification let current_height = env::current_timepoint() - require current_height <= 18446744073706923615, "Lock range overflow" + require current_height <= U64_MAX - MAX_LOCK_PERIOD, "Lock range overflow" require unlock_heights[0] > current_height + MIN_LOCK_PERIOD, "Unlock height too close" require unlock_heights[1] > current_height + MIN_LOCK_PERIOD, "Unlock height too close" require unlock_heights[2] > current_height + MIN_LOCK_PERIOD, "Unlock height too close" diff --git a/examples/timelock/Cell.lock b/examples/timelock/Cell.lock new file mode 100644 index 00000000..b5854be2 --- /dev/null +++ b/examples/timelock/Cell.lock @@ -0,0 +1,23 @@ +version = 3 +schema = "cellscript-lock-v0.24-graph-v1" + +[package] +edition = "2026" +name = "timelock" +version = "0.1.0" +source_hash = "7c7c1b3b1f1e69b6b94edd5e185f3810a1220e713f09299c2847bbd65e72ece5" + +[root] +manifest_digest = "sha256:1d08c7e381fa2c03426a077bc8366ce0778f00b39e7978b1cbd5cbf518ddfc2d" + +[root.dependencies] +token = "token@0.1.0|path:../token|env=default|features=default" + +[dependencies."token@0.1.0|path:../token|env=default|features=default"] +name = "token" +version = "0.1.0" +source_hash = "d5ecb3241eeb97983005b9622cbe7a9540cbdec133ec22105c07955f99c7b889" +manifest_digest = "sha256:3adaae2b8a70a8a49246c28c95a6bd3686fefaed383dfb598c7d35da2a2ad4f9" + +[dependencies."token@0.1.0|path:../token|env=default|features=default".source.Path] +path = "../token" diff --git a/examples/timelock/Cell.toml b/examples/timelock/Cell.toml index 24afedbf..f61a6de6 100644 --- a/examples/timelock/Cell.toml +++ b/examples/timelock/Cell.toml @@ -1,4 +1,5 @@ [package] +edition = "2026" name = "timelock" version = "0.1.0" diff --git a/examples/timelock/src/main.cell b/examples/timelock/src/main.cell index c2296d65..92eb54fc 100644 --- a/examples/timelock/src/main.cell +++ b/examples/timelock/src/main.cell @@ -2,6 +2,8 @@ module cellscript::timelock use cellscript::fungible_token::Token +const U64_MAX: u64 = 18446744073709551615 + const MIN_LOCK_PERIOD: u64 = 10 const MAX_LOCK_PERIOD: u64 = 2628000 @@ -52,7 +54,7 @@ receipt EmergencyRelease has create, consume, replace, burn { action create_absolute_lock(lock_id: Hash, owner: Address, unlock_height: u64) -> created_lock: TimeLock { verification let current_height = env::current_timepoint() - require current_height <= 18446744073706923615, "Lock range overflow" + require current_height <= U64_MAX - MAX_LOCK_PERIOD, "Lock range overflow" require unlock_height > current_height + MIN_LOCK_PERIOD, "Unlock height too close" require unlock_height <= current_height + MAX_LOCK_PERIOD, "Unlock height too far" create created_lock = TimeLock { lock_id, owner, lock_type: LockType::Absolute, unlock_height, created_at: current_height } @@ -63,7 +65,7 @@ action create_relative_lock(lock_id: Hash, owner: Address, lock_period: u64) -> let current_height = env::current_timepoint() require lock_period >= MIN_LOCK_PERIOD, "Lock period too short" require lock_period <= MAX_LOCK_PERIOD, "Lock period too long" - require lock_period <= 18446744073709551615 - current_height, "Unlock height overflow" + require lock_period <= U64_MAX - current_height, "Unlock height overflow" create created_lock = TimeLock { lock_id, owner, lock_type: LockType::Relative, unlock_height: current_height + lock_period, created_at: current_height } } @@ -145,9 +147,9 @@ action extend_lock(time_lock_before: TimeLock, additional_period: u64, owner: Ad let current_height = env::current_timepoint() require time_lock_before.owner == owner, "Not the owner" require !can_unlock(&time_lock_before, current_height), "Already unlocked" - require additional_period <= 18446744073709551615 - time_lock_before.unlock_height, "Unlock height overflow" + require additional_period <= U64_MAX - time_lock_before.unlock_height, "Unlock height overflow" let new_unlock_height = time_lock_before.unlock_height + additional_period - require current_height <= 18446744073706923615, "Lock range overflow" + require current_height <= U64_MAX - MAX_LOCK_PERIOD, "Lock range overflow" require new_unlock_height <= current_height + MAX_LOCK_PERIOD, "New unlock height too far" preserve time_lock_after from time_lock_before { lock_id @@ -161,7 +163,7 @@ action extend_lock(time_lock_before: TimeLock, additional_period: u64, owner: Ad action batch_create_locks(lock_ids: [Hash; 4], owners: [Address; 4], unlock_heights: [u64; 4]) -> (lock0: TimeLock, lock1: TimeLock, lock2: TimeLock, lock3: TimeLock) { verification let current_height = env::current_timepoint() - require current_height <= 18446744073706923615, "Lock range overflow" + require current_height <= U64_MAX - MAX_LOCK_PERIOD, "Lock range overflow" require unlock_heights[0] > current_height + MIN_LOCK_PERIOD, "Unlock height too close" require unlock_heights[1] > current_height + MIN_LOCK_PERIOD, "Unlock height too close" require unlock_heights[2] > current_height + MIN_LOCK_PERIOD, "Unlock height too close" diff --git a/examples/token/Cell.lock b/examples/token/Cell.lock new file mode 100644 index 00000000..b432def1 --- /dev/null +++ b/examples/token/Cell.lock @@ -0,0 +1,13 @@ +version = 3 +schema = "cellscript-lock-v0.24-graph-v1" + +[package] +edition = "2026" +name = "token" +version = "0.1.0" +source_hash = "d5ecb3241eeb97983005b9622cbe7a9540cbdec133ec22105c07955f99c7b889" + +[root] +manifest_digest = "sha256:3adaae2b8a70a8a49246c28c95a6bd3686fefaed383dfb598c7d35da2a2ad4f9" + +[dependencies] diff --git a/examples/token/Cell.toml b/examples/token/Cell.toml index a5bc33ad..1ff06bb6 100644 --- a/examples/token/Cell.toml +++ b/examples/token/Cell.toml @@ -1,3 +1,4 @@ [package] +edition = "2026" name = "token" version = "0.1.0" diff --git a/examples/vesting/Cell.lock b/examples/vesting/Cell.lock new file mode 100644 index 00000000..27f37e2b --- /dev/null +++ b/examples/vesting/Cell.lock @@ -0,0 +1,23 @@ +version = 3 +schema = "cellscript-lock-v0.24-graph-v1" + +[package] +edition = "2026" +name = "vesting" +version = "0.1.0" +source_hash = "8e15e7c0a6d9a50e5f5489b576354e7bbe1465e8ed89c7c78e1001d56915a243" + +[root] +manifest_digest = "sha256:82df84e167c51a173115714ddba2cb43114d1f3e19092dc035941d6638839d11" + +[root.dependencies] +token = "token@0.1.0|path:../token|env=default|features=default" + +[dependencies."token@0.1.0|path:../token|env=default|features=default"] +name = "token" +version = "0.1.0" +source_hash = "d5ecb3241eeb97983005b9622cbe7a9540cbdec133ec22105c07955f99c7b889" +manifest_digest = "sha256:3adaae2b8a70a8a49246c28c95a6bd3686fefaed383dfb598c7d35da2a2ad4f9" + +[dependencies."token@0.1.0|path:../token|env=default|features=default".source.Path] +path = "../token" diff --git a/examples/vesting/Cell.toml b/examples/vesting/Cell.toml index 8fc96874..6e28d28f 100644 --- a/examples/vesting/Cell.toml +++ b/examples/vesting/Cell.toml @@ -1,4 +1,5 @@ [package] +edition = "2026" name = "vesting" version = "0.1.0" diff --git a/integrations/myelin/cellscript-0.24-handoff-contract.json b/integrations/myelin/cellscript-0.24-handoff-contract.json new file mode 100644 index 00000000..affb4591 --- /dev/null +++ b/integrations/myelin/cellscript-0.24-handoff-contract.json @@ -0,0 +1,65 @@ +{ + "schema": "cellscript-myelin-handoff-contract-v1", + "release_line": "0.24", + "adoption_state": "pending-external-release-pin", + "repository": "https://github.com/CellScript-Labs/CellScript", + "source_revision_policy": "exact-40-hex-release-commit-required", + "compiler": { + "package": "cellscript", + "package_version": "0.24.0", + "rust_toolchain": "1.97.1", + "edition": "2026", + "target_profile": "ckb" + }, + "compatibility_profile": { + "schema": "cellscript-resolved-compatibility-profile-v1", + "source_semantics": "cellscript-source-semantics-2026", + "metadata_schema_version": 58, + "source_metadata_schema_version": 2, + "artifact_metadata_schema_version": 1, + "constraints_metadata_schema_version": 2, + "entry_witness_payload_abi": "cellscript-entry-witness-v1", + "entry_witness_placement_abi": "cellscript-witnessargs-input-type-v2", + "entry_witness_placement_field": "input_type", + "entry_witness_placement_source": "group-input-0-then-group-output-0", + "raw_entry_witness_payload_compatible": false + }, + "verified_artifact": { + "checker_name": "cellscript-artifact-checker", + "checker_policy_schema": "cellscript-artifact-checker-policy-v1", + "lowering_record_schema": "cellscript-verified-lowering-record-v1", + "source_map_schema": "cellscript-source-artifact-map-v1", + "semantic_equivalence_claimed": false + }, + "required_exact_bindings": [ + "compiler_binary_sha256", + "source_revision", + "source_tree_digest", + "artifact_ckb_blake2b256", + "metadata_ckb_blake2b256", + "compatibility_profile_ckb_blake2b256", + "lowering_record_ckb_blake2b256", + "source_map_ckb_blake2b256", + "checker_binary_sha256", + "checker_policy_ckb_blake2b256" + ], + "scheduler_boundary": { + "compiler_access_template_authority": "untrusted-template", + "authenticated_concrete_cell_resolution": "myelin-owned", + "scheduler_plan_location": "sidecar", + "raw_transaction_identity_binding_required": true + }, + "forbidden_cellscript_profiles": [ + "MyelinExtended", + "myelin", + "myelin_extended", + "off-chain-session" + ], + "allow_legacy_fallback": false, + "external_adoption_requires": [ + "clean-cellscript-release-commit", + "exact-myelin-toolchain-lock-update", + "fresh-compiler-and-checker-attestations", + "myelin-production-gate" + ] +} diff --git a/proposals/evolving-dob/evolving-dob-profile-v1 b/proposals/evolving-dob/evolving-dob-profile-v1 index 609bd595..30709c97 160000 --- a/proposals/evolving-dob/evolving-dob-profile-v1 +++ b/proposals/evolving-dob/evolving-dob-profile-v1 @@ -1 +1 @@ -Subproject commit 609bd595334efdd535235125ae9423240c197181 +Subproject commit 30709c97bc8972ea255bbc5b9bf9fba484bb99cc diff --git a/proposals/novaseal b/proposals/novaseal index 37f0b224..2db4af38 160000 --- a/proposals/novaseal +++ b/proposals/novaseal @@ -1 +1 @@ -Subproject commit 37f0b22498e471af30bd6408d2a9d93e83127176 +Subproject commit 2db4af3886e238f0ff7dea2150482579b340f065 diff --git a/roadmap/CELLSCRIPT_0_23_ROADMAP.md b/roadmap/CELLSCRIPT_0_23_ROADMAP.md index 9d27f469..e54be2c9 100644 --- a/roadmap/CELLSCRIPT_0_23_ROADMAP.md +++ b/roadmap/CELLSCRIPT_0_23_ROADMAP.md @@ -1,10 +1,13 @@ # CellScript 0.23 Roadmap -**Status**: Draft, pending release-line coordination before adoption -**Scope**: public registry production deployment on `cellscript.dev`, Python -test/fixture scaffolding ported to Rust, deeper RGB++ / Fiber integration, and -a Myelin-aligned Off-Chain Session Runtime profile with initial concurrency -support +**Status**: Implementation scope frozen on 2026-08-09; stable release and +production CKB claims still require their documented clean-tree gates and +external evidence +**Scope**: one Edition 2026 source-semantics epoch, an independently resolved +compatibility profile, canonical CKB +`WitnessArgs.input_type` entry placement, public registry production deployment +on `cellscript.dev`, completed native test/fixture tooling, and the bounded +Fiber/RGB++ evidence actually obtained on this line **Depends on**: the 0.22 typed transaction views, bounded collections, stable `E2xxx` diagnostics, the existing `cellscript-fiber-adapter` no-profile path, the implemented `services/registry-api` write boundary, the production @@ -12,68 +15,270 @@ boundary ADR, and the Myelin Session L2 plan ## Goal -0.23 is the first CellScript release whose headline is *operational* rather +0.23 is the first CellScript release line whose headline is *operational* rather than language-theoretic. 0.22 closed the first slice of the type/set roadmap and the bounded Fiber path; 0.23 turns those compiler facts into a running -public package registry, drops Python from the project's tooling contract, -pushes RGB++ / Fiber further toward production, and introduces a new -Off-Chain Session Runtime profile so that the Myelin vendored fork can stop -diverging. +public package registry, drops Python from the project's tooling contract, and +preserves the bounded Fiber/RGB++ path without overstating incomplete external +evidence. + +The four original pillars below were independent planning packages. The final +scope adopts the first two, preserves only the bounded completed slice of the +third, and retires the fourth. They share one discipline: every claim remains +tied to compiler evidence or builder-backed chain evidence, and every +"production" word distinguishes *deployed and observed* from *gated and +certified*. + +This file preserves the original four-pillar plan and records the final scope +decision below. It must be read with `CHANGELOG.md`, the 0.23 release notes, and +the release gate; historical planned text is not evidence that a deferred item +shipped. + +## Final Scope Decision (2026-08-09) + +The 0.23 implementation boundary is frozen around the work that is present and +gated in this repository: + +- Edition 2026, the resolved compatibility profile, metadata schema 57, + canonical `WitnessArgs.input_type` placement, and the persisted identity cut; +- the deployed public Registry read/write/source-package verification slice, + generalized artifact/reproduction/deployment/commitment support, the + publisher browser-session flow, and the isolated Pudge sandbox; +- the Rust/shell/Node native tooling boundary and repository source policy; +- the content-addressed bounded Fiber evidence/adapter improvements actually + recorded by the release line; and +- the website, Wiki, release-note, and toolchain freshness closure enforced by + `dev` and `ci`. + +Three categories are deliberately not relabelled as completed 0.23 work: + +1. Deploying the canonical Registry Scripts on CKB mainnet, spending a real + publisher wallet, and publishing the first real non-CellScript commitment + need operator authority, funds, wallet approval, public-chain transactions, + confirmations, and configuration readback. They remain external operational + checkpoints rather than local implementation tasks. +2. The complete pinned Fiber lifecycle/negative matrix and protocol-level + RGB++ promotion remain external evidence work. Representative devnet rows do + not close those matrices. +3. The proposed Off-Chain Session Runtime compiler profile is retired rather + than shipped. Current Myelin no longer vendors CellScript: it invokes an + independently versioned compiler process through an attested adapter, + compiles production requests under `ckb`, keeps scheduler plans as sidecar + evidence, and forces `CkbStrict` for session/court paths. Moving + `MyelinExtended` semantics into CellScript would weaken that separation. + +The independent artifact checker, executable package tests, source maps, +Myelin adapter handoff, and conditional Fiber/RGB++ promotion are specified in +the [0.24 roadmap](CELLSCRIPT_0_24_ROADMAP.md). This scope decision follows the +risk register's existing permission to cut the CellScript release when Myelin +or external evidence slips; it does not claim that the deferred evidence +passed. + +## Completed Release-Line Foundation: Source Edition And Compatibility Axes + +Before the four operational pillars, 0.23 closes two compiler-wide contracts +that every later package and builder depends on. + +### One Edition Contract + +Edition 2026 is the first and only CellScript edition. Every package declares +`edition = "2026"`; missing or different values fail during manifest parsing. +There is no migration command, implicit alternate edition, or compatibility +parser because no published CellScript ecosystem needs one. `2026` is a +long-lived source-semantics epoch label, not a promise to mint one edition per +year. + +The edition owns source-language meaning: syntax ambiguity, name resolution, +type/coercion behavior, desugaring, and source-observable semantics. Target +profile, primitive assurance, entry-payload encoding, CKB witness placement, +metadata schemas, and compiler SemVer are independent version axes. The +compiler composes all compatibility-relevant axes except compiler SemVer into +`cellscript-resolved-compatibility-profile-v1`, emits it in metadata, and +hashes it into registry records, `Cell.lock`, `Deployed.toml`, compile +receipts, and generated action builders. A tool cannot change one axis while +continuing to claim the same build identity. + +The same edition value crosses every compiler consumer: + +- CLI package commands read it from `Cell.toml`; +- standalone and in-memory compiler calls use the current edition explicitly; +- LSP-loaded modules carry the edition into compilation; +- WASM exports require the caller to pass `"2026"`; and +- the website worker and generated TypeScript bindings pass and report the same + value. + +### Canonical Entry Witness Placement + +The entry payload keeps the self-identifying `cellscript-entry-witness-v1` +format (`CSARGv1\0` plus positional arguments). The independently versioned +placement ABI gives it one CKB location: + +```mermaid +flowchart LR + A["CKB witnesses: Bytes[]"] --> B["GroupInput#0
or GroupOutput#0"] + B --> C["Molecule WitnessArgs"] + C --> L["lock: signer/Lock Script data"] + C --> I["input_type: CSARGv1 CellScript entry payload"] + C --> O["output_type: other Type Script data"] +``` + +The generated entry wrapper first loads `GroupInput#0`; for an output-only +script group it uses `GroupOutput#0`. It validates the Molecule table and the +`BytesOpt` field before decoding `input_type`. A raw `CSARGv1` payload, malformed +table, absent `input_type`, or payload placed in `lock`/`output_type` fails +closed with runtime error 25. Builders preserve the other two fields and reject +an occupied `input_type` instead of silently overwriting it. + +This removes the former ambiguity between two byte layouts without inventing a +CellScript-specific replacement for CKB's shared Witness convention. + +### Persisted Identity Cut -The four pillars below are independent enough to be tracked as separate work -packages, but they share one discipline: every claim must remain tied to -compiler evidence or builder-backed chain evidence, and every "production" -word must distinguish *deployed and observed* from *gated and certified*. +Because no ecosystem migration is required, 0.23 accepts only the new identity +set: -This is a draft roadmap, not an implementation contract. It must be matched -against `CHANGELOG.md`, the release gate, and any in-flight branch before -adoption. +- compile metadata schema 57 with source schema 2, artifact schema 1, and + constraints schema 2; +- `Cell.lock` version 2; +- `Deployed.toml` version 2 with + `cellscript-deployed-v0.23-edition-2026`; +- edition-bound compile receipts and generated action builders; and +- registry build records with a required compatibility-profile hash. + +Readers reject missing, mismatched, or superseded identities. They do not +infer ABI or metadata versions from Edition 2026. + +### Acceptance Boundary + +The foundation is complete only when manifest parsing, compile metadata, +artifact verification, registry resolution, lock/deployment checks, CLI, LSP, +WASM, website bindings, entry-wrapper codegen, builders, examples, and docs all +agree. Valid and invalid CKB-VM fixtures must cover canonical +`WitnessArgs.input_type`, malformed offsets, absent fields, raw-payload +rejection, and output-only group selection. NovaSeal live and planned-profile +devnet constructors must use the same placement rather than maintaining a +release-only raw-witness path. Routine merge evidence is `dev` and `ci`; because +witness placement changes generated RISC-V, the clean-tree `backend` gate +remains required before a production claim. + +The 2026-07-31 syntax audit also closed the source-level consistency slice of +this foundation: + +- comma-terminated type fields are the formatter's canonical output, while + comma-free fields remain accepted compatibility input; +- quick, CI, and deep syntax-combination matrices require both field forms; +- the checked atomic-swap, NFT, timelock, and multi-phase-DAO example pairs use + local `U64_MAX` constants instead of opaque maximum or `MAX - delta` + literals; and +- CKB-VM crypto primitive fixtures place `CSARGv1` in + `WitnessArgs.input_type`, so they exercise placement ABI v2 rather than the + retired raw-witness alias. + +The `dev` and `ci` gates enforce the canonical example and integer-boundary +rules. This closure does not add syntax, relax the entry ABI, or expand the +production example matrix. + +Source documents: + +- [0.23 development release notes](../docs/releases/CELLSCRIPT_0_23_RELEASE_NOTES.md) +- [CellScript Edition Policy](../docs/CELLSCRIPT_EDITION_POLICY.md) +- [Entry Witness ABI](../docs/CELLSCRIPT_ENTRY_WITNESS_ABI.md) +- [Metadata verification tutorial](../docs/wiki/Tutorial-06-Metadata-Verification-and-Production-Gates.md) ## Pillar 1: Public Registry Production Deployment -The registry is the largest 0.23 feature. The write API (`services/registry-api`) -is already implemented to the boundary described in +**Status (2026-08-02): production infrastructure, public reads, website, CLI +resolution, evidence promotion, and the bounded automatic source/build +verification pipeline are implemented and deployed. The generalized artifact, +independent reproduction, mainnet deployment, and configured chain-commitment +paths are implemented in-tree. Production chain commitment is not active until +the canonical Registry Type Script, commitment custody Lock, and both code +CellDeps are deployed, sufficiently confirmed, and configured. A publisher-owned wallet publication, a real +non-CellScript mainnet artifact, and clean-machine consumption remain adoption +checkpoints.** + +The canonical Registry Type Script source, reproducible release identity, +CKB-VM lifecycle/authorization tests, and production configuration pinning are +now implemented in-tree. The unchecked item below is specifically the external +mainnet transaction, confirmation, and operator configuration checkpoint. + +The registry is the largest 0.23 feature. The write API +(`services/registry-api`) implements the boundary described in [`docs/CELLSCRIPT_REGISTRY_PRODUCTION_BOUNDARY_ADR.md`](../docs/CELLSCRIPT_REGISTRY_PRODUCTION_BOUNDARY_ADR.md): -JoyID-rooted capability authorisation, scoped capability keys, namespace claim -cooldown, R2 source snapshots, Neon Postgres state, static `/packages/*` read -path, idempotent publish, and admin-gated status transitions. The 0.23 work -is to actually deploy it on `cellscript.dev` and to wire the frontend and CLI -into the same trust model. +wallet-rooted JoyID or CKB secp256k1 capability authorisation, scoped capability +keys, namespace claim +cooldown, content-addressed source snapshots, Postgres state, a separate static +`/packages/*` read path, idempotent publish, admin-gated suppressive +transitions, and evidence-gated assurance promotion. + +The Edition 2026 plus resolved-profile contract slice is complete across the +Rust publisher/reader, API validation, deployed Postgres schema, +version-addressed package JSON, checked-in registry fixture, and website data +model. These surfaces accept one complete entry shape; there is no fallback +reader for omitted fields. Generic admin status changes cannot create +`verified_build`, `deployed`, or `on_chain_committed` claims. The ordered +`/promote` endpoint requires identity-bound evidence for each transition. ### Production Domains And Hosting ```text -cellscript.dev -> Astro site (Cloudflare Pages) + playground -registry.cellscript.dev -> static/CDN read path backed by R2 objects -api.registry.cellscript.dev -> authenticated Cloudflare Worker write API +cellscript.dev -> Astro static site + WASM playground +registry.cellscript.dev -> read-only nginx over the Registry object volume +api.registry.cellscript.dev -> Node 22 Registry API + Postgres 17 +HTTPS -> shared HTTPS Portal with persisted ACME state ``` -The site stays static where it can and only the write path is dynamic. -Ordinary package reads never touch Hyperdrive or the write store; the -`/packages/:namespace/:name/versions/:version.json` route is served from R2 -with CDN cache headers. +All three public hosts are live on the production server. The API/database use +an isolated internal network; only the API and static read container join the +existing TLS proxy network. Source snapshots and package JSON share a +persistent volume, mounted read/write by the API and read-only by nginx. +Ordinary direct package reads therefore do not touch Postgres or the write +process. The Cloudflare Worker/Hyperdrive/R2 implementation remains a portable +alternative deployment, not a claim about the current topology. ### Scope -- Stand up `services/registry-api` on Cloudflare Workers against a real Neon - Postgres instance through Hyperdrive, with the `REGISTRY_ADMIN_TOKEN`, - Hyperdrive, and R2 bindings configured as secrets/bindings rather than in - `wrangler.toml`. -- Provision the two R2 buckets (`REGISTRY_OBJECTS`, `SOURCE_SNAPSHOTS`) and - the static `/packages/*` write-before-admit path described in the ADR. -- Bring the staging slice (`staging-registry.cellscript.dev`) up first; the - production slice is cut over only after staging has run the acceptance - scenarios end to end. -- Wire the Astro frontend (the existing `website/src/pages/registry*` surface - and `RegistryLayout.astro`) to the live read path so the website renders - real registry entries instead of the static - `website/src/data/registry-packages.json` snapshot. -- Replace the website publish page with a real JoyID/CCC-backed submit flow - that signs `cellscript-registry-auth-v1` capability payloads through the - CCC JoyID CKB signer and posts them to `/v1/capabilities`. -- Keep the existing `npm run prepare:registry` regeneration as a fallback - fixture path; it must not become the read authority for the production - site. +- [x] Deploy `services/registry-api` with generated database/admin secrets, + persistent Postgres/object volumes, migrations, health checks, bounded + request bodies, read-only root filesystems, structured logs, and log rotation. +- [x] Serve version-addressed `/packages/*` JSON from a read-only process + independent of the API and Postgres. +- [x] Publish trusted TLS for `registry.cellscript.dev` and + `api.registry.cellscript.dev`; configure the API vhost for the publish + contract's 8 MiB proxy limit. +- [x] Wire the Astro Registry list and dynamic detail pages to the live API, + remove Coming Soon, and label the checked-in fixture strictly as a read-only + mirror used only when the API is unavailable. +- [x] Keep the CCC wallet submit page on the same canonical + `cellscript-registry-auth-v1` capability protocol. +- [x] Expose namespace ownership as an explicit first-publish step through + `cellc auth namespace claim` and the submit page, matching the deployed + `/v1/namespaces/claim` admission boundary. +- [x] Implement and expose public search/detail/evidence reads plus ordered + evidence promotions. +- [x] Make publish admission enqueue a transactional verification job; claim it + with Postgres leases and `SKIP LOCKED`; authenticate and compile the immutable + snapshot in a bounded, least-privilege worker; atomically promote it to + `verified_build`; converge the static version object; and expose queue + metrics, dead letters, and audited manual requeue. +- [x] Keep unverified versions available by direct URL and explicit status + query, while limiting the default public list/search and resolver to + `verified_build`, `deployed`, and `on_chain_committed`. +- [x] Separate CellScript dependencies, deployable CKB executables, runtime + verifiers, reproducible binaries, and copy-only templates with closed + artifact/profile/language/consumption contracts across API, CLI, verifier, + website, and immutable bundles. +- [x] Require independent `reproduced_build` reports before a reproducible + artifact can become verified or acquire deployment evidence. +- [x] Generate wallet-ready Registry commitment intents, scan exact configured + Type Script matches, and reconcile spent commitments or stale deployments + without deleting historical evidence. +- [ ] Deploy and configure the canonical mainnet Registry Type Script, + commitment custody Lock, and both code CellDeps; then publish and commit the first real non-CellScript + mainnet artifact. +- [ ] Complete a publisher-owned wallet capability, namespace claim, + publication, replay, revocation, and first clean-machine install against + production. ### CLI Alignment @@ -81,45 +286,84 @@ with CDN cache headers. keeping `--offline` and the Git/`registry.json` path as explicit audit and fallback modes. - Verify `cellc auth capability create/submit/revoke` against the deployed - Worker end to end, including the JoyID signature verification, capability - key persistence in the OS keychain, and CI signing via + write service end to end, including JoyID and CKB secp256k1 signature + verification, capability-key persistence in the OS keychain, and CI signing via `CELLSCRIPT_CAPABILITY_PRIVATE_KEY_PKCS8_B64`. - Confirm idempotency (`Idempotency-Key`, `x-idempotency-status: replayed`), - nonce consumption ordering, and the fail-fast-before-object-storage rule - against the live Worker. -- Ensure `cellc install`/`cellc update` resolve against - `registry.cellscript.dev` by default and keep hash-first verification - (source hash, manifest hash, build identity) intact. + request-owned nonce release on pre-admission failure, transactional admission, + and the fail-fast-before-object-storage rule against the live write service. +- `cellc install`/`cellc update` now query + `api.registry.cellscript.dev` by default, select only accepted public + statuses, then download the version's immutable Registry snapshot and verify + its SHA-256 descriptor, per-file BLAKE2b hashes, safe paths, edition/profile, + and whole-tree source hash. `CELLSCRIPT_REGISTRY_URL` remains the explicit + legacy Git/`registry.json` offline authority override. ### Acceptance Boundary -Production-readiness for the registry means all of: - -- staging runs the full positive and negative publish flow (capability - creation, JoyID signature, namespace claim cooldown, publish, replay, - revoke, quarantine, yank); -- the static read path survives a write-store outage (packages remain - readable from R2); -- existing package versions are rejected before source snapshot writes; -- per-IP/ASN/principal/capability/namespace/package quotas behave under - forged-payload, replay, and burst tests; -- the admin audit log records every capability, namespace, publish, and - override transition with an attributable actor; -- the first real CellScript source package is published through the - production flow and resolves on a clean machine via `cellc install`. - -The existing `services/registry-api` test suite is the baseline. New -end-to-end coverage belongs in a deployable scenario harness, not in the -compiler test gate. +Production-readiness evidence for the already deployed source-package slice +currently proves: + +- all API type checks and 42 admission/state-machine tests pass locally; +- the independent Rust verifier compiles a generated snapshot with the real + compiler and rejects source, manifest, and compatibility-profile drift; +- an isolated production Compose topology completed a real `cellc publish` from + queue admission through leased compilation, evidence persistence, + `verified_build`, default-list visibility, and static-object publication; +- the live production topology repeated that path from external `cellc publish` + through real compiler verification and a fresh consumer install/check/build + without `--allow-unverified`; the explicitly seeded smoke identity and its + served rows/objects were removed afterward; +- live health/readiness checks cover Postgres, the object volume, runtime, and + admin configuration; +- the website has a tracked read-only nginx/Compose deployment, and all three + public surfaces preserve their intended security headers through TLS; +- the proxy admits a 2 MiB body to application validation and the Node adapter + rejects 7 MiB + 1 byte with a structured 413; +- unauthorised admin writes, invalid public queries, static POSTs, and traversal + attempts are rejected; +- immutable snapshot descriptors are present in public/static version records, + and the resolver fails closed on opaque archives, traversal, file-hash drift, + object-hash drift, or source-tree drift; +- API restart recovery preserves the database, audit log, and object volumes; +- the daily systemd backup produces checksum-verified Postgres and object-store + archives, and a post-`0002` backup captured the migrated, cleaned production + state; an isolated Postgres 17/object-volume drill restored both migrations, + all seven core tables, and the complete object archive; +- the website serves the live Registry and contains no Coming Soon surface. +- a cryptographically valid WebAuthn-shaped P-256 fixture completes capability + registration, explicit namespace claim, signed publish, idempotent replay, + API/static/snapshot reads, and a fresh-directory install/check/build against + production; this proves deployment mechanics but is not publisher-owned + JoyID evidence. + +The remaining deployment checkpoints are intentionally concrete: complete a +positive publisher-owned wallet flow and install its first accepted source +package on a clean machine; then deploy/configure the canonical Registry +Scripts and exercise reproduction, deployment, commitment, index discovery, +and lifecycle demotion with a real mainnet non-CellScript artifact. Unit-test +signatures, transaction intents, or direct database seeding do not satisfy +those checkpoints. + +The existing `services/registry-api` typecheck, unit suite, Node API/verifier +builds, dry-run Worker build, and the independent Rust verifier tests/clippy run +in the unified `ci` gate as the local contract baseline. `dev` checks the Rust +verifier crate. Deployed end-to-end coverage still belongs in a staging +scenario harness; local compiler CI is not evidence for either the self-hosted +runtime or the optional Cloudflare/R2/Hyperdrive/Neon adapter. ### Non-Goals -- No on-chain deployment record submission in the first slice. On-chain - attestation uses the same identity model but is feature-gated and must not - be mixed into the first write API. +- No claim that a transaction intent is an on-chain commitment. Only a + sufficiently confirmed live mainnet Cell matching the configured Registry + Type Script, commitment Lock, exact commitment data, and both live code + CellDeps can produce current `on_chain_committed` state. +- No Registry ownership of application business Cells. The Registry identifies + code, build, TCB, deployment, and commitment evidence; application state Cells + remain under their own Lock/Type Scripts and transaction protocols. - No bond or refundable deposit mechanism; the schema leaves `policy_hooks` and `bond_policy_hooks` for later. -- No non-`joyid_ckb` publisher principals. +- No testnet Registry authorisation, deployment, or commitment state. - No D1 as primary database. Source documents: @@ -128,63 +372,39 @@ Source documents: - [Registry Phase 1 walkthrough](../docs/CELLSCRIPT_REGISTRY_PHASE1.md) - [Registry API service README](../services/registry-api/README.md) -## Pillar 2: Python Tooling Ported To Rust - -CellScript currently carries a non-trivial Python surface in `scripts/`, -`proposals/*/scripts/`, and `website/scripts/`. None of it is the compiler, -but several pieces are load-bearing for the gate, for NovaSeal/Evolving-DOB -evidence, and for the website registry data: - -- `cellscript_strict_backend_audit.py` — drives the strict backend audit - mode of the gate. -- `cellscript_syntax_combo_audit.py` — drives the syntax-combination matrix - in `tests/syntax_combo/`. -- `validate_ckb_cellscript_production_evidence.py`, - `validate_cellscript_tooling_release.py` — release evidence validators - consumed by `scripts/ckb_cellscript_acceptance.sh` and the gate. -- `novaseal_*.py` and `evolving_dob_*.py` — proposal-scoped devnet/stateful - harnesses, signing vectors, and external evidence adapters under - `proposals/novaseal/scripts/`, `proposals/novaseal/v0-mvp-skeleton/scripts/`, - `proposals/novaseal/agreement-profile-v0/scripts/`, and - `proposals/evolving-dob/evolving-dob-profile-v1/scripts/`. -- `check_cellscript_skill_pack.py` — validates the CellScript programming - skill pack surface. -- `website/scripts/regen-website-data.py`, - `website/scripts/generate-registry-data.py`, - `website/scripts/fetch-github-data.py` — website data regeneration. - -### Scope - -Port the load-bearing Python surface into Rust workspace members or -crate-local test harnesses, with one rule: any ported tool that the release -gate depends on must continue to produce byte-identical evidence reports so -historical comparisons remain valid. - -Concretely: - -- introduce a `cellscript-tools` workspace crate (already partially present - as `crates/cellscript-tools`) that hosts the Rust ports of the - backend-audit, syntax-combo driver, production-evidence validator, and - tooling-release validator. Each port keeps the same output schema and the - same exit-code contract as the Python original. -- move the NovaSeal and Evolving-DOB proposal scripts into per-proposal - Rust harnesses under their existing `proposals/*/` trees, preserving the - content-addressed evidence-file discipline (CKB Blake2b-256 digest, - non-empty regular file, reject symlinks/parent traversal/absolute paths). -- replace `website/scripts/*.py` with TypeScript/Node scripts under - `website/scripts/` that the Astro build already understands, so the - website build stops pulling a Python runtime. -- delete the original Python files only after the Rust/TS port passes the - same gate mode that the Python original gated. -- update `scripts/cellscript_gate.sh` mode definitions (`dev`, `ci`, - `backend`, `release`, `release-quick`) to invoke the Rust/TS ports, and - drop the `python3` shell-syntax check arm once no tracked Python remains. +## Pillar 2: Native Tooling Migration Complete + +CellScript's load-bearing tooling is now Python-free. Gate, evidence, and +proposal logic lives in Rust; Astro-facing website data generation stays in +the website's native Node runtime. + +### Implemented Scope + +- `crates/cellscript-tools` owns strict backend and syntax-combination audits, + repository checks, release validators, CKB acceptance, NovaSeal fixtures, + external-evidence adapters, Fiber experiments, and live/stateful runners. +- `proposals/novaseal/tools` owns NovaSeal package-local vector, schema, ABI, + audit-surface, and fixture harnesses. +- `proposals/evolving-dob/evolving-dob-profile-v1/tools` owns registry pressure + and devnet workflow validation. +- `website/scripts/*.mjs` owns registry, compiler-output, and GitHub activity + data generation without introducing a second runtime into the Astro build. +- `scripts/cellscript_gate.sh` invokes only Rust, shell, and Node tooling. The + retired syntax-check arm and all tracked interpreter sources have been + removed; a repository-wide native source policy prevents reintroduction. +- Evidence producers preserve their established JSON shape where it remains + part of the release contract; implementation-origin fields now truthfully + identify the Rust harness and transaction-recipe replay path. +- Profile-operator fixture generation accepts an explicit evidence root, and + its integration coverage constructs isolated reports instead of depending + on stale developer-machine files below `target/`. ### Acceptance Boundary -- `./scripts/cellscript_gate.sh dev` and `ci` pass without Python installed. -- Every historical evidence report a ported tool used to produce can still be - reproduced bit-for-bit from the same inputs. +- `./scripts/cellscript_gate.sh dev` and `ci` pass with only the declared Rust, + shell, and Node runtimes. +- Deterministic static reports remain byte-stable for the same inputs; live + reports preserve their schemas while binding fresh devnet transactions. - The NovaSeal verifier pinning check still recomputes BLAKE2b and SHA-256 over the same ELF and compares against the same `Cell.toml` and `proofs/*.template.json` hashes. @@ -194,8 +414,8 @@ Concretely: ### Non-Goals - No rewrite of the compiler, the gate script's bash orchestration, or the - CKB acceptance harness's bash wrappers. Only the Python leaves the - contract. + CKB acceptance harness's bash wrappers. The migration changes the native + tooling implementation, not those orchestration boundaries. - No change to the evidence schema or file naming. - No dropping of historical evidence files; the ports must keep reading them. @@ -208,6 +428,12 @@ Source documents: ## Pillar 3: RGB++ And Fiber Integration +**Final 0.23 disposition**: bounded adapter and content-addressed evidence +hardening is retained; the complete external Fiber matrix and RGB++ protocol +promotion move to the conditional evidence track in 0.24. This section records +the original target and the remaining boundary, not a claim that every item +below completed. + 0.22 shipped a narrow, no-profile Fiber path: the dedicated `fungible-type-group-v1` compiler entry, the `cellscript-fiber-adapter`, and bounded local-devnet scenarios. Phase 5 (gate promotion and optional hot @@ -282,105 +508,64 @@ Source documents: ## Pillar 4: Off-Chain Session Runtime Profile (Myelin Alignment) -The Myelin repository vendors a copy of CellScript at -`/Users/arthur/RustroverProjects/Myelin/cellscript`, currently pinned at -`0.21.1`. It has already diverged: the workspace members differ, the -vendored fork is behind the 0.22 type/set surface, and Myelin's own session -L2 plan calls for a court-facing `CkbStrict` VM profile and a finite session -ledger whose disputed chunks project into CKB-compatible replay. 0.23 -absorbs the language-side needs of that plan so Myelin can stop carrying a -private fork. - -### Scope - -Introduce an `Off-Chain Session Runtime` target profile in the CellScript -compiler that gives Myelin (and any other bounded off-chain session runtime) -a first-class, fail-closed compilation entry for session-shaped contracts. -The profile is opt-in and does not change the default CKB profile. - -The profile's initial deliverables: - -- A new target profile metadata entry, distinct from the existing `ckb` - profile, that records: - - `vm_profile` (e.g. `ckb_strict` vs `myelin_extended`); - - session commitment shape (`SessionId`, `ChunkCommitment`, - `DisputeBundle`, `SettlementIntent` references, not values); - - whether the artifact is court-facing or off-chain-only; - - whether concurrency is permitted. -- A bounded concurrency primitive surface for the off-chain path only. This - is the *initial* concurrency support: a finite, scheduler-visible set of - session-scoped operations whose semantics are well-defined under Myelin's - session model (ordered chunk commitments, deterministic state-root - transitions, scheduler commitments). It is **not** a general - threading/actor model and does not enter the CKB profile. -- A fail-closed rule: any artifact compiled under the Off-Chain Session - Runtime profile that is later projected into a CKB court path must - recompile under `ckb_strict` and must not carry `MyelinExtended` semantics - unless the projection layer explicitly proves compatibility. -- Compiler metadata and `cellc explain-*` output that distinguish - court-facing from off-chain-only artifacts, so auditors can tell which - profile an artifact was built under. - -### Myelin Re-Convergence - -After the profile lands in upstream CellScript: - -- Myelin drops its vendored fork and consumes the published CellScript - release as a normal dependency. -- The Myelin Session L2 P0 skeleton (`SessionOpen`, `ChunkCommitment`, - `DisputeBundle`, `SettlementIntent`) consumes the new profile instead of - patching the compiler. -- The `CkbStrict` default for court-facing execution becomes a CellScript - profile fact, not a Myelin-local deviation. -- Legacy group-source encoding and other deviations recorded in - `MYELIN_CKB_SEMANTIC_DEVIATIONS.md` move into the upstream profile contract - or are removed. - -### Acceptance Boundary - -- The Off-Chain Session Runtime profile is parser/type/lowering/metadata/ - codegen/LSP/docs gated just like any other target profile. -- The concurrency surface is bounded: every permitted concurrent operation - has a documented scheduler contract, a deterministic replay story, and a - fail-closed fallback when the host runtime does not provide it. -- No `MyelinExtended` artifact may claim CKB court compatibility without an - explicit projection proof in metadata. -- The Myelin Teeworlds fixture still finalises with both the static - committee and Tendermint and produces identical state-transition - commitments but different finality evidence. - -### Non-Goals - -- No general `channel` or session-type syntax in the core language. The 0.22 - type/set roadmap already defers this; 0.23 keeps it deferred. -- No independent app-chain features for Myelin: block production, P2P - gossip, fork choice, validator-set lifecycle, slashing, fee markets, or - app-chain governance stay out of scope, matching the Myelin Session L2 - plan. -- No implicit promotion of off-chain semantics onto the CKB court path. +**Final 0.23 disposition**: superseded and not implemented as a CellScript +target profile. Myelin's current repository has already removed the vendored +compiler architecture assumed by this proposal. Its `cellscript-adapter` +attests an independently versioned external compiler, production requests use +the CellScript `ckb` target, scheduler plans remain off-chain sidecars, and +session/court execution is Myelin-owned `CkbStrict`. The 0.24 handoff therefore +updates that adapter to the completed CellScript identity/checker boundary +without introducing `MyelinExtended` semantics into CellScript. + +### Current Boundary + +- CellScript owns source semantics, the `ckb` target contract, generated + artifact/metadata identities, and scheduler access templates. +- Myelin owns its finite-session VM, authenticated state resolution, conflict + hashes, scheduler plans, finality, DA, projection receipts, and the + distinction between `CkbStrict` and `MyelinExtended` execution. +- Scheduler binding names are diagnostics. Myelin resolves every final + conflict key from authenticated concrete Cells and validated type-script + identity. +- Myelin compiler fixtures may live in Myelin, but compiler source and + workspace crates do not. + +### 0.24 Handoff + +The next integration step is one explicit adapter-lock transition to the +completed Edition 2026, metadata/profile, and canonical witness identities, +followed by adoption of the independent artifact checker. No fallback reader, +raw-witness alias, off-chain compiler target, or general concurrency syntax is +added to make that transition easier. + +Acceptance belongs to the 0.24 roadmap: the adapter must verify exact compiler, +source, artifact, metadata, source-map, lowering-record, and checker identities; +court-facing requests stay on `ckb`; and the same deterministic session +transition must retain consensus-independent state commitments with distinct +finality evidence. Source documents: -- [Myelin Session L2 plan](../../Myelin/MYELIN_SESSION_L2_PLAN.md) -- [Myelin CKB semantic deviations](../../Myelin/MYELIN_CKB_SEMANTIC_DEVIATIONS.md) -- [Myelin production gate](../../Myelin/MYELIN_PRODUCTION_GATE.md) +- [Myelin Session L2 plan](https://github.com/Myelin-Labs/Myelin/blob/main/MYELIN_SESSION_L2_PLAN.md) +- [Myelin CKB semantic deviations](https://github.com/Myelin-Labs/Myelin/blob/main/MYELIN_CKB_SEMANTIC_DEVIATIONS.md) +- [Myelin production gate](https://github.com/Myelin-Labs/Myelin/blob/main/MYELIN_PRODUCTION_GATE.md) - [0.22 type/set roadmap (session-type deferral)](CELLSCRIPT_0_22_TYPE_AND_SET_THEORY_ROADMAP.md) +- [0.24 roadmap](CELLSCRIPT_0_24_ROADMAP.md) ## Cross-Cutting Discipline 0.23 does not relax any existing project contract: -- Trailing-whitespace, forbidden tracked-file, and `git diff --check` gates - still apply. The Python-to-Rust port must re-run `cargo fmt` and fix - whitespace. +- Trailing-whitespace, native source-policy, and `git diff --check` gates still + apply. Native tooling changes must re-run `cargo fmt` and fix whitespace. - The website build still regenerates `website/src/data/registry-packages.json` and fails if it is dirty in the working tree; if the production registry changes what gets regenerated, commit the result. - The wasm bundle size budget (600 KB gzip) still holds. Any compiler - surface added for the Off-Chain Session Runtime profile must be gated so - the `wasm32-unknown-unknown` playground build does not pull native-IO or - concurrency deps. + surface must be gated so the `wasm32-unknown-unknown` playground build does + not pull native-IO or host-runtime concurrency dependencies. The retired + Off-Chain Session Runtime proposal adds no 0.23 WASM surface. - Release notes continue to separate highlights, scope boundaries, validation commands, and detailed docs. Roadmap promises stay out of `docs/` and in `roadmap/`. @@ -390,41 +575,52 @@ Source documents: ## Sequencing -The four pillars are largely independent and can be tracked as parallel -work streams. Suggested ordering for *release-blocking* slices: +The final 0.23 sequence is: + +1. native tooling migration and source-policy enforcement; +2. Edition/profile/entry-ABI and persisted-identity closure; +3. public Registry infrastructure, automatic source verification, artifact + evidence, publisher-session, Pudge, website, and documentation closure; and +4. bounded Fiber evidence hardening without promoting an incomplete external + matrix. -1. Pillar 2 (Python → Rust) lands first, because it changes the shape of the - gate itself and every later pillar's evidence runs through that gate. -2. Pillar 1 (registry production) lands next, because it unblocks real - package publishing for everything else. -3. Pillar 4 (Off-Chain Session Runtime profile) lands next, because Myelin - re-convergence depends on it and it is the riskiest compiler change. -4. Pillar 3 (RGB++ / Fiber) lands last, because it is the most - evidence-bound and the least likely to be fully "done" in one release; - partial closure with an explicit pending matrix is acceptable. +The proposed Off-Chain Session Runtime profile is not inserted after those +steps. The current Myelin process-adapter architecture makes that compiler +surface unnecessary. Independent artifact verification, executable package +tests, source maps, the Myelin adapter handoff, and conditional Fiber/RGB++ +promotion start from the 0.24 roadmap. ## Risk Register -- **Registry production cut-over**. The write API is implemented but has - only run locally and in tests. The first real deployment may surface - Hyperdrive/R2/Neon integration issues that the test suite does not cover. - Mitigation: staging-first, fail-fast-before-object-storage, full admin - audit log. -- **Python-to-Rust port drift**. A subtle difference in evidence-report - formatting breaks historical comparisons. Mitigation: byte-identical - output requirement, parallel-run period before Python deletion. -- **Off-Chain Session Runtime scope creep**. The profile can easily grow - into a general concurrency model. Mitigation: bounded scheduler-visible - operations only, fail-closed when the host does not provide them, no - core-language channel/session syntax. +- **Registry publisher adoption**. The self-hosted production stack and public + read surfaces are live, but the first publisher-owned wallet package has not + completed the positive publication/install loop. Mitigation: keep + source-published entries out of default resolution, require the existing + evidence chain, and do not replace the final interactive checkpoint with + seeded database state. +- **Registry chain activation**. Transaction intent, Script-indexed discovery, + and lifecycle reconciliation are implemented, but no public commitment may + be claimed until the canonical mainnet Registry Type Script, commitment Lock, + and both code CellDeps are deployed and pinned. Mitigation: leave all four + settings absent, fail readiness on partial or immature configuration, and require a real live-Cell + drill before marking the checkpoint complete. +- **Native tooling serialization drift**. A subtle difference in + evidence-report formatting breaks historical comparisons. Mitigation: + byte-identical output requirements, stable schemas, and regression vectors. +- **Off-Chain Session Runtime scope creep**. The proposed compiler profile + would duplicate Myelin-owned VM/session semantics and blur court-facing CKB + claims. Resolution: retire the profile proposal; keep production compilation + on `ckb`, keep `MyelinExtended` inside Myelin, and harden the external adapter + and independent checker boundary in 0.24. - **Fiber full matrix never closing**. The matrix is large and depends on an external Fiber binary. Mitigation: keep the harness standalone and non-gating until the matrix is complete; release 0.23 with an explicit pending matrix rather than blocking on it. -- **Myelin re-convergence slip**. If the profile lands late, Myelin keeps - diverging. Mitigation: land the profile early in the cycle and cut a - CellScript release that Myelin can consume even if the other pillars slip - to 0.24. +- **Myelin handoff drift**. Myelin's current adapter lock still identifies an + earlier reviewed compiler line while CellScript 0.23 changes edition, + schemas, profile identity, and witness placement. Mitigation: do not add + compatibility aliases to 0.23; coordinate one explicit adapter-lock and + fixture transition under the 0.24 checker contract. ## Roadmap Discipline diff --git a/roadmap/CELLSCRIPT_0_24_ROADMAP.md b/roadmap/CELLSCRIPT_0_24_ROADMAP.md new file mode 100644 index 00000000..9aaef92b --- /dev/null +++ b/roadmap/CELLSCRIPT_0_24_ROADMAP.md @@ -0,0 +1,597 @@ +# CellScript 0.24 Roadmap + +**Status**: Core implemented and in stable-release closure on `nightly-0.24`; +external Myelin lock adoption and conditional Fiber/RGB++ evidence remain +separately pending + +**Theme**: independently verified artifacts, executable package evidence, and +bounded runtime integration + +**Depends on**: Edition 2026, metadata schema 58, the resolved compatibility +profile, canonical `WitnessArgs.input_type` placement, the native +`cellscript-tools` gate, the public Registry verification worker, the existing +CKB-VM acceptance harnesses, and Myelin's external compiler-process adapter + +## Goal + +0.24 should reduce the amount of CellScript that a consumer must trust without +pretending that an untyped RISC-V ELF has the same verification surface as a +typed virtual-machine bytecode. + +The release has two mandatory outcomes: + +1. a small, bounded checker independently validates a stable lowering record, + its metadata claims, and the structural CKB RISC-V artifact contract; and +2. `cellc test` executes package-authored positive and negative scenarios and + can promote selected cases to authoritative CKB-VM evidence. + +Source-to-artifact maps connect those outcomes. They let the checker, test +runner, trace tools, Registry worker, and auditors refer to the same action, +lock, basic block, ProofPlan obligation, runtime error, and instruction range. + +The release also completes the safe integration handoff that 0.23 originally +described too broadly. Myelin remains a separate finite-Cell session runtime. +It consumes the upstream compiler and the independent checker through an +attested process boundary; `MyelinExtended` remains Myelin-owned semantics and +does not become a CellScript target profile. Fiber and RGB++ promotion remains +evidence-gated and cannot turn an incomplete external matrix into a compiler +claim. + +An additional delivered Registry slice makes LS-IDL a first-class interface +for deployed CKB Lock Scripts. The profile preserves exact upstream IDL bytes, +binds their SHA-256 to the executable suffix, validates them in both Registry +verifier boundaries, and resolves them by chain-verified Script identity. It +also pins the complete current client vectors and derive/example IDLs and +provides an opt-in direct test in which the upstream Rust client calls the +Registry compatibility route. The website names this surface `LS-IDL` +explicitly and aligns it with the full-width Browse surface. This does not +expand the language edition or claim implementation correctness. + +The mainnet and Pudge Testnet Registry sites also share one versioned interface +contract. Both builds must expose the same six Registry routes and load the +same byte-identical visual and interactive assets. Only network authority and +network-derived state may differ: API/static origins, address prefix, chain, +sandbox expiry and indexing policy, and the records admitted to each isolated +store. Testnet lookup, API examples, Manage defaults, and artifact fallbacks +must never silently select mainnet. + +## Why This Is The Next Boundary + +CellScript 0.23 completed an operational distribution and evidence layer: +Edition 2026 identities, canonical entry placement, the public Registry, +compiler-backed source-package verification, reproducible artifact evidence, +native gate tooling, and bounded CKB/Fiber evidence. The remaining trust gap is +not another syntax feature. It is that the compiler still creates most of the +facts later consumed by `verify-artifact`. + +Sui Move provides a useful comparison, but not a design to copy literally. Its +typed bytecode is independently checked for control-flow, stack, type, +resource, reference, and platform-specific object rules, and the verifier +itself is metered. See the pinned upstream +[Move bytecode verifier contract](https://github.com/MystenLabs/sui/blob/5a9f37431c473fa2f6d49abecbcc6a6d7190f533/external-crates/move/crates/move-bytecode-verifier/README.md). +CellScript emits untyped RISC-V for CKB-VM, so equivalent assurance requires a +verifiable lowering boundary before machine code plus a separate structural +ELF checker. Recovering the complete CellScript type/resource semantics from an +arbitrary ELF is not a credible 0.24 promise. + +The same comparison informed the package trust closure. Sui's new package +design records complete dependency graphs, manifest digests, +environment-specific resolution, and explicit repinning. CellScript adopts +those resolution principles in `Cell.lock` v3, adapted to CKB genesis identity +and immutable Registry snapshots, without importing Move/Sui package or object +identity. See the pinned upstream +[package design](https://github.com/MystenLabs/sui/blob/5a9f37431c473fa2f6d49abecbcc6a6d7190f533/external-crates/move/crates/move-package-alt/design/DESIGN.md). + +## Release Principles + +1. **Generation and admission are different authorities.** The compiler emits + artifacts and evidence; a smaller checker decides whether the declared + artifact contract is internally valid. +2. **The checker is bounded.** Every module, function, basic block, edge, + instruction, source-map record, and proof record has an explicit count or + byte limit before traversal. +3. **Machine-code claims stay structural unless independently replayed.** An + ELF checker may prove section, instruction, CFG, frame, ABI, and syscall + invariants. It must not claim full source equivalence merely because hashes + and metadata agree. +4. **Fast tests and authoritative tests are labelled separately.** Simulator + success is development evidence; CKB-VM execution is runtime evidence; live + RPC acceptance and commitment remain chain evidence. +5. **No new source edition.** Edition 2026 remains the sole source-semantics + epoch. A metadata or artifact-contract schema may advance independently once + its exact shape is frozen. +6. **Runtime adapters do not become hidden language semantics.** Fiber, RGB++, + and Myelin continue to consume explicit compiler, artifact, deployment, and + chain evidence through separate adapters. + +## Pillar 1: Verified Artifact Boundary + +### 1.1 Stable Verified Lowering Record + +Define one canonical, versioned lowering record emitted after typed semantic +analysis and before final assembly layout. It is an audit artifact, not a +second executable format. + +The first version records only facts that a small checker can validate: + +- module, compiler, edition, resolved-profile, source, and artifact identities; +- action, lock, and reachable helper entry identities; +- typed function signatures and fixed-width storage classes used by lowering; +- basic-block identifiers, terminators, typed edges, and call edges; +- frame size, stack-slot kind/width/alignment, outgoing argument area, and + declared scratch-register avoid sets; +- effect/capability summaries and the exact ProofPlan obligations assigned to + each entry/block; +- CKB syscall contracts, source/index domains, return-code checks, and bounded + buffers used by each call site; +- runtime-error exits and their stable error codes; +- final machine-code range and digest for each mapped block after assembly + layout. + +The record must use canonical serialization and a domain-separated hash. It +must not include compiler-internal pointers, nondeterministic map order, +absolute build paths, or opaque prose as an enforcement field. + +### 1.2 Independent Checker Crate + +Add a standalone checker crate with a deliberately narrow dependency graph. +It must not call the parser, resolver, type checker, optimizer, normal lowering +pipeline, or code generator. Shared types are limited to a versioned schema, +stable diagnostics, canonical hashing, and minimal ELF/Molecule utilities. + +The checker validates: + +- record schema, canonical order, referential integrity, uniqueness, and + declared limits; +- CFG entry/exit shape, terminators, branch targets, call targets, recursion + policy, unreachable-block policy, and frame/call ABI consistency; +- stack-slot width/alignment, outgoing stack arguments, fixed-byte storage, and + scratch-register declarations; +- effect/capability and ProofPlan coverage consistency at the stable lowering + boundary; +- ELF class, architecture, sections, entry, text/rodata bounds, prohibited + dynamic/linker state, and artifact identity; +- the emitted RISC-V instruction allowlist, aligned instruction decoding, + mapped branch/call targets, stack-pointer deltas, return paths, and declared + syscall sites; and +- agreement among source-map ranges, lowering-block digests, artifact bytes, + compile metadata, receipt, and resolved compatibility profile. + +The first checker is not required to prove arbitrary instruction-level +equivalence between the typed IR and RISC-V. Any unproven relationship remains +named `binding-verified` or `structurally-verified`, never +`semantically-equivalent`. + +### 1.3 Metering And Failure Contract + +Checker budgets are inputs to the compatibility profile or admission policy, +not ambient host limits. At minimum, enforce limits for artifact bytes, +record bytes, functions, blocks, edges, instructions, call depth, stack-frame +bytes, proof records, source-map intervals, and diagnostic output. + +Budget exhaustion returns one stable rejection code. Invalid input must never +panic, recurse without a checked bound, allocate from attacker-controlled +counts before validation, or emit unbounded diagnostics. + +### 1.4 Mutation, Property, And Corpus Evidence + +Maintain three independent evidence sets: + +- valid compiler-produced artifacts that must pass; +- deterministic mutations of sections, instructions, branches, frames, call + sites, hashes, proof links, and source maps that must fail with the expected + checker code; and +- parser/checker fuzz inputs whose minimum requirement is bounded execution and + no panic. + +At least one mutation must target every enforced invariant. A test that merely +changes a hash does not cover CFG, ABI, stack, syscall, or ProofPlan checking. + +### Acceptance Boundary + +- Every production example ELF and Registry verifier fixture passes the + standalone checker. +- Every seeded invalid mutation is rejected with its expected stable code. +- The checker has no dependency on compiler front-end or codegen crates. +- Re-running the checker on the same input produces byte-identical JSON. +- Budget exhaustion and malformed length/count fields are negative tests. +- `cellc verify-artifact` reports binding verification, structural + verification, lowering-record verification, CKB-VM evidence, and chain + evidence as separate fields. +- The Registry worker can execute the checker in a least-privilege process + without loading the compiler for artifact-only admission. + +## Pillar 2: Executable Package Tests And Source Maps + +### 2.1 `cellc test` Executes + +Keep the existing package test discovery and expectation conventions where +possible, but make success mean that a selected execution backend actually ran. + +The initial backends are: + +- `simulator`: deterministic, fast, explicitly non-consensus execution for + development feedback; and +- `ckb-vm`: compiled ELF execution through the maintained CKB test boundary, + used for authoritative runtime acceptance. + +Test output always records backend, compiler/artifact/checker identities, +profile, entry, inputs, result, runtime error, cycles when available, and the +evidence tier. A package cannot label simulator-only success as CKB-VM evidence. + +### 2.2 Versioned Scenario Contract + +Define a small, versioned scenario format for transaction-shaped tests. It may +be TOML or JSON after an implementation spike, but one canonical format must be +chosen before release. It describes: + +- input and output Cells, capacities, data, lock/type scripts, and named prior + outputs; +- CellDeps, header deps, `since`, witnesses, lock args, and canonical + `WitnessArgs.input_type` entry data; +- the action or lock entry under test and its typed parameters; +- positive acceptance or one exact `CellScriptRuntimeError` expectation; +- multi-step Cell replacement with explicit consumed/live state; and +- cycle, transaction-size, and occupied-capacity limits when the backend can + measure them. + +Unknown fields, ambiguous indexes, duplicate names, stale references, missing +Cells, unsupported evidence requests, and mismatched target profiles fail +before execution. + +### 2.3 Semantic Coverage + +Coverage is tied to compiler evidence rather than only source lines. Reports +include: + +- action and lock entries; +- source branches and lowering blocks; +- ProofPlan obligations and evidence tiers; +- runtime-error paths; +- CKB syscall sites; and +- positive/negative transition edges for declared flows. + +Coverage never claims that an unexecuted branch is safe. It only says which +declared contract surfaces were exercised by which backend and fixture. + +### 2.4 Source-To-Artifact Map + +Emit a canonical source map from source spans through typed/lowering blocks to +assembly/ELF instruction ranges. The map must survive deterministic rebuilds, +exclude absolute paths, reject overlapping or out-of-range records, and bind to +the artifact and lowering-record hashes. + +Extend existing inspect/trace surfaces rather than creating unrelated tools: + +- source-linked artifact inspection; +- source-linked CKB-VM trace rows; +- source-linked checker diagnostics; and +- coverage views keyed by action, lock, ProofPlan obligation, and runtime error. + +### Acceptance Boundary + +- A package test cannot pass without naming and running a backend. +- Positive and negative fixtures execute under `ckb-vm`; expected failures + match exact stable runtime codes. +- Multi-step scenarios prove consumed inputs become dead and declared outputs + become the next step's live inputs in the local harness. +- Source maps round-trip every mapped instruction range and reject overlap, + gaps that claim coverage, path escape, and artifact mismatch. +- Coverage reports distinguish simulator, CKB-VM, and chain evidence. +- Existing stateful release scenarios remain the oracle and are reused or + imported; the package runner does not fork their CKB semantics. + +## Pillar 3: Myelin Adapter Re-Convergence + +### Scope Decision + +Do not add an `off-chain-session`, `myelin`, or `myelin_extended` CellScript +target profile in 0.24. + +Myelin's current architecture already removes the 0.23 roadmap's original +reason for such a profile: + +- CellScript is not vendored into the Myelin workspace; +- Myelin calls an independently versioned compiler process through a lock and + binary/source/artifact/metadata attestation boundary; +- production compiler requests use the `ckb` target profile; +- session and court execution force Myelin's `CkbStrict` VM semantics; and +- Myelin-only scheduler/finality/DA commitments remain explicit sidecar + evidence rather than CKB transaction fields. + +Putting `MyelinExtended` into CellScript would blur, not close, that boundary. + +### 3.1 Upstream Toolchain Handoff + +Coordinate one explicit adapter-lock transition from the reviewed 0.22 patch +line to the completed 0.23 identity set: + +- Edition 2026; +- current compiler release/revision and Rust toolchain; +- metadata/source/artifact/constraints schema versions; +- resolved compatibility-profile hash; +- canonical `WitnessArgs.input_type` ABI with no raw-witness compatibility; +- compiler executable, source revision, artifact, metadata, lowering record, + source map, and checker digests; and +- the independent checker version and policy budget. + +No fallback reader or alias is added merely to accept the older adapter lock. + +### 3.2 Scheduler Evidence Boundary + +Continue using CellScript's typed access/scheduler metadata as an untrusted +template. Myelin resolves final conflict hashes from authenticated concrete +Cells and a validated full type-script declaration. Binding names remain +diagnostics, and scheduler plans remain sidecars bound to the raw transaction +identity. + +The 0.24 checker validates only that the compiler's access template is +internally well-formed and bound to the artifact. It does not claim that a +Myelin conflict key was resolved correctly; that remains Myelin state-layer +evidence. + +### Acceptance Boundary + +- Myelin contains no vendored CellScript compiler source or workspace member. +- The adapter rejects the old raw-witness compatibility identity and every + mismatched compiler/checker/source/artifact/metadata digest. +- Court-facing requests compile under `ckb`; `MyelinExtended` never appears in + a CellScript compatibility profile. +- The deterministic session fixture produces the same state-transition + commitments under the static committee and Tendermint, with different + finality evidence. +- Myelin's production gate verifies the exact pinned CellScript/checker pair; + skipped external workloads remain labelled skipped rather than passed. + +## Pillar 4: Conditional Fiber And RGB++ Evidence Promotion + +This is a coordinated evidence track, not a reason to weaken the core 0.24 exit +criteria. + +### Fiber + +- Complete the declared pinned lifecycle and negative matrix using regular, + non-empty, content-addressed evidence files under an explicit evidence root. +- Bind Fiber binary revision, build provenance, node configuration, restart or + capability-detected hot-load state, asset deployment identity, transaction + hashes, and negative outcomes independently. +- Promote `scripts/cellscript_fiber_acceptance.sh` into release mode only after + the complete reproducible matrix passes from a clean environment. +- Preserve the no-profile compiler rule and the distinct evidence states + `StaticallyCompatible`, `LocalNodeConfiguredRestartRequired`, + `LocalNodeAdvertised`, `ChannelReady`, and `TopologyCertified`. + +### RGB++ + +- Keep RGB++ outside `std::*` and package it as an ecosystem adapter. +- Pin RgbppLock, BtcTimeLock, BTC SPV, witness/commitment, deployment, and + confirmation identities before promotion. +- Require paired CKB and Bitcoin-side fixtures, including reorg/finality + assumptions and negative cases. +- Do not call hash/Merkle helpers Bitcoin SPV and do not compose a Spore-over- + RGB++ claim before both adapters independently pass. + +### Acceptance Boundary + +- Incomplete rows remain pending; representative samples do not close the + matrix. +- External evidence is content-addressed and path-confined. +- Operator identity, binary reproducibility, configuration, live transaction + observation, and topology certification remain separate claims. +- Failure to obtain external evidence does not relax or relabel the core + compiler/checker/test outcomes. + +## Package Evolution Closure + +0.24 now ships the resolution subset of the package-evolution design: + +- `Cell.lock` v3 carries a canonical source DAG with outgoing alias edges, + dependency-manifest digests, source hashes, and exact source identities; +- build/check/test are lock-authoritative, while lock/update/add/remove/install + are the explicit repin boundary; +- standard SemVer, local package aliases, features, optional dependencies, and + test-only roots are resolved into mode-qualified graph nodes; +- CKB environment roots bind `chain_id` plus genesis hash and require explicit + selection when dependency overrides exist; +- Git branches normalize to full commits, Registry versions to exact snapshot + URLs and SHA-256 revisions, and frozen/offline builds use only immutable + caches; and +- bounded SHA-256-pinned external resolvers normalize to ordinary immutable + sources at update time and never execute during a locked build. + +The remaining package-evolution work stays later-release scope: source/API and +action/lock ABI upgrade reports; Cell/Molecule layout, ProofPlan/effect, +builder, Type ID, and CellDep compatibility; visibility-default changes; and +the independent live-state readability/spendability versus authorization/ +predicate-security axes. Merely resolving two nodes does not make conflicting +CellScript module/type identities compatible. + +## Website And Stable-Release Integrity + +The 2026-08-13 production deployment audit found that the 0.24 website branch +had diverged before the two website commits that published the 0.23 stable +release identity. The resulting build contained the 0.24 Registry and +Playground experience, but its homepage still advertised `v0.22.0`, its +Playground still loaded the 0.22 WASM bundle, and its distribution regression +test incorrectly required those stale identities. A green website build was +therefore not sufficient evidence that the public release identity was current. + +The corrected website gitlink `00f0e2cb184c1343d2c6b57aa6a413028976a3e0` +closes that gap: + +- the homepage release card names the current stable release `v0.23.0`, its + 2026-08-11 publication date, and the exact GitHub release URL; +- the Playground loads the released 0.23 WASM asset identified by + `20260811-v0.23.0-fa369818` and SHA-256 + `fa369818631532c657e73e970b6138e3a231d532a073d428dfe7f61686135dd5`; +- the homepage and distribution checks reject a stale release link, tag, + compiler asset version, compiler version, or WASM digest; and +- the production site is built from the parent repository's exact website + gitlink rather than from whichever branch happens to be checked out in a + developer's submodule worktree. + +Before any later website deployment, the checked-in GitHub activity snapshot +must be regenerated and reviewed against GitHub's published release state, and +the website branch must include the latest stable-release synchronization +before feature work is layered on top. The homepage continues to advertise the +latest stable tag; the 0.24 nightly branch and these development release notes +do not turn into a stable release merely because their website changes are +deployed. + +## Gate Integration + +### `dev` + +- schema/canonicalization tests; +- quick checker pass over representative artifacts; +- quick invalid-mutation corpus; +- simulator package tests; +- source-map structural checks; and +- `git diff --check` plus existing native source policy. + +### `ci` + +- all standalone checker tests and clippy; +- complete deterministic invalid-mutation/property corpus; +- package simulator and CKB-VM tests; +- source-map round-trip and semantic coverage fixtures; +- Registry worker/checker integration; and +- current website/WASM/package checks, including the stable release tag, + compiler asset identity, and exact WASM digest. + +### `backend` + +- full lowering-record validation over all generated backend surfaces; +- source-map-to-ELF range validation; +- instruction/CFG/frame/ABI/syscall checks; +- full backend mutation corpus; and +- existing stateful CKB scenarios. + +### `release` + +- all production artifacts rebuilt cleanly and accepted by the standalone + checker; +- Registry admission evidence names the exact checker and policy; +- authoritative package scenarios are CKB-VM executed; +- production acceptance remains builder- and chain-evidence backed; and +- conditional Fiber/RGB++ evidence is promoted only when its separate matrix + is complete. + +## Sequencing + +1. Freeze the threat model, trust states, schema ownership, and checker budgets. +2. Emit deterministic source maps and the minimum stable lowering record. +3. Implement the standalone record/ELF checker and stable diagnostics. +4. Build mutation/property/fuzz evidence and integrate the checker into gates. +5. Turn `cellc test` into an executable simulator/CKB-VM runner with exact + failure expectations and semantic coverage. +6. Integrate the checker with Registry artifact admission. +7. Coordinate the Myelin adapter-lock handoff to the completed 0.23 identities + and then to the 0.24 checker contract. +8. Promote Fiber/RGB++ only if their external evidence independently closes. +9. Land the lock-authoritative package graph and versioned Registry profile + catalog without expanding the source edition or artifact resolver boundary. + +Source-map and record schemas land before checker or debugger UX so later +surfaces consume one contract. Myelin handoff follows checker stabilization; +it must not force compatibility aliases into the compiler. + +## Risk Register + +- **Checker duplicates the compiler**. A second front end would share the same + bugs and explode the trusted codebase. Mitigation: validate a deliberately + smaller stable lowering contract and structural ELF properties only. +- **Certificate theatre**. Hashing compiler-authored JSON can look like proof + without adding an independent check. Mitigation: every promoted claim names + the independently recomputed invariant and has a matching negative mutation. +- **Verifier denial of service**. Malformed counts or graphs can exhaust the + worker. Mitigation: validate lengths before allocation and meter every scope. +- **Source-map drift**. Optimizer/layout changes can silently detach diagnostics + from code. Mitigation: canonical post-layout ranges, non-overlap checks, + block-byte digests, and rebuild tests. +- **Simulator mistaken for consensus**. Fast tests may be overclaimed. + Mitigation: mandatory backend/evidence-tier fields and CKB-VM promotion for + authoritative cases. +- **Myelin semantics leak into CKB**. A convenience profile could make + off-chain extensions look court-compatible. Mitigation: keep the compiler + target `ckb`; record Myelin semantics and projection receipts in Myelin. +- **External matrices block core progress**. Fiber/RGB++ depend on external + binaries, networks, and operators. Mitigation: preserve independent pending + states and never lower the core checker/test exit criteria. +- **Package extensibility expands the build TCB**. Mutable branch lookup, + plugin execution, and broad artifact coercion could make builds + non-reproducible. Mitigation: explicit repinning, exact cached sources, + bounded hash-pinned update-time resolvers, and a fail-closed profile catalog; + visibility, semantic upgrade policy, and transaction composition remain out + of scope. + +## Non-Goals + +- No Move bytecode, Move VM, Sui object model, UID, shared-object consensus, + dynamic fields, or `TxContext` surface. +- No verifier for arbitrary RISC-V programs. +- No claim of complete source-to-ELF semantic equivalence in the first checker. +- No new CellScript edition or annual edition cadence. +- No general threading, actor, channel, or session-type syntax. +- No `MyelinExtended` CellScript target profile. +- No Fiber-specific compiler profile or name-matched structural widening. +- No claim that local CKB-VM evidence is mainnet deployment or commitment. +- No visibility-default break, implicit environment selection, unrestricted + resolver plugins, automatic semantic upgrade policy, or claim that + multi-node resolution makes conflicting module/type identities compatible. +- No formal prover clone as a substitute for executable and independently + checked evidence. + +## Exit Criteria + +The 0.24 core is implemented. The checklist distinguishes repository-owned +evidence from the remaining external handoff and promotion checkpoints: + +- [x] The verified lowering record and source-map schemas are versioned, + canonical, documented, hash-bound, and rejected on unknown fields/versions. +- [x] The standalone checker is independent of the compiler front end/codegen, + bounded, panic-free under its corpus, and emits stable rejection codes. +- [x] The deterministic mutation and malformed-input corpora cover every stable + rejection class, including CFG reachability and machine-stack declarations; + compiler-produced ELF fixtures pass. Full production-example acceptance is + retained by the release gate. +- [x] `verify-artifact` distinguishes binding, structural, lowering-record, + CKB-VM, and chain evidence. +- [x] `cellc test` executes both named backends, and authoritative negative + cases match exact runtime errors. +- [x] Multi-step package scenarios and semantic coverage reports pass and bind + to the exact artifact/checker identities. +- [x] Source-linked checker, trace, and coverage records round-trip to valid ELF + instruction ranges. +- [x] Registry artifact-only verification uses the standalone checker in a + bounded worker and records its version/policy. +- [x] `Cell.lock` v3 is manifest-bound and graph-structured; standard SemVer, + feature/test roots, aliases, exact Git/Registry pins, CKB environments, + frozen/offline behavior, explicit repinning, and bounded external resolver + normalization have positive and fail-closed regressions. +- [x] Registry profile admission uses a versioned catalog and only + `cellscript_source` is dependency-resolving. +- [x] The production website advertises the actual current stable release, + binds the matching released Playground WASM, and rejects stale release or + compiler-asset identities in its build regressions. +- [ ] The Myelin adapter pins and verifies the upstream compiler/checker + contract without vendoring compiler source or accepting raw-witness aliases. + CellScript publishes and tests the versioned handoff contract; Myelin's exact + release lock remains pending the final clean CellScript release commit. +- [x] `dev`, `ci`, and `backend` pass for merge readiness; `release` is required + before any production CKB claim. +- [x] Fiber/RGB++ remain explicitly pending because their complete declared + external matrices are not present; no sample has been promoted or relabelled. + +## Roadmap Discipline + +- Completed work points to tests, reports, or release notes. +- Deferred work names the missing authority, evidence, or design decision. +- A generated certificate is not called independently verified until a smaller + checker recomputes the claimed invariant. +- Simulator, CKB-VM, RPC admission, commitment, and confirmation remain + separate evidence tiers. +- CKB source, Script, transaction, syscall, RPC, and deployment claims are + checked against official CKB sources rather than memory. +- No feature is called implemented until compiler, metadata, CLI, LSP/editor, + tests, examples, docs, and the matching gate agree on the same boundary. diff --git a/roadmap/CELLSCRIPT_ROADMAP.md b/roadmap/CELLSCRIPT_ROADMAP.md index 24ae83f1..7ae43490 100644 --- a/roadmap/CELLSCRIPT_ROADMAP.md +++ b/roadmap/CELLSCRIPT_ROADMAP.md @@ -1,6 +1,6 @@ # CellScript Roadmap -**Updated**: 2026-07-27 +**Updated**: 2026-08-09 This roadmap is the high-level planning map for CellScript. It links the release-specific trackers and the deeper design notes so the project does not @@ -15,7 +15,12 @@ The current project direction is simple: capacity, witness, or lock-group boundaries; 4. keep syntax sugar audit-visible by requiring parser, formatter, type, lowering, metadata, codegen, docs, and automated syntax-combination gates to - agree before release. + agree before release; +5. finish the trusted package-distribution loop before expanding the language + surface: authenticated publish, accepted-status resolution, reproducible + source verification, evidence promotion, and a usable public website. +6. separate compiler generation from artifact admission through a bounded, + independent checker and executable evidence. ## Current State @@ -32,7 +37,8 @@ The current project direction is simple: | 0.21 planned scope | Semantic closure, authenticated compiler evidence, CLI UX reorganisation, dedicated MCP server and CellScript programming skills, derived cyclic graph views, type-level TemplateLayout metadata, and deferred optional template Merkleisation. | [0.21 roadmap](../docs/CELLSCRIPT_0_21_ROADMAP.md), [0.21 CLI UX plan](CELLSCRIPT_0_21_CLI_UX_PLAN.md) | | 0.22 release scope | Released typed transaction views, finite invariant quantifiers, bounded collections, capability entailment, concrete payload enums, validity blocks, borrow regions, stable `E2xxx` diagnostics, and metadata schema 55. | [0.22 release notes](../docs/releases/CELLSCRIPT_0_22_RELEASE_NOTES.md), [0.22 type/set roadmap](CELLSCRIPT_0_22_TYPE_AND_SET_THEORY_ROADMAP.md) | | 0.22 bounded Fiber interoperability | The dedicated `fungible-type-group-v1` compiler/adapter path and local-devnet scenarios are implemented. The pinned complete external lifecycle/negative matrix remains pending, so this is not a production-readiness claim. | [0.22 Fiber plan](CELLSCRIPT_0_22_FIBER_NATIVE_SUPPORT_PLAN.md), [operator guide](../examples/fiber/README.md) | -| 0.23 planned scope | Public registry production deployment on `cellscript.dev`, Python test/fixture scaffolding ported to Rust, deeper RGB++ / Fiber integration, and an Off-Chain Session Runtime profile with initial concurrency support so the Myelin vendored fork can re-converge on upstream. | [0.23 roadmap](CELLSCRIPT_0_23_ROADMAP.md) | +| 0.23 implementation scope | Frozen around Edition 2026/profile/entry identities, the deployed Registry and publisher-session path, native tooling, the website workbench, and bounded Fiber evidence. Mainnet Registry activation, publisher-owned adoption, and complete Fiber/RGB++ matrices remain external checkpoints. The proposed Off-Chain Session Runtime target was retired because current Myelin uses an attested external compiler adapter and keeps extended semantics outside CellScript. | [0.23 roadmap](CELLSCRIPT_0_23_ROADMAP.md), [0.23 release notes](../docs/releases/CELLSCRIPT_0_23_RELEASE_NOTES.md) | +| 0.24 implementation | Core implemented: stable verified lowering records, bounded standalone checker, executable package scenarios, source maps, Registry structural admission, lock-authoritative `Cell.lock` v3 package graphs, a versioned fail-closed Registry profile catalog, and a versioned Myelin handoff contract. Exact external Myelin lock adoption and complete Fiber/RGB++ matrices remain pending. | [0.24 roadmap](CELLSCRIPT_0_24_ROADMAP.md), [0.24 release notes](../docs/releases/CELLSCRIPT_0_24_RELEASE_NOTES.md) | | CKB language fit | CKB-first design is confirmed; remaining gaps are signer binding, continuity policy, capacity policy, and declarative time policy. | [CKB target profiles](../docs/wiki/Tutorial-05-CKB-Target-Profiles.md), [production gates](../docs/wiki/Tutorial-06-Metadata-Verification-and-Production-Gates.md) | | Surface syntax | Low-risk syntax pass and 0.13.2 syntax-governance hardening are implemented; authority-sensitive syntax remains staged. | [Surface elegance RFC](../docs/CELLSCRIPT_SURFACE_ELEGANCE_RFC.md), [Syntax-combination audit](../docs/CELLSCRIPT_SYNTAX_COMBO_AUDIT_METHODOLOGY.md) | | Collections | Stack-backed fixed-width `Vec` helper surface is implemented; cell-backed and generic map ownership remain fail-closed. | [Collections support matrix](../docs/CELLSCRIPT_COLLECTIONS_SUPPORT_MATRIX.md), [0.13 release scope](../docs/releases/CELLSCRIPT_0_13_RELEASE_SCOPE.md) | @@ -300,35 +306,33 @@ Detailed status: - [0.22 bounded Fiber plan](CELLSCRIPT_0_22_FIBER_NATIVE_SUPPORT_PLAN.md) - [Fiber operator guide](../examples/fiber/README.md) -### 0.23: Production Registry, Rust Tooling, Fiber/RGB++, Off-Chain Sessions +### 0.23: Production Registry, Edition/ABI Closure, And Native Tooling -0.23 is the first CellScript release whose headline is operational rather +0.23 is the first CellScript release line whose headline is operational rather than language-theoretic. It turns the 0.22 compiler facts into running -infrastructure and absorbs Myelin's off-chain needs into upstream: - -- **Public registry production deployment**: stand up the implemented - `services/registry-api` Cloudflare Worker on `cellscript.dev` with Neon - Postgres via Hyperdrive and R2 source snapshots; wire the Astro frontend - and `cellc publish` / `cellc auth capability *` to the live JoyID-rooted - write API; keep hash-first verification and the static - `/packages/*` read path as the read authority. -- **Python tooling ported to Rust**: move the gate-driving Python - (`cellscript_strict_backend_audit.py`, `cellscript_syntax_combo_audit.py`, - the production-evidence and tooling-release validators, the NovaSeal / - Evolving-DOB proposal scripts, and the website data scripts) into the - `cellscript-tools` crate or TS scripts, with byte-identical evidence - output and the same exit-code contract. -- **Deeper RGB++ and Fiber integration**: close the pinned Fiber full - lifecycle/negative matrix, promote the Fiber harness to a release-mode - gate once it is reproducible, and advance the RGB++ ecosystem adapter - from identity-adapter to pinned-deployment evidence without entering - `std::*`. -- **Off-Chain Session Runtime profile**: a new opt-in target profile with - initial bounded concurrency support for off-chain session runtimes - (Myelin), plus a fail-closed court-projection rule so `MyelinExtended` - artifacts cannot claim CKB court compatibility without an explicit proof. - Myelin drops its `0.21.1` vendored fork and consumes the published - release. +infrastructure and freezes one coherent source/profile/entry identity: + +- **Public registry production deployment**: the self-hosted Node/Postgres + write service, read-only static object service, live Astro frontend, and + compiler-backed verification worker are deployed on the public domains. + Hash-first resolution stays limited to accepted evidence states. The first + publisher-owned JoyID capability/publication/install is the remaining + interactive adoption checkpoint; Cloudflare/Hyperdrive/R2 stays an optional + alternative topology. +- **Native tooling migration complete**: the gate-driving backend, syntax, + production-evidence, tooling-release, NovaSeal, and Evolving-DOB tools now + live in Rust crates; website data generation uses Node modules. Evidence + schemas and exit-code contracts remain stable, and every gate enforces the + repository-wide native source policy. +- **Bounded ecosystem evidence**: retain the no-profile Fiber adapter and the + content-addressed evidence/path-validation work actually completed. The full + external Fiber lifecycle/negative matrix and RGB++ protocol promotion remain + pending rather than being inferred from representative devnet runs. +- **Explicit Myelin boundary**: retire the proposed Off-Chain Session Runtime + target. Current Myelin already calls an independently versioned compiler + process, uses the CellScript `ckb` profile for production requests, forces + `CkbStrict` for court/session paths, and owns its extended semantics. 0.23 + does not recreate a compiler fork as a target profile. Detailed status: @@ -337,7 +341,39 @@ Detailed status: - [Registry API service](../services/registry-api/README.md) - [0.22 Fiber plan (carried forward)](CELLSCRIPT_0_22_FIBER_NATIVE_SUPPORT_PLAN.md) - [Spore/RGB++ interop plan](CELLSCRIPT_SPORE_RGBPP_INTEROP_PLAN.md) -- [Myelin Session L2 plan](../../Myelin/MYELIN_SESSION_L2_PLAN.md) +- [Myelin Session L2 plan](https://github.com/Myelin-Labs/Myelin/blob/main/MYELIN_SESSION_L2_PLAN.md) + +### 0.24: Independently Verified Artifacts And Executable Evidence + +0.24 moves the trust boundary below compiler-authored metadata without claiming +that arbitrary RISC-V can recover typed source semantics: + +- define a canonical verified lowering record and source-to-artifact map; +- add a small, metered checker independent of the compiler front end and + codegen; +- validate lowering, CFG, frame, ABI, syscall, ProofPlan-link, source-map, and + structural ELF contracts with stable diagnostics and mutation evidence; +- make `cellc test` execute simulator and authoritative CKB-VM backends, + including multi-step Cell scenarios, exact runtime failures, and semantic + coverage; +- integrate the checker into `verify-artifact`, Registry artifact admission, + and the unified gates; +- publish and test the exact CellScript-side Myelin handoff contract without + adding `MyelinExtended` to CellScript; external lock adoption follows the + final clean release identity; and +- promote Fiber/RGB++ only when their separate external matrices close. + +`Cell.lock` v3, semantic upgrade policies, package visibility changes, and +typed multi-action composition remain design handoff work until the trust and +test boundaries are stable. + +Detailed status: + +- [0.24 roadmap](CELLSCRIPT_0_24_ROADMAP.md) +- [Verified artifact boundary](../docs/CELLSCRIPT_VERIFIED_ARTIFACT_BOUNDARY.md) +- [Executable test scenarios](../docs/CELLSCRIPT_EXECUTABLE_TEST_SCENARIOS.md) +- [0.23 release notes](../docs/releases/CELLSCRIPT_0_23_RELEASE_NOTES.md) +- [Metadata and production gates](../docs/wiki/Tutorial-06-Metadata-Verification-and-Production-Gates.md) ### Next Authorization Hardening Track diff --git a/roadmap/CELLSCRIPT_ROADMAP_OVERVIEW.md b/roadmap/CELLSCRIPT_ROADMAP_OVERVIEW.md index 50196eac..20503ed0 100644 --- a/roadmap/CELLSCRIPT_ROADMAP_OVERVIEW.md +++ b/roadmap/CELLSCRIPT_ROADMAP_OVERVIEW.md @@ -1,7 +1,7 @@ -# CellScript Roadmap: v0.12 -> v0.23 +# CellScript Roadmap: v0.12 -> v0.24 > From Production Foundation to Protocol Builders -**Updated**: 2026-07-27 +**Updated**: 2026-08-09 **Status**: Living Document **Audience**: CKB Smart Contract Developers **Canonical folder**: `roadmap/` @@ -68,12 +68,16 @@ Each release answers a specific question: cell-collection design, type validity blocks, explicit borrow regions, capability algebra diagnostics, concrete payload ADTs, and ProtocolGraph role UX while keeping the action core intact. -- **v0.23** — *Can the compiler ship as running infrastructure and absorb - off-chain runtimes?* Deploy the public package registry on `cellscript.dev`, - port the Python test/fixture scaffolding to Rust, close the next slice of - RGB++ / Fiber integration, and add an Off-Chain Session Runtime profile - with initial concurrency support so the Myelin vendored fork re-converges - on upstream. +- **v0.23** — *Can the compiler ship as running infrastructure with one honest + identity boundary?* Deploy the public package registry on `cellscript.dev`, + enforce Edition 2026 and canonical witness placement across consumers, close + the native test/fixture tooling migration, and retain only bounded ecosystem + evidence actually obtained on the line. +- **v0.24** — *Can consumers admit compiler artifacts without trusting the + whole compiler, and can package tests produce executable evidence?* Add a + stable verified lowering record, bounded independent artifact checker, + source-to-artifact maps, simulator/CKB-VM package scenarios, the Myelin + adapter handoff, and conditional Fiber/RGB++ evidence promotion. --- @@ -93,7 +97,8 @@ Each release answers a specific question: | v0.21 planned scope | Semantic closure, authenticated compiler evidence, CLI UX reorganisation, dedicated MCP server and CellScript programming skills, derived cyclic ProtocolGraph views, type-level TemplateLayout metadata, and deferred optional template Merkleisation. | [v0.21 roadmap](../docs/CELLSCRIPT_0_21_ROADMAP.md), [v0.21 CLI UX plan](CELLSCRIPT_0_21_CLI_UX_PLAN.md) | | v0.22 draft scope | Draft type-theory and set-theory guided language hardening proposal. This scope requires pre-talk soundness fixes and Nervos Talk Discussion before adoption: callable effects for ordinary functions, terminal flow metadata, typed transaction-view handles, finite source-view quantifiers, bounded cell-collection design, type validity blocks, explicit borrow regions, capability algebra explanations, concrete payload ADTs, and ProtocolGraph role UX. | [v0.22 type and set theory roadmap draft](CELLSCRIPT_0_22_TYPE_AND_SET_THEORY_ROADMAP.md) | | v0.22 Fiber native-support proposal | Proposed no-profile integration for structurally compatible fungible CellScript Type Scripts. Compatibility must be derived from compiler evidence, requires no Fiber fork, and is not complete until the pinned CKB/Fiber lifecycle matrix passes. | [v0.22 no-profile Fiber native-support plan](CELLSCRIPT_0_22_FIBER_NATIVE_SUPPORT_PLAN.md) | -| v0.23 planned scope | Public registry production deployment on `cellscript.dev`, Python test/fixture scaffolding ported to Rust, deeper RGB++ / Fiber integration, and an Off-Chain Session Runtime profile with initial concurrency support so the Myelin vendored fork can re-converge on upstream. | [v0.23 roadmap](CELLSCRIPT_0_23_ROADMAP.md) | +| v0.23 implementation scope | Frozen around Edition 2026/profile/entry identities, the deployed Registry and publisher-session path, native tooling, the website workbench, and bounded Fiber evidence. External mainnet/adoption and complete Fiber/RGB++ evidence remain checkpoints. The proposed Off-Chain Session Runtime target is retired because current Myelin uses an attested external compiler adapter and owns its extended semantics. | [v0.23 roadmap](CELLSCRIPT_0_23_ROADMAP.md), [v0.23 release notes](../docs/releases/CELLSCRIPT_0_23_RELEASE_NOTES.md) | +| v0.24 implementation | Core implemented: independently checked lowering/artifact contracts, executable package scenarios, source maps, Registry checker admission, lock-authoritative `Cell.lock` v3 package graphs, and a fail-closed Registry profile catalog. The versioned Myelin handoff awaits its final external lock pin; Fiber/RGB++ promotion remains evidence-pending. | [v0.24 roadmap](CELLSCRIPT_0_24_ROADMAP.md), [v0.24 release notes](../docs/releases/CELLSCRIPT_0_24_RELEASE_NOTES.md) | | Spore/RGB++ adapters | Proposed package/adapter slices for a deployable signature verifier, executable bounded CellDep scans, bounded hash/Merkle primitives, and pinned Spore/RGB++ cookbook integrations. None are current production-support claims. | [Spore/RGB++ interoperability plan](CELLSCRIPT_SPORE_RGBPP_INTEROP_PLAN.md) | | CKB language fit | CKB-first design is confirmed; remaining hardening areas are signer binding, continuity policy, capacity policy, and declarative time policy. | [CKB target profiles](../docs/wiki/Tutorial-05-CKB-Target-Profiles.md), [production gates](../docs/wiki/Tutorial-06-Metadata-Verification-and-Production-Gates.md) | | Surface syntax | Low-risk syntax pass is implemented; authority-sensitive syntax remains staged. | [Surface elegance RFC](../docs/CELLSCRIPT_SURFACE_ELEGANCE_RFC.md) | @@ -118,7 +123,8 @@ Each release answers a specific question: | v0.20 | Generated Builder and Live Registry Proof | "Turn verified artifacts into valid transactions through registry-bound builders." | In progress: generated TypeScript builders, live registry verification, VS Code commands, and generated-builder tooling-gate checks are active. | | v0.21 | Semantic Closure and Authenticated Evidence | "Make declared protocol law executable and tamper-evident without changing the action core." | Implementation checkpoint: RC cut 2026-07-01 as 0.21.0-rc.1; aggregate lowering, flow-edge validation, compile receipts, nested CLI, MCP server + 6 skills, ProtocolGraph view, and TemplateLayout metadata are active; v0.21.0 tag pending. | | v0.22 | Theory-Guided Protocol Law | "Make protocol law readable, finite, effect-aware, and evidence-tiered." | Draft: requires pre-talk soundness fixes and Nervos Talk Discussion before adoption; proposed scope covers callable effects, terminal flow metadata, typed transaction-view handles, bounded quantifiers, bounded cell collections, validity blocks, borrow regions, capability algebra, payload ADTs, and ProtocolGraph role UX. | -| v0.23 | Production Registry, Rust Tooling, Fiber/RGB++, Off-Chain Sessions | "Ship the compiler as running infrastructure and absorb off-chain runtimes." | Draft: deploy the public package registry on `cellscript.dev`, port the Python test/fixture scaffolding to Rust, close the next RGB++ / Fiber integration slice, and add an Off-Chain Session Runtime profile so the Myelin vendored fork re-converges on upstream. | +| v0.23 | Production Registry, Edition/ABI Closure, And Native Tooling | "Ship the compiler as running infrastructure with one honest identity boundary." | Implementation scope frozen; stable release and production CKB claims still require their documented gates and external evidence. | +| v0.24 | Independently Verified Artifacts And Executable Evidence | "Make generated claims independently checkable and package tests executable." | Core implemented on `nightly-0.24`; external Myelin lock adoption and complete Fiber/RGB++ evidence remain pending, and production claims still require the release gate. | The roadmap is intentionally cumulative. Later releases should not re-open an earlier feature boundary unless the prior boundary was proven unsafe or diff --git a/rust-toolchain.toml b/rust-toolchain.toml index df9beda2..2a6a60aa 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] channel = "1.97.1" profile = "minimal" -components = ["clippy", "rustfmt"] +components = ["clippy", "llvm-tools-preview", "rustfmt"] targets = ["riscv64imac-unknown-none-elf", "wasm32-unknown-unknown"] diff --git a/scripts/__pycache__/cellscript_syntax_combo_audit.cpython-314.pyc b/scripts/__pycache__/cellscript_syntax_combo_audit.cpython-314.pyc deleted file mode 100644 index b22ddc49..00000000 Binary files a/scripts/__pycache__/cellscript_syntax_combo_audit.cpython-314.pyc and /dev/null differ diff --git a/scripts/cellscript_0_14_scope_audit.sh b/scripts/cellscript_0_14_scope_audit.sh index 134a2017..cb69c28d 100755 --- a/scripts/cellscript_0_14_scope_audit.sh +++ b/scripts/cellscript_0_14_scope_audit.sh @@ -32,7 +32,6 @@ require_doc_boundary() { } require_cmd cargo -require_cmd python3 require_cmd rg if [[ -z "${CELLC_BIN:-}" ]]; then @@ -75,135 +74,7 @@ for example in "${examples[@]}"; do metadata_files+=("$asm_out.meta.json") done -python3 - "$OUT_DIR" "${metadata_files[@]}" <<'PY' -import json -import sys -from pathlib import Path - -out_dir = Path(sys.argv[1]) -paths = [Path(path) for path in sys.argv[2:]] - -def fail(message): - raise SystemExit(f"0.14 scope metadata oracle failed: {message}") - -def require(condition, message): - if not condition: - fail(message) - -def collect_accesses(metadata): - accesses = list(metadata.get("runtime", {}).get("ckb_runtime_accesses", [])) - for entry in metadata.get("actions", []): - accesses.extend(entry.get("ckb_runtime_accesses", [])) - for entry in metadata.get("locks", []): - accesses.extend(entry.get("ckb_runtime_accesses", [])) - return accesses - -def collect_create_set(metadata): - create_set = [] - for entry in metadata.get("actions", []): - create_set.extend(entry.get("create_set", [])) - for entry in metadata.get("locks", []): - create_set.extend(entry.get("create_set", [])) - return create_set - -require(len(paths) == 7, f"expected 7 v0.14 language metadata files, got {len(paths)}") - -features = set() -operations = set() -script_reference_purposes = set() -capacity_floor_types = set() -has_type_id_plan = False -has_output_data_binding = False -metadata_names = [] - -for path in paths: - require(path.exists(), f"missing metadata file {path}") - metadata = json.loads(path.read_text()) - metadata_names.append(path.name) - target_profile = metadata.get("target_profile", {}) - require(target_profile.get("name") == "ckb", f"{path} did not compile under ckb profile") - require(target_profile.get("source_encoding") == "ckb-source-group-high-bit", f"{path} missing CKB Source encoding") - require(target_profile.get("witness_abi") == "ckb-molecule-witness-args+cellscript-entry-witness-v1", f"{path} missing WitnessArgs ABI") - require(target_profile.get("spawn_ipc_abi") == "ckb-vm-v2-spawn-ipc-syscalls-2601-2608", f"{path} missing Spawn/IPC ABI") - require(target_profile.get("output_data_abi") == "ckb-outputs-and-outputs-data-index-aligned", f"{path} missing outputs_data ABI") - require(target_profile.get("type_id_abi") == "ckb-type-id-v1", f"{path} missing TYPE_ID ABI") - require(metadata.get("artifact_hash"), f"{path} missing artifact hash") - require(metadata.get("artifact_size_bytes", 0) > 0, f"{path} missing artifact size") - - ckb_constraints = metadata.get("constraints", {}).get("ckb") - require(isinstance(ckb_constraints, dict), f"{path} missing constraints.ckb") - abi = ckb_constraints.get("profile_abi_contract", {}) - require(abi.get("witness_abi") == target_profile.get("witness_abi"), f"{path} profile ABI witness drift") - require(abi.get("output_data_abi") == target_profile.get("output_data_abi"), f"{path} profile ABI output_data drift") - - features.update(metadata.get("runtime", {}).get("ckb_runtime_features", [])) - for access in collect_accesses(metadata): - operations.add(access.get("operation")) - for reference in ckb_constraints.get("script_references", []): - script_reference_purposes.add(reference.get("purpose")) - if reference.get("purpose") == "spawn-target": - require(reference.get("dep_source") == "CellDep-or-DepGroup", f"{path} spawn target dep_source overclaimed") - require(reference.get("status") == "runtime-required-builder-resolved", f"{path} spawn target status drift") - require(reference.get("code_hash") is None and reference.get("hash_type") is None and reference.get("args") is None, f"{path} spawn target must remain builder-resolved") - for floor in ckb_constraints.get("declared_capacity_floors", []): - capacity_floor_types.add(floor.get("type_name")) - require(floor.get("source") == "dsl-with_capacity_floor", f"{path} capacity floor source drift") - require(floor.get("shannons", 0) > 0, f"{path} non-positive capacity floor") - for create in collect_create_set(metadata): - has_type_id_plan = has_type_id_plan or create.get("ckb_type_id") is not None - has_output_data_binding = has_output_data_binding or create.get("ckb_output_data") is not None - -required_features = { - "ckb-spawn-ipc", - "ckb-source-view", - "ckb-witness-args", - "ckb-lock-args", - "ckb-sighash-all", - "ckb-declarative-since", - "ckb-declarative-capacity", - "ckb-blake2b", -} -missing_features = sorted(required_features - features) -require(not missing_features, f"missing runtime features: {missing_features}") - -required_operations = { - "spawn", - "wait", - "pipe", - "pipe-write", - "pipe-read", - "close-fd", - "source-group-input", - "witness-lock", - "lock-args", - "sighash-all", - "require-maturity", - "require-time", - "require-epoch-after", - "require-epoch-relative", - "occupied-capacity", - "hash-blake2b", -} -missing_operations = sorted(required_operations - operations) -require(not missing_operations, f"missing runtime operations: {missing_operations}") - -require("spawn-target" in script_reference_purposes, "missing spawn target script-reference obligation") -require("type-id-create-output" in script_reference_purposes, "missing TYPE_ID create script-reference obligation") -require("TimedToken" in capacity_floor_types, "missing TimedToken capacity floor") -require(has_type_id_plan, "missing TYPE_ID output plan in language examples") -require(has_output_data_binding, "missing outputs_data binding in language examples") - -report = { - "status": "passed", - "metadata_files": metadata_names, - "features": sorted(features), - "operations": sorted(operation for operation in operations if operation), - "script_reference_purposes": sorted(purpose for purpose in script_reference_purposes if purpose), - "capacity_floor_types": sorted(kind for kind in capacity_floor_types if kind), -} -report_path = out_dir / "cellscript-0-14-scope-audit-report.json" -report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n") -print(f"valid CellScript 0.14 scope audit: {report_path}") -PY +run cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root "$ROOT_DIR" scope014 "$OUT_DIR" "${metadata_files[@]}" printf '\nCellScript 0.14 scope audit passed: %s\n' "$OUT_DIR" diff --git a/scripts/cellscript_cellfabric_bridge_smoke.sh b/scripts/cellscript_cellfabric_bridge_smoke.sh index fb74d7f7..59c8d2f3 100755 --- a/scripts/cellscript_cellfabric_bridge_smoke.sh +++ b/scripts/cellscript_cellfabric_bridge_smoke.sh @@ -22,15 +22,6 @@ Builds a CellScript CellFabric intent envelope, imports it with the sibling CellFabric example, submits the signed dummy intent through the strict gateway, builds a validated bundle, soft-confirms it as non-final, and checks the bridge contract summary. - -Environment: - CELLFABRIC_DIR Defaults to ../CellFabric. - CELLSCRIPT_CELLFABRIC_INPUT Defaults to examples/token. - CELLSCRIPT_CELLFABRIC_ACTION Defaults to mint. - CELLSCRIPT_CELLFABRIC_TARGET_PROFILE Defaults to ckb. - CELLSCRIPT_CELLFABRIC_AUTHOR_LOCK_SCRIPT_HASH - Defaults to 0x11...11. - CELLSCRIPT_CELLFABRIC_NONCE Defaults to 1. USAGE } @@ -52,7 +43,6 @@ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then fi require_cmd cargo -require_cmd python3 if [[ ! -f "$CELLFABRIC_DIR/Cargo.toml" ]]; then echo "CELLFABRIC_DIR does not point to a CellFabric checkout: $CELLFABRIC_DIR" >&2 @@ -60,91 +50,14 @@ if [[ ! -f "$CELLFABRIC_DIR/Cargo.toml" ]]; then fi mkdir -p "$RUN_DIR" - cd "$REPO_ROOT" run cargo run --locked -p cellscript --bin cellc -- \ - action build "$INPUT" \ - --action "$ACTION" \ - --target-profile "$TARGET_PROFILE" \ - --fabric-intent \ - --output "$ENVELOPE_JSON" - + action build "$INPUT" --action "$ACTION" --target-profile "$TARGET_PROFILE" \ + --fabric-intent --output "$ENVELOPE_JSON" run cargo run --locked --manifest-path "$CELLFABRIC_DIR/Cargo.toml" --example cellscript_flow -- \ --summary-only "$ENVELOPE_JSON" "$AUTHOR_LOCK_SCRIPT_HASH" "$NONCE" >"$SUMMARY_JSON" - -python3 - "$ENVELOPE_JSON" "$SUMMARY_JSON" <<'PY' -import json -import sys - -envelope_path, summary_path = sys.argv[1:] -with open(envelope_path, "r", encoding="utf-8") as handle: - envelope = json.load(handle) -with open(summary_path, "r", encoding="utf-8") as handle: - summary = json.load(handle) - -expected_schema = "cellscript-cellfabric-intent-envelope-v0.20" -expected_status = "requires-runtime-binding" - -checks = [ - (envelope.get("schema") == expected_schema, "envelope schema mismatch"), - (envelope.get("status") == expected_status, "envelope status mismatch"), - (summary.get("schema") == expected_schema, "summary schema mismatch"), - (summary.get("import_status") == expected_status, "import status mismatch"), - ( - summary.get("status") == "submitted-and-soft-confirmed-non-final", - "flow status mismatch", - ), - ( - summary.get("action_plan_hash_hex") == envelope["source"]["action_plan_hash"], - "action_plan_hash mismatch", - ), - (summary.get("chain_id") == envelope["source"]["target_profile"], "chain_id mismatch"), - (summary.get("app_namespace") == envelope["source"]["module"], "app_namespace mismatch"), - (summary.get("action") == envelope["source"]["action"], "action mismatch"), - (summary.get("payload_format") == "cellscript-action-plan-json-v1", "payload format mismatch"), - (summary.get("requires_signature") is True, "summary must require signature"), - (summary.get("submitted") is True, "summary must claim gateway submission"), - (summary.get("soft_confirmed") is True, "summary must claim soft confirmation"), - (summary.get("l1_final") is False, "summary must not claim L1 finality"), - (summary.get("gateway_status") == "Indexed", "gateway status mismatch"), - ( - isinstance(summary.get("ledger_status"), dict) - and isinstance(summary["ledger_status"].get("status"), dict) - and "SoftConfirmed" in summary["ledger_status"]["status"] - and summary["ledger_status"]["status"]["SoftConfirmed"].get("non_final") is True, - "ledger status mismatch", - ), - (summary.get("bundle_intent_count") == 1, "bundle must contain one intent"), - (summary.get("excluded_conflict_count") == 0, "unexpected excluded conflicts"), - (summary.get("receipt_non_final") is True, "receipt must remain non-final"), - ( - summary.get("soft_confirmation_confidence") == "unsigned-non-final-receipt", - "unexpected soft confirmation confidence label", - ), - ( - summary.get("settlement_requires_external_builder") is True, - "CellScript settlement must require external runtime builder", - ), - ( - isinstance(summary.get("intent_id"), str) - and summary["intent_id"].startswith("0x") - and len(summary["intent_id"]) == 66, - "intent_id must be 0x-prefixed 32-byte hash", - ), - ( - isinstance(summary.get("bundle_id"), str) - and summary["bundle_id"].startswith("0x") - and len(summary["bundle_id"]) == 66, - "bundle_id must be 0x-prefixed 32-byte hash", - ), -] - -for passed, message in checks: - if not passed: - raise SystemExit(message) - -print("valid CellScript -> CellFabric bridge flow summary") -PY +run cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root "$REPO_ROOT" cellfabric-bridge "$ENVELOPE_JSON" "$SUMMARY_JSON" printf '\nCellScript CellFabric bridge smoke passed.\n' printf ' Envelope: %s\n' "$ENVELOPE_JSON" diff --git a/scripts/cellscript_ckb_adapter_acceptance.sh b/scripts/cellscript_ckb_adapter_acceptance.sh index 7dbed538..dfb3bd85 100755 --- a/scripts/cellscript_ckb_adapter_acceptance.sh +++ b/scripts/cellscript_ckb_adapter_acceptance.sh @@ -19,11 +19,8 @@ CKB_REPO="${CKB_REPO:-$(default_ckb_repo)}" CKB_BIN="${CKB_BIN:-}" RUN_ID="$(date +%Y%m%d-%H%M%S)-$$" RUN_DIR="$REPO_ROOT/target/ckb-cellscript-adapter-acceptance/$RUN_ID" -CKB_DIR="$RUN_DIR/ckb-node" -CKB_LOG="$RUN_DIR/ckb.log" REPORT_JSON="$RUN_DIR/cellscript-ckb-adapter-acceptance-report.json" ACTION_PLAN_JSON="$RUN_DIR/action-plan.json" -CKB_PID="" usage() { cat <<'USAGE' @@ -66,64 +63,10 @@ while [[ $# -gt 0 ]]; do esac done -require_cmd() { - if ! command -v "$1" >/dev/null 2>&1; then - echo "missing required command: $1" >&2 - exit 127 - fi -} - -pick_port() { - python3 - <<'PY' -import socket - -with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: - sock.bind(("127.0.0.1", 0)) - print(sock.getsockname()[1]) -PY -} - -resolve_ckb_bin() { - if [[ -n "$CKB_BIN" ]]; then - if [[ ! -x "$CKB_BIN" ]]; then - echo "CKB_BIN is not executable: $CKB_BIN" >&2 - exit 1 - fi - printf '%s\n' "$CKB_BIN" - return - fi - - local candidate - for candidate in "$CKB_REPO/target/debug/ckb" "$CKB_REPO/target/release/ckb"; do - if [[ -x "$candidate" ]]; then - printf '%s\n' "$candidate" - return - fi - done - - echo "No existing CKB executable found; building parent CKB checkout with cargo build --bin ckb" >&2 - (cd "$CKB_REPO" && cargo build --bin ckb) - candidate="$CKB_REPO/target/debug/ckb" - if [[ ! -x "$candidate" ]]; then - echo "CKB build finished but executable was not found at $candidate" >&2 - exit 1 - fi - printf '%s\n' "$candidate" -} - -stop_ckb() { - if [[ -n "$CKB_PID" ]] && kill -0 "$CKB_PID" >/dev/null 2>&1; then - kill "$CKB_PID" >/dev/null 2>&1 || true - wait "$CKB_PID" >/dev/null 2>&1 || true - fi - CKB_PID="" -} -trap stop_ckb EXIT - -require_cmd cargo -require_cmd curl -require_cmd python3 - +if ! command -v cargo >/dev/null 2>&1; then + echo "missing required command: cargo" >&2 + exit 127 +fi if [[ ! -d "$CKB_REPO" ]]; then echo "CKB repo does not exist: $CKB_REPO" >&2 exit 1 @@ -134,332 +77,21 @@ if [[ ! -f "$CKB_REPO/test/template/ckb.toml" ]]; then fi mkdir -p "$RUN_DIR" - -CKB_BIN="$(resolve_ckb_bin)" -CKB_REPO="$(cd "$CKB_REPO" && pwd)" -CKB_BIN="$(cd "$(dirname "$CKB_BIN")" && pwd)/$(basename "$CKB_BIN")" -RPC_PORT="$(pick_port)" -P2P_PORT="$(pick_port)" -RPC_URL="http://127.0.0.1:$RPC_PORT" - -mkdir -p "$CKB_DIR" -cp -R "$CKB_REPO/test/template/." "$CKB_DIR/" - -python3 - "$CKB_DIR/ckb.toml" "$RPC_PORT" "$P2P_PORT" <<'PY' -import pathlib -import re -import sys - -path = pathlib.Path(sys.argv[1]) -rpc_port = sys.argv[2] -p2p_port = sys.argv[3] -text = path.read_text(encoding="utf-8") -text = re.sub( - r'listen_address = "127\.0\.0\.1:\d+"', - f'listen_address = "127.0.0.1:{rpc_port}"', - text, - count=1, -) -text = re.sub( - r'listen_addresses = \["/ip4/0\.0\.0\.0/tcp/\d+"\]', - f'listen_addresses = ["/ip4/127.0.0.1/tcp/{p2p_port}"]', - text, - count=1, -) -path.write_text(text, encoding="utf-8") -PY - -cargo run --locked -p cellscript --bin cellc -- action build examples/token.cell --action mint_with_authority --json >"$ACTION_PLAN_JSON" +cd "$REPO_ROOT" +cargo run --locked -p cellscript --bin cellc -- \ + action build examples/token.cell --action mint_with_authority --json >"$ACTION_PLAN_JSON" cargo test --locked -p cellscript-ckb-adapter materializes_resolved_action_with_ckb_sdk_transaction_builder -- --test-threads=1 cargo test --locked -p cellscript-ckb-adapter builds_deploy_transaction_with_type_id_code_cell -- --test-threads=1 -"$CKB_BIN" -C "$CKB_DIR" run --ba-advanced >"$CKB_LOG" 2>&1 & -CKB_PID="$!" - -for _ in $(seq 1 120); do - if curl -sS \ - -H 'content-type: application/json' \ - -d '{"id":1,"jsonrpc":"2.0","method":"get_tip_header","params":[]}' \ - "$RPC_URL" >"$RUN_DIR/rpc-ready.json" 2>/dev/null; then - break - fi - sleep 0.25 -done - -if ! grep -q '"result"' "$RUN_DIR/rpc-ready.json" 2>/dev/null; then - echo "CKB RPC did not become ready at $RPC_URL. Log: $CKB_LOG" >&2 - exit 1 +command=(cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- + --root "$REPO_ROOT" ckb-adapter-live + --ckb-repo "$CKB_REPO" + --run-dir "$RUN_DIR" + --action-plan "$ACTION_PLAN_JSON" + --report "$REPORT_JSON") +if [[ -n "$CKB_BIN" ]]; then + command+=(--ckb-bin "$CKB_BIN") fi - -python3 - "$RPC_URL" "$ACTION_PLAN_JSON" "$REPORT_JSON" "$CKB_REPO" "$CKB_BIN" "$CKB_LOG" <<'PY' -import hashlib -import json -import pathlib -import sys -import time -import urllib.error -import urllib.request - -rpc_url, action_plan_path, report_path, ckb_repo, ckb_bin, ckb_log = sys.argv[1:] -action_plan_path = pathlib.Path(action_plan_path) -report_path = pathlib.Path(report_path) - -ALWAYS_SUCCESS_CODE_HASH = "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5" -ALWAYS_SUCCESS_INDEX = 5 -FEE = 1_000 - -def rpc(method, params=None): - body = json.dumps({"id": 42, "jsonrpc": "2.0", "method": method, "params": params or []}).encode("utf-8") - request = urllib.request.Request(rpc_url, data=body, headers={"Content-Type": "application/json"}) - try: - with urllib.request.urlopen(request, timeout=20) as response: - payload = json.loads(response.read().decode("utf-8")) - except urllib.error.URLError as error: - raise RuntimeError(f"RPC {method} failed to connect: {error}") from error - if payload.get("error"): - raise RuntimeError(f"RPC {method} returned error: {payload['error']}") - return payload.get("result") - -def hex_u64(value): - return hex(value if isinstance(value, int) else int(value, 16)) - -def out_point(tx_hash, index): - return {"tx_hash": tx_hash, "index": hex_u64(index)} - -def wait_live_cell(tx_hash, index, attempts=20, delay_seconds=0.05): - last_result = None - for _ in range(attempts): - last_result = rpc("get_live_cell", [out_point(tx_hash, index), True]) - if last_result and last_result.get("status") == "live": - return last_result - time.sleep(delay_seconds) - return last_result - -def always_success_lock(args="0x"): - return {"code_hash": ALWAYS_SUCCESS_CODE_HASH, "hash_type": "data", "args": args} - -def get_block_by_number(number): - block = rpc("get_block_by_number", [hex_u64(number)]) - if block is None: - raise RuntimeError(f"block number not found: {number}") - return block - -def find_spendable_cellbase(max_blocks=64): - for _ in range(max_blocks): - block_hash = rpc("generate_block") - block = rpc("get_block", [block_hash]) - cellbase = block["transactions"][0] - for index, output in enumerate(cellbase.get("outputs", [])): - capacity = int(output["capacity"], 16) - if capacity <= FEE: - continue - live = wait_live_cell(cellbase["hash"], index) - if live and live.get("status") == "live": - return { - "block_hash": block_hash, - "tx_hash": cellbase["hash"], - "index": index, - "capacity": capacity, - } - raise RuntimeError(f"no spendable cellbase output found after {max_blocks} generated blocks") - -def transaction(input_cell, output, outputs_data, cell_deps, witnesses=None, header_deps=None): - return { - "version": "0x0", - "cell_deps": cell_deps, - "header_deps": header_deps or [], - "inputs": [{ - "previous_output": out_point(input_cell["tx_hash"], input_cell["index"]), - "since": "0x0", - }], - "outputs": [output], - "outputs_data": outputs_data, - "witnesses": witnesses or [], - } - -def json_serialized_size_bytes(value): - return len(json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")) - -def ckb_blake2b(data): - return "0x" + hashlib.blake2b(data, digest_size=32, person=b"ckb-default-hash").hexdigest() - -action_plan = json.loads(action_plan_path.read_text(encoding="utf-8")) -genesis = get_block_by_number(0) -genesis_cellbase_hash = genesis["transactions"][0]["hash"] -always_success_dep = { - "out_point": out_point(genesis_cellbase_hash, ALWAYS_SUCCESS_INDEX), - "dep_type": "code", -} - -# ---- Phase 1: Action transaction smoke test ---- -funding = find_spendable_cellbase() -output = { - "capacity": hex_u64(funding["capacity"] - FEE), - "lock": always_success_lock(), - "type": None, -} -tx = transaction(funding, output, ["0x"], [always_success_dep]) -estimate = rpc("estimate_cycles", [tx]) -tx_pool_accept = rpc("test_tx_pool_accept", [tx, "passthrough"]) - -# ---- Phase 2: Deploy probe with TYPE_ID code cell ---- -# Build a deploy transaction that places a pseudo-artifact as a code cell -# with a TYPE_ID type script, exactly as build_deploy_transaction() does. -# -# TYPE_ID args = blake2b(first_input_tx_hash || first_input_index_u64_le || output_index_u64_le) -# where first_input_index is the CellInput.previous_output.index. -# -# The code cell uses hash_type="type" so code_hash = type_script_hash. - -deploy_funding = find_spendable_cellbase() - -# Pseudo-artifact: 32 bytes of test data. -artifact_data = bytes(range(32)) -artifact_data_hex = "0x" + artifact_data.hex() -artifact_data_hash = ckb_blake2b(artifact_data) - -# TYPE_ID args = blake2b(first_input_tx_hash || output_index_le) -first_input_tx_hash_bytes = bytes.fromhex(deploy_funding["tx_hash"][2:]) -type_id_args_input = first_input_tx_hash_bytes + (0).to_bytes(8, "little") + (0).to_bytes(8, "little") -type_id_args = "0x" + hashlib.blake2b(type_id_args_input, digest_size=32, person=b"ckb-default-hash").hexdigest() - -# TYPE_ID type script: For devnet testing we use always_success with hash_type="data" -# since the always_success binary is deployed in genesis with data hash. -# Production TYPE_ID uses hash_type="type" with the TYPE_ID script code_hash. -type_script = { - "code_hash": ALWAYS_SUCCESS_CODE_HASH, - "hash_type": "data", - "args": type_id_args, -} - -# Code output: lock = always_success, type = TYPE_ID type script. -# Use a generous capacity (200 CKB = 200_000_000_000 shannons) for the code cell -# to ensure it exceeds the occupied floor regardless of exact molecule overhead. -# The adapter crate's build_deploy_transaction() computes exact occupied capacity; -# here we just need the transaction to pass devnet validation. -code_output_capacity = 200_000_000_000 -change_capacity = deploy_funding["capacity"] - code_output_capacity - FEE -if change_capacity < 0: - raise RuntimeError(f"deploy funding {deploy_funding['capacity']} insufficient for code output {code_output_capacity} + fee {FEE}") - -code_output = { - "capacity": hex_u64(code_output_capacity), - "lock": always_success_lock(), - "type": type_script, -} -change_output = { - "capacity": hex_u64(change_capacity), - "lock": always_success_lock(), - "type": None, -} - -deploy_tx = { - "version": "0x0", - "cell_deps": [always_success_dep], - "header_deps": [], - "inputs": [{ - "previous_output": out_point(deploy_funding["tx_hash"], deploy_funding["index"]), - "since": "0x0", - }], - "outputs": [code_output, change_output], - "outputs_data": [artifact_data_hex, "0x"], - "witnesses": ["0x0000000000000000"], # placeholder witness for always_success -} - -deploy_estimate = rpc("estimate_cycles", [deploy_tx]) -deploy_tx_pool_accept = rpc("test_tx_pool_accept", [deploy_tx, "passthrough"]) - -# ---- Phase 3: Submit deploy transaction and verify commitment ---- -deploy_tx_hash = rpc("send_transaction", [deploy_tx, "passthrough"]) -# Generate a block to commit the transaction. -rpc("generate_block") -# Wait for the transaction to be committed: keep generating blocks until the code cell is live. -commit_evidence_status = "unknown" -commit_block_hash = "0x" -for _ in range(10): - time.sleep(0.5) - rpc("generate_block") - commit_live_check = wait_live_cell(deploy_tx_hash, 0, attempts=3) - if commit_live_check and commit_live_check.get("status") == "live": - commit_evidence_status = "committed" - break -if commit_evidence_status != "committed": - raise RuntimeError(f"deploy transaction {deploy_tx_hash} not committed after 10 generated blocks") -commit_live = commit_live_check -commit_live_output = commit_live["cell"]["output"] if commit_live.get("cell") else {} - -report = { - "schema": "cellscript-ckb-adapter-local-node-acceptance-v0.19", - "status": "passed", - "rpc_url": rpc_url, - "ckb_repo": ckb_repo, - "ckb_bin": ckb_bin, - "ckb_log": ckb_log, - "action_plan": { - "policy": action_plan.get("policy"), - "action": action_plan.get("action"), - "adapter_contract_schema": (action_plan.get("adapter_contract") or {}).get("schema"), - "can_submit": (action_plan.get("transaction_draft") or {}).get("can_submit"), - "requires_packed_materialization": (action_plan.get("transaction_draft") or {}).get("requires_packed_materialization"), - }, - "adapter_materialization": { - "crate": "crates/cellscript-ckb-adapter", - "test": "materializes_resolved_action_with_ckb_sdk_transaction_builder", - "status": "passed", - }, - "adapter_deploy_probe": { - "crate": "crates/cellscript-ckb-adapter", - "test": "builds_deploy_transaction_with_type_id_code_cell", - "status": "passed", - }, - "local_node": { - "estimate_cycles": estimate, - "test_tx_pool_accept": tx_pool_accept, - "tx_size_json_bytes": json_serialized_size_bytes(tx), - "output_capacity_shannons": funding["capacity"] - FEE, - "fee_shannons": FEE, - "cell_deps": tx["cell_deps"], - "header_deps": tx["header_deps"], - "witnesses": tx["witnesses"], - "outputs_data_count": len(tx["outputs_data"]), - "outputs_count": len(tx["outputs"]), - "lineage": [{ - "from": out_point(funding["tx_hash"], funding["index"]), - "to_output_index": 0, - "relation": "adapter-local-node-smoke", - }], - "tx_shape_hash": ckb_blake2b(json.dumps(tx, sort_keys=True, separators=(",", ":")).encode("utf-8")), - }, - "deploy_probe": { - "status": "passed", - "type_id_args": type_id_args, - "artifact_data_hash": artifact_data_hash, - "code_output_capacity_shannons": code_output_capacity, - "change_output_capacity_shannons": change_capacity, - "fee_shannons": FEE, - "estimate_cycles": deploy_estimate, - "test_tx_pool_accept": deploy_tx_pool_accept, - "tx_size_json_bytes": json_serialized_size_bytes(deploy_tx), - "outputs_count": len(deploy_tx["outputs"]), - "outputs_data_count": len(deploy_tx["outputs_data"]), - "cell_deps_count": len(deploy_tx["cell_deps"]), - }, - "commit_evidence": { - "status": commit_evidence_status, - "deploy_tx_hash": deploy_tx_hash, - "commit_block_hash": commit_block_hash, - "code_cell_live": True, - "code_cell_has_type_script": commit_live_output.get("type") is not None, - }, - "known_limitations": [ - "This focused adapter acceptance proves CKB SDK/RPC materialization boundary evidence, not full CellScript business-flow semantics.", - "Stateful business-flow semantics remain covered by ckb_cellscript_acceptance.sh and release gates.", - "No wallet UI, CellFabric intent DAG, external audit, or mainnet-value certification is claimed.", - "The deploy probe uses always_success with hash_type=data as the type script for devnet acceptance; production TYPE_ID uses hash_type=type with the actual TYPE_ID script code_hash.", - ], -} -report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") -print(report_path) -PY +"${command[@]}" echo "CellScript CKB adapter acceptance report: $REPORT_JSON" diff --git a/scripts/cellscript_ckb_ecosystem_reuse_gate.sh b/scripts/cellscript_ckb_ecosystem_reuse_gate.sh index 90e42187..426a6f1d 100755 --- a/scripts/cellscript_ckb_ecosystem_reuse_gate.sh +++ b/scripts/cellscript_ckb_ecosystem_reuse_gate.sh @@ -34,6 +34,7 @@ cargo_fmt_workspace() { --package cellscript \ --package cellscript-ckb-adapter \ --package cellscript-fiber-adapter \ + --package cellscript-tools \ --package cellscript-wasm \ --package cellscript-ckb-sdk-builder-example \ "$@" @@ -52,56 +53,13 @@ validate_cli_contract_outputs() { run_capture "$compat_json" cargo run --locked -p cellscript --bin cellc -- ckb-std-compat --json run_capture "$action_json" cargo run --locked -p cellscript --bin cellc -- action build examples/token.cell --action mint_with_authority --json - - run python3 - "$compat_json" "$action_json" <<'PY' -import json -import sys - -compat_path, action_path = sys.argv[1:3] -with open(compat_path, "r", encoding="utf-8") as handle: - compat = json.load(handle) -with open(action_path, "r", encoding="utf-8") as handle: - action = json.load(handle) - -assert compat["status"] == "ok" -assert compat["schema"] == "cellscript-ckb-std-compat-report-v0.19" -assert compat["inline_abi"]["syscalls"]["load_cell_by_field"] == 2081 -assert compat["inline_abi"]["syscalls"]["load_witness"] == 2074 -assert compat["inline_abi"]["sources"]["group_input"] == ((1 << 56) | 1) -assert compat["inline_abi"]["sources"]["group_output"] == ((1 << 56) | 2) -assert compat["witness_args_policy"]["entry_payload_abi"] == "cellscript-entry-witness-v1" -assert compat["witness_args_policy"]["final_witness_args_owner"] == "adapter" -assert compat["adapter_boundary"]["compiler_core_uses_ckb_sdk_rust"] is False -assert compat["test_evidence"]["script_construction_api"] is True -assert compat["adapter_boundary"]["script_construction"]["packed_type"] == "ckb_types::packed::Script" -assert compat["adapter_boundary"]["script_construction"]["evidence_schema"] == "cellscript-ckb-script-evidence-v0.19" -assert "args_exact_prefix_suffix" in compat["adapter_boundary"]["script_construction"]["supports"] -assert "script_ref_readback" in compat["adapter_boundary"]["script_construction"]["supports"] -assert "explicit_cell_dep_binding" in compat["adapter_boundary"]["script_construction"]["supports"] - -assert action["status"] == "ok" -assert action["policy"] == "cellscript-action-builder-plan-v1" -assert action["headless"] is True -assert action["ui_scope"] == "none" -assert action["transaction_draft"]["state"] == "ActionPlan" -assert action["transaction_draft"]["can_submit"] is False -assert action["transaction_draft"]["requires_packed_materialization"] is True -assert action["transaction_draft"]["packed_materialization"]["transaction"] == "ckb_types::packed::Transaction" -assert action["transaction_draft"]["packed_materialization"]["script"] == "ckb_types::packed::Script" -assert action["transaction_draft"]["packed_materialization"]["out_point"] == "ckb_types::packed::OutPoint" -assert action["adapter_contract"]["schema"] == "cellscript-ckb-adapter-contract-v0.19" -assert action["adapter_contract"]["witness_policy"]["default_action_payload_field"] == "input_type" -assert action["adapter_contract"]["witness_policy"]["lock_signature_policy"] == "explicit-adapter-owned-do-not-overwrite" -required_fields = set(action["adapter_contract"]["resolved_tx_required_fields"]) -assert {"outputs_data", "cell_deps", "lineage"}.issubset(required_fields) -assert action["adapter_contract"]["acceptance_report_template"]["schema"] == "cellscript-ckb-action-acceptance-report-v0.19" -PY + run cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root "$ROOT_DIR" ecosystem-reuse-contracts "$compat_json" "$action_json" } run_quick_gate() { require_cmd cargo require_cmd git - require_cmd python3 cargo_fmt_workspace --check run cargo test --locked -p cellscript --test ckb_std_compat -- --test-threads=1 diff --git a/scripts/cellscript_ckb_stateful_scenarios.sh b/scripts/cellscript_ckb_stateful_scenarios.sh index 99e21945..b9493ad6 100755 --- a/scripts/cellscript_ckb_stateful_scenarios.sh +++ b/scripts/cellscript_ckb_stateful_scenarios.sh @@ -3,4 +3,9 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [[ -n "${CELLSCRIPT_CKB_REPO:-}" ]]; then + exec "$SCRIPT_DIR/ckb_cellscript_acceptance.sh" --production --stateful-scenarios \ + --ckb-repo "$CELLSCRIPT_CKB_REPO" "$@" +fi + exec "$SCRIPT_DIR/ckb_cellscript_acceptance.sh" --production --stateful-scenarios "$@" diff --git a/scripts/cellscript_fiber_acceptance.sh b/scripts/cellscript_fiber_acceptance.sh index 937c2b7e..84d5cb82 100755 --- a/scripts/cellscript_fiber_acceptance.sh +++ b/scripts/cellscript_fiber_acceptance.sh @@ -110,25 +110,9 @@ if [[ "$actual_revision" != "$FIBER_REVISION" ]]; then exit 1 fi -python3 - "$COMPATIBILITY_REPORT" "$ACCEPTANCE_REPORT" "$FIBER_REVISION" <<'PY' -import json -import pathlib -import sys - -compatibility_path = pathlib.Path(sys.argv[1]) -acceptance_path = pathlib.Path(sys.argv[2]) -expected_fiber_revision = sys.argv[3] - -compatibility = json.loads(compatibility_path.read_text(encoding="utf-8")) -acceptance = json.loads(acceptance_path.read_text(encoding="utf-8")) - -if compatibility.get("binding", {}).get("fiber_revision") != expected_fiber_revision: - raise SystemExit("compatibility report Fiber revision does not match the pinned checkout") -if compatibility.get("binding_fingerprint") != acceptance.get("binding_fingerprint"): - raise SystemExit("acceptance report is not bound to compatibility.json") -if compatibility.get("status") not in {"LocalNodeAdvertised", "ChannelReady", "TopologyCertified"}: - raise SystemExit("full acceptance requires at least LocalNodeAdvertised compatibility evidence") -PY +cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root "$REPO_ROOT" fiber-report-binding \ + "$COMPATIBILITY_REPORT" "$ACCEPTANCE_REPORT" "$FIBER_REVISION" cargo run --locked -p cellscript-fiber-adapter --bin cellscript-fiber -- accept "$ACCEPTANCE_REPORT" \ --compatibility-report "$COMPATIBILITY_REPORT" \ diff --git a/scripts/cellscript_gate.sh b/scripts/cellscript_gate.sh index 6a8957c1..4878be33 100755 --- a/scripts/cellscript_gate.sh +++ b/scripts/cellscript_gate.sh @@ -22,6 +22,16 @@ require_cmd() { fi } +require_node_22() { + require_cmd node + local node_major + node_major="$(node --version | sed -n 's/^v\([0-9][0-9]*\).*/\1/p')" + if [[ "$node_major" != "22" ]]; then + printf 'Node.js 22 is required by the CellScript website and Registry toolchain; found %s\n' "$(node --version)" >&2 + exit 1 + fi +} + run() { printf '\n==> %s\n' "$*" "$@" @@ -38,22 +48,40 @@ cargo_fmt_workspace() { run cargo fmt \ --manifest-path "$ROOT_DIR/Cargo.toml" \ --package cellscript \ + --package cellscript-artifact-checker \ --package cellscript-ckb-adapter \ --package cellscript-fiber-adapter \ + --package cellscript-tools \ --package cellscript-wasm \ --package cellscript-ckb-sdk-builder-example \ "$@" } -python_syntax_check() { - python3 - "$@" <<'PY' -import sys -from pathlib import Path +check_canonical_cellscript_format() { + run cargo run --quiet --locked -p cellscript --bin cellc -- \ + fmt --check "$ROOT_DIR/examples/language/canonical_style.cell" +} + +check_example_u64_boundaries() { + local example_files=( + "examples/atomic_swap.cell" + "examples/atomic_swap/src/main.cell" + "examples/multi_phase_dao.cell" + "examples/multi_phase_dao/src/main.cell" + "examples/nft.cell" + "examples/nft/src/main.cell" + "examples/timelock.cell" + "examples/timelock/src/main.cell" + ) -for raw in sys.argv[1:]: - path = Path(raw) - compile(path.read_text(encoding="utf-8"), str(path), "exec") -PY + if rg -n '18446744073709551615' "${example_files[@]}" | rg -v 'const U64_MAX: u64 = 18446744073709551615'; then + printf '\nRaw u64 maximum found outside a U64_MAX declaration.\n' >&2 + exit 1 + fi + if rg -n '18446744073709551515|18446744073706923615' "${example_files[@]}"; then + printf '\nRaw MAX-delta boundary found in a checked CellScript example.\n' >&2 + exit 1 + fi } check_trailing_whitespace() { @@ -69,7 +97,7 @@ check_trailing_whitespace() { local tracked_website_file while IFS= read -r tracked_website_file; do case "$tracked_website_file" in - website/*.json|website/*.mjs|website/**/*.astro|website/**/*.css|website/**/*.js|website/**/*.json|website/**/*.py|website/**/*.ts) + website/*.json|website/*.mjs|website/**/*.astro|website/**/*.css|website/**/*.js|website/**/*.json|website/**/*.mjs|website/**/*.ts) if [[ -f "$tracked_website_file" ]]; then tracked_website_files+=("$tracked_website_file") fi @@ -112,12 +140,8 @@ check_trailing_whitespace() { "scripts/cellscript_ckb_release_gate.sh" "scripts/cellscript_0_14_scope_audit.sh" "scripts/cellscript_syntax_combo_audit.sh" - "scripts/cellscript_syntax_combo_audit.py" "scripts/cellscript_strict_backend_audit.sh" - "scripts/cellscript_strict_backend_audit.py" "scripts/ckb_cellscript_acceptance.sh" - "scripts/validate_cellscript_tooling_release.py" - "scripts/validate_ckb_cellscript_production_evidence.py" "tests/syntax_combo/matrix.toml" "tests/syntax_combo/seeds/require-block-lifecycle.cell" "docs/releases/CELLSCRIPT_0_20_RELEASE_NOTES.md" @@ -142,223 +166,9 @@ check_trailing_whitespace() { fi } -check_forbidden_tracked_files() { - local forbidden=() - local path - while IFS= read -r path; do - forbidden+=("$path") - done < <(git ls-files '*DS_Store') - - if ((${#forbidden[@]} > 0)); then - printf 'Forbidden macOS metadata files are tracked:\n' >&2 - printf ' %s\n' "${forbidden[@]}" >&2 - exit 1 - fi -} - check_novaseal_verifier_pinning() { - python3 - <<'PY' -import hashlib -import json -import subprocess -import sys -from pathlib import Path - -try: - import tomllib -except ModuleNotFoundError: - print("Python tomllib is required for NovaSeal verifier pinning checks", file=sys.stderr) - sys.exit(127) - -root = Path.cwd() -core_root = root / "proposals/novaseal/v0-mvp-skeleton" -release_elf = ( - core_root - / "verifier/novaseal_btc_verifier_riscv/target/" - / "riscv64imac-unknown-none-elf/release/novaseal_btc_verifier_riscv" -) -if not release_elf.is_file(): - print(f"missing NovaSeal RISC-V verifier release ELF: {release_elf}", file=sys.stderr) - sys.exit(1) - -artifact = release_elf.read_bytes() -artifact_hash = "0x" + hashlib.sha256(artifact).hexdigest() -data_hash = "0x" + hashlib.blake2b(artifact, digest_size=32, person=b"ckb-default-hash").hexdigest() -size_bytes = len(artifact) - -failures: list[str] = [] - -manifest_paths = [ - root / rel - for rel in subprocess.check_output( - ["git", "ls-files", "proposals/novaseal/**/Cell.toml"], - cwd=root, - text=True, - ).splitlines() -] -novaseal_root = root / "proposals/novaseal" -if novaseal_root.is_dir(): - manifest_paths.extend( - novaseal_root / rel - for rel in subprocess.check_output( - ["git", "-C", str(novaseal_root), "ls-files", "**/Cell.toml"], - cwd=root, - text=True, - ).splitlines() - ) -manifest_paths = sorted(set(manifest_paths)) -if not manifest_paths: - failures.append("no tracked NovaSeal Cell.toml manifests found") - -for path in manifest_paths: - manifest = tomllib.loads(path.read_text(encoding="utf-8")) - deps = manifest.get("deploy", {}).get("ckb", {}).get("cell_deps", []) - runtime_deps = [ - dep - for dep in deps - if dep.get("role") == "runtime_verifier" - or dep.get("name") == "cellscript_btc_bip340_verifier_riscv" - ] - if not runtime_deps: - failures.append(f"{path.relative_to(root)} has no NovaSeal runtime verifier CellDep") - continue - for index, dep in enumerate(runtime_deps): - if dep.get("data_hash") != data_hash: - failures.append( - f"{path.relative_to(root)} runtime verifier #{index} data_hash " - f"{dep.get('data_hash')} != {data_hash}" - ) - if dep.get("artifact_hash") != artifact_hash: - failures.append( - f"{path.relative_to(root)} runtime verifier #{index} artifact_hash " - f"{dep.get('artifact_hash')} != {artifact_hash}" - ) - -def source_tree_hash() -> str: - verifier_dirs = [ - core_root / "verifier/novaseal_btc_verifier_core", - core_root / "verifier/novaseal_btc_verifier_riscv", - core_root / "verifier/novaseal_btc_verifier", - ] - files: list[Path] = [] - for verifier_dir in verifier_dirs: - for path in verifier_dir.rglob("*"): - rel_parts = path.relative_to(verifier_dir).parts - if any(part in {"target", "build", ".git", "__pycache__"} for part in rel_parts): - continue - if path.is_symlink(): - failures.append(f"{path.relative_to(root)} is a symlink inside the NovaSeal verifier TCB source tree") - continue - if not path.is_file(): - continue - if path.suffix in {".rs", ".sh"} or path.name in {"Cargo.toml", "Cargo.lock", "README.md"}: - files.append(path) - tree_hash = hashlib.sha256() - for path in sorted(files): - rel = path.relative_to(root).as_posix() - digest = hashlib.sha256(path.read_bytes()).digest() - tree_hash.update(rel.encode("utf-8")) - tree_hash.update(b"\0") - tree_hash.update(digest) - return "0x" + tree_hash.hexdigest() - -current_source_tree_hash = source_tree_hash() - -def profile_source_tree_hash(paths: list[str]) -> str: - files: set[Path] = set() - allowed_suffixes = {".cell", ".schema", ".toml", ".py", ".json", ".rs"} - for raw in paths: - path = root / raw - if path.is_symlink(): - failures.append(f"{path.relative_to(root)} is a symlink inside the NovaSeal profile source tree") - continue - if path.is_file(): - files.add(path) - elif path.is_dir(): - for child in path.rglob("*"): - rel_parts = child.relative_to(path).parts - if any(part in {"target", "build", ".git", "__pycache__"} for part in rel_parts): - continue - if child.is_symlink(): - failures.append(f"{child.relative_to(root)} is a symlink inside the NovaSeal profile source tree") - continue - if child.is_file() and (child.name == "Cargo.lock" or child.suffix in allowed_suffixes): - files.add(child) - h = hashlib.sha256() - for path in sorted(files): - rel_path = path.relative_to(root).as_posix() - h.update(rel_path.encode("utf-8")) - h.update(b"\0") - h.update(hashlib.sha256(path.read_bytes()).digest()) - return "0x" + h.hexdigest() - -public_template_path = core_root / "proofs/public_shared_cell_dep_attestation.template.json" -public_template = json.loads(public_template_path.read_text(encoding="utf-8")) -public_template_hash = public_template.get("runtime_verifier", {}).get("artifact_hash") -if public_template_hash != artifact_hash: - failures.append( - f"{public_template_path.relative_to(root)} runtime_verifier.artifact_hash " - f"{public_template_hash} != {artifact_hash}" - ) - -external_template_path = core_root / "proofs/bip340_external_tcb_review_attestation.template.json" -external_template = json.loads(external_template_path.read_text(encoding="utf-8")) -if external_template.get("artifact_hash") != artifact_hash: - failures.append( - f"{external_template_path.relative_to(root)} artifact_hash " - f"{external_template.get('artifact_hash')} != {artifact_hash}" - ) -if external_template.get("source_tree_sha256") != current_source_tree_hash: - failures.append( - f"{external_template_path.relative_to(root)} source_tree_sha256 " - f"{external_template.get('source_tree_sha256')} != {current_source_tree_hash}" - ) - -rwa_source_tree_hash = profile_source_tree_hash( - [ - "proposals/novaseal/rwa-receipt-profile-v0/Cell.toml", - "proposals/novaseal/rwa-receipt-profile-v0/src/nova_rwa_receipt_type.cell", - "proposals/novaseal/rwa-receipt-profile-v0/src/nova_rwa_receipt_lifecycle_type.cell", - "proposals/novaseal/rwa-receipt-profile-v0/schemas", - "proposals/novaseal/rwa-receipt-profile-v0/fixtures", - "proposals/novaseal/rwa-receipt-profile-v0/proofs/invariant_matrix.json", - ] -) -rwa_template_path = root / "proposals/novaseal/rwa-receipt-profile-v0/proofs/legal_registry_review_evidence.template.json" -rwa_template = json.loads(rwa_template_path.read_text(encoding="utf-8")) -if rwa_template.get("profile_source_tree_sha256") != rwa_source_tree_hash: - failures.append( - f"{rwa_template_path.relative_to(root)} profile_source_tree_sha256 " - f"{rwa_template.get('profile_source_tree_sha256')} != {rwa_source_tree_hash}" - ) - -mapping_path = core_root / "proofs/proofplan_mapping.json" -mapping = json.loads(mapping_path.read_text(encoding="utf-8")) -artifact_summary = mapping.get("btc_verifier_riscv_shell_artifact", {}).get("current_summary", {}) -if artifact_summary.get("staged_release_elf_sha256") != artifact_hash.removeprefix("0x"): - failures.append( - f"{mapping_path.relative_to(root)} staged_release_elf_sha256 " - f"{artifact_summary.get('staged_release_elf_sha256')} != {artifact_hash.removeprefix('0x')}" - ) -if artifact_summary.get("staged_release_elf_size_bytes") != size_bytes: - failures.append( - f"{mapping_path.relative_to(root)} staged_release_elf_size_bytes " - f"{artifact_summary.get('staged_release_elf_size_bytes')} != {size_bytes}" - ) - -if failures: - print("NovaSeal verifier pinning check failed:", file=sys.stderr) - for failure in failures: - print(f" - {failure}", file=sys.stderr) - sys.exit(1) - -print( - "NovaSeal verifier pinning check passed: " - f"artifact_hash={artifact_hash} data_hash={data_hash} " - f"source_tree_sha256={current_source_tree_hash} " - f"rwa_profile_source_tree_sha256={rwa_source_tree_hash} size_bytes={size_bytes}" -) -PY + run cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root "$ROOT_DIR" check-novaseal-verifier-pinning } check_release_roadmap_docs() { @@ -410,167 +220,52 @@ check_ckb_release_docs() { } check_cellscript_doc_status_freshness() { - python3 - <<'PY' -import re -import sys -from pathlib import Path - -root = Path.cwd() -readme = (root / "README.md").read_text(encoding="utf-8") -readme_links = sorted( - set(re.findall(r"\]\((docs/CELLSCRIPT_[^)#]+\.md)(?:#[^)]+)?\)", readme)) -) - -tracked_docs = [] -try: - import subprocess - - tracked_docs = subprocess.check_output( - ["git", "ls-files", "docs/CELLSCRIPT_*.md"], - cwd=root, - text=True, - ).splitlines() -except Exception: - tracked_docs = [] - -filesystem_docs = [ - str(path.relative_to(root)) - for path in (root / "docs").glob("CELLSCRIPT_*.md") -] -tracked_existing_docs = [ - rel for rel in tracked_docs - if (root / rel).is_file() -] -docs_to_scan = sorted(set(readme_links + filesystem_docs + tracked_existing_docs)) -stale_patterns = [ - "formal 0.19 headless Rust adapter crate", - "0.19 scope compatibility contract", - "Active 0.19 grammar-governance contract", - "Proposed. Implementation gated", - "**Status**: In progress", -] - -failures: list[str] = [] -for rel in docs_to_scan: - path = root / rel - if not path.is_file(): - failures.append(f"README-linked CellScript doc is missing: {rel}") - continue - head = "\n".join(path.read_text(encoding="utf-8").splitlines()[:40]) - normalized_head = " ".join(head.split()) - for pattern in stale_patterns: - if pattern in normalized_head: - failures.append(f"{rel} has stale Status header pattern: {pattern}") - -required_current = { - "docs/CELLSCRIPT_CKB_ADAPTER.md": "production contract for the current CellScript CKB profile", - "docs/CELLSCRIPT_CKB_STD_COMPAT.md": "production compatibility contract for the current CellScript CKB profile", - "docs/CELLSCRIPT_GRAMMAR_GOVERNANCE_RFC.md": "Active grammar-governance contract", - "docs/CELLSCRIPT_WEBSITE_PARADIGM_UPGRADE_RFC.md": "Implemented across the 0.20-0.21 line", -} -for rel, marker in required_current.items(): - path = root / rel - head = "\n".join(path.read_text(encoding="utf-8").splitlines()[:20]) - normalized_head = " ".join(head.split()) - if marker not in normalized_head: - failures.append(f"{rel} Status header is missing freshness marker: {marker}") - -if failures: - print("CellScript documentation Status freshness check failed:", file=sys.stderr) - for failure in failures: - print(f" - {failure}", file=sys.stderr) - sys.exit(1) -PY + run cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root "$ROOT_DIR" check-doc-status } check_markdown_local_links() { - python3 - <<'PY' -import os -import re -import sys -import urllib.parse -from pathlib import Path - -root = Path.cwd() -scan_roots = [ - root / "README.md", - root / "docs", - root / "roadmap", - root / "editors/vscode-cellscript/README.md", - root / "editors/vscode-cellscript/docs", -] -skip_dirs = {".git", ".mavis", "dist", "node_modules", "target"} -markdown_files: list[Path] = [] - -for start in scan_roots: - if start.is_file(): - markdown_files.append(start) - elif start.is_dir(): - for dirpath, dirnames, filenames in os.walk(start): - dirnames[:] = [name for name in dirnames if name not in skip_dirs] - for filename in filenames: - if filename.endswith(".md"): - markdown_files.append(Path(dirpath) / filename) - -link_re = re.compile(r"(?!!)\[[^\]]+\]\(([^)\s]+(?:\s+\"[^\"]*\")?)\)") -failures: list[str] = [] - -for path in sorted(markdown_files): - text = path.read_text(encoding="utf-8") - for lineno, line in enumerate(text.splitlines(), 1): - for match in link_re.finditer(line): - raw = match.group(1).strip() - if " " in raw and not raw.startswith("<"): - raw = raw.split(" ", 1)[0] - raw = raw.strip("<>") - target = raw.split("#", 1)[0] - if not target: - continue - if target.startswith(("#", "http://", "https://", "mailto:", "tel:", "app://")): - continue - if target.startswith("/"): - continue - candidate = (path.parent / urllib.parse.unquote(target)).resolve() - if not candidate.exists(): - failures.append(f"{path.relative_to(root)}:{lineno}: missing local markdown link target {raw}") - -if failures: - print("Local markdown link check failed:", file=sys.stderr) - for failure in failures: - print(f" - {failure}", file=sys.stderr) - sys.exit(1) -PY + run cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root "$ROOT_DIR" check-markdown-links +} + +check_source_policy() { + run cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root "$ROOT_DIR" check-source-policy } check_ckb_acceptance_boundaries() { local required=( 'scripts/ckb_cellscript_acceptance.sh::Usage: scripts/ckb_cellscript_acceptance.sh' - 'scripts/ckb_cellscript_acceptance.sh::strict-original-ckb' - 'scripts/ckb_cellscript_acceptance.sh::bundled_examples_exact_order' - 'scripts/ckb_cellscript_acceptance.sh::language_examples_exact_order' - 'scripts/ckb_cellscript_acceptance.sh::strict_original_ckb_compile_policy_fail_closed' - 'scripts/ckb_cellscript_acceptance.sh::strict_original_ckb_compile_unexpected_failures' - 'scripts/ckb_cellscript_acceptance.sh::SOURCE_PROVENANCE_SCHEMA' - 'scripts/ckb_cellscript_acceptance.sh::BUILD_REPORT_SCHEMA' - 'scripts/ckb_cellscript_acceptance.sh::tracked_source_sha256' - 'scripts/ckb_cellscript_acceptance.sh::ckb_acceptance_pin.json' - 'scripts/ckb_cellscript_acceptance.sh::cellscript-ckb-runtime-provenance-v0.22' - 'scripts/ckb_cellscript_acceptance.sh::fresh-dedicated-cargo-target' - 'scripts/ckb_cellscript_acceptance.sh::binary_archived_with_report' - 'scripts/ckb_cellscript_acceptance.sh::cellscript-public-builder-contract-gate-v0.22' - 'scripts/ckb_cellscript_acceptance.sh::cellscript_build_reports' - 'scripts/ckb_cellscript_acceptance.sh::live_code_cell_data_hash_matches_artifact' - 'scripts/ckb_cellscript_acceptance.sh::public_builder_contract_action_count' - 'scripts/ckb_cellscript_acceptance.sh::final_production_hardening_gate' - 'scripts/validate_ckb_cellscript_production_evidence.py::validate_source_provenance' - 'scripts/validate_ckb_cellscript_production_evidence.py::validate_public_builder_contracts' - 'scripts/validate_ckb_cellscript_production_evidence.py::validate_ckb_runtime_provenance' - 'scripts/validate_ckb_cellscript_production_evidence.py::fresh-dedicated-cargo-target' - 'scripts/validate_ckb_cellscript_production_evidence.py::stateful branch scenarios must cover every action absent from end-to-end flows exactly once' - 'scripts/validate_ckb_cellscript_production_evidence.py::validate_build_reports' - 'scripts/validate_ckb_cellscript_production_evidence.py::tracked_source_sha256' - 'scripts/validate_ckb_cellscript_production_evidence.py::valid CKB CellScript' - 'scripts/validate_cellscript_tooling_release.py::valid CellScript tooling release boundary' + 'scripts/ckb_cellscript_acceptance.sh::ckb-acceptance' + 'crates/cellscript-tools/src/ckb_acceptance.rs::requires_all_bundled_examples_strict_original_ckb' + 'crates/cellscript-tools/src/ckb_acceptance.rs::bundled_examples_exact_order' + 'crates/cellscript-tools/src/ckb_acceptance.rs::language_examples_exact_order' + 'crates/cellscript-tools/src/ckb_acceptance.rs::strict_original_ckb_compile_policy_fail_closed' + 'crates/cellscript-tools/src/ckb_acceptance.rs::strict_original_ckb_compile_unexpected_failures' + 'crates/cellscript-tools/src/ckb_acceptance.rs::SOURCE_PROVENANCE_SCHEMA' + 'crates/cellscript-tools/src/ckb_acceptance.rs::BUILD_REPORT_SCHEMA' + 'crates/cellscript-tools/src/ckb_acceptance.rs::"source_provenance":source_provenance(root)?' + 'crates/cellscript-tools/src/ckb_acceptance_live.rs::ckb_acceptance_pin.json' + 'crates/cellscript-tools/src/ckb_acceptance_live.rs::cellscript-ckb-runtime-provenance-v0.22' + 'crates/cellscript-tools/src/ckb_acceptance_live.rs::fresh-dedicated-cargo-target' + 'crates/cellscript-tools/src/ckb_acceptance_live.rs::ckb-librocksdb-sys-8.5.4-explicit-cstdint-v1' + 'crates/cellscript-tools/src/ckb_acceptance_live.rs::binary_archived_with_report' + 'crates/cellscript-tools/src/ckb_acceptance.rs::cellscript-public-builder-contract-gate-v0.22' + 'crates/cellscript-tools/src/ckb_acceptance.rs::cellscript_build_reports' + 'crates/cellscript-tools/src/ckb_acceptance_live.rs::live_code_cell_data_hash_matches_artifact' + 'crates/cellscript-tools/src/ckb_acceptance_live.rs::public_builder_contract_action_count' + 'crates/cellscript-tools/src/ckb_acceptance_live.rs::final_production_hardening_gate' + 'crates/cellscript-tools/src/production_evidence.rs::validate_source_provenance' + 'crates/cellscript-tools/src/production_evidence.rs::validate_public_builder_contracts' + 'crates/cellscript-tools/src/production_evidence.rs::validate_ckb_runtime_provenance' + 'crates/cellscript-tools/src/production_evidence.rs::fresh-dedicated-cargo-target' + 'crates/cellscript-tools/src/production_evidence.rs::ckb-librocksdb-sys-8.5.4-explicit-cstdint-v1' + 'crates/cellscript-tools/src/production_evidence.rs::stateful branch scenarios must cover every action absent from end-to-end flows exactly once' + 'crates/cellscript-tools/src/production_evidence.rs::validate_build_reports' + 'crates/cellscript-tools/src/production_evidence.rs::tracked_source_sha256' + 'crates/cellscript-tools/src/production_evidence.rs::valid CKB CellScript' + 'crates/cellscript-tools/src/tooling_release.rs::valid CellScript tooling release boundary' 'src/lib.rs::cellscript-template-layout-v0.21' 'src/cli/commands.rs::cellscript-protocol-graph-v0.22' 'src/cli/commands.rs::cellscript-action-scan-selectors-v0.21' @@ -597,13 +292,13 @@ check_novaseal_acceptance_boundaries() { 'src/cli/novaseal_certification.rs::real BTC SPV and Fiber endpoint production acceptance' 'src/cli/novaseal_certification.rs::current_source_valid' 'src/cli/novaseal_certification.rs::source_tree_invalid_paths_empty' - 'scripts/novaseal_bip340_tcb_review.py::invalid_paths' - 'scripts/novaseal_devnet_stateful_live.py::invalid_paths' - 'scripts/novaseal_external_evidence_handoff_bundle.py::source tree path must not be a symlink' - 'scripts/cellscript_gate.sh::is a symlink inside the NovaSeal' - 'scripts/novaseal_devnet_stateful_acceptance.sh::acceptance_blocker_count' - 'scripts/novaseal_devnet_stateful_acceptance.sh::local_blocker_count' - 'scripts/novaseal_devnet_stateful_acceptance.sh::blocker_count' + 'crates/cellscript-tools/src/bip340_tcb.rs::invalid_paths' + 'crates/cellscript-tools/src/ckb_devnet.rs::invalid_paths' + 'crates/cellscript-tools/src/external_handoff.rs::source tree path must not be a symlink' + 'crates/cellscript-tools/src/verifier_pinning.rs::is a symlink inside the NovaSeal' + 'scripts/novaseal_devnet_stateful_acceptance.sh::novaseal-acceptance-summary' + 'scripts/novaseal_devnet_stateful_acceptance.sh::local_blockers acceptance_blockers blockers external_endpoint_status' + 'scripts/novaseal_devnet_stateful_acceptance.sh::$acceptance_blockers" == "1"' 'scripts/novaseal_devnet_stateful_acceptance.sh::acceptance_blockers=%s' 'scripts/novaseal_devnet_stateful_acceptance.sh::external_endpoint_status=%s' 'scripts/novaseal_devnet_stateful_acceptance.sh::certifier_status=%s' @@ -634,49 +329,8 @@ check_package_contents() { package_files="$(mktemp)" printf '\n==> cargo package --list --locked --allow-dirty --offline\n' cargo package --list --locked --allow-dirty --offline | tee "$package_files" - if ! python3 - "$package_files" <<'PY'; then -import sys -from pathlib import Path - -allowed_root_files = { - ".cargo_vcs_info.json", - "Cargo.lock", - "Cargo.toml", - "Cargo.toml.orig", - "CHANGELOG.md", - "CODING_STYLE.md", - "LICENSE-MIT", - "README.md", -} -allowed_root_dirs = { - "assets", - "examples", - "roadmap", - "scripts", - "src", - "tests", -} -forbidden_suffixes = (".pyc", ".pyo") - -unexpected: list[str] = [] -for raw in Path(sys.argv[1]).read_text(encoding="utf-8").splitlines(): - path = raw.strip() - if not path: - continue - root = path.split("/", 1)[0] - if path.endswith(forbidden_suffixes) or "__pycache__/" in path: - unexpected.append(path) - elif "/" not in path and path not in allowed_root_files: - unexpected.append(path) - elif "/" in path and root not in allowed_root_dirs: - unexpected.append(path) - -if unexpected: - print("crates.io package includes repository-only files:", file=sys.stderr) - for path in unexpected: - print(f" {path}", file=sys.stderr) - sys.exit(1) -PY + if ! cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root "$ROOT_DIR" check-package-contents "$package_files"; then printf 'crates.io package includes repository-only files or unpublished helper binaries\n' >&2 exit 1 fi @@ -690,22 +344,15 @@ check_script_syntax() { shell_scripts+=("$shell_script") done < <(git ls-files '*.sh') for shell_script in "${shell_scripts[@]}"; do - run bash -n "$shell_script" + if [[ -f "$shell_script" ]]; then + run bash -n "$shell_script" + fi done - local python_scripts=() - local python_script - while IFS= read -r python_script; do - python_scripts+=("$python_script") - done < <(git ls-files '*.py') - if ((${#python_scripts[@]} > 0)); then - run python_syntax_check "${python_scripts[@]}" - fi } check_release_source_identity() { require_cmd git - require_cmd python3 local dirty version expected_tag exact_tags dirty="$(git status --porcelain --untracked-files=all)" @@ -714,15 +361,8 @@ check_release_source_identity() { exit 1 fi - version="$(python3 - "$ROOT_DIR/Cargo.toml" <<'PY' -import sys -import tomllib -from pathlib import Path - -manifest = tomllib.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) -print(manifest["package"]["version"]) -PY -)" + version="$(cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root "$ROOT_DIR" workspace-version)" if [[ -n "${CELLSCRIPT_RELEASE_VERSION:-}" && "$CELLSCRIPT_RELEASE_VERSION" != "$version" ]]; then printf 'release version mismatch: requested %s, Cargo workspace declares %s\n' "$CELLSCRIPT_RELEASE_VERSION" "$version" >&2 exit 1 @@ -744,7 +384,6 @@ PY run_website_build_check() { require_cmd npm - require_cmd python3 if [[ ! -d website/node_modules ]]; then run npm --prefix website ci @@ -759,8 +398,73 @@ run_website_build_check() { exit 1 fi - run_in_dir website npm exec -- astro check - run_in_dir website npm exec -- astro build + run npm --prefix website run build:ci +} + +run_registry_api_check() { + local registry_verifier_target_dir="${CARGO_TARGET_DIR:-$ROOT_DIR/services/registry-verifier/target}" + if [[ "$registry_verifier_target_dir" != /* ]]; then + registry_verifier_target_dir="$ROOT_DIR/$registry_verifier_target_dir" + fi + local registry_artifact_verifier_target_dir="$ROOT_DIR/services/registry-artifact-verifier/target" + + if [[ ! -d services/registry-api/node_modules ]]; then + run npm --prefix services/registry-api ci + fi + run npm --prefix services/registry-api run check + run cargo build --locked --manifest-path services/registry-verifier/Cargo.toml \ + --target-dir "$registry_verifier_target_dir" + run cargo build --locked --manifest-path services/registry-artifact-verifier/Cargo.toml \ + --target-dir "$registry_artifact_verifier_target_dir" + run env CELLSCRIPT_REGISTRY_VERIFIER_TEST_BINARY="$registry_verifier_target_dir/debug/cellscript-registry-verify" \ + CELLSCRIPT_REGISTRY_ARTIFACT_VERIFIER_TEST_BINARY="$registry_artifact_verifier_target_dir/debug/cellscript-registry-artifact-verify" \ + npm --prefix services/registry-api test + run npm --prefix services/registry-api run build + run npm --prefix services/registry-api run build:node + run cargo fmt --manifest-path services/registry-verifier/Cargo.toml -- --check + run cargo fmt --manifest-path services/registry-artifact-verifier/Cargo.toml -- --check + run cargo test --locked --manifest-path services/registry-verifier/Cargo.toml + run cargo test --locked --manifest-path services/registry-artifact-verifier/Cargo.toml + run cargo clippy --locked --manifest-path services/registry-verifier/Cargo.toml --all-targets -- -D warnings + run cargo clippy --locked --manifest-path services/registry-artifact-verifier/Cargo.toml --all-targets -- -D warnings +} + +check_registry_artifact_verifier_dependency_boundary() { + if cargo tree --locked --manifest-path services/registry-artifact-verifier/Cargo.toml --edges normal --prefix none \ + | rg --quiet '^cellscript v'; then + printf 'Registry artifact verifier production dependency graph must not contain the CellScript compiler\n' >&2 + return 1 + fi +} + +check_artifact_checker_dependency_boundary() { + if cargo tree --locked --manifest-path Cargo.toml -p cellscript-artifact-checker --edges normal --prefix none \ + | rg --quiet '^cellscript v'; then + printf 'Artifact checker production dependency graph must not contain the CellScript compiler\n' >&2 + return 1 + fi +} + +run_executable_package_scenarios() { + local backend="$1" + run cargo run --quiet --locked -p cellscript --bin cellc -- test scenarios --backend "$backend" +} + +run_registry_type_script_check() { + run cargo fmt --manifest-path contracts/registry-type-script/Cargo.toml -- --check + run contracts/registry-type-script/build_reproducible_release.sh + run cargo test --locked --manifest-path contracts/registry-type-script/Cargo.toml + local registry_type_script_hash + registry_type_script_hash="$(sed -n 's/.*"ckb_data_hash": "\(0x[0-9a-f]*\)".*/\1/p' \ + contracts/registry-type-script/release-manifest.json)" + if [[ ! "$registry_type_script_hash" =~ ^0x[0-9a-f]{64}$ ]]; then + printf 'Registry Type Script release manifest has no canonical CKB data hash\n' >&2 + return 1 + fi + if ! rg --fixed-strings --quiet "$registry_type_script_hash" services/registry-api/src/index.ts; then + printf 'Registry API canonical Type Script identity is stale: expected %s\n' "$registry_type_script_hash" >&2 + return 1 + fi } check_wasm_release_bundle() { @@ -773,8 +477,50 @@ check_wasm_release_bundle() { fi } +release_ckb_repo_from_args() { + local ckb_repo="$ROOT_DIR/../ckb" + while (($# > 0)); do + case "$1" in + --ckb-repo) + if (($# < 2)); then + printf 'missing value for --ckb-repo\n' >&2 + return 2 + fi + ckb_repo="$2" + shift 2 + ;; + *) + shift + ;; + esac + done + printf '%s\n' "$ckb_repo" +} + check_ckb_tx_measure_tool() { - run cargo test --manifest-path tools/ckb-tx-measure/Cargo.toml --locked + local ckb_repo="$1" + local default_ckb_repo="$ROOT_DIR/../ckb" + if [[ ! -d "$ckb_repo" ]]; then + printf 'CKB checkout does not exist: %s\n' "$ckb_repo" >&2 + return 1 + fi + ckb_repo="$(cd "$ckb_repo" && pwd -P)" + if [[ -d "$default_ckb_repo" ]]; then + default_ckb_repo="$(cd "$default_ckb_repo" && pwd -P)" + if [[ "$ckb_repo" == "$default_ckb_repo" ]]; then + run cargo test --manifest-path tools/ckb-tx-measure/Cargo.toml --locked + return + fi + fi + + local staging_dir + staging_dir="$(mktemp -d "$ROOT_DIR/target/cellscript-ckb-tx-measure.XXXXXX")" + mkdir -p "$staging_dir/cellscript/tools/ckb-tx-measure" "$staging_dir/cellscript/src/bin" + cp tools/ckb-tx-measure/Cargo.toml tools/ckb-tx-measure/Cargo.lock \ + "$staging_dir/cellscript/tools/ckb-tx-measure/" + cp src/bin/ckb_tx_measure.rs "$staging_dir/cellscript/src/bin/" + ln -s "$ckb_repo" "$staging_dir/ckb" + run cargo test --manifest-path "$staging_dir/cellscript/tools/ckb-tx-measure/Cargo.toml" --locked } check_novaseal_rust_tooling() { @@ -794,21 +540,35 @@ run_dev_gate() { exit 2 fi require_cmd cargo - require_cmd python3 require_cmd rg cargo_fmt_workspace + run cargo fmt --manifest-path services/registry-verifier/Cargo.toml + run cargo fmt --manifest-path services/registry-artifact-verifier/Cargo.toml run cargo check --locked -p cellscript --all-targets + run cargo check --locked -p cellscript-artifact-checker --all-targets + run cargo test --locked -p cellscript-artifact-checker + run cargo test --locked -p cellscript --test artifact_checker --test myelin_handoff run cargo check --locked -p cellscript-fiber-adapter --all-targets run cargo check --locked -p cellscript-ckb-adapter --all-targets run cargo check --locked -p cellscript-wasm --all-targets --features wasm run cargo check --locked -p cellscript-ckb-sdk-builder-example --all-targets + run cargo check --locked -p cellscript-tools --all-targets + run cargo check --locked --manifest-path services/registry-verifier/Cargo.toml --all-targets + run cargo check --locked --manifest-path services/registry-artifact-verifier/Cargo.toml --all-targets + check_registry_artifact_verifier_dependency_boundary + check_artifact_checker_dependency_boundary + run_registry_type_script_check + check_canonical_cellscript_format + check_example_u64_boundaries run ./scripts/cellscript_strict_backend_audit.sh quick run ./scripts/cellscript_syntax_combo_audit.sh quick - run python3 scripts/check_cellscript_skill_pack.py + run_executable_package_scenarios simulator + run cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root "$ROOT_DIR" check-skill-pack check_cellscript_doc_status_freshness check_markdown_local_links - check_forbidden_tracked_files + check_source_policy run git diff --check } @@ -818,32 +578,47 @@ run_ci_gate() { exit 2 fi require_cmd cargo - require_cmd python3 require_cmd rg require_cmd npm + require_node_22 printf '{"status":"not-generated","reason":"test suite did not reach backend shape report generation"}\n' >"$CELLSCRIPT_BACKEND_SHAPE_REPORT" cargo_fmt_workspace --check + check_canonical_cellscript_format + check_example_u64_boundaries run cargo test --locked -p cellscript -- --test-threads=1 + run cargo test --locked -p cellscript-artifact-checker -- --test-threads=1 + check_artifact_checker_dependency_boundary run cargo test --locked -p cellscript-fiber-adapter -- --test-threads=1 run cargo test --locked -p cellscript-ckb-adapter -- --test-threads=1 run cargo test --locked -p cellscript-wasm --features wasm -- --test-threads=1 run cargo test --locked -p cellscript-ckb-sdk-builder-example -- --test-threads=1 + run cargo test --locked -p cellscript-tools -- --test-threads=1 + run_executable_package_scenarios all run cargo clippy --locked -p cellscript --all-targets -- -D warnings + run cargo clippy --locked -p cellscript-artifact-checker --all-targets -- -D warnings run cargo clippy --locked -p cellscript-fiber-adapter --all-targets -- -D warnings run cargo clippy --locked -p cellscript-ckb-adapter --all-targets -- -D warnings run cargo clippy --locked -p cellscript-wasm --all-targets --features wasm -- -D warnings run cargo clippy --locked -p cellscript-ckb-sdk-builder-example --all-targets -- -D warnings + run cargo clippy --locked -p cellscript-tools --all-targets -- -D warnings + run_registry_type_script_check + run cargo clippy --locked --manifest-path contracts/registry-type-script/Cargo.toml --tests -- -D warnings run ./scripts/cellscript_strict_backend_audit.sh ci - run python3 scripts/check_cellscript_skill_pack.py + run cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root "$ROOT_DIR" check-skill-pack check_cellscript_doc_status_freshness check_markdown_local_links check_package_contents - run cargo package --locked --offline --allow-dirty + run cargo package --manifest-path crates/cellscript-artifact-checker/Cargo.toml --locked --offline --allow-dirty + run cargo --config "patch.crates-io.cellscript-artifact-checker.path=\"$ROOT_DIR/crates/cellscript-artifact-checker\"" \ + package --locked --offline --allow-dirty + run_registry_api_check + check_registry_artifact_verifier_dependency_boundary run_website_build_check check_script_syntax run git diff --check - check_forbidden_tracked_files + check_source_policy check_trailing_whitespace } @@ -853,29 +628,38 @@ run_backend_gate() { exit 2 fi require_cmd cargo - require_cmd python3 require_cmd rg + check_source_policy + cargo_fmt_workspace --check run cargo check --locked -p cellscript --all-targets + run cargo check --locked -p cellscript-artifact-checker --all-targets run cargo check --locked -p cellscript-fiber-adapter --all-targets run cargo test --locked -p cellscript + run cargo test --locked -p cellscript-artifact-checker run cargo test --locked -p cellscript-fiber-adapter -- --test-threads=1 run cargo clippy --locked -p cellscript --all-targets -- -D warnings + run cargo clippy --locked -p cellscript-artifact-checker --all-targets -- -D warnings run cargo clippy --locked -p cellscript-fiber-adapter --all-targets -- -D warnings + check_registry_artifact_verifier_dependency_boundary + check_artifact_checker_dependency_boundary + run_executable_package_scenarios all run ./scripts/cellscript_strict_backend_audit.sh full run git diff --check } run_release_auxiliary_checks() { + local ckb_repo="$1" require_cmd npm - run python3 scripts/validate_cellscript_tooling_release.py + run cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root "$ROOT_DIR" validate-tooling-release check_release_roadmap_docs check_ckb_release_docs check_ckb_acceptance_boundaries check_novaseal_acceptance_boundaries - check_ckb_tx_measure_tool + check_ckb_tx_measure_tool "$ckb_repo" check_novaseal_rust_tooling check_novaseal_verifier_pinning check_wasm_release_bundle @@ -887,18 +671,22 @@ run_release_auxiliary_checks() { } run_release_quick_gate() { + local ckb_repo + ckb_repo="$(release_ckb_repo_from_args "$@")" check_release_source_identity run_ci_gate - run_release_auxiliary_checks + run_release_auxiliary_checks "$ckb_repo" run ./scripts/ckb_cellscript_acceptance.sh --compile-only --production "$@" printf '\nCellScript backend shape report: %s\n' "$CELLSCRIPT_BACKEND_SHAPE_REPORT" printf 'CellScript Molecule schema manifest report: %s\n' "$CELLSCRIPT_MOLECULE_SCHEMA_MANIFEST_REPORT" } run_release_gate() { + local ckb_repo + ckb_repo="$(release_ckb_repo_from_args "$@")" check_release_source_identity run_ci_gate - run_release_auxiliary_checks + run_release_auxiliary_checks "$ckb_repo" run ./scripts/ckb_cellscript_acceptance.sh --production --stateful-scenarios "$@" printf '\nCellScript backend shape report: %s\n' "$CELLSCRIPT_BACKEND_SHAPE_REPORT" printf 'CellScript Molecule schema manifest report: %s\n' "$CELLSCRIPT_MOLECULE_SCHEMA_MANIFEST_REPORT" diff --git a/scripts/cellscript_ls_idl_upstream_acceptance.sh b/scripts/cellscript_ls_idl_upstream_acceptance.sh new file mode 100755 index 00000000..ce4f4875 --- /dev/null +++ b/scripts/cellscript_ls_idl_upstream_acceptance.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +DERIVE_REPO="${CKB_IDL_DERIVE_REPO:-$REPO_ROOT/../ckb-idl-derive}" +CLIENT_REPO="${CKB_IDL_CLIENT_REPO:-$REPO_ROOT/../ckb-idl-client}" +SCRIPTS_REPO="${CKB_IDL_SCRIPTS_REPO:-$REPO_ROOT/../ckb_sudt_script}" + +DERIVE_COMMIT="e7ee35766b9084099e9d840ccd37d2b5d40074a1" +CLIENT_COMMIT="7d883e0abccba56d423449b673567ee817747936" +SCRIPTS_COMMIT="c20ce3f4813100b78076fd447a0234bb5ad46bbb" +RUNTIME_RUST_TOOLCHAIN="1.97.1" +RUNTIME_PARENT="" +RUNTIME_REPO="" + +cleanup_runtime_worktree() { + if [[ -n "$RUNTIME_REPO" && -d "$RUNTIME_REPO" ]]; then + git -C "$SCRIPTS_REPO" worktree remove --force "$RUNTIME_REPO" >/dev/null 2>&1 || true + fi + if [[ -n "$RUNTIME_PARENT" && -d "$RUNTIME_PARENT" ]]; then + rmdir "$RUNTIME_PARENT" >/dev/null 2>&1 || true + fi +} +trap cleanup_runtime_worktree EXIT + +usage() { + cat <<'USAGE' +Usage: scripts/cellscript_ls_idl_upstream_acceptance.sh \ + [--derive-repo ] [--client-repo ] [--scripts-repo ] + +Runs the opt-in LS-IDL compatibility check against clean, pinned upstream +checkouts. It validates upstream IDL bytes, runs upstream schema/wire tests, +executes the actual ckb-idl-client Rust crate against CellScript Registry's +/idl/:code_hash compatibility handler, and builds the merged upstream runtime +fixes in a disposable worktree before binding and running the example Lock +Scripts in CKB-VM. + +This script is not part of any CellScript release gate. +USAGE +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --derive-repo) + DERIVE_REPO="${2:?missing value for --derive-repo}" + shift 2 + ;; + --derive-repo=*) + DERIVE_REPO="${1#*=}" + shift + ;; + --client-repo) + CLIENT_REPO="${2:?missing value for --client-repo}" + shift 2 + ;; + --client-repo=*) + CLIENT_REPO="${1#*=}" + shift + ;; + --scripts-repo) + SCRIPTS_REPO="${2:?missing value for --scripts-repo}" + shift 2 + ;; + --scripts-repo=*) + SCRIPTS_REPO="${1#*=}" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +for command in cargo git make mktemp node npm sha256sum; do + if ! command -v "$command" >/dev/null 2>&1; then + echo "missing required command: $command" >&2 + exit 127 + fi +done + +node_major="$(node --version | sed -E 's/^v([0-9]+).*/\1/')" +if [[ "$node_major" != "22" ]]; then + echo "LS-IDL upstream acceptance requires Node.js 22; found $(node --version)" >&2 + exit 1 +fi + +require_pinned_repo() { + local label="$1" path="$2" expected_commit="$3" actual_commit tracked_changes + if [[ ! -d "$path/.git" ]]; then + echo "$label checkout is missing: $path" >&2 + exit 1 + fi + actual_commit="$(git -C "$path" rev-parse HEAD)" + if [[ "$actual_commit" != "$expected_commit" ]]; then + echo "$label must be at $expected_commit; found $actual_commit" >&2 + exit 1 + fi + tracked_changes="$(git -C "$path" status --short --untracked-files=no)" + if [[ -n "$tracked_changes" ]]; then + echo "$label checkout has tracked changes: $path" >&2 + echo "$tracked_changes" >&2 + exit 1 + fi +} + +require_sha256() { + local path="$1" expected="$2" actual + if [[ ! -f "$path" ]]; then + echo "pinned LS-IDL fixture is missing: $path" >&2 + exit 1 + fi + actual="$(sha256sum "$path" | awk '{print $1}')" + if [[ "$actual" != "$expected" ]]; then + echo "raw-byte SHA-256 mismatch for $path: expected $expected, found $actual" >&2 + exit 1 + fi +} + +require_pinned_repo "ckb-idl-derive" "$DERIVE_REPO" "$DERIVE_COMMIT" +require_pinned_repo "ckb-idl-client" "$CLIENT_REPO" "$CLIENT_COMMIT" +require_pinned_repo "ckb_sudt_script" "$SCRIPTS_REPO" "$SCRIPTS_COMMIT" + +require_sha256 "$CLIENT_REPO/test-vectors.json" "a9a6dca4fd0c5fcd2ca7aea6468784be7fdb29d6274049f07090cbab0ce9c1bb" +require_sha256 "$DERIVE_REPO/example-idls/multisig-2of2-nonce/idl.json" "587098bbe12e37a7394d06ff711a59242f033759e9ba7f5b62b8f6a234275063" +require_sha256 "$DERIVE_REPO/example-idls/pow-lock/idl.json" "d551803734459f28b2849f13b2111778d3753b518701a86a434e9438df86e2d6" +require_sha256 "$DERIVE_REPO/example-idls/schnorr-pubkey-recovery/idl.json" "b37329b5fb13b25de94ef068724839f356096bc3516dda461b516ee983a8d371" +require_sha256 "$DERIVE_REPO/example-idls/secp256k1-timelock/idl.json" "056bc4f2b11bc7f0dfead9f2dcc0ec5097b42b353d4577b3836ef872b121710f" +require_sha256 "$DERIVE_REPO/example-idls/simple-lock/idl.json" "d28abead992546908eb483c24667e58302f193c00e08f6cbed1a6302995ca1c0" +require_sha256 "$SCRIPTS_REPO/contracts/simple-lock/idl.json" "6fd2ab0171167c6862582c4e95a6de7b1cd153f77a936af7e52be6599ddddd31" +require_sha256 "$SCRIPTS_REPO/contracts/timelock-lock/idl.json" "18ae57828b5fbd0c8df0900eed1153e7585587d4049900c50729616227a9beda" + +cargo test --locked --manifest-path "$DERIVE_REPO/Cargo.toml" +cargo test --locked --manifest-path "$CLIENT_REPO/Cargo.toml" --lib +cargo test --locked --manifest-path "$SCRIPTS_REPO/Cargo.toml" -p tests witness_validation +cargo test --locked --manifest-path "$SCRIPTS_REPO/Cargo.toml" -p tests test_idl_has_three_fields + +idl_files=( + "$DERIVE_REPO/example-idls/multisig-2of2-nonce/idl.json" + "$DERIVE_REPO/example-idls/pow-lock/idl.json" + "$DERIVE_REPO/example-idls/schnorr-pubkey-recovery/idl.json" + "$DERIVE_REPO/example-idls/secp256k1-timelock/idl.json" + "$DERIVE_REPO/example-idls/simple-lock/idl.json" + "$SCRIPTS_REPO/contracts/simple-lock/idl.json" + "$SCRIPTS_REPO/contracts/timelock-lock/idl.json" +) +for idl_file in "${idl_files[@]}"; do + cargo run --quiet --locked --manifest-path "$REPO_ROOT/Cargo.toml" -p cellscript --bin cellc -- \ + artifact ls-idl validate --idl "$idl_file" +done + +cargo test --locked --manifest-path "$REPO_ROOT/Cargo.toml" -p cellscript --test ls_idl_upstream +CELLSCRIPT_CKB_IDL_CLIENT_REPO="$CLIENT_REPO" \ +CELLSCRIPT_LS_IDL_CARGO_TARGET_DIR="$REPO_ROOT/target/ls-idl-upstream-client" \ + npm --prefix "$REPO_ROOT/services/registry-api" test -- \ + test/registry-api.test.ts -t "interoperates with the pinned upstream Rust client" + +RUNTIME_PARENT="$(mktemp -d "${TMPDIR:-/tmp}/cellscript-ls-idl-runtime.XXXXXX")" +RUNTIME_REPO="$RUNTIME_PARENT/ckb_sudt_script" +git -C "$SCRIPTS_REPO" worktree add --quiet --detach "$RUNTIME_REPO" "$SCRIPTS_COMMIT" + +RUSTUP_TOOLCHAIN="$RUNTIME_RUST_TOOLCHAIN" RUSTC_WRAPPER= \ + make -C "$RUNTIME_REPO" build CARGO_ARGS=--locked + +BOUND_DIR="$RUNTIME_REPO/build/ls-idl-bound" +mkdir -p "$BOUND_DIR" +for script_name in simple-lock timelock-lock; do + idl_path="$RUNTIME_REPO/contracts/$script_name/idl.json" + executable_path="$RUNTIME_REPO/build/release/$script_name" + bound_path="$BOUND_DIR/$script_name" + cargo run --quiet --locked --manifest-path "$REPO_ROOT/Cargo.toml" -p cellscript --bin cellc -- \ + artifact ls-idl bind --idl "$idl_path" --executable "$executable_path" --output "$bound_path" + cargo run --quiet --locked --manifest-path "$REPO_ROOT/Cargo.toml" -p cellscript --bin cellc -- \ + artifact ls-idl validate --idl "$idl_path" --executable "$bound_path" + cp "$bound_path" "$executable_path" +done + +RUSTUP_TOOLCHAIN="$RUNTIME_RUST_TOOLCHAIN" RUSTC_WRAPPER= \ + cargo test --locked --manifest-path "$RUNTIME_REPO/Cargo.toml" -p tests -- --test-threads=1 + +echo "Pinned LS-IDL upstream compatibility acceptance passed." diff --git a/scripts/cellscript_strict_backend_audit.py b/scripts/cellscript_strict_backend_audit.py deleted file mode 100755 index 34e8978a..00000000 --- a/scripts/cellscript_strict_backend_audit.py +++ /dev/null @@ -1,210 +0,0 @@ -#!/usr/bin/env python3 -import json -import os -import subprocess -import sys -import time -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[1] - - -FEATURE_IDS = [ - "ir.cfg.block-id-uniqueness", - "ir.cfg.terminator-targets", - "ir.cfg.reachability", - "ir.defs.must-define-before-use", - "ir.abi.call-arg-types", - "ir.abi.return-types", - "codegen.psabi.sp-delta-alignment", - "codegen.psabi.outgoing-stack-args-0-through-20", - "codegen.tuple-return-register-contract", - "codegen.runtime-fail-closed-syscall-contracts", - "riscv.oracle.core-instruction-bytes", - "riscv.oracle.immediate-boundaries", - "riscv.branch-relaxation.near-and-far", - "riscv.machine-cfg.layout-coverage", - "riscv.elf.header-and-segment-layout", - "edge.match-wildcard-order", - "edge.tuple-projection-through-branching", - "edge.bytestring-length", - "edge.import-alias-callable-rename", - "metamorphic.numeric-type-equality-commutative", - "acceptance.syntax-combo", - "acceptance.ckb-stateful-scenarios", -] - - -def command_plan(mode: str) -> list[dict]: - commands = [ - { - "id": "strict-rust-contract-tests", - "feature_ids": [ - "ir.cfg.block-id-uniqueness", - "ir.cfg.terminator-targets", - "ir.cfg.reachability", - "ir.defs.must-define-before-use", - "ir.abi.call-arg-types", - "ir.abi.return-types", - "codegen.psabi.sp-delta-alignment", - "riscv.oracle.core-instruction-bytes", - "riscv.oracle.immediate-boundaries", - "riscv.elf.header-and-segment-layout", - ], - "argv": ["cargo", "test", "--locked", "-p", "cellscript", "strict_audit", "--", "--nocapture"], - }, - { - "id": "outgoing-stack-abi-matrix", - "feature_ids": ["codegen.psabi.outgoing-stack-args-0-through-20"], - "argv": [ - "cargo", - "test", - "--locked", - "-p", - "cellscript", - "outgoing_stack_arg_area_is_16_byte_aligned_at_call_boundaries", - "--", - "--nocapture", - ], - }, - { - "id": "assembler-emitted-surface", - "feature_ids": ["riscv.machine-cfg.layout-coverage"], - "argv": ["cargo", "test", "--locked", "-p", "cellscript", "internal_assembler_encodes_emitted_instruction_surface", "--", "--nocapture"], - }, - { - "id": "branch-relaxation-contracts", - "feature_ids": ["riscv.branch-relaxation.near-and-far"], - "argv": ["cargo", "test", "--locked", "-p", "cellscript", "relaxes", "--", "--nocapture"], - }, - { - "id": "tuple-return-abi-contracts", - "feature_ids": ["codegen.tuple-return-register-contract"], - "argv": ["cargo", "test", "--locked", "-p", "cellscript", "tuple_return_abi_rejects_more_than_eight_fields", "--", "--nocapture"], - }, - { - "id": "runtime-fail-closed-contracts", - "feature_ids": ["codegen.runtime-fail-closed-syscall-contracts"], - "argv": ["cargo", "test", "--locked", "-p", "cellscript", "ckb_u64_syscall_helpers_check_return_code_and_size", "--", "--nocapture"], - }, - { - "id": "backend-shape-contracts", - "feature_ids": ["riscv.machine-cfg.layout-coverage"], - "argv": ["cargo", "test", "--locked", "-p", "cellscript", "bundled_examples_stay_within_backend_shape_budgets", "--", "--nocapture"], - }, - { - "id": "wildcard-match-order-contract", - "feature_ids": ["edge.match-wildcard-order"], - "argv": ["cargo", "test", "--locked", "-p", "cellscript", "compile_rejects_invalid_enum_match_patterns", "--", "--nocapture"], - }, - { - "id": "tuple-projection-branching-contracts", - "feature_ids": ["edge.tuple-projection-through-branching"], - "argv": ["cargo", "test", "--locked", "-p", "cellscript", "compile_preserves_", "--", "--nocapture"], - }, - { - "id": "bytestring-length-contracts", - "feature_ids": ["edge.bytestring-length"], - "argv": ["cargo", "test", "--locked", "-p", "cellscript", "byte_string", "--", "--nocapture"], - }, - { - "id": "import-alias-callable-rename-contract", - "feature_ids": ["edge.import-alias-callable-rename"], - "argv": [ - "cargo", - "test", - "--locked", - "-p", - "cellscript", - "compile_package_import_alias_emits_matching_external_callable", - "--", - "--nocapture", - ], - }, - { - "id": "numeric-type-equality-metamorphic-contract", - "feature_ids": ["metamorphic.numeric-type-equality-commutative"], - "argv": ["cargo", "test", "--locked", "-p", "cellscript", "numeric_named_type_equality_is_commutative", "--", "--nocapture"], - }, - ] - if mode in {"ci", "full", "nightly"}: - commands.append( - { - "id": "syntax-combo-audit", - "feature_ids": ["acceptance.syntax-combo"], - "argv": ["scripts/cellscript_syntax_combo_audit.sh", "ci"], - } - ) - if mode in {"full", "nightly"}: - commands.append( - { - "id": "ckb-stateful-scenarios", - "feature_ids": ["acceptance.ckb-stateful-scenarios"], - "argv": ["scripts/cellscript_ckb_stateful_scenarios.sh"], - } - ) - return commands - - -def run_command(spec: dict) -> dict: - started = time.time() - proc = subprocess.run(spec["argv"], cwd=ROOT, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - duration = round(time.time() - started, 3) - output = (proc.stdout + "\n" + proc.stderr).strip() - return { - "id": spec["id"], - "feature_ids": spec["feature_ids"], - "argv": spec["argv"], - "status": "passed" if proc.returncode == 0 else "failed", - "exit_code": proc.returncode, - "duration_seconds": duration, - "output_tail": output[-12000:], - } - - -def default_report_path(mode: str) -> Path: - stamp = time.strftime("%Y%m%d-%H%M%S") - return ROOT / "target" / "cellscript-strict-backend-audit" / f"strict-backend-audit-{mode}-{stamp}.json" - - -def main() -> int: - mode = sys.argv[1] if len(sys.argv) > 1 else "quick" - if mode not in {"quick", "ci", "full", "nightly"}: - print("usage: cellscript_strict_backend_audit.py [quick|ci|full|nightly]", file=sys.stderr) - return 2 - - report_path = Path(os.environ.get("CELLSCRIPT_STRICT_BACKEND_AUDIT_REPORT", default_report_path(mode))) - report_path.parent.mkdir(parents=True, exist_ok=True) - - commands = command_plan(mode) - results = [] - tested = set() - for spec in commands: - print(f"==> {spec['id']}: {' '.join(spec['argv'])}", flush=True) - result = run_command(spec) - results.append(result) - if result["status"] == "passed": - tested.update(result["feature_ids"]) - - missing = sorted(set(FEATURE_IDS) - tested) - failed = [result["id"] for result in results if result["status"] != "passed"] - report = { - "audit": "cellscript-strict-codegen-ir-riscv", - "mode": mode, - "status": "failed" if failed else "passed", - "feature_ids": FEATURE_IDS, - "tested_feature_ids": sorted(tested), - "missing_feature_ids": missing, - "failed_commands": failed, - "artifact_hashes": [], - "ckb_vm": {"cycles": None, "transaction_size_bytes": None}, - "commands": results, - } - report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n") - print(f"strict backend audit report: {report_path}") - return 1 if failed else 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/cellscript_strict_backend_audit.sh b/scripts/cellscript_strict_backend_audit.sh index c0a715a3..64e7974c 100755 --- a/scripts/cellscript_strict_backend_audit.sh +++ b/scripts/cellscript_strict_backend_audit.sh @@ -8,4 +8,5 @@ if [[ $# -gt 0 ]]; then fi cd "$ROOT_DIR" -python3 scripts/cellscript_strict_backend_audit.py "$MODE" "$@" +exec cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root "$ROOT_DIR" strict-backend "$MODE" "$@" diff --git a/scripts/cellscript_syntax_combo_audit.py b/scripts/cellscript_syntax_combo_audit.py deleted file mode 100755 index cf0866c6..00000000 --- a/scripts/cellscript_syntax_combo_audit.py +++ /dev/null @@ -1,2364 +0,0 @@ -#!/usr/bin/env python3 -"""Matrix-driven CellScript syntax-combination audit runner. - -The runner is intentionally token-light: stdout prints a compact summary and the -full command outputs/artifacts stay under target/syntax-combo-audit/. -""" - -from __future__ import annotations - -import argparse -import datetime as dt -import hashlib -import json -import os -import random -import shutil -import subprocess -import sys -import textwrap -from dataclasses import dataclass, field -from pathlib import Path -from typing import Any - -try: - import tomllib -except ModuleNotFoundError: # pragma: no cover - exercised only by older Python runners. - try: - import tomli as tomllib # type: ignore[import-not-found] - except ModuleNotFoundError: - tomllib = None # type: ignore[assignment] - - -ROOT = Path(__file__).resolve().parents[1] -MATRIX = ROOT / "tests" / "syntax_combo" / "matrix.toml" -SEEDS = ROOT / "tests" / "syntax_combo" / "seeds" - -MODE_RANK = {"quick": 0, "ci": 1, "deep": 2, "repro": 3} - -GOVERNANCE_RELEASE_MATRIX: tuple[dict[str, str], ...] = ( - { - "track": "canonical_action_lock_surface", - "layer": "parser_formatter_lsp_docs", - "status": "covered_by_gate", - "evidence": "action and lock cases parse, format, and use the verification section", - "gate": "syntax-combo accepted action/lock cases plus VS Code validate/dry-run in release gate", - }, - { - "track": "local_explicit_sugar", - "layer": "type_lowering_metadata", - "status": "covered_by_gate", - "evidence": "preserve and anonymous require-block cases are type/effect checked and metadata-checked", - "gate": "syntax-combo preserve/require-block positive and negative cases", - }, - { - "track": "stdlib_lifecycle_patterns", - "layer": "type_lowering_metadata_codegen", - "status": "covered_by_gate", - "evidence": "transfer/claim/settle emit consume, create, locked output, and field obligations", - "gate": "syntax-combo stdlib lifecycle metadata oracles", - }, - { - "track": "source_qualifier_boundary", - "layer": "type_effect", - "status": "covered_by_gate", - "evidence": "read/protected/witness/lock_args boundaries reject linear lifecycle misuse", - "gate": "syntax-combo lock source qualifier and read-param reject cases", - }, - { - "track": "deferred_rejected_surfaces", - "layer": "parser_type_policy", - "status": "covered_by_gate", - "evidence": "unknown stdlib patterns and hidden lifecycle proof forms fail closed", - "gate": "syntax-combo reject seeds and required bug classes", - }, - { - "track": "metadata_fidelity", - "layer": "ir_metadata_codegen", - "status": "covered_by_gate", - "evidence": "accepted cases compile to non-empty assembly and metadata matches consume/create/lock obligations", - "gate": "syntax-combo metadata/codegen oracles", - }, -) - -BUG_CLASS_CONTRACTS: tuple[dict[str, Any], ...] = ( - { - "id": "SCA-BUG-STD-LIFECYCLE-LOCKED-OUTPUT", - "name": "stdlib lifecycle pattern must create and lock the declared output", - "min_mode": "quick", - "required_cases": ("stdlib-transfer",), - "required_origins": ("generated",), - "release_boundary": "std::lifecycle::transfer(input, output, to) cannot drop to or omit create output with_lock(to)", - }, - { - "id": "SCA-BUG-PRESERVE-TYPE-EQUIVALENCE", - "name": "preserve sugar must be type-equivalent to canonical require equality", - "min_mode": "quick", - "required_cases": ("reject-preserve-type-mismatch",), - "required_origins": ("generated",), - "release_boundary": "preserve output from input { field } must reject field type mismatches", - }, - { - "id": "SCA-BUG-REQUIRE-BLOCK-PURITY", - "name": "anonymous require block cannot hide lifecycle or verifier-boundary operations", - "min_mode": "quick", - "required_cases": ("reject-require-block-lifecycle", "seed-require-block-lifecycle"), - "required_origins": ("generated", "tests/syntax_combo/seeds/require-block-lifecycle.cell"), - "release_boundary": "require { ... } remains pure boolean grouping sugar", - }, - { - "id": "SCA-BUG-STDLIB-NAMESPACE-FAIL-CLOSED", - "name": "unknown stdlib namespaces and helper names fail closed", - "min_mode": "quick", - "required_cases": ("reject-unknown-stdlib",), - "required_origins": ("generated",), - "release_boundary": "unsupported std::* calls cannot compile as inert boolean expressions", - }, - { - "id": "SCA-BUG-SOURCE-QUALIFIER-LINEARITY", - "name": "source-qualified values cannot be consumed by lifecycle operations", - "min_mode": "quick", - "required_cases": ("reject-consume-read-param",), - "required_origins": ("generated",), - "release_boundary": "read/protected/witness/lock_args values do not escape into consume/destroy/stdlib lifecycle", - }, - { - "id": "SCA-BUG-RECEIPT-CLAIM-CONTRACT", - "name": "receipt claim helpers require receipt inputs and declared claim output type", - "min_mode": "quick", - "required_cases": ("reject-claim-without-output-arrow",), - "required_origins": ("generated",), - "release_boundary": "claim semantics come from stdlib helper validation, not action names", - }, - { - "id": "SCA-BUG-LOCK-SOURCE-QUALIFIERS", - "name": "lock protected, witness, and lock_args source qualifiers stay parse/type checked", - "min_mode": "quick", - "required_cases": ("lock-source-qualifiers",), - "required_origins": ("generated",), - "release_boundary": "lock authorization data sources remain explicit in the surface and metadata path", - }, - { - "id": "SCA-BUG-0.22-CELLSET-UNBOUNDED", - "name": "transaction-backed Cell collections require an explicit finite maximum cardinality", - "min_mode": "quick", - "required_cases": ("seed-bounded-collection-missing-cardinality-reject",), - "required_origins": ("tests/syntax_combo/seeds/bounded-collection-missing-cardinality-reject.cell",), - "release_boundary": "BoundedCellSet cannot omit N or use an unbounded transaction source", - }, - { - "id": "SCA-BUG-0.22-CELLSET-VEC-RESOURCE", - "name": "generic Vec cannot stand in for a source-aware Cell set", - "min_mode": "quick", - "required_cases": ("seed-bounded-collection-vec-resource-reject",), - "required_origins": ("tests/syntax_combo/seeds/bounded-collection-vec-resource-reject.cell",), - "release_boundary": "transaction Cell membership and ownership are never inferred from local Vec storage", - }, - { - "id": "SCA-BUG-0.22-CONSUME-EACH-DUPLICATE", - "name": "consume_each consumes one bounded Cell set exactly once", - "min_mode": "quick", - "required_cases": ("seed-bounded-collection-duplicate-consume-reject",), - "required_origins": ("tests/syntax_combo/seeds/bounded-collection-duplicate-consume-reject.cell",), - "release_boundary": "linear bounded input sets cannot be consumed twice or silently partially consumed", - }, - { - "id": "SCA-BUG-0.22-CREATE-EACH-CARDINALITY-MISSING", - "name": "create_each carries output cardinality and capacity builder obligations", - "min_mode": "quick", - "required_cases": ("seed-bounded-collection",), - "required_origins": ("tests/syntax_combo/seeds/bounded-collection.cell",), - "release_boundary": "bounded output plans compile only with metadata and ProofPlan builder-evidence contracts", - }, - { - "id": "SCA-BUG-0.22-VALIDITY-EVIDENCE-MISSING", - "name": "type validity predicates carry canonical metadata and ProofPlan evidence tiers", - "min_mode": "quick", - "required_cases": ("seed-type-validity",), - "required_origins": ("tests/syntax_combo/seeds/type-validity.cell",), - "release_boundary": "every accepted validity predicate is paired with a canonical evidence tier and ProofPlan record", - }, - { - "id": "SCA-BUG-0.22-VALIDITY-ENV-UNKNOWN", - "name": "unknown validity environment reads fail closed", - "min_mode": "quick", - "required_cases": ("seed-type-validity-unknown-env-reject",), - "required_origins": ("tests/syntax_combo/seeds/type-validity-unknown-env-reject.cell",), - "release_boundary": "env::block_number is the only approved 0.22 validity environment read", - }, - { - "id": "SCA-BUG-0.22-BORROW-EFFECT-COMPAT", - "name": "borrowed linear views may reach only Pure or ReadOnly helpers with dedicated &T parameters", - "min_mode": "quick", - "required_cases": ("seed-explicit-borrow", "seed-explicit-borrow-effect-reject"), - "required_origins": ( - "tests/syntax_combo/seeds/explicit-borrow.cell", - "tests/syntax_combo/seeds/explicit-borrow-effect-reject.cell", - ), - "release_boundary": "borrow calls are checked against authenticated callable effects and explicit read-only reference parameters", - }, - { - "id": "SCA-BUG-0.22-BORROW-ESCAPE", - "name": "borrowed View markers cannot acquire layout, storage, ABI, or return representation", - "min_mode": "quick", - "required_cases": ("seed-explicit-borrow-escape-reject",), - "required_origins": ("tests/syntax_combo/seeds/explicit-borrow-escape-reject.cell",), - "release_boundary": "borrow markers cannot escape through local aggregates, assignments, returns, or generic calls", - }, - { - "id": "SCA-BUG-0.22-BORROW-CROSSES-CONSUME", - "name": "borrowed views cannot cross lifecycle discharge of their linear root", - "min_mode": "quick", - "required_cases": ("seed-explicit-borrow-cross-consume-reject",), - "required_origins": ("tests/syntax_combo/seeds/explicit-borrow-cross-consume-reject.cell",), - "release_boundary": "every path rejects consume, destroy, transfer, claim, or settle of a root while its borrow block is active", - }, - { - "id": "SCA-BUG-0.22-CAPABILITY-OVERGRANT", - "name": "composite lifecycle authority is derived only by the closed versioned entailment relation", - "min_mode": "quick", - "required_cases": ("seed-capability-entailment", "seed-capability-missing-identity-reject"), - "required_origins": ( - "tests/syntax_combo/seeds/capability-entailment.cell", - "tests/syntax_combo/seeds/capability-missing-identity-reject.cell", - ), - "release_boundary": "destroy requires consume+burn and replace_unique requires replace plus an exact declared identity condition", - }, - { - "id": "SCA-BUG-0.22-CAPABILITY-TRANSITIVE-GRANT", - "name": "container capability sets never grant authority over another Cell resource", - "min_mode": "quick", - "required_cases": ("seed-capability-transitive-grant-reject",), - "required_origins": ("tests/syntax_combo/seeds/capability-transitive-grant-reject.cell",), - "release_boundary": "capability lookup uses the exact lifecycle operand type and does not traverse container-like declarations", - }, - { - "id": "SCA-BUG-0.22-PAYLOAD-MATCH-NONEXHAUSTIVE", - "name": "payload enum matches remain exhaustive after destructuring", - "min_mode": "quick", - "required_cases": ("seed-payload-enum", "seed-payload-enum-nonexhaustive-reject"), - "required_origins": ( - "tests/syntax_combo/seeds/payload-enum.cell", - "tests/syntax_combo/seeds/payload-enum-nonexhaustive-reject.cell", - ), - "release_boundary": "every concrete payload variant is covered exactly once unless a final non-linear wildcard arm is explicit", - }, - { - "id": "SCA-BUG-0.22-PAYLOAD-DYNAMIC-ACCEPTED", - "name": "payload enum layout accepts only concrete fixed-width values", - "min_mode": "quick", - "required_cases": ("seed-payload-enum-dynamic-reject", "seed-payload-enum-generic-reject"), - "required_origins": ( - "tests/syntax_combo/seeds/payload-enum-dynamic-reject.cell", - "tests/syntax_combo/seeds/payload-enum-generic-reject.cell", - ), - "release_boundary": "dynamic and generic payload ADTs fail closed before IR, ABI, or metadata claims are emitted", - }, - { - "id": "SCA-BUG-0.22-PAYLOAD-LINEAR-DROP", - "name": "linear Cell payload ownership is discharged inside every match arm", - "min_mode": "quick", - "required_cases": ("seed-payload-enum-linear-drop-reject",), - "required_origins": ("tests/syntax_combo/seeds/payload-enum-linear-drop-reject.cell",), - "release_boundary": "a Cell payload cannot disappear through wildcard binding or implicit arm-local drop", - }, - { - "id": "SCA-BUG-0.22-PROTOCOLGRAPH-ROLE-OVERCLAIM", - "name": "field-name role hints remain weak metadata and never authorization evidence", - "min_mode": "quick", - "required_cases": ("seed-protocolgraph-role-weak",), - "required_origins": ("tests/syntax_combo/seeds/protocolgraph-role-weak.cell",), - "release_boundary": "a participant-like Address field records source=field-name, evidence_tier=metadata-only, and authorization_proven=false", - }, - { - "id": "SCA-BUG-0.22-PROTOCOLGRAPH-ROLE-CONFLICT", - "name": "conflicting ProtocolGraph role sources remain attributed and deterministically ordered", - "min_mode": "quick", - "required_cases": ("seed-protocolgraph-role-conflict",), - "required_origins": ("tests/syntax_combo/seeds/protocolgraph-role-conflict.cell",), - "release_boundary": "explicit predicates precede witness/lock_args bindings and weak field names without entering ProofPlan", - }, - { - "id": "SCA-BUG-STDLIB-ARGUMENT-VALIDATION", - "name": "stdlib lifecycle helpers validate arity, cell kind, lock target, and claim output", - "min_mode": "ci", - "required_cases": ( - "matrix-reject-claim-non-receipt", - "matrix-reject-claim-extra-args", - "matrix-reject-transfer-extra-args", - "matrix-reject-settle-missing-args", - "matrix-reject-claim-output-type-mismatch", - "matrix-reject-settle-lock-target-type", - ), - "required_origins": ("matrix:reject/stdlib-lifecycle",), - "release_boundary": "stdlib lifecycle patterns fail closed before lowering when arguments, lock targets, or claim outputs are invalid", - }, - { - "id": "SCA-BUG-METADATA-HELPER-VALIDATION", - "name": "cell metadata helpers reject non-cell arguments", - "min_mode": "ci", - "required_cases": ("matrix-reject-cell-metadata-non-cell",), - "required_origins": ("matrix:reject/metadata",), - "release_boundary": "std::cell::* metadata helpers cannot be used as generic boolean predicates", - }, - { - "id": "SCA-BUG-RECEIPT-LIFECYCLE-OUTPUT", - "name": "receipt claim and settle helpers emit locked output obligations", - "min_mode": "ci", - "required_cases": ("matrix-stdlib-claim-require-block", "matrix-stdlib-settle-preserve-capacity"), - "required_origins": ("matrix:receipt/proof", "matrix:receipt/metadata"), - "release_boundary": "claim/settle helpers must lower to explicit consume/create/lock obligations", - }, - { - "id": "SCA-BUG-DEEP-HIDDEN-LIFECYCLE", - "name": "deep reject variants keep stdlib lifecycle out of pure proof positions", - "min_mode": "deep", - "required_cases": ("matrix-deep-reject-require-block-transfer",), - "required_origins": ("matrix:deep/reject/proof-purity", "seeded:deep/reject"), - "release_boundary": "release-local deep replay covers hidden lifecycle mutations beyond the quick corpus", - }, - { - "id": "SCA-BUG-DEEP-READ-STDLIB-LIFECYCLE", - "name": "deep reject variants cover stdlib lifecycle on read parameters", - "min_mode": "deep", - "required_cases": ("matrix-deep-reject-transfer-read-param",), - "required_origins": ("matrix:deep/reject/source-qualifier",), - "release_boundary": "read-param lifecycle rejection is covered for both explicit consume and stdlib lifecycle syntax", - }, - { - "id": "SCA-BUG-DEEP-UNKNOWN-STDLIB", - "name": "deep reject variants cover unknown stdlib helper families", - "min_mode": "deep", - "required_cases": ("matrix-deep-reject-unknown-accounting",), - "required_origins": ("matrix:deep/reject/stdlib-namespace",), - "release_boundary": "unsupported helper families stay rejected under release-local deep replay", - }, - { - "id": "SCA-BUG-FLOW-EDGE-UNDECLARED", - "name": "flow state transitions must use edges declared in the flow block", - "min_mode": "ci", - "required_cases": ("reject-flow-undeclared-edge", "accept-flow-declared-cyclic-edge"), - "required_origins": ("generated",), - "release_boundary": "transition input.state: A -> output.state: B must fail closed when A -> B is not a declared flow edge", - }, - { - "id": "SCA-BUG-FLOW-CREATE-STATE-CONTRACT", - "name": "initial create of a flow type must set a statically known declared state", - "min_mode": "ci", - "required_cases": ("reject-flow-create-missing-state", "reject-flow-create-non-static-initial"), - "required_origins": ("generated",), - "release_boundary": "flow-typed create must set the state field to a declared state literal, not a runtime value", - }, - { - "id": "SCA-BUG-AGGREGATE-INVARIANT-CONTRACT", - "name": "xUDT group amount conservation invariant must lower to the matching runtime helper", - "min_mode": "ci", - "required_cases": ("accept-invariant-xudt-conserved",), - "required_origins": ("generated",), - "release_boundary": "assert_sum(group_outputs.amount) == assert_sum(group_inputs.amount) is recognised as the xUDT conserved aggregate and surfaces the runtime-helper-required gap", - }, -) - - -@dataclass(frozen=True) -class Expected: - phase: str - contains: tuple[str, ...] = () - - -@dataclass(frozen=True) -class Oracle: - action: str | None = None - consume_bindings: tuple[str, ...] = () - create_bindings: tuple[str, ...] = () - locked_outputs: tuple[str, ...] = () - create_fields: dict[str, tuple[str, ...]] = field(default_factory=dict) - obligation_contains: tuple[str, ...] = () - validity_type: str | None = None - validity_tiers: tuple[str, ...] = () - borrow_scope: str | None = None - borrow_view_type: str | None = None - capability_operation: str | None = None - capability_type: str | None = None - payload_enum: str | None = None - protocol_role_action: str | None = None - protocol_role: str | None = None - protocol_role_source: str | None = None - protocol_role_conflict: bool | None = None - - -@dataclass(frozen=True) -class AuditCase: - name: str - source: str - expected: Expected - oracle: Oracle = field(default_factory=Oracle) - origin: str = "generated" - - @property - def case_id(self) -> str: - digest = hashlib.blake2b( - f"{self.name}\n{self.source}".encode("utf-8"), - digest_size=6, - ).hexdigest() - return digest - - -def read_matrix() -> dict[str, Any]: - if not MATRIX.exists(): - return {} - text = MATRIX.read_text(encoding="utf-8") - if tomllib is not None: - return tomllib.loads(text) - return parse_matrix_toml_subset(text) - - -def parse_matrix_toml_subset(text: str) -> dict[str, Any]: - """Parse the matrix file subset needed by this runner. - - This fallback intentionally supports only the simple TOML shapes used by - tests/syntax_combo/matrix.toml: dotted tables, scalar ints/bools/strings, - and string arrays. - """ - root: dict[str, Any] = {} - current = root - lines = text.splitlines() - index = 0 - while index < len(lines): - raw = lines[index].strip() - index += 1 - if not raw or raw.startswith("#"): - continue - if raw.startswith("[") and raw.endswith("]"): - current = root - for part in raw[1:-1].split("."): - current = current.setdefault(part, {}) - continue - if "=" not in raw: - continue - key, value = [part.strip() for part in raw.split("=", 1)] - if value == "[": - items: list[str] = [] - while index < len(lines): - item = lines[index].strip() - index += 1 - if item == "]": - break - item = item.rstrip(",") - if item.startswith('"') and item.endswith('"'): - items.append(item[1:-1]) - current[key] = items - elif value.startswith("[") and value.endswith("]"): - raw_items = value[1:-1].strip() - current[key] = [] if not raw_items else [item.strip().strip('"') for item in raw_items.split(",")] - elif value.startswith('"') and value.endswith('"'): - current[key] = value[1:-1] - elif value in {"true", "false"}: - current[key] = value == "true" - else: - current[key] = int(value) - return root - - -def compact(text: str, limit: int = 1200) -> str: - text = text.replace(str(ROOT), "$ROOT") - if len(text) <= limit: - return text - return text[:limit] + "\n......" - - -def run_cmd(cmd: list[str], *, timeout: int = 30) -> subprocess.CompletedProcess[str]: - return subprocess.run( - cmd, - cwd=ROOT, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - timeout=timeout, - check=False, - ) - - -def require_tool(path_or_name: str) -> str: - if "/" in path_or_name: - path = Path(path_or_name) - if path.exists() and os.access(path, os.X_OK): - return str(path) - resolved = shutil.which(path_or_name) - if not resolved: - raise SystemExit(f"missing required tool: {path_or_name}") - return resolved - - -def cellc_bin() -> str: - env = os.environ.get("CELLC_BIN") - if env: - return require_tool(env) - target_dir_env = os.environ.get("CARGO_TARGET_DIR") - target_dir = Path(target_dir_env) if target_dir_env else ROOT / "target" - if not target_dir.is_absolute(): - target_dir = ROOT / target_dir - candidate = target_dir / "debug" / "cellc" - if candidate.exists() and os.access(candidate, os.X_OK): - return str(candidate) - build = run_cmd(["cargo", "build", "--locked", "--bin", "cellc"], timeout=120) - if build.returncode != 0: - raise SystemExit(compact(build.stdout, 4000)) - return str(candidate) - - -BASE_TYPES = """\ -module cellscript::audit::{module_name} - -resource Coin has store, create, consume, replace, burn, relock {{ - amount: u64, - nonce: u64, -}} - -receipt Voucher -> Coin has create, consume, burn {{ - amount: u64, - nonce: u64, - holder: Address, -}} - -resource Wallet has store, create, consume, replace, burn, relock {{ - owner: Address, -}} -""" - - -def module_source(module_name: str, body: str) -> str: - return BASE_TYPES.format(module_name=module_name) + "\n" + textwrap.dedent(body).strip() + "\n" - - -def matrix_cases(include_deep: bool) -> list[AuditCase]: - cases: list[AuditCase] = [] - - helper_specs = [ - ("preserve_type", "std::cell::preserve_type", ()), - ("same_lock", "std::cell::same_lock", ("cell-metadata-equality:lock_hash",)), - ("preserve_lock", "std::cell::preserve_lock", ("cell-metadata-equality:lock_hash",)), - ("preserve_capacity", "std::cell::preserve_capacity", ("cell-metadata-equality:capacity",)), - ("conserved", "std::accounting::conserved", ()), - ] - for short_name, helper, obligations in helper_specs: - action = f"matrix_{short_name}" - cases.append( - AuditCase( - name=f"matrix-cell-helper-{short_name}", - source=module_source( - f"matrix_cell_helper_{short_name}", - f""" - action {action}(coin_before: Coin) -> coin_after: Coin {{ - verification - {helper}(coin_after, coin_before) - }} - """, - ), - expected=Expected("accept"), - oracle=Oracle(action=action, obligation_contains=obligations), - origin="matrix:continuity/std-cell", - ) - ) - - cases.extend( - [ - AuditCase( - name="matrix-explicit-transfer-branch-require", - source=module_source( - "matrix_explicit_transfer_branch_require", - """ - action branch_keep(coin: Coin, to: Address) -> next_coin: Coin { - verification - consume coin - - create next_coin = Coin { - amount: coin.amount, - nonce: coin.nonce - } with_lock(to) - - if next_coin.amount == coin.amount { - require next_coin.nonce == coin.nonce - } else { - require next_coin.nonce == coin.nonce - } - } - """, - ), - expected=Expected("accept"), - oracle=Oracle( - action="branch_keep", - consume_bindings=("coin",), - create_bindings=("next_coin",), - locked_outputs=("next_coin",), - create_fields={"next_coin": ("amount", "nonce")}, - ), - origin="matrix:lifecycle/proof/control-flow", - ), - AuditCase( - name="matrix-explicit-transfer-let-proof", - source=module_source( - "matrix_explicit_transfer_let_proof", - """ - action let_keep(coin: Coin, to: Address) -> next_coin: Coin { - verification - consume coin - - create next_coin = Coin { - amount: coin.amount, - nonce: coin.nonce - } with_lock(to) - - let same_amount = next_coin.amount == coin.amount - require same_amount - require next_coin.nonce == coin.nonce - } - """, - ), - expected=Expected("accept"), - oracle=Oracle( - action="let_keep", - consume_bindings=("coin",), - create_bindings=("next_coin",), - locked_outputs=("next_coin",), - create_fields={"next_coin": ("amount", "nonce")}, - ), - origin="matrix:lifecycle/proof/local-binding", - ), - AuditCase( - name="matrix-stdlib-transfer-require-block", - source=module_source( - "matrix_stdlib_transfer_require_block", - """ - action transfer_with_block(coin: Coin, to: Address) -> next_coin: Coin { - verification - std::lifecycle::transfer(coin, next_coin, to) { - amount - nonce - } - - require { - next_coin.amount == coin.amount - next_coin.nonce == coin.nonce - } - } - """, - ), - expected=Expected("accept"), - oracle=Oracle( - action="transfer_with_block", - consume_bindings=("coin",), - create_bindings=("next_coin",), - locked_outputs=("next_coin",), - create_fields={"next_coin": ("amount", "nonce")}, - obligation_contains=("create-output-lock", "consume-input:Coin:coin"), - ), - origin="matrix:stdlib-lifecycle/proof", - ), - AuditCase( - name="matrix-stdlib-transfer-lock-capacity", - source=module_source( - "matrix_stdlib_transfer_lock_capacity", - """ - action transfer_with_metadata(coin: Coin, to: Address) -> next_coin: Coin { - verification - std::lifecycle::transfer(coin, next_coin, to) { - amount - nonce - } - std::cell::preserve_lock(next_coin, coin) - std::cell::preserve_capacity(next_coin, coin) - } - """, - ), - expected=Expected("accept"), - oracle=Oracle( - action="transfer_with_metadata", - consume_bindings=("coin",), - create_bindings=("next_coin",), - locked_outputs=("next_coin",), - create_fields={"next_coin": ("amount", "nonce")}, - obligation_contains=("cell-metadata-equality:lock_hash", "cell-metadata-equality:capacity"), - ), - origin="matrix:stdlib-lifecycle/metadata", - ), - AuditCase( - name="matrix-stdlib-claim-require-block", - source=module_source( - "matrix_stdlib_claim_require_block", - """ - action claim_with_block(voucher: Voucher) -> coin: Coin { - verification - std::receipt::claim(voucher, coin, voucher.holder) { - amount - nonce - } - - require { - coin.amount == voucher.amount - coin.nonce == voucher.nonce - } - } - """, - ), - expected=Expected("accept"), - oracle=Oracle( - action="claim_with_block", - consume_bindings=("voucher",), - create_bindings=("coin",), - locked_outputs=("coin",), - create_fields={"coin": ("amount", "nonce")}, - ), - origin="matrix:receipt/proof", - ), - AuditCase( - name="matrix-stdlib-settle-preserve-capacity", - source=module_source( - "matrix_stdlib_settle_preserve_capacity", - """ - action settle_with_capacity(voucher: Voucher) -> coin: Coin { - verification - std::lifecycle::settle(voucher, coin, voucher.holder) { - amount - nonce - } - std::cell::preserve_capacity(coin, voucher) - } - """, - ), - expected=Expected("accept"), - oracle=Oracle( - action="settle_with_capacity", - consume_bindings=("voucher",), - create_bindings=("coin",), - locked_outputs=("coin",), - create_fields={"coin": ("amount", "nonce")}, - obligation_contains=("cell-metadata-equality:capacity",), - ), - origin="matrix:receipt/metadata", - ), - AuditCase( - name="matrix-lock-protected-only", - source=module_source( - "matrix_lock_protected_only", - """ - lock protected_wallet(protected wallet: Wallet) -> bool { - verification - require wallet.owner == wallet.owner - } - """, - ), - expected=Expected("accept"), - origin="matrix:lock/source-qualifier", - ), - AuditCase( - name="matrix-lock-witness-only", - source=module_source( - "matrix_lock_witness_only", - """ - lock witness_owner(witness owner: Address) -> bool { - verification - require owner == owner - } - """, - ), - expected=Expected("accept"), - origin="matrix:lock/source-qualifier", - ), - AuditCase( - name="matrix-lock-args-only", - source=module_source( - "matrix_lock_args_only", - """ - lock args_owner(lock_args owner: Address) -> bool { - verification - require owner == owner - } - """, - ), - expected=Expected("accept"), - origin="matrix:lock/source-qualifier", - ), - AuditCase( - name="matrix-reject-require-block-assignment", - source=module_source( - "matrix_reject_require_block_assignment", - """ - action hidden_mutation(flag: bool) { - verification - let mut ok = flag - require { - ok = false - } - } - """, - ), - expected=Expected("reject_compile", ("require block", "assignment")), - origin="matrix:reject/proof-purity", - ), - AuditCase( - name="matrix-reject-claim-non-receipt", - source=module_source( - "matrix_reject_claim_non_receipt", - """ - action bad_claim(coin: Coin, to: Address) -> next_coin: Coin { - verification - std::receipt::claim(coin, next_coin, to) { - amount - nonce - } - } - """, - ), - expected=Expected("reject_compile", ("claim requires a receipt",)), - origin="matrix:reject/stdlib-lifecycle", - ), - AuditCase( - name="matrix-reject-claim-extra-args", - source=module_source( - "matrix_reject_claim_extra_args", - """ - action bad_claim(voucher: Voucher) -> coin: Coin { - verification - std::receipt::claim(voucher, coin, voucher.holder, voucher.holder) { - amount - nonce - } - } - """, - ), - expected=Expected("reject_compile", ("claim expects 3 arguments",)), - origin="matrix:reject/stdlib-lifecycle", - ), - AuditCase( - name="matrix-reject-transfer-extra-args", - source=module_source( - "matrix_reject_transfer_extra_args", - """ - action bad_transfer(coin: Coin, to: Address) -> next_coin: Coin { - verification - std::lifecycle::transfer(coin, next_coin, to, to) { - amount - nonce - } - } - """, - ), - expected=Expected("reject_compile", ("transfer expects 3 arguments",)), - origin="matrix:reject/stdlib-lifecycle", - ), - AuditCase( - name="matrix-reject-settle-missing-args", - source=module_source( - "matrix_reject_settle_missing_args", - """ - action bad_settle(voucher: Voucher) -> coin: Coin { - verification - std::lifecycle::settle(voucher, coin) { - amount - nonce - } - } - """, - ), - expected=Expected("reject_compile", ("settle expects 3 arguments",)), - origin="matrix:reject/stdlib-lifecycle", - ), - AuditCase( - name="matrix-reject-claim-output-type-mismatch", - source=module_source( - "matrix_reject_claim_output_type_mismatch", - """ - resource Badge has store, create, consume, replace, burn, relock { - amount: u64, - nonce: u64, - } - - action bad_claim_output(voucher: Voucher, to: Address) -> badge: Badge { - verification - std::receipt::claim(voucher, badge, to) { - amount - nonce - } - } - """, - ), - expected=Expected("reject_compile", ("claim output type mismatch",)), - origin="matrix:reject/stdlib-lifecycle", - ), - AuditCase( - name="matrix-reject-settle-lock-target-type", - source=module_source( - "matrix_reject_settle_lock_target_type", - """ - action bad_settle_lock(voucher: Voucher) -> coin: Coin { - verification - std::lifecycle::settle(voucher, coin, voucher.amount) { - amount - nonce - } - } - """, - ), - expected=Expected("reject_compile", ("settle lock target must be Address or Hash",)), - origin="matrix:reject/stdlib-lifecycle", - ), - AuditCase( - name="matrix-reject-cell-metadata-non-cell", - source=module_source( - "matrix_reject_cell_metadata_non_cell", - """ - action bad_metadata(amount: u64) -> out: Coin { - verification - std::cell::preserve_capacity(out, amount) - } - """, - ), - expected=Expected("reject_compile", ("preserve_capacity input must be a cell-backed value",)), - origin="matrix:reject/metadata", - ), - ] - ) - - if include_deep: - cases.extend( - [ - AuditCase( - name="matrix-deep-reject-transfer-read-param", - source=module_source( - "matrix_deep_reject_transfer_read_param", - """ - action bad_transfer(read coin: Coin, to: Address) -> next_coin: Coin { - verification - std::lifecycle::transfer(coin, next_coin, to) { - amount - nonce - } - } - """, - ), - expected=Expected("reject_compile", ("cell-backed linear",)), - origin="matrix:deep/reject/source-qualifier", - ), - AuditCase( - name="matrix-deep-reject-require-block-transfer", - source=module_source( - "matrix_deep_reject_require_block_transfer", - """ - action hidden_transfer(coin: Coin, to: Address) -> next_coin: Coin { - verification - require { - std::lifecycle::transfer(coin, next_coin, to) { - amount - nonce - } - } - } - """, - ), - expected=Expected("reject_compile", ("require block", "verifier-boundary syntax")), - origin="matrix:deep/reject/proof-purity", - ), - AuditCase( - name="matrix-deep-reject-unknown-accounting", - source=module_source( - "matrix_deep_reject_unknown_accounting", - """ - action bad_accounting(coin_before: Coin) -> coin_after: Coin { - verification - std::accounting::minted(coin_after, coin_before) - } - """, - ), - expected=Expected("reject_compile", ("unknown stdlib pattern",)), - origin="matrix:deep/reject/stdlib-namespace", - ), - ] - ) - - return cases - - -def generated_cases() -> list[AuditCase]: - cases: list[AuditCase] = [ - AuditCase( - name="explicit-transfer", - source=module_source( - "explicit_transfer", - """ - action transfer_coin(coin: Coin, to: Address) -> next_coin: Coin { - verification - consume coin - - create next_coin = Coin { - amount: coin.amount, - nonce: coin.nonce - } with_lock(to) - } - """, - ), - expected=Expected("accept"), - oracle=Oracle( - action="transfer_coin", - consume_bindings=("coin",), - create_bindings=("next_coin",), - locked_outputs=("next_coin",), - create_fields={"next_coin": ("amount", "nonce")}, - obligation_contains=("create-output-lock",), - ), - ), - AuditCase( - name="pure-require-block", - source=module_source( - "pure_require_block", - """ - action keep_fields(coin: Coin, to: Address) -> next_coin: Coin { - verification - consume coin - - create next_coin = Coin { - amount: coin.amount, - nonce: coin.nonce - } with_lock(to) - - require { - next_coin.amount == coin.amount - next_coin.nonce == coin.nonce - } - } - """, - ), - expected=Expected("accept"), - oracle=Oracle( - action="keep_fields", - consume_bindings=("coin",), - create_bindings=("next_coin",), - locked_outputs=("next_coin",), - create_fields={"next_coin": ("amount", "nonce")}, - ), - ), - AuditCase( - name="preserve-sugar", - source=module_source( - "preserve_sugar", - """ - action preserve_fields(coin: Coin, to: Address) -> next_coin: Coin { - verification - consume coin - - create next_coin = Coin { - amount: coin.amount, - nonce: coin.nonce - } with_lock(to) - - preserve next_coin from coin { - amount - nonce - } - } - """, - ), - expected=Expected("accept"), - oracle=Oracle( - action="preserve_fields", - consume_bindings=("coin",), - create_bindings=("next_coin",), - locked_outputs=("next_coin",), - create_fields={"next_coin": ("amount", "nonce")}, - ), - ), - AuditCase( - name="stdlib-transfer", - source=module_source( - "stdlib_transfer", - """ - action transfer_coin(coin: Coin, to: Address) -> next_coin: Coin { - verification - std::lifecycle::transfer(coin, next_coin, to) { - amount - nonce - } - } - """, - ), - expected=Expected("accept"), - oracle=Oracle( - action="transfer_coin", - consume_bindings=("coin",), - create_bindings=("next_coin",), - locked_outputs=("next_coin",), - create_fields={"next_coin": ("amount", "nonce")}, - obligation_contains=("create-output-lock", "consume-input:Coin:coin"), - ), - ), - AuditCase( - name="stdlib-claim", - source=module_source( - "stdlib_claim", - """ - action claim_voucher(voucher: Voucher) -> coin: Coin { - verification - std::receipt::claim(voucher, coin, voucher.holder) { - amount - nonce - } - } - """, - ), - expected=Expected("accept"), - oracle=Oracle( - action="claim_voucher", - consume_bindings=("voucher",), - create_bindings=("coin",), - locked_outputs=("coin",), - create_fields={"coin": ("amount", "nonce")}, - ), - ), - AuditCase( - name="stdlib-settle", - source=module_source( - "stdlib_settle", - """ - action settle_voucher(voucher: Voucher) -> coin: Coin { - verification - std::lifecycle::settle(voucher, coin, voucher.holder) { - amount - nonce - } - } - """, - ), - expected=Expected("accept"), - oracle=Oracle( - action="settle_voucher", - consume_bindings=("voucher",), - create_bindings=("coin",), - locked_outputs=("coin",), - create_fields={"coin": ("amount", "nonce")}, - ), - ), - AuditCase( - name="cell-metadata-helpers", - source=module_source( - "cell_metadata_helpers", - """ - action preserve_boundary(coin_before: Coin) -> coin_after: Coin { - verification - std::cell::preserve_type(coin_after, coin_before) - std::cell::preserve_lock(coin_after, coin_before) - std::cell::preserve_capacity(coin_after, coin_before) - std::accounting::conserved(coin_after, coin_before) - } - """, - ), - expected=Expected("accept"), - oracle=Oracle( - action="preserve_boundary", - obligation_contains=( - "cell-metadata-equality:lock_hash", - "cell-metadata-equality:capacity", - ), - ), - ), - AuditCase( - name="lock-source-qualifiers", - source=module_source( - "lock_source_qualifiers", - """ - lock owner_only( - protected wallet: Wallet, - lock_args owner: Address, - witness claimed_owner: Address - ) -> bool { - verification - require wallet.owner == owner - require claimed_owner == owner - } - """, - ), - expected=Expected("accept"), - ), - AuditCase( - name="if-tuple-projection", - source=module_source( - "if_tuple_projection", - """ - action choose(flag: bool) -> u64 { - verification - let pair = if flag { (1, 2) } else { (3, 4) } - return pair.0 - } - """, - ), - expected=Expected("accept"), - origin="matrix:edge/tuple-projection", - ), - AuditCase( - name="match-tuple-projection", - source=module_source( - "match_tuple_projection", - """ - enum Flag { - Off, - On, - } - - action choose(flag: Flag) -> u64 { - verification - let pair = match flag { - Flag::Off => { (1, 2) }, - _ => { (3, 4) }, - } - return pair.1 - } - """, - ), - expected=Expected("accept"), - origin="matrix:edge/tuple-projection", - ), - AuditCase( - name="byte-string-fixed-length", - source=module_source( - "byte_string_fixed_length", - """ - action symbol() -> [u8; 4] { - verification - return b"TEST" - } - """, - ), - expected=Expected("accept"), - origin="matrix:edge/bytestring-length", - ), - AuditCase( - name="reject-require-block-lifecycle", - source=module_source( - "reject_require_block_lifecycle", - """ - action bad(voucher: Voucher) -> coin: Coin { - verification - require { - std::receipt::claim(voucher, coin, voucher.holder) { - amount - nonce - } - } - } - """, - ), - expected=Expected("reject_compile", ("require block", "verifier-boundary syntax")), - ), - AuditCase( - name="reject-wildcard-match-non-last", - source=module_source( - "reject_wildcard_match_non_last", - """ - enum Flag { - Off, - On, - } - - action bad(flag: Flag) -> u64 { - verification - return match flag { - _ => { 1 }, - Flag::Off => { 2 }, - } - } - """, - ), - expected=Expected("reject_compile", ("wildcard pattern '_'", "last match arm")), - origin="matrix:edge/wildcard-match-order", - ), - AuditCase( - name="reject-byte-string-length-mismatch", - source=module_source( - "reject_byte_string_length_mismatch", - """ - action bad() -> [u8; 3] { - verification - return b"TEST" - } - """, - ), - expected=Expected("reject_compile", ("type mismatch",)), - origin="matrix:edge/bytestring-length", - ), - AuditCase( - name="reject-preserve-type-mismatch", - source="""\ -module cellscript::audit::reject_preserve_type_mismatch - -resource Coin has store, create, consume, replace, burn, relock { - amount: u64, -} - -resource BadCoin has store, create, consume, replace, burn, relock { - amount: bool, -} - -action bad(coin: Coin) -> bad_coin: BadCoin { - verification - preserve bad_coin from coin { - amount - } -} -""", - expected=Expected("reject_compile", ("type mismatch",)), - ), - AuditCase( - name="reject-transfer-missing-field", - source=module_source( - "reject_transfer_missing_field", - """ - action bad(coin: Coin, to: Address) -> next_coin: Coin { - verification - std::lifecycle::transfer(coin, next_coin, to) { - amount - } - } - """, - ), - expected=Expected("reject_compile", ("missing nonce",)), - ), - AuditCase( - name="reject-consume-read-param", - source=module_source( - "reject_consume_read_param", - """ - action bad(read coin: Coin) { - verification - consume coin - } - """, - ), - expected=Expected("reject_compile", ("cell-backed linear",)), - ), - AuditCase( - name="reject-unknown-stdlib", - source=module_source( - "reject_unknown_stdlib", - """ - action bad(coin_before: Coin) -> coin_after: Coin { - verification - std::cell::teleport(coin_after, coin_before) - } - """, - ), - expected=Expected("reject_compile", ("unknown stdlib pattern",)), - ), - AuditCase( - name="reject-claim-without-output-arrow", - source="""\ -module cellscript::audit::reject_claim_without_output_arrow - -resource Coin has store, create, consume, replace, burn, relock { - amount: u64, - nonce: u64, -} - -receipt Voucher has create, consume, burn { - amount: u64, - nonce: u64, - holder: Address, -} - -action bad(voucher: Voucher) -> coin: Coin { - verification - std::receipt::claim(voucher, coin, voucher.holder) { - amount - nonce - } -} -""", - expected=Expected("reject_compile", ("declare a claim output type",)), - ), - AuditCase( - name="reject-flow-undeclared-edge", - source="""\ -module cellscript::audit::reject_flow_undeclared_edge - -resource Offer has store { - state: u8 - amount: u64 -} - -flow Offer.state { - Live -> Filled; - Filled -> Cancelled; - Cancelled -> Filled; -} - -action cancel(input: Offer) -> output: Offer { - transition input.state: Live -> output.state: Cancelled - verification - require input.amount == output.amount -} -""", - expected=Expected("reject_compile", ("is not declared in the flow",)), - ), - AuditCase( - name="accept-flow-declared-cyclic-edge", - source="""\ -module cellscript::audit::accept_flow_declared_cyclic_edge - -resource Pool has store { - state: u8 - reserve: u64 -} - -flow Pool.state { - Open -> Closed; - Closed -> Open; -} - -action close(pool_before: Pool) -> pool_after: Pool { - transition pool_before.state: Open -> pool_after.state: Closed - verification - require pool_after.reserve == pool_before.reserve -} - -action reopen(pool_before: Pool) -> pool_after: Pool { - transition pool_before.state: Closed -> pool_after.state: Open - verification - require pool_after.reserve == pool_before.reserve -} -""", - expected=Expected("accept"), - ), - AuditCase( - name="reject-flow-create-missing-state", - source="""\ -module cellscript::audit::reject_flow_create_missing_state - -resource Offer has store, create { - state: u8 - amount: u64 -} - -flow Offer.state { - Live -> Filled; -} - -action seed(recipient: Address) -> output: Offer { - verification - create output = Offer { amount: 0 } with_lock(recipient) -} -""", - expected=Expected("reject_compile", ("must set its state field",)), - ), - AuditCase( - name="reject-flow-create-non-static-initial", - source="""\ -module cellscript::audit::reject_flow_create_non_static_initial - -resource Offer has store, create { - state: u8 - amount: u64 -} - -flow Offer.state { - Live -> Filled; -} - -action seed(dynamic_state: u8, recipient: Address) -> output: Offer { - verification - create output = Offer { state: dynamic_state, amount: 0 } with_lock(recipient) -} -""", - expected=Expected("reject_compile", ("must use a statically known declared state",)), - ), - AuditCase( - name="accept-invariant-xudt-conserved", - source="""\ -module cellscript::audit::accept_invariant_xudt_conserved - -resource Token has store, create, consume { - amount: u128, -} - -invariant xudt_group_transfer_conservation { - trigger: type_group - scope: group - reads: group_inputs.amount, group_outputs.amount - assert_sum(group_outputs.amount) == assert_sum(group_inputs.amount) -} - -action transfer(input: Token) -> output: Token { - verification - xudt::require_group_amount_conserved() - preserve output from input { - amount - } -} -""", - expected=Expected("accept"), - ), - ] - return cases - - -def seeded_deep_cases(seed: int) -> list[AuditCase]: - rng = random.Random(seed) - suffix = f"{seed & 0xffff_ffff:x}" - field_order = ["amount", "nonce"] - rng.shuffle(field_order) - transfer_fields = "\n".join(f" {field}" for field in field_order) - helper = rng.choice( - [ - "std::cell::preserve_type", - "std::cell::same_lock", - "std::cell::preserve_lock", - "std::cell::preserve_capacity", - ] - ) - reject = rng.choice( - [ - ( - "require_block_lifecycle", - """ - action seeded_reject_lifecycle_{suffix}(coin: Coin, to: Address) -> next_coin: Coin { - verification - require { - std::lifecycle::transfer(coin, next_coin, to) { - amount - nonce - } - } - } - """, - ("require block", "verifier-boundary syntax"), - ), - ( - "unknown_stdlib", - """ - action seeded_reject_unknown_{suffix}(coin_before: Coin) -> coin_after: Coin { - verification - std::cell::teleport(coin_after, coin_before) - } - """, - ("unknown stdlib pattern",), - ), - ( - "transfer_missing_field", - """ - action seeded_reject_missing_{suffix}(coin: Coin, to: Address) -> next_coin: Coin { - verification - std::lifecycle::transfer(coin, next_coin, to) { - amount - } - } - """, - ("missing nonce",), - ), - ] - ) - reject_name, reject_body, reject_tokens = reject - return [ - AuditCase( - name=f"seeded-deep-transfer-{suffix}", - source=module_source( - f"seeded_deep_transfer_{suffix}", - f""" - action seeded_transfer_{suffix}(coin: Coin, to: Address) -> next_coin: Coin {{ - verification - std::lifecycle::transfer(coin, next_coin, to) {{ -{transfer_fields} - }} - }} - """, - ), - expected=Expected("accept"), - oracle=Oracle( - action=f"seeded_transfer_{suffix}", - consume_bindings=("coin",), - create_bindings=("next_coin",), - locked_outputs=("next_coin",), - create_fields={"next_coin": tuple(field_order)}, - obligation_contains=("create-output-lock", "consume-input:Coin:coin"), - ), - origin="seeded:deep/stdlib-lifecycle", - ), - AuditCase( - name=f"seeded-deep-cell-helper-{suffix}", - source=module_source( - f"seeded_deep_cell_helper_{suffix}", - f""" - action seeded_helper_{suffix}(coin_before: Coin) -> coin_after: Coin {{ - verification - {helper}(coin_after, coin_before) - }} - """, - ), - expected=Expected("accept"), - oracle=Oracle(action=f"seeded_helper_{suffix}"), - origin="seeded:deep/cell-helper", - ), - AuditCase( - name=f"seeded-deep-reject-{reject_name}-{suffix}", - source=module_source( - f"seeded_deep_reject_{reject_name}_{suffix}", - reject_body.replace("{suffix}", suffix), - ), - expected=Expected("reject_compile", reject_tokens), - origin="seeded:deep/reject", - ), - ] - - -def parse_seed(path: Path) -> AuditCase: - text = path.read_text(encoding="utf-8") - phase = "accept" - contains: list[str] = [] - validity_type: str | None = None - validity_tiers: list[str] = [] - borrow_scope: str | None = None - borrow_view_type: str | None = None - capability_operation: str | None = None - capability_type: str | None = None - payload_enum: str | None = None - protocol_role_action: str | None = None - protocol_role: str | None = None - protocol_role_source: str | None = None - protocol_role_conflict: bool | None = None - for line in text.splitlines(): - stripped = line.strip() - if not stripped.startswith("// audit:"): - continue - payload = stripped.removeprefix("// audit:").strip() - if "=" not in payload: - continue - key, value = [part.strip() for part in payload.split("=", 1)] - if key == "phase": - phase = value - elif key == "contains": - contains.append(value) - elif key == "validity_type": - validity_type = value - elif key == "validity_tier": - validity_tiers.append(value) - elif key == "borrow_scope": - borrow_scope = value - elif key == "borrow_view_type": - borrow_view_type = value - elif key == "capability_operation": - capability_operation = value - elif key == "capability_type": - capability_type = value - elif key == "payload_enum": - payload_enum = value - elif key == "protocol_role_action": - protocol_role_action = value - elif key == "protocol_role": - protocol_role = value - elif key == "protocol_role_source": - protocol_role_source = value - elif key == "protocol_role_conflict": - protocol_role_conflict = value.lower() == "true" - return AuditCase( - name=f"seed-{path.stem}", - source=text, - expected=Expected(phase, tuple(contains)), - oracle=Oracle( - validity_type=validity_type, - validity_tiers=tuple(validity_tiers), - borrow_scope=borrow_scope, - borrow_view_type=borrow_view_type, - capability_operation=capability_operation, - capability_type=capability_type, - payload_enum=payload_enum, - protocol_role_action=protocol_role_action, - protocol_role=protocol_role, - protocol_role_source=protocol_role_source, - protocol_role_conflict=protocol_role_conflict, - ), - origin=str(path.relative_to(ROOT)), - ) - - -def load_cases(mode: str, budget: int | None, seed: int) -> list[AuditCase]: - include_matrix = mode in {"ci", "deep", "repro"} - include_deep = mode in {"deep", "repro"} - cases = generated_cases() - if include_matrix: - cases.extend(matrix_cases(include_deep=include_deep)) - if include_deep: - cases.extend(seeded_deep_cases(seed)) - - seed_cases: list[AuditCase] = [] - if SEEDS.exists(): - seed_cases = [parse_seed(path) for path in sorted(SEEDS.glob("*.cell")) if path.is_file()] - - if mode == "quick": - default_budget = read_matrix().get("mode", {}).get("quick", {}).get("budget", len(cases)) - elif mode == "ci": - default_budget = read_matrix().get("mode", {}).get("ci", {}).get("budget", len(cases)) - else: - default_budget = read_matrix().get("mode", {}).get("deep", {}).get("budget", len(cases)) - limit = budget or default_budget or len(cases) - selected = cases[: min(limit, len(cases))] - - # Regression seeds are never dropped by a small generation budget. - existing = {case.name for case in selected} - for seed_case in seed_cases: - if seed_case.name not in existing: - selected.append(seed_case) - existing.add(seed_case.name) - return selected - - -def contract_failure(code: str, summary: str) -> dict[str, Any]: - return { - "case": "-", - "name": "mode-contract", - "origin": str(MATRIX.relative_to(ROOT)), - "phase": "contract", - "code": code, - "summary": summary, - "shrunk": "", - "output": "", - } - - -def required_for_mode(contract: dict[str, Any], mode: str) -> bool: - min_mode = str(contract.get("min_mode", "quick")) - return MODE_RANK.get(mode, 0) >= MODE_RANK.get(min_mode, 0) - - -def evaluate_bug_class_coverage(mode: str, cases: list[AuditCase]) -> list[dict[str, Any]]: - case_names = {case.name for case in cases} - origins = {case.origin for case in cases} - coverage: list[dict[str, Any]] = [] - for contract in BUG_CLASS_CONTRACTS: - required = required_for_mode(contract, mode) - required_cases = tuple(contract.get("required_cases", ())) - required_origins = tuple(contract.get("required_origins", ())) - missing_cases = [name for name in required_cases if name not in case_names] - missing_origins = [origin for origin in required_origins if origin not in origins] - status = "covered" if not missing_cases and not missing_origins else "missing" - coverage.append( - { - "id": contract["id"], - "name": contract["name"], - "status": status if required else "not_required_for_mode", - "required": required, - "min_mode": contract.get("min_mode", "quick"), - "required_cases": list(required_cases), - "required_origins": list(required_origins), - "missing_cases": missing_cases if required else [], - "missing_origins": missing_origins if required else [], - "release_boundary": contract["release_boundary"], - } - ) - return coverage - - -def governance_oracles() -> dict[str, bool]: - configured = read_matrix().get("required_oracles", {}) - return { - "parser": bool(configured.get("parse")), - "formatter_roundtrip": bool(configured.get("formatter_roundtrip")), - "type_effect": bool(configured.get("type_effect")), - "ir_metadata": bool(configured.get("ir_metadata")), - "codegen_assembly": bool(configured.get("codegen_assembly")), - "compact_report": bool(configured.get("compact_report")), - } - - -def validate_mode_contract(mode: str, report: dict[str, Any]) -> list[dict[str, Any]]: - if mode == "repro": - return [] - config = read_matrix().get("mode", {}).get(mode, {}) - failures: list[dict[str, Any]] = [] - numeric_contracts = [ - ("min_cases", "generated", "SCA-CONTRACT-CASES"), - ("min_accept", "accepted", "SCA-CONTRACT-ACCEPT"), - ("min_reject", "rejected", "SCA-CONTRACT-REJECT"), - ] - for config_key, report_key, code in numeric_contracts: - expected = config.get(config_key) - if expected is None: - continue - actual = report.get(report_key, 0) - if actual < expected: - failures.append(contract_failure(code, f"{mode} {report_key} floor {expected} not met; got {actual}")) - - origins = report.get("origins", {}) - missing_origins = [origin for origin in config.get("required_origins", []) if origin not in origins] - if missing_origins: - failures.append(contract_failure("SCA-CONTRACT-ORIGIN", f"{mode} missing required origins: {', '.join(missing_origins)}")) - missing_bug_classes = [ - item - for item in report.get("known_bug_classes", []) - if item.get("required") and item.get("status") != "covered" - ] - for item in missing_bug_classes: - details: list[str] = [] - if item.get("missing_cases"): - details.append("missing cases: " + ", ".join(item["missing_cases"])) - if item.get("missing_origins"): - details.append("missing origins: " + ", ".join(item["missing_origins"])) - failures.append(contract_failure(item["id"], f"{mode} bug-class coverage missing for {item['name']}: {'; '.join(details)}")) - return failures - - -def failure( - case: AuditCase, - phase: str, - code: str, - summary: str, - run_dir: Path, - output: str = "", -) -> dict[str, Any]: - shrink_dir = run_dir / "shrink" - shrink_dir.mkdir(parents=True, exist_ok=True) - shrink_path = shrink_dir / f"{case.case_id}.cell" - compact_source = "\n".join( - line for line in case.source.splitlines() if line.strip() and not line.strip().startswith("//") - ) - shrink_path.write_text(compact_source + "\n", encoding="utf-8") - return { - "case": case.case_id, - "name": case.name, - "origin": case.origin, - "phase": phase, - "code": code, - "summary": summary, - "shrunk": str(shrink_path.relative_to(run_dir)), - "output": compact(output), - } - - -def output_matches(text: str, needles: tuple[str, ...]) -> bool: - if not needles: - return True - lowered = text.lower() - return all(needle.lower() in lowered for needle in needles) - - -def find_action(metadata: dict[str, Any], name: str) -> dict[str, Any] | None: - for action in metadata.get("actions", []): - if action.get("name") == name: - return action - return None - - -def validate_metadata(case: AuditCase, metadata_path: Path, run_dir: Path) -> list[dict[str, Any]]: - failures: list[dict[str, Any]] = [] - try: - metadata = json.loads(metadata_path.read_text(encoding="utf-8")) - except Exception as exc: # noqa: BLE001 - report compact audit failure - return [failure(case, "metadata", "SCA-META-JSON", f"metadata JSON decode failed: {exc}", run_dir)] - - required_keys = {"actions", "compiler_version", "constraints", "lowering", "runtime", "target_profile"} - missing = sorted(required_keys - set(metadata)) - if missing: - failures.append(failure(case, "metadata", "SCA-META-KEYS", f"metadata missing keys: {', '.join(missing)}", run_dir)) - - target_profile = metadata.get("target_profile", {}) - if target_profile.get("name") != "ckb": - failures.append(failure(case, "metadata", "SCA-META-PROFILE", "metadata target_profile.name is not ckb", run_dir)) - - oracle = case.oracle - if oracle.capability_operation: - registry = metadata.get("capability_registry", {}) - canonical = ["store", "create", "consume", "destroy", "replace", "burn", "relock", "retarget_type", "read_ref"] - if registry.get("capability_set_version") != 1 or registry.get("entailment_version") != 1: - failures.append( - failure(case, "metadata", "SCA-META-CAPABILITY-VERSION", "capability registry versions are not set to v1", run_dir) - ) - if registry.get("capabilities") != canonical: - failures.append( - failure(case, "metadata", "SCA-META-CAPABILITY-REGISTRY", "capability registry is not canonical", run_dir) - ) - proofs = [ - proof - for proof in metadata.get("runtime", {}).get("capability_proofs", []) - if proof.get("operation") == oracle.capability_operation - and (oracle.capability_type is None or proof.get("type_name") == oracle.capability_type) - ] - if not proofs: - failures.append( - failure( - case, - "metadata", - "SCA-META-CAPABILITY-PROOF", - f"missing capability proof for {oracle.capability_operation}", - run_dir, - ) - ) - else: - proof = proofs[0] - required_fields = {"required", "provided", "entailed", "missing", "capability_set_version", "entailment_version"} - if not required_fields.issubset(proof) or proof.get("missing") != []: - failures.append( - failure( - case, - "metadata", - "SCA-META-CAPABILITY-EVIDENCE", - "capability proof is missing required/provided/entailed/missing/version evidence", - run_dir, - ) - ) - if oracle.payload_enum: - layouts = [layout for layout in metadata.get("enum_layouts", []) if layout.get("name") == oracle.payload_enum] - if not layouts: - failures.append( - failure(case, "metadata", "SCA-META-PAYLOAD-ENUM", f"missing payload enum layout for {oracle.payload_enum}", run_dir) - ) - else: - layout = layouts[0] - variants = layout.get("variants", []) - payload_fields = [field for variant in variants for field in variant.get("fields", [])] - if ( - layout.get("generic") is not False - or layout.get("layout") != "packed-tagged-union-v1" - or layout.get("tag_width_bytes") != 1 - or layout.get("encoded_size_bytes", 0) <= 1 - or not payload_fields - ): - failures.append( - failure( - case, - "metadata", - "SCA-META-PAYLOAD-ENUM-LAYOUT", - "payload enum metadata is missing its concrete fixed-width tagged-union contract", - run_dir, - ) - ) - if oracle.protocol_role_action: - action = find_action(metadata, oracle.protocol_role_action) - if action is None: - failures.append( - failure( - case, - "metadata", - "SCA-META-PROTOCOL-ROLE-ACTION", - f"missing ProtocolGraph role action {oracle.protocol_role_action}", - run_dir, - ) - ) - else: - candidates = action.get("protocol_role_candidates", []) - if not candidates: - failures.append( - failure(case, "metadata", "SCA-META-PROTOCOL-ROLE", "missing attributed role candidates", run_dir) - ) - else: - selected = candidates[0] - if selected.get("role") != oracle.protocol_role or selected.get("source") != oracle.protocol_role_source: - failures.append( - failure( - case, - "metadata", - "SCA-META-PROTOCOL-ROLE-PRECEDENCE", - f"selected role/source {selected.get('role')!r}@{selected.get('source')!r} does not match {oracle.protocol_role!r}@{oracle.protocol_role_source!r}", - run_dir, - ) - ) - if any( - candidate.get("evidence_tier") != "metadata-only" or candidate.get("authorization_proven") is not False - for candidate in candidates - ): - failures.append( - failure( - case, - "metadata", - "SCA-META-PROTOCOL-ROLE-OVERCLAIM", - "role candidates must remain metadata-only with authorization_proven=false", - run_dir, - ) - ) - roles = {candidate.get("role") for candidate in candidates} - actual_conflict = len(roles) > 1 - if oracle.protocol_role_conflict is not None and actual_conflict != oracle.protocol_role_conflict: - failures.append( - failure( - case, - "metadata", - "SCA-META-PROTOCOL-ROLE-CONFLICT", - f"role conflict={actual_conflict} does not match expected {oracle.protocol_role_conflict}", - run_dir, - ) - ) - if any(plan.get("category") == "protocol-role" for plan in action.get("proof_plan", [])): - failures.append( - failure( - case, - "metadata", - "SCA-META-PROTOCOL-ROLE-PROOFPLAN", - "ProtocolGraph roles must not appear as ProofPlan authorization evidence", - run_dir, - ) - ) - if oracle.borrow_scope: - borrow_regions = [ - region - for region in metadata.get("runtime", {}).get("borrow_regions", []) - if region.get("scope_name") == oracle.borrow_scope - ] - if not borrow_regions: - failures.append( - failure(case, "metadata", "SCA-META-BORROW-REGION", f"missing borrow metadata for {oracle.borrow_scope}", run_dir) - ) - else: - region = borrow_regions[0] - expected_view = oracle.borrow_view_type - if expected_view and region.get("view_type") != expected_view: - failures.append( - failure( - case, - "metadata", - "SCA-META-BORROW-VIEW", - f"borrow view type {region.get('view_type')!r} does not match {expected_view!r}", - run_dir, - ) - ) - if region.get("storage") != "none" or region.get("abi") != "none" or region.get("evidence_tier") != "checked-static": - failures.append( - failure( - case, - "metadata", - "SCA-META-BORROW-EVIDENCE", - "borrow region must declare storage=none, abi=none, and checked-static evidence", - run_dir, - ) - ) - proof_plan = metadata.get("runtime", {}).get("proof_plan", []) - borrow_plans = [ - plan - for plan in proof_plan - if str(plan.get("origin", "")).startswith(f"action:{oracle.borrow_scope}#borrow-region:") - ] - if not borrow_plans or borrow_plans[0].get("evidence_tier") != "checked-static": - failures.append( - failure( - case, - "metadata", - "SCA-META-BORROW-PROOFPLAN", - "borrow region is missing a checked-static ProofPlan record", - run_dir, - ) - ) - if oracle.validity_type: - type_metadata = next((item for item in metadata.get("types", []) if item.get("name") == oracle.validity_type), None) - if type_metadata is None: - failures.append( - failure(case, "metadata", "SCA-META-VALIDITY-TYPE", f"missing type metadata for {oracle.validity_type}", run_dir) - ) - else: - predicates = type_metadata.get("validity_predicates", []) - if not predicates: - failures.append( - failure(case, "metadata", "SCA-META-VALIDITY", "validity metadata has no predicate records", run_dir) - ) - canonical_tiers = { - "checked-static", - "checked-runtime", - "runtime-helper-required", - "builder-evidence-required", - "metadata-only", - "chain-evidence-required", - } - actual_tiers = tuple(predicate.get("evidence_tier") for predicate in predicates) - if any(tier not in canonical_tiers for tier in actual_tiers): - failures.append( - failure( - case, - "metadata", - "SCA-META-VALIDITY-TIER", - f"validity metadata contains non-canonical evidence tiers: {actual_tiers!r}", - run_dir, - ) - ) - for tier in oracle.validity_tiers: - if tier not in actual_tiers: - failures.append( - failure( - case, - "metadata", - "SCA-META-VALIDITY-TIER", - f"validity metadata is missing evidence tier {tier!r}", - run_dir, - ) - ) - proof_plan = metadata.get("runtime", {}).get("proof_plan", []) - validity_plans = [ - plan for plan in proof_plan if str(plan.get("origin", "")).startswith(f"validity:{oracle.validity_type}#") - ] - if len(validity_plans) < len(predicates): - failures.append( - failure( - case, - "metadata", - "SCA-META-VALIDITY-PROOFPLAN", - f"validity ProofPlan count {len(validity_plans)} is smaller than predicate count {len(predicates)}", - run_dir, - ) - ) - if oracle.action: - action = find_action(metadata, oracle.action) - if action is None: - failures.append(failure(case, "metadata", "SCA-META-ACTION", f"missing action metadata for {oracle.action}", run_dir)) - return failures - - consume_bindings = tuple(item.get("binding") for item in action.get("consume_set", [])) - if oracle.consume_bindings and consume_bindings != oracle.consume_bindings: - failures.append( - failure( - case, - "metadata", - "SCA-META-CONSUME", - f"consume bindings {consume_bindings!r} != {oracle.consume_bindings!r}", - run_dir, - ) - ) - if len(consume_bindings) != len(set(consume_bindings)): - failures.append(failure(case, "metadata", "SCA-META-DUP-CONSUME", "duplicate consume binding", run_dir)) - - create_set = action.get("create_set", []) - create_by_binding = {item.get("binding"): item for item in create_set} - for binding in oracle.create_bindings: - if binding not in create_by_binding: - failures.append(failure(case, "metadata", "SCA-META-CREATE", f"missing create binding {binding}", run_dir)) - for binding in oracle.locked_outputs: - if not create_by_binding.get(binding, {}).get("has_lock"): - failures.append(failure(case, "metadata", "SCA-META-LOCK", f"create binding {binding} is not locked", run_dir)) - for binding, fields in oracle.create_fields.items(): - actual = tuple(create_by_binding.get(binding, {}).get("fields", [])) - if actual != fields: - failures.append( - failure( - case, - "metadata", - "SCA-META-FIELDS", - f"create fields for {binding} {actual!r} != {fields!r}", - run_dir, - ) - ) - - obligations_text = json.dumps(action.get("verifier_obligations", []), sort_keys=True) - for needle in oracle.obligation_contains: - if needle not in obligations_text: - failures.append( - failure( - case, - "metadata", - "SCA-META-OBLIGATION", - f"missing obligation containing {needle!r}", - run_dir, - ) - ) - - if action.get("fail_closed_runtime_features"): - failures.append( - failure( - case, - "metadata", - "SCA-META-FAIL-CLOSED", - "accepted audit case contains fail_closed_runtime_features", - run_dir, - ) - ) - return failures - - -def audit_case(case: AuditCase, run_dir: Path, cellc: str) -> tuple[str, list[dict[str, Any]]]: - # Parse-reject cases are isolated in a separate directory so that their - # intentionally-invalid syntax does not contaminate compile runs of other - # cases that share the cases/ directory (cellc resolves sibling modules). - if case.expected.phase == "reject_parse": - case_path = run_dir / "parse_reject" / f"{case.case_id}.cell" - else: - case_path = run_dir / "cases" / f"{case.case_id}.cell" - fmt_path = run_dir / "fmt" / f"{case.case_id}.cell" - asm_path = run_dir / "asm" / f"{case.case_id}.s" - meta_path = run_dir / "meta" / f"{case.case_id}.json" - for path in [case_path.parent, fmt_path.parent, asm_path.parent, meta_path.parent]: - path.mkdir(parents=True, exist_ok=True) - case_path.write_text(case.source, encoding="utf-8") - - parse = run_cmd([cellc, "--parse", str(case_path)], timeout=20) - if case.expected.phase == "reject_parse": - if parse.returncode == 0: - return "failed", [failure(case, "parse", "SCA-PARSE-ACCEPTED", "expected parse rejection, got success", run_dir, parse.stdout)] - if not output_matches(parse.stdout, case.expected.contains): - return "failed", [ - failure( - case, - "parse", - "SCA-PARSE-DIAGNOSTIC", - f"parse diagnostic missing expected tokens {case.expected.contains!r}", - run_dir, - parse.stdout, - ) - ] - return "rejected", [] - if parse.returncode != 0: - return "failed", [failure(case, "parse", "SCA-PARSE-FAILED", "unexpected parse failure", run_dir, parse.stdout)] - - if case.expected.phase == "accept": - fmt_path.write_text(case.source, encoding="utf-8") - fmt = run_cmd([cellc, "fmt", "--json", str(fmt_path)], timeout=20) - if fmt.returncode != 0: - return "failed", [failure(case, "fmt", "SCA-FMT-FAILED", "formatter failed", run_dir, fmt.stdout)] - fmt_check = run_cmd([cellc, "fmt", "--check", "--json", str(fmt_path)], timeout=20) - if fmt_check.returncode != 0: - return "failed", [failure(case, "fmt", "SCA-FMT-NON-IDEMPOTENT", "formatted source is not idempotent", run_dir, fmt_check.stdout)] - parse_fmt = run_cmd([cellc, "--parse", str(fmt_path)], timeout=20) - if parse_fmt.returncode != 0: - return "failed", [failure(case, "fmt", "SCA-FMT-PARSE", "formatted source does not parse", run_dir, parse_fmt.stdout)] - - compile_cmd = [ - cellc, - str(case_path), - "--target", - "riscv64-asm", - "--target-profile", - "ckb", - "--primitive-strict", - "0.15", - "-o", - str(asm_path), - ] - compiled = run_cmd(compile_cmd, timeout=30) - if case.expected.phase == "reject_compile": - if compiled.returncode == 0: - return "failed", [ - failure(case, "compile", "SCA-COMPILE-ACCEPTED", "expected compile rejection, got success", run_dir, compiled.stdout) - ] - if not output_matches(compiled.stdout, case.expected.contains): - return "failed", [ - failure( - case, - "compile", - "SCA-COMPILE-DIAGNOSTIC", - f"compile diagnostic missing expected tokens {case.expected.contains!r}", - run_dir, - compiled.stdout, - ) - ] - return "rejected", [] - if compiled.returncode != 0: - return "failed", [failure(case, "compile", "SCA-COMPILE-FAILED", "unexpected compile failure", run_dir, compiled.stdout)] - - if not asm_path.exists() or asm_path.stat().st_size == 0: - return "failed", [failure(case, "codegen", "SCA-CODEGEN-EMPTY", "assembly output is missing or empty", run_dir, compiled.stdout)] - asm_text = asm_path.read_text(encoding="utf-8", errors="replace") - for obsolete in ("IrTransfer", "IrClaim", "IrSettle"): - if obsolete in asm_text: - return "failed", [failure(case, "codegen", "SCA-CODEGEN-OBSOLETE", f"assembly contains obsolete token {obsolete}", run_dir)] - - metadata = run_cmd( - [ - cellc, - "metadata", - str(case_path), - "--target", - "riscv64-asm", - "--target-profile", - "ckb", - "-o", - str(meta_path), - ], - timeout=30, - ) - if metadata.returncode != 0: - return "failed", [failure(case, "metadata", "SCA-META-FAILED", "metadata command failed", run_dir, metadata.stdout)] - meta_failures = validate_metadata(case, meta_path, run_dir) - if meta_failures: - return "failed", meta_failures - return "accepted", [] - - -def write_reports(run_dir: Path, report: dict[str, Any], failures: list[dict[str, Any]]) -> None: - (run_dir / "report.json").write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") - with (run_dir / "report.jsonl").open("w", encoding="utf-8") as handle: - for item in failures: - handle.write(json.dumps(item, sort_keys=True) + "\n") - - -def main(argv: list[str]) -> int: - parser = argparse.ArgumentParser(description="Run CellScript syntax-combination audit") - parser.add_argument("mode", nargs="?", default="quick", choices=["quick", "ci", "deep", "repro"]) - parser.add_argument("--seed", type=int, default=20260503) - parser.add_argument("--budget", type=int) - parser.add_argument("--case", help="case name or id for repro mode") - args = parser.parse_args(argv) - - require_tool("cargo") - require_tool("python3") - cellc = cellc_bin() - - timestamp = dt.datetime.now(dt.timezone.utc).strftime("%Y%m%d-%H%M%S") - run_dir = ROOT / "target" / "syntax-combo-audit" / f"{timestamp}-{args.mode}-{args.seed}" - run_dir.mkdir(parents=True, exist_ok=True) - - cases = load_cases(args.mode, args.budget, args.seed) - if args.mode == "repro": - if not args.case: - raise SystemExit("repro mode requires --case ") - cases = [case for case in cases if case.name == args.case or case.case_id == args.case] - if not cases: - raise SystemExit(f"unknown repro case: {args.case}") - - failures: list[dict[str, Any]] = [] - accepted = 0 - rejected = 0 - phase_counts: dict[str, dict[str, int]] = {} - origin_counts: dict[str, int] = {} - - for case in cases: - origin_counts[case.origin] = origin_counts.get(case.origin, 0) + 1 - status, case_failures = audit_case(case, run_dir, cellc) - expected_phase = case.expected.phase - phase_counts.setdefault(expected_phase, {"passed": 0, "failed": 0}) - if case_failures: - phase_counts[expected_phase]["failed"] += 1 - failures.extend(case_failures) - else: - phase_counts[expected_phase]["passed"] += 1 - if status == "accepted": - accepted += 1 - elif status == "rejected": - rejected += 1 - - report = { - "status": "passed" if not failures else "failed", - "mode": args.mode, - "seed": args.seed, - "generated": len(cases), - "accepted": accepted, - "rejected": rejected, - "failures_count": len(failures), - "governance_release_matrix": list(GOVERNANCE_RELEASE_MATRIX), - "governance_oracles": governance_oracles(), - "known_bug_classes": evaluate_bug_class_coverage(args.mode, cases), - "phases": phase_counts, - "origins": origin_counts, - "failures": failures[:10], - } - contract_failures = validate_mode_contract(args.mode, report) - if contract_failures: - failures.extend(contract_failures) - report["status"] = "failed" - report["failures_count"] = len(failures) - report["failures"] = failures[:10] - write_reports(run_dir, report, failures) - - print( - "syntax-combo-audit: " - f"{report['status']} seed={args.seed} mode={args.mode} " - f"generated={len(cases)} accepted={accepted} rejected={rejected} failures={len(failures)}" - ) - print(f"report={run_dir / 'report.json'}") - if failures: - print("top:") - for item in failures[:5]: - print(f" {item['code']} {item['summary']} case={item['case']} phase={item['phase']}") - return 1 - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/scripts/cellscript_syntax_combo_audit.sh b/scripts/cellscript_syntax_combo_audit.sh index 94d5da84..021af590 100755 --- a/scripts/cellscript_syntax_combo_audit.sh +++ b/scripts/cellscript_syntax_combo_audit.sh @@ -15,4 +15,5 @@ if [[ -z "${CELLC_BIN:-}" ]]; then export CELLC_BIN="$TARGET_DIR/debug/cellc" fi -python3 scripts/cellscript_syntax_combo_audit.py "$MODE" "$@" +cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root "$ROOT_DIR" syntax-combo-audit "$MODE" "$@" diff --git a/scripts/check_cellscript_skill_pack.py b/scripts/check_cellscript_skill_pack.py deleted file mode 100644 index dc747a59..00000000 --- a/scripts/check_cellscript_skill_pack.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env python3 -"""Validate the CellScript programming skill pack freshness contract.""" - -from __future__ import annotations - -import json -import re -import sys -from pathlib import Path - - -EXPECTED_SKILLS = { - "cellscript-language-basics", - "cellscript-ckb-model", - "cellscript-package-cli", - "cellscript-metadata-audit", - "cellscript-builder-deployment", - "cellscript-diagnostics", -} - - -def parse_front_matter(path: Path) -> dict[str, list[str] | str]: - text = path.read_text(encoding="utf-8") - if not text.startswith("---\n"): - raise ValueError(f"{path} is missing YAML-style front matter") - try: - header = text.split("---\n", 2)[1] - except IndexError as error: - raise ValueError(f"{path} has unterminated front matter") from error - result: dict[str, list[str] | str] = {} - current_list: str | None = None - for raw_line in header.splitlines(): - line = raw_line.rstrip() - if not line: - continue - if line.startswith(" - "): - if current_list is None: - raise ValueError(f"{path} has a list item outside a list: {line}") - value = line[4:].strip() - result.setdefault(current_list, []) - assert isinstance(result[current_list], list) - result[current_list].append(value) - continue - current_list = None - if ":" not in line: - raise ValueError(f"{path} has malformed front matter line: {line}") - key, value = line.split(":", 1) - key = key.strip() - value = value.strip() - if value: - result[key] = value - else: - result[key] = [] - current_list = key - return result - - -def visible_command_names(repo_root: Path) -> set[str]: - source = (repo_root / "src/cli/commands.rs").read_text(encoding="utf-8") - names = set(re.findall(r'ClapCommand::new\("([^"]+)"\)', source)) - names.update({"cellc"}) - return names - - -def validate_skill(repo_root: Path, path: Path, command_names: set[str]) -> list[str]: - failures: list[str] = [] - front_matter = parse_front_matter(path) - name = str(front_matter.get("name", "")).strip() - if not name: - failures.append(f"{path}: missing name") - references = front_matter.get("references") - if not isinstance(references, list) or not references: - failures.append(f"{path}: missing references list") - references = [] - commands = front_matter.get("commands") - if not isinstance(commands, list) or not commands: - failures.append(f"{path}: missing commands list") - commands = [] - - has_current_doc_or_example = False - for reference in references: - ref_path = reference.split("#", 1)[0] - if ref_path.startswith("../") or "/../" in ref_path: - failures.append(f"{path}: reference escapes repo root: {reference}") - continue - full = repo_root / ref_path - if not full.exists(): - failures.append(f"{path}: referenced file does not exist: {reference}") - continue - if ref_path.startswith(("docs/wiki/", "docs/CELLSCRIPT_", "examples/")): - has_current_doc_or_example = True - if not has_current_doc_or_example: - failures.append(f"{path}: references must include current docs/wiki, docs/CELLSCRIPT_*, or examples files") - - for command in commands: - parts = command.split() - if not parts or parts[0] != "cellc": - failures.append(f"{path}: command must start with 'cellc': {command}") - continue - for part in parts[1:]: - if part.startswith("-") or part.startswith("<"): - continue - if part not in command_names: - failures.append(f"{path}: command token is not present in CLI registry: {command} ({part})") - return failures - - -def main() -> int: - repo_root = Path(__file__).resolve().parents[1] - skill_files = sorted((repo_root / "docs/skills").glob("cellscript-*/SKILL.md")) - found = {path.parent.name for path in skill_files} - failures: list[str] = [] - missing = sorted(EXPECTED_SKILLS - found) - extra = sorted(found - EXPECTED_SKILLS) - if missing: - failures.append(f"missing skill directories: {', '.join(missing)}") - if extra: - failures.append(f"unexpected CellScript skill directories: {', '.join(extra)}") - command_names = visible_command_names(repo_root) - for path in skill_files: - failures.extend(validate_skill(repo_root, path, command_names)) - - report = { - "schema": "cellscript-skill-pack-freshness-v0.22", - "status": "failed" if failures else "passed", - "skills": sorted(found), - "skill_count": len(skill_files), - "failures": failures, - } - print(json.dumps(report, indent=2, sort_keys=True)) - if failures: - return 1 - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/ckb_cellscript_acceptance.sh b/scripts/ckb_cellscript_acceptance.sh index 7b15b20b..17d755ca 100755 --- a/scripts/ckb_cellscript_acceptance.sh +++ b/scripts/ckb_cellscript_acceptance.sh @@ -3,94 +3,38 @@ set -Eeuo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -CKB_PIN_FILE="$SCRIPT_DIR/ckb_acceptance_pin.json" - -default_ckb_repo() { - local parent grandparent - parent="$(cd "$REPO_ROOT/.." && pwd)" - grandparent="$(cd "$REPO_ROOT/../.." && pwd)" - if [[ -d "$parent/ckb" ]]; then - printf '%s\n' "$parent/ckb" - else - printf '%s\n' "$grandparent/ckb" - fi -} - -CKB_REPO="${CKB_REPO:-$(default_ckb_repo)}" -CKB_BIN="${CKB_BIN:-}" -RUN_ONCHAIN=1 -RUN_STATEFUL_SCENARIOS="${RUN_STATEFUL_SCENARIOS:-0}" -KEEP_NODE_LOGS=1 -ACCEPTANCE_MODE="production" -RUN_ID="$(date +%Y%m%d-%H%M%S)-$$" -RUN_DIR="$REPO_ROOT/target/ckb-cellscript-acceptance/$RUN_ID" -CKB_DIR="$RUN_DIR/ckb-node" -CKB_LOG="$RUN_DIR/ckb.log" -REPORT_JSON="$RUN_DIR/ckb-cellscript-acceptance-report.json" -CKB_PID="" -CKB_BUILD_TARGET_DIR="$RUN_DIR/.ckb-build-target" usage() { cat <<'USAGE' Usage: scripts/ckb_cellscript_acceptance.sh [--ckb-repo ] [--ckb-bin ] [--compile-only] [--stateful-scenarios] [--production|--bounded] -Runs CellScript CKB compatibility acceptance against a local CKB integration -devnet from the parent CKB repository. The default mode is the production gate: -it fails closed if any CKB coverage still depends on synthetic harnesses, -expected fail-closed entries, or non-original artifacts. +Runs the Rust-native CellScript CKB acceptance gate. Production mode is the +default and fails closed unless the source tree and pinned CKB checkout are +clean. The compile-only mode verifies compiler artifacts, ELF entry ABI, +public builder contracts, and production evidence structure without claiming +live node readiness. Options: - --ckb-repo Parent CKB checkout. Defaults to ../ckb. - --ckb-bin Existing CKB executable for bounded on-chain runs only. - Production rejects this option and freshly rebuilds the - pinned source in an isolated Cargo target directory. - --compile-only Compile and verify the CKB-profile CellScript artifacts, - but skip local CKB node deployment/spend checks. This - mode does not require a CKB checkout or executable. + --ckb-repo Pinned CKB checkout. Defaults to ../ckb. + --ckb-bin Existing CKB executable for bounded live runs only. + --compile-only Skip local-node transaction execution. --stateful-scenarios - Run additional local CKB transactions that feed live - outputs from one action into the next. Production - on-chain mode always enables this requirement. - --production Enforce the production gate. This is the default. - --bounded Run the bounded development coverage matrix. This keeps - bounded harnesses visible, but it is not a - production-readiness claim. + Execute the complete stateful action recipe matrix. + --production Enforce the production gate (default). + --bounded Run bounded development evidence without a production claim. -h, --help Show this help. USAGE } +args=() while [[ $# -gt 0 ]]; do case "$1" in - --ckb-repo) - CKB_REPO="${2:?missing value for --ckb-repo}" - shift 2 - ;; - --ckb-repo=*) - CKB_REPO="${1#*=}" - shift - ;; - --ckb-bin) - CKB_BIN="${2:?missing value for --ckb-bin}" - shift 2 - ;; - --ckb-bin=*) - CKB_BIN="${1#*=}" - shift - ;; - --compile-only) - RUN_ONCHAIN=0 - shift - ;; - --stateful-scenarios) - RUN_STATEFUL_SCENARIOS=1 - shift - ;; --production) - ACCEPTANCE_MODE="production" + args+=(--mode production) shift ;; --bounded) - ACCEPTANCE_MODE="bounded" + args+=(--mode bounded) shift ;; -h|--help) @@ -98,7833 +42,14 @@ while [[ $# -gt 0 ]]; do exit 0 ;; *) - echo "unknown argument: $1" >&2 - usage >&2 - exit 2 + args+=("$1") + shift ;; esac done -if [[ "$ACCEPTANCE_MODE" == "production" ]]; then - if [[ -n "$(git -C "$REPO_ROOT" status --porcelain --untracked-files=all)" ]]; then - echo "production acceptance requires a clean CellScript source tree" >&2 - git -C "$REPO_ROOT" status --short >&2 - exit 1 - fi - if [[ "$RUN_ONCHAIN" == "1" ]]; then - RUN_STATEFUL_SCENARIOS=1 - fi -fi - -require_cmd() { - if ! command -v "$1" >/dev/null 2>&1; then - echo "missing required command: $1" >&2 - exit 127 - fi -} - -pick_port() { - python3 - <<'PY' -import socket - -with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: - sock.bind(("127.0.0.1", 0)) - print(sock.getsockname()[1]) -PY -} - -resolve_ckb_bin() { - if [[ "$ACCEPTANCE_MODE" == "production" ]]; then - if [[ -n "$CKB_BIN" ]]; then - echo "production acceptance does not accept --ckb-bin/CKB_BIN; the pinned CKB source must be rebuilt in a fresh target directory" >&2 - exit 1 - fi - - local fresh_candidate archived_candidate - mkdir -p "$CKB_BUILD_TARGET_DIR" "$RUN_DIR/ckb-runtime" - echo "Building pinned CKB checkout in a fresh dedicated Cargo target directory" >&2 - ( - cd "$CKB_REPO" - cargo build --locked --bin ckb --target-dir "$CKB_BUILD_TARGET_DIR" - ) - fresh_candidate="$CKB_BUILD_TARGET_DIR/debug/ckb" - if [[ ! -x "$fresh_candidate" ]]; then - echo "fresh CKB build finished but executable was not found at $fresh_candidate" >&2 - exit 1 - fi - archived_candidate="$RUN_DIR/ckb-runtime/ckb" - cp "$fresh_candidate" "$archived_candidate" - chmod 0755 "$archived_candidate" - printf '%s\n' "$archived_candidate" - return - fi - - if [[ -n "$CKB_BIN" ]]; then - if [[ ! -x "$CKB_BIN" ]]; then - echo "CKB_BIN is not executable: $CKB_BIN" >&2 - exit 1 - fi - printf '%s\n' "$CKB_BIN" - return - fi - - local candidate - for candidate in "$CKB_REPO/target/debug/ckb" "$CKB_REPO/target/release/ckb"; do - if [[ -x "$candidate" ]]; then - printf '%s\n' "$candidate" - return - fi - done - - echo "No existing CKB executable found; building pinned CKB checkout with cargo build --locked --bin ckb" >&2 - (cd "$CKB_REPO" && cargo build --locked --bin ckb) - candidate="$CKB_REPO/target/debug/ckb" - if [[ ! -x "$candidate" ]]; then - echo "CKB build finished but executable was not found at $candidate" >&2 - exit 1 - fi - printf '%s\n' "$candidate" -} - -stop_ckb() { - if [[ -n "$CKB_PID" ]] && kill -0 "$CKB_PID" >/dev/null 2>&1; then - kill "$CKB_PID" >/dev/null 2>&1 || true - wait "$CKB_PID" >/dev/null 2>&1 || true - fi - CKB_PID="" -} - -cleanup() { - stop_ckb - if [[ "$KEEP_NODE_LOGS" != "1" && -f "$CKB_LOG" ]]; then - rm -f "$CKB_LOG" - fi - if [[ -n "$CKB_BUILD_TARGET_DIR" && "$CKB_BUILD_TARGET_DIR" == "$RUN_DIR/"* && -d "$CKB_BUILD_TARGET_DIR" ]]; then - rm -rf -- "$CKB_BUILD_TARGET_DIR" - fi -} -trap cleanup EXIT - -require_cmd cargo -require_cmd python3 -if [[ "$RUN_ONCHAIN" == "1" ]]; then - require_cmd git - require_cmd curl -fi - -mkdir -p "$RUN_DIR" - -RPC_URL="" -if [[ "$RUN_ONCHAIN" == "1" ]]; then - if [[ ! -d "$CKB_REPO" ]]; then - echo "CKB repo does not exist: $CKB_REPO" >&2 - exit 1 - fi - if [[ ! -f "$CKB_REPO/test/template/ckb.toml" ]]; then - echo "CKB repo does not contain test/template/ckb.toml: $CKB_REPO" >&2 - exit 1 - fi - if [[ ! -f "$CKB_PIN_FILE" ]]; then - echo "missing CKB acceptance pin: $CKB_PIN_FILE" >&2 - exit 1 - fi - - CKB_PIN_VALUES=() - while IFS= read -r value; do - CKB_PIN_VALUES[${#CKB_PIN_VALUES[@]}]="$value" - done < <(python3 - "$CKB_PIN_FILE" <<'PY' -import json -import pathlib -import sys - -pin = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")) -print(pin["revision"]) -print(pin["version"]) -for path in pin["template_paths"]: - print(path) -PY -) - CKB_PIN_REVISION="${CKB_PIN_VALUES[0]}" - CKB_PIN_VERSION="${CKB_PIN_VALUES[1]}" - CKB_PIN_TEMPLATE="${CKB_PIN_VALUES[2]}" - CKB_PIN_SPEC="${CKB_PIN_VALUES[3]}" - CKB_REPO="$(cd "$CKB_REPO" && pwd)" - CKB_REPO_HEAD="$(git -C "$CKB_REPO" rev-parse HEAD)" - if [[ "$CKB_REPO_HEAD" != "$CKB_PIN_REVISION" ]]; then - echo "CKB acceptance revision mismatch: checkout has $CKB_REPO_HEAD, pin requires $CKB_PIN_REVISION" >&2 - exit 1 - fi - if [[ -n "$(git -C "$CKB_REPO" status --porcelain --untracked-files=all)" ]]; then - echo "CKB acceptance requires a clean pinned CKB checkout: $CKB_REPO" >&2 - git -C "$CKB_REPO" status --short >&2 - exit 1 - fi - for required_template in "$CKB_PIN_TEMPLATE" "$CKB_PIN_SPEC"; do - if [[ ! -f "$CKB_REPO/$required_template" ]]; then - echo "pinned CKB checkout is missing template file: $required_template" >&2 - exit 1 - fi - done - - CKB_BIN="$(resolve_ckb_bin)" - CKB_BIN="$(cd "$(dirname "$CKB_BIN")" && pwd)/$(basename "$CKB_BIN")" - CKB_BIN_VERSION_OUTPUT="$("$CKB_BIN" --version)" - if [[ "$CKB_BIN_VERSION_OUTPUT" != *"$CKB_PIN_VERSION"* || "$CKB_BIN_VERSION_OUTPUT" != *"${CKB_PIN_REVISION:0:7}"* ]]; then - echo "CKB executable provenance mismatch: '$CKB_BIN_VERSION_OUTPUT' does not match version $CKB_PIN_VERSION at ${CKB_PIN_REVISION:0:7}" >&2 - exit 1 - fi - RPC_PORT="$(pick_port)" - P2P_PORT="$(pick_port)" - RPC_URL="http://127.0.0.1:$RPC_PORT" - - mkdir -p "$CKB_DIR" - cp -R "$CKB_REPO/test/template/." "$CKB_DIR/" - - python3 - "$CKB_DIR/ckb.toml" "$RPC_PORT" "$P2P_PORT" <<'PY' -import pathlib -import re -import sys - -path = pathlib.Path(sys.argv[1]) -rpc_port = sys.argv[2] -p2p_port = sys.argv[3] -text = path.read_text(encoding="utf-8") -text = re.sub( - r'listen_address = "127\.0\.0\.1:\d+"', - f'listen_address = "127.0.0.1:{rpc_port}"', - text, - count=1, -) -text = re.sub( - r'listen_addresses = \["/ip4/0\.0\.0\.0/tcp/\d+"\]', - f'listen_addresses = ["/ip4/127.0.0.1/tcp/{p2p_port}"]', - text, - count=1, -) -path.write_text(text, encoding="utf-8") -PY -else - if [[ -d "$CKB_REPO" ]]; then - CKB_REPO="$(cd "$CKB_REPO" && pwd)" - fi - if [[ -n "$CKB_BIN" && -e "$CKB_BIN" ]]; then - CKB_BIN="$(cd "$(dirname "$CKB_BIN")" && pwd)/$(basename "$CKB_BIN")" - fi -fi - -CELLC_BUILD_JSON="$RUN_DIR/cellc-build.jsonl" -CELLC_TARGET_DIR="${CELLSCRIPT_CELLC_TARGET_DIR:-$REPO_ROOT/target/cellscript-cellc}" -if ! cargo build \ - --locked \ +exec cargo run --quiet --locked \ --manifest-path "$REPO_ROOT/Cargo.toml" \ - --bin cellc \ - --target-dir "$CELLC_TARGET_DIR" \ - --message-format=json-render-diagnostics \ - >"$CELLC_BUILD_JSON"; then - cat "$CELLC_BUILD_JSON" >&2 - exit 1 -fi -CELLC_BIN="$(python3 - "$CELLC_BUILD_JSON" <<'PY' -import json -import pathlib -import sys - -for line in pathlib.Path(sys.argv[1]).read_text(encoding="utf-8").splitlines(): - try: - message = json.loads(line) - except json.JSONDecodeError: - continue - if message.get("reason") != "compiler-artifact": - continue - target = message.get("target") or {} - if target.get("name") != "cellc" or "bin" not in target.get("kind", []): - continue - executable = message.get("executable") - if executable: - print(executable) - break -PY -)" -if [[ -z "$CELLC_BIN" || ! -x "$CELLC_BIN" ]]; then - cat "$CELLC_BUILD_JSON" >&2 - echo "cellc build finished but Cargo did not report an executable artifact" >&2 - exit 1 -fi - -python3 - "$CELLC_BIN" "$REPO_ROOT" "$RUN_DIR" "$REPORT_JSON" "$ACCEPTANCE_MODE" <<'PY' -import datetime -import hashlib -import json -import math -import os -import pathlib -import re -import shutil -import struct -import subprocess -import sys - -cellc = pathlib.Path(sys.argv[1]) -repo_root = pathlib.Path(sys.argv[2]) -run_dir = pathlib.Path(sys.argv[3]) -report_path = pathlib.Path(sys.argv[4]) -acceptance_mode = sys.argv[5] - -SOURCE_PROVENANCE_SCHEMA = "cellscript-ckb-acceptance-source-provenance-v0.22" -BUILD_REPORT_SCHEMA = "cellscript-ckb-build-report-v0.20" -SOURCE_PROVENANCE_PATHS = [ - "Cargo.lock", - "Cargo.toml", - "rust-toolchain.toml", - ".github/workflows/release.yml", - "src", - "examples", - "scripts/cellscript_gate.sh", - "scripts/cellscript_ckb_release_gate.sh", - "scripts/ckb_acceptance_pin.json", - "scripts/ckb_cellscript_acceptance.sh", - "scripts/validate_ckb_cellscript_production_evidence.py", -] - -EXAMPLES = [ - "amm_pool.cell", - "launch.cell", - "multisig.cell", - "nft.cell", - "timelock.cell", - "token.cell", - "vesting.cell", -] -NON_PRODUCTION_EXAMPLES = [ - # 0.13 bounded collection helper coverage. This is intentionally exercised - # by broader CellScript tooling tests, not by the CKB production - # bundled-contract matrix. - "registry.cell", - # 0.21 business-flow examples. These illustrate flow-edge validation, - # state transitions, and cross-module composition for auditing and docs. - # They are not part of the production bundled-contract deployment matrix. - "atomic_swap.cell", - "multi_phase_dao.cell", -] -LANGUAGE_EXAMPLES = [ - "canonical_style.cell", - "order_book.cell", - "registry.cell", - "stdlib.cell", - "v0_14_capacity_time.cell", - "v0_14_ckb_type_id_create.cell", - "v0_14_delegate_verify.cell", - "v0_14_hash_blake2b.cell", - "v0_14_multi_step_pipeline.cell", - "v0_14_witness_source.cell", - "v0_15_identity_lifecycle.cell", - "v0_15_scoped_invariant.cell", - "v0_22_borrow.cell", - "v0_22_bounded_lifecycle.cell", - "v0_22_transaction_views.cell", -] -EXAMPLE_SCOPE = { - "production_bundled_examples": EXAMPLES, - "non_production_top_level_examples": NON_PRODUCTION_EXAMPLES, - "non_production_language_examples": LANGUAGE_EXAMPLES, - "production_scope_note": ( - "Only production_bundled_examples are deployed and action-exercised by this CKB production " - "acceptance report. non_production_top_level_examples and non_production_language_examples are " - "covered by compiler/tooling tests unless they are promoted into production_bundled_examples." - ), -} -LOCK_ACCEPTANCE_SCOPE = { - "strict_compile_only": True, - "onchain_lock_spend_matrix": False, - "pending_onchain_lock_spend_matrix": { - "multisig.cell": ["is_signer_lock", "can_execute", "can_cancel", "has_enough_approvals", "not_expired"], - "nft.cell": ["nft_ownership", "listing_seller", "offer_buyer", "valid_royalty", "collection_creator"], - "timelock.cell": ["can_unlock_lock", "is_owner", "lock_id_commitment", "asset_matches", "not_expired", "emergency_approved"], - "vesting.cell": ["vesting_admin"], - }, - "required_cases_per_lock_when_promoted": ["valid_spend", "invalid_spend"], - "scope_note": ( - "Scoped lock entries are strict-compiled under the CKB profile and counted as strict lock coverage. " - "They are not counted as on-chain acceptance-harness lock spend/deny-spend transactions." - ), -} -LOCK_BEHAVIOR_ACCEPTANCE_SCOPE = { - "strict_compile_only": False, - "onchain_lock_spend_matrix": True, - "onchain_lock_spend_matrix_scope": { - "multisig.cell": ["is_signer_lock", "can_execute", "can_cancel", "has_enough_approvals", "not_expired"], - "nft.cell": ["nft_ownership", "listing_seller", "offer_buyer", "valid_royalty", "collection_creator"], - "timelock.cell": ["can_unlock_lock", "is_owner", "lock_id_commitment", "asset_matches", "not_expired", "emergency_approved"], - "vesting.cell": ["vesting_admin"], - }, - "required_cases_per_lock": ["valid_spend", "invalid_spend"], - "scope_note": ( - "Scoped lock entries are strict-compiled under the CKB profile and each lock is exercised " - "through handwritten Python acceptance-harness valid-spend and invalid-spend transactions." - ), -} -TRUNCATE = 12000 -UNEXPECTED_PROFILE_TRAILER = bytes.fromhex("53504f5241424900") -ELF_ENTRY_ABI_SCHEMA = "cellscript-ckb-elf-entry-abi-v0.22" -ELF64_HEADER_SIZE = 64 -ELF64_PROGRAM_HEADER_SIZE = 56 -ELF_PT_LOAD = 1 -ELF_PF_X = 1 -ELF_PF_W = 2 -ELF_PF_R = 4 -ELF_EM_RISCV = 243 -ENTRY_TRAMPOLINE_SIZE = 20 -CRITICAL_0_20_DEVNET_EXAMPLES = ["launch.cell", "token.cell", "amm_pool.cell"] - -examples_dir = repo_root / "examples" -language_examples_dir = examples_dir / "language" - -def production_example_path(name): - return examples_dir / name - -def production_example_build_path(name): - """Return the path to pass to cellc for building. Uses the workspace package directory when available.""" - pkg_dir = examples_dir / name.replace(".cell", "") - if (pkg_dir / "Cell.toml").is_file(): - return pkg_dir - return examples_dir / name - -def language_example_path(name): - source = language_examples_dir / name - if source.is_file(): - return source - return examples_dir / name - -def language_example_build_path(name): - """Return the path to pass to cellc for building. Uses the workspace package directory when available.""" - pkg_dir = examples_dir / "language" - if (pkg_dir / "Cell.toml").is_file(): - return pkg_dir - return language_example_path(name) - -actual_flat_examples = sorted( - path.name - for path in examples_dir.glob("*.cell") - if path.is_file() and path.name not in NON_PRODUCTION_EXAMPLES -) -if actual_flat_examples != sorted(EXAMPLES): - raise SystemExit(f"canonical bundled examples changed: expected {sorted(EXAMPLES)}, found {actual_flat_examples}") -actual_non_production_examples = sorted( - path.name - for path in examples_dir.glob("*.cell") - if path.is_file() and path.name in NON_PRODUCTION_EXAMPLES -) -if actual_non_production_examples != sorted(NON_PRODUCTION_EXAMPLES): - raise SystemExit( - f"non-production top-level examples changed: expected {sorted(NON_PRODUCTION_EXAMPLES)}, " - f"found {actual_non_production_examples}" - ) -actual_language_examples = sorted(path.name for path in language_examples_dir.glob("*.cell") if path.is_file()) -if actual_language_examples != sorted(LANGUAGE_EXAMPLES): - raise SystemExit(f"language examples changed: expected {sorted(LANGUAGE_EXAMPLES)}, found {actual_language_examples}") -for stale_dir in ("business", "acceptance"): - stale_path = examples_dir / stale_dir - if stale_path.exists(): - raise SystemExit(f"stale checked-in example mirror directory must be removed: {stale_path.relative_to(repo_root)}") -for name in NON_PRODUCTION_EXAMPLES: - if not (examples_dir / name).is_file(): - raise SystemExit(f"missing non-production top-level example: {name}") -for name in LANGUAGE_EXAMPLES: - if not (language_examples_dir / name).is_file(): - raise SystemExit(f"missing non-production language example: {name}") - -source_root = run_dir / "generated-sources" -baseline_source_root = source_root / "baseline" -token_action_source_root = source_root / "token-actions" -nft_action_source_root = source_root / "nft-actions" -timelock_action_source_root = source_root / "timelock-actions" -amm_action_source_root = source_root / "amm-actions" -multisig_action_source_root = source_root / "multisig-actions" -launch_action_source_root = source_root / "launch-actions" -artifact_root = run_dir / "artifacts" -strict_root = run_dir / "strict-original-ckb" -for path in ( - baseline_source_root, - token_action_source_root, - nft_action_source_root, - timelock_action_source_root, - amm_action_source_root, - multisig_action_source_root, - launch_action_source_root, - artifact_root, - strict_root, -): - path.mkdir(parents=True, exist_ok=True) - -baseline_source = baseline_source_root / "ckb_noop.cell" -baseline_source.write_text( - """module acceptance::ckb_noop - -action main() -> u64 { - verification - 0 -} -""", - encoding="utf-8", -) - -TOKEN_TYPES_SOURCE = """resource Token has store, create, consume, replace, burn, relock { - amount: u64 - symbol: [u8; 8] -} - -resource MintAuthority has store, create, replace { - token_symbol: [u8; 8] - max_supply: u64 - minted: u64 -} -""" - -TOKEN_ACTION_SOURCES = { - "mint_with_authority": """ -action mint_with_authority(auth_before: MintAuthority, to: Address, amount: u64) -> (auth_after: MintAuthority, token: Token) { - verification - require auth_before.minted + amount <= auth_before.max_supply - - require auth_after.token_symbol == auth_before.token_symbol - require auth_after.max_supply == auth_before.max_supply - require auth_after.minted == auth_before.minted + amount - - create token = Token { - amount: amount, - symbol: auth_before.token_symbol - } with_lock(to) -} -""", - "transfer_token": """ -action transfer_token(token: Token, to: Address) -> next_token: Token { - verification - consume token - create next_token = Token { - amount: token.amount, - symbol: token.symbol - } with_lock(to) -} -""", - "burn": """ -action burn(token: Token) { - verification - require token.amount > 0 - destroy token -} -""", - "merge": """ -action merge(a: Token, b: Token, to: Address) -> merged: Token { - verification - require a.symbol == b.symbol - let total = a.amount + b.amount - consume a - consume b - - create merged = Token { - amount: total, - symbol: a.symbol - } with_lock(to) -} -""", -} - -for action, source in TOKEN_ACTION_SOURCES.items(): - (token_action_source_root / f"token_{action}.cell").write_text( - f"module acceptance::token_{action}\n\n" + TOKEN_TYPES_SOURCE + "\n" + source, - encoding="utf-8", - ) - -NFT_TYPES_SOURCE = """resource NFT has store, create, consume, replace, burn, relock, read_ref { - token_id: u64 - owner: Address - metadata_hash: Hash - royalty_recipient: Address - royalty_bps: u16 -} - -resource Collection has store, create, replace { - creator: Address - total_supply: u64 - max_supply: u64 -} - -receipt Listing has create, consume, burn { - token_id: u64 - seller: Address - price: u64 - created_at: u64 -} - -receipt Offer has create, consume, burn { - token_id: u64 - buyer: Address - price: u64 - expires_at: u64 -} - -receipt RoyaltyPayment has create { - token_id: u64 - recipient: Address - amount: u64 -} -""" - -NFT_ACTION_SOURCES = { - "create_collection": """ -action create_collection(creator: Address, max_supply: u64) -> collection: Collection { - verification - require max_supply > 0, "max supply must be positive" - require max_supply <= 10000, "max supply too high" - - create collection = Collection { - creator: creator, - total_supply: 0, - max_supply: max_supply - } with_lock(creator) -} -""", - "mint": """ -action mint(collection_before: Collection, to: Address, metadata_hash: Hash) -> (collection_after: Collection, nft: NFT) { - verification - require collection_before.total_supply < collection_before.max_supply - let token_id = collection_before.total_supply + 1 - - require collection_after.creator == collection_before.creator - require collection_after.max_supply == collection_before.max_supply - require collection_after.total_supply == token_id - - create nft = NFT { - token_id: token_id, - owner: to, - metadata_hash: metadata_hash, - royalty_recipient: collection_before.creator, - royalty_bps: 250 - } -} -""", - "transfer": """ -action transfer(nft_before: NFT, to: Address) -> nft_after: NFT { - verification - require nft_before.owner != to - require nft_after.token_id == nft_before.token_id - require nft_after.owner == to - require nft_after.metadata_hash == nft_before.metadata_hash - require nft_after.royalty_recipient == nft_before.royalty_recipient - require nft_after.royalty_bps == nft_before.royalty_bps -} -""", - "create_listing": """ -action create_listing(read nft: NFT, price: u64, current_time: u64) -> listing: Listing { - verification - require price > 0 - create listing = Listing { - token_id: nft.token_id, - seller: nft.owner, - price: price, - created_at: current_time - } -} -""", - "cancel_listing": """ -action cancel_listing(listing: Listing) { - verification - destroy listing -} -""", - "buy_from_listing": """ -action buy_from_listing(nft_before: NFT, listing: Listing, buyer: Address, seller: Address, payment: u64) -> (nft_after: NFT, royalty_payment: RoyaltyPayment, seller_payment: RoyaltyPayment) { - verification - require payment >= listing.price - - let royalty_amount = payment * nft_before.royalty_bps / 10000 - let seller_amount = payment - royalty_amount - - require nft_after.token_id == nft_before.token_id - require nft_after.owner == buyer - require nft_after.metadata_hash == nft_before.metadata_hash - require nft_after.royalty_recipient == nft_before.royalty_recipient - require nft_after.royalty_bps == nft_before.royalty_bps - - destroy listing - - create royalty_payment = RoyaltyPayment { - token_id: nft_before.token_id, - recipient: nft_before.royalty_recipient, - amount: royalty_amount - } - - create seller_payment = RoyaltyPayment { - token_id: nft_before.token_id, - recipient: seller, - amount: seller_amount - } -} -""", - "create_offer": """ -action create_offer(token_id: u64, buyer: Address, price: u64, expires_at: u64) -> offer: Offer { - verification - require price > 0 - require expires_at > 0 - create offer = Offer { - token_id: token_id, - buyer: buyer, - price: price, - expires_at: expires_at - } -} -""", - "accept_offer": """ -action accept_offer(nft_before: NFT, offer: Offer, buyer: Address, seller: Address, price: u64, current_time: u64) -> (nft_after: NFT, royalty_payment: RoyaltyPayment, seller_payment: RoyaltyPayment) { - verification - require current_time < offer.expires_at - - let royalty_amount = price * nft_before.royalty_bps / 10000 - let seller_amount = price - royalty_amount - - require nft_after.token_id == nft_before.token_id - require nft_after.owner == buyer - require nft_after.metadata_hash == nft_before.metadata_hash - require nft_after.royalty_recipient == nft_before.royalty_recipient - require nft_after.royalty_bps == nft_before.royalty_bps - - destroy offer - - create royalty_payment = RoyaltyPayment { - token_id: nft_before.token_id, - recipient: nft_before.royalty_recipient, - amount: royalty_amount - } - - create seller_payment = RoyaltyPayment { - token_id: nft_before.token_id, - recipient: seller, - amount: seller_amount - } -} -""", - "burn": """ -action burn(nft: NFT) { - verification - destroy nft -} -""", - "batch_mint": """ -action batch_mint( - collection_before: Collection, - recipients: [Address; 4], - metadata_hashes: [Hash; 4], -) -> (collection_after: Collection, nft0: NFT, nft1: NFT, nft2: NFT, nft3: NFT) { - verification - require collection_before.total_supply + 4 <= collection_before.max_supply - let first_token_id = collection_before.total_supply + 1 - - require collection_after.creator == collection_before.creator - require collection_after.max_supply == collection_before.max_supply - require collection_after.total_supply == collection_before.total_supply + 4 - - create nft0 = NFT { - token_id: first_token_id, - owner: recipients[0], - metadata_hash: metadata_hashes[0], - royalty_recipient: collection_before.creator, - royalty_bps: 250 - } - create nft1 = NFT { - token_id: first_token_id + 1, - owner: recipients[1], - metadata_hash: metadata_hashes[1], - royalty_recipient: collection_before.creator, - royalty_bps: 250 - } - create nft2 = NFT { - token_id: first_token_id + 2, - owner: recipients[2], - metadata_hash: metadata_hashes[2], - royalty_recipient: collection_before.creator, - royalty_bps: 250 - } - create nft3 = NFT { - token_id: first_token_id + 3, - owner: recipients[3], - metadata_hash: metadata_hashes[3], - royalty_recipient: collection_before.creator, - royalty_bps: 250 - } -} -""", -} - -for action, source in NFT_ACTION_SOURCES.items(): - (nft_action_source_root / f"nft_{action}.cell").write_text( - f"module acceptance::nft_{action}\n\n" + NFT_TYPES_SOURCE + "\n" + source, - encoding="utf-8", - ) - -TIMELOCK_TYPES_SOURCE = """resource TimeLock has store, create, consume, replace, burn, read_ref { - owner: Address - lock_type: u8 - unlock_height: u64 - created_at: u64 -} - -resource LockedAsset has store, create, consume, burn { - amount: u64 - lock_hash: Hash -} - -receipt ReleaseRequest has create, consume, burn { - lock_hash: Hash - requester: Address - requested_at: u64 -} - -receipt EmergencyRelease has create, consume, replace, burn { - lock_hash: Hash - requester: Address - requested_at: u64 - approvals: u8 -} - -receipt ReleaseRecord has create { - lock_hash: Hash - released_at: u64 - released_by: Address -} -""" - -TIMELOCK_ACTION_SOURCES = { - "create_absolute_lock": """ -action create_absolute_lock(owner: Address, unlock_height: u64, current_height: u64) -> created_lock: TimeLock { - verification - require unlock_height > current_height + 10 - require unlock_height <= current_height + 2628000 - create created_lock = TimeLock { - owner: owner, - lock_type: 0, - unlock_height: unlock_height, - created_at: current_height - } -} -""", - "create_relative_lock": """ -action create_relative_lock(owner: Address, lock_period: u64, current_height: u64) -> created_lock: TimeLock { - verification - require lock_period >= 10 - require lock_period <= 2628000 - create created_lock = TimeLock { - owner: owner, - lock_type: 1, - unlock_height: current_height + lock_period, - created_at: current_height - } -} -""", - "lock_asset": """ -action lock_asset(read time_lock: TimeLock, lock_hash: Hash, amount: u64) -> locked: LockedAsset { - verification - require amount > 0 - create locked = LockedAsset { - amount: amount, - lock_hash: lock_hash - } -} -""", - "request_release": """ -action request_release(read time_lock: TimeLock, lock_hash: Hash, requester: Address, current_height: u64) -> request: ReleaseRequest { - verification - require current_height >= time_lock.unlock_height - create request = ReleaseRequest { - lock_hash: lock_hash, - requester: requester, - requested_at: current_height - } -} -""", - "request_emergency_release": """ -action request_emergency_release(read time_lock: TimeLock, lock_hash: Hash, requester: Address, current_height: u64) -> emergency: EmergencyRelease { - verification - require time_lock.owner == requester - require current_height < time_lock.unlock_height - create emergency = EmergencyRelease { - lock_hash: lock_hash, - requester: requester, - requested_at: current_height, - approvals: 0 - } -} -""", - "approve_emergency_release": """ -action approve_emergency_release(emergency_before: EmergencyRelease, approver: Address, required_approvals: u8) -> emergency_after: EmergencyRelease { - verification - require emergency_before.approvals < required_approvals - require emergency_after.lock_hash == emergency_before.lock_hash - require emergency_after.requester == emergency_before.requester - require emergency_after.requested_at == emergency_before.requested_at - require emergency_after.approvals == emergency_before.approvals + 1 -} -""", - "extend_lock": """ -action extend_lock(time_lock_before: TimeLock, additional_period: u64, owner: Address, current_height: u64) -> time_lock_after: TimeLock { - verification - require time_lock_before.owner == owner - require current_height < time_lock_before.unlock_height - - let new_unlock_height = time_lock_before.unlock_height + additional_period - require new_unlock_height <= current_height + 2628000 - - require time_lock_after.owner == time_lock_before.owner - require time_lock_after.lock_type == time_lock_before.lock_type - require time_lock_after.unlock_height == new_unlock_height - require time_lock_after.created_at == time_lock_before.created_at -} -""", - "execute_release": """ -action execute_release( - time_lock: TimeLock, - locked_asset: LockedAsset, - request: ReleaseRequest, - executor: Address -) -> record: ReleaseRecord { - verification - require time_lock.owner == executor - require locked_asset.lock_hash == request.lock_hash - - create record = ReleaseRecord { - lock_hash: request.lock_hash, - released_at: 125, - released_by: executor - } - - destroy time_lock - destroy locked_asset - destroy request -} -""", - "execute_emergency_release": """ -action execute_emergency_release( - time_lock: TimeLock, - locked_asset: LockedAsset, - emergency: EmergencyRelease, - executor: Address, - required_approvals: u8 -) -> record: ReleaseRecord { - verification - require time_lock.owner == executor - require emergency.approvals >= required_approvals - require locked_asset.lock_hash == emergency.lock_hash - - create record = ReleaseRecord { - lock_hash: emergency.lock_hash, - released_at: 125, - released_by: executor - } - - destroy time_lock - destroy locked_asset - destroy emergency -} -""", - "batch_create_locks": """ -action batch_create_locks( - owners: [Address; 4], - unlock_heights: [u64; 4], - current_height: u64, -) -> (lock0: TimeLock, lock1: TimeLock, lock2: TimeLock, lock3: TimeLock) { - verification - require unlock_heights[0] > current_height + 10 - require unlock_heights[1] > current_height + 10 - require unlock_heights[2] > current_height + 10 - require unlock_heights[3] > current_height + 10 - require unlock_heights[0] <= current_height + 2628000 - require unlock_heights[1] <= current_height + 2628000 - require unlock_heights[2] <= current_height + 2628000 - require unlock_heights[3] <= current_height + 2628000 - - create lock0 = TimeLock { - owner: owners[0], - lock_type: 0, - unlock_height: unlock_heights[0], - created_at: current_height - } - create lock1 = TimeLock { - owner: owners[1], - lock_type: 0, - unlock_height: unlock_heights[1], - created_at: current_height - } - create lock2 = TimeLock { - owner: owners[2], - lock_type: 0, - unlock_height: unlock_heights[2], - created_at: current_height - } - create lock3 = TimeLock { - owner: owners[3], - lock_type: 0, - unlock_height: unlock_heights[3], - created_at: current_height - } -} -""", -} - -for action, source in TIMELOCK_ACTION_SOURCES.items(): - (timelock_action_source_root / f"timelock_{action}.cell").write_text( - f"module acceptance::timelock_{action}\n\n" + TIMELOCK_TYPES_SOURCE + "\n" + source, - encoding="utf-8", - ) - -AMM_ACTION_SOURCES = { - "seed_pool": """ -resource Token has store, create, consume { - amount: u64 - symbol: [u8; 8] -} - -shared Pool has store, create, replace { - token_a_symbol: [u8; 8] - token_b_symbol: [u8; 8] - reserve_a: u64 - reserve_b: u64 - total_lp: u64 - fee_rate_bps: u16 -} - -receipt LPReceipt has store, create, consume { - pool_id: Hash - lp_amount: u64 - provider: Address -} - -action seed_pool(token_a: Token, token_b: Token, fee_rate_bps: u16, provider: Address) -> (pool: Pool, receipt: LPReceipt) { - verification - require token_a.symbol != token_b.symbol - require token_a.amount > 0 && token_b.amount > 0 - require fee_rate_bps <= 10000 - - let initial_lp = isqrt(token_a.amount * token_b.amount) - - consume token_a - consume token_b - - create pool = Pool { - token_a_symbol: token_a.symbol, - token_b_symbol: token_b.symbol, - reserve_a: token_a.amount, - reserve_b: token_b.amount, - total_lp: initial_lp, - fee_rate_bps: fee_rate_bps - } - - create receipt = LPReceipt { - pool_id: pool.type_hash(), - lp_amount: initial_lp, - provider: provider - } with_lock(provider) -} - -fn isqrt(n: u64) -> u64 { - if n == 0 { - return 0 - } - - let mut x = n - let mut y = (x + 1) / 2 - - while y < x { - x = y - y = (x + n / x) / 2 - } - - x -} -""", - "add_liquidity": """ -resource Token has store, create, consume { - amount: u64 - symbol: [u8; 8] -} - -shared Pool has store, create, replace { - token_a_symbol: [u8; 8] - token_b_symbol: [u8; 8] - reserve_a: u64 - reserve_b: u64 - total_lp: u64 - fee_rate_bps: u16 -} - -receipt LPReceipt has store, create, consume { - pool_id: Hash - lp_amount: u64 - provider: Address -} - -action add_liquidity(pool_before: Pool, token_a: Token, token_b: Token, provider: Address) -> (pool_after: Pool, receipt: LPReceipt) { - verification - require token_a.symbol == pool_before.token_a_symbol - require token_b.symbol == pool_before.token_b_symbol - - let lp_from_a = token_a.amount * pool_before.total_lp / pool_before.reserve_a - let lp_from_b = token_b.amount * pool_before.total_lp / pool_before.reserve_b - let lp_amount = min(lp_from_a, lp_from_b) - - consume token_a - consume token_b - - require pool_after.token_a_symbol == pool_before.token_a_symbol - require pool_after.token_b_symbol == pool_before.token_b_symbol - require pool_after.reserve_a == pool_before.reserve_a + token_a.amount - require pool_after.reserve_b == pool_before.reserve_b + token_b.amount - require pool_after.total_lp == pool_before.total_lp + lp_amount - require pool_after.fee_rate_bps == pool_before.fee_rate_bps - - create receipt = LPReceipt { - pool_id: pool_before.type_hash(), - lp_amount: lp_amount, - provider: provider - } with_lock(provider) -} - -fn min(a: u64, b: u64) -> u64 { - if a < b { a } else { b } -} -""", - "swap_a_for_b": """ -resource Token has store, create, consume { - amount: u64 - symbol: [u8; 8] -} - -shared Pool has store, create, replace { - token_a_symbol: [u8; 8] - token_b_symbol: [u8; 8] - reserve_a: u64 - reserve_b: u64 - total_lp: u64 - fee_rate_bps: u16 -} - -action swap_a_for_b(pool_before: Pool, input: Token, min_output: u64, to: Address) -> (pool_after: Pool, token_out: Token) { - verification - require input.symbol == pool_before.token_a_symbol - - let fee = input.amount * pool_before.fee_rate_bps as u64 / 10000 - let net_input = input.amount - fee - - let amount_out = pool_before.reserve_b * net_input / (pool_before.reserve_a + net_input) - - require amount_out >= min_output - require amount_out < pool_before.reserve_b - - consume input - - require pool_after.token_a_symbol == pool_before.token_a_symbol - require pool_after.token_b_symbol == pool_before.token_b_symbol - require pool_after.reserve_a == pool_before.reserve_a + input.amount - require pool_after.reserve_b == pool_before.reserve_b - amount_out - require pool_after.total_lp == pool_before.total_lp - require pool_after.fee_rate_bps == pool_before.fee_rate_bps - - create token_out = Token { - amount: amount_out, - symbol: pool_before.token_b_symbol - } with_lock(to) -} -""", - "remove_liquidity": """ -resource Token has store, create, consume { - amount: u64 - symbol: [u8; 8] -} - -shared Pool has store, create, replace { - token_a_symbol: [u8; 8] - token_b_symbol: [u8; 8] - reserve_a: u64 - reserve_b: u64 - total_lp: u64 - fee_rate_bps: u16 -} - -receipt LPReceipt has store, create, consume { - pool_id: Hash - lp_amount: u64 - provider: Address -} - -action remove_liquidity(pool_before: Pool, receipt: LPReceipt, provider: Address) -> (pool_after: Pool, token_a_out: Token, token_b_out: Token) { - verification - require receipt.pool_id == pool_before.type_hash() - - let amount_a = receipt.lp_amount * pool_before.reserve_a / pool_before.total_lp - let amount_b = receipt.lp_amount * pool_before.reserve_b / pool_before.total_lp - - consume receipt - - require pool_after.token_a_symbol == pool_before.token_a_symbol - require pool_after.token_b_symbol == pool_before.token_b_symbol - require pool_after.reserve_a == pool_before.reserve_a - amount_a - require pool_after.reserve_b == pool_before.reserve_b - amount_b - require pool_after.total_lp == pool_before.total_lp - receipt.lp_amount - require pool_after.fee_rate_bps == pool_before.fee_rate_bps - - create token_a_out = Token { - amount: amount_a, - symbol: pool_before.token_a_symbol - } with_lock(provider) - - create token_b_out = Token { - amount: amount_b, - symbol: pool_before.token_b_symbol - } with_lock(provider) -} -""", -} - -for action, source in AMM_ACTION_SOURCES.items(): - (amm_action_source_root / f"amm_{action}.cell").write_text( - f"module acceptance::amm_{action}\n\n" + source, - encoding="utf-8", - ) - -MULTISIG_TYPES_SOURCE = """resource MultisigWallet has store, create, replace, read_ref { - wallet_id: Hash - signer_a: Address - signer_b: Address - threshold: u8 - nonce: u64 - created_at: u64 -} - -receipt Proposal has create, consume, replace, burn { - wallet_id: Hash - proposal_id: u64 - proposer: Address - operation: u8 - target: Address - amount: u64 - required_approvals: u8 - approval_count: u8 - created_at: u64 - expires_at: u64 -} - -receipt ApprovalConfirmation has create { - proposal_id: u64 - approver: Address - reported_at: u64 -} - -receipt ExecutionRecord has create { - proposal_id: u64 - executor: Address - executed_at: u64 - success: u8 -} -""" - -MULTISIG_ACTION_SOURCES = { - "create_wallet": """ -action create_wallet(wallet_id: Hash, signer_a: Address, signer_b: Address, threshold: u8, current_time: u64) -> wallet: MultisigWallet { - verification - require signer_a != signer_b - require threshold >= 2 - require threshold <= 2 - - create wallet = MultisigWallet { - wallet_id: wallet_id, - signer_a: signer_a, - signer_b: signer_b, - threshold: threshold, - nonce: 0, - created_at: current_time - } -} -""", - "propose_transfer": """ -action propose_transfer(wallet_before: MultisigWallet, proposer: Address, target: Address, amount: u64, current_time: u64) -> (wallet_after: MultisigWallet, proposal: Proposal) { - verification - require proposer == wallet_before.signer_a - require amount > 0 - - let proposal_id = wallet_before.nonce + 1 - - require wallet_after.wallet_id == wallet_before.wallet_id - require wallet_after.signer_a == wallet_before.signer_a - require wallet_after.signer_b == wallet_before.signer_b - require wallet_after.threshold == wallet_before.threshold - require wallet_after.nonce == proposal_id - require wallet_after.created_at == wallet_before.created_at - - create proposal = Proposal { - wallet_id: wallet_before.wallet_id, - proposal_id: proposal_id, - proposer: proposer, - operation: 0, - target: target, - amount: amount, - required_approvals: wallet_before.threshold, - approval_count: 0, - created_at: current_time, - expires_at: current_time + 1440 - } -} -""", - "record_approval": """ -action record_approval(proposal_before: Proposal, approver: Address, reported_time: u64) -> (proposal_after: Proposal, confirmation: ApprovalConfirmation) { - verification - require reported_time < proposal_before.expires_at - require proposal_before.approval_count < proposal_before.required_approvals - - require proposal_after.wallet_id == proposal_before.wallet_id - require proposal_after.proposal_id == proposal_before.proposal_id - require proposal_after.proposer == proposal_before.proposer - require proposal_after.operation == proposal_before.operation - require proposal_after.target == proposal_before.target - require proposal_after.amount == proposal_before.amount - require proposal_after.required_approvals == proposal_before.required_approvals - require proposal_after.approval_count == proposal_before.approval_count + 1 - require proposal_after.created_at == proposal_before.created_at - require proposal_after.expires_at == proposal_before.expires_at - - create confirmation = ApprovalConfirmation { - proposal_id: proposal_before.proposal_id, - approver: approver, - reported_at: reported_time - } -} -""", - "propose_add_signer": """ -action propose_add_signer(wallet_before: MultisigWallet, proposer: Address, new_signer: Address, current_time: u64) -> (wallet_after: MultisigWallet, proposal: Proposal) { - verification - require proposer == wallet_before.signer_a - require new_signer != wallet_before.signer_a - require new_signer != wallet_before.signer_b - - let proposal_id = wallet_before.nonce + 1 - - require wallet_after.wallet_id == wallet_before.wallet_id - require wallet_after.signer_a == wallet_before.signer_a - require wallet_after.signer_b == wallet_before.signer_b - require wallet_after.threshold == wallet_before.threshold - require wallet_after.nonce == proposal_id - require wallet_after.created_at == wallet_before.created_at - - create proposal = Proposal { - wallet_id: wallet_before.wallet_id, - proposal_id: proposal_id, - proposer: proposer, - operation: 1, - target: new_signer, - amount: 0, - required_approvals: wallet_before.threshold, - approval_count: 0, - created_at: current_time, - expires_at: current_time + 1440 - } -} -""", - "propose_remove_signer": """ -action propose_remove_signer(wallet_before: MultisigWallet, proposer: Address, signer_to_remove: Address, current_time: u64) -> (wallet_after: MultisigWallet, proposal: Proposal) { - verification - require proposer == wallet_before.signer_a - require signer_to_remove == wallet_before.signer_b - require wallet_before.threshold <= 1 - - let proposal_id = wallet_before.nonce + 1 - - require wallet_after.wallet_id == wallet_before.wallet_id - require wallet_after.signer_a == wallet_before.signer_a - require wallet_after.signer_b == wallet_before.signer_b - require wallet_after.threshold == wallet_before.threshold - require wallet_after.nonce == proposal_id - require wallet_after.created_at == wallet_before.created_at - - create proposal = Proposal { - wallet_id: wallet_before.wallet_id, - proposal_id: proposal_id, - proposer: proposer, - operation: 2, - target: signer_to_remove, - amount: 0, - required_approvals: wallet_before.threshold, - approval_count: 0, - created_at: current_time, - expires_at: current_time + 1440 - } -} -""", - "propose_change_threshold": """ -action propose_change_threshold(wallet_before: MultisigWallet, proposer: Address, new_threshold: u8, current_time: u64) -> (wallet_after: MultisigWallet, proposal: Proposal) { - verification - require proposer == wallet_before.signer_a - require new_threshold >= 1 - require new_threshold <= 2 - - let proposal_id = wallet_before.nonce + 1 - - require wallet_after.wallet_id == wallet_before.wallet_id - require wallet_after.signer_a == wallet_before.signer_a - require wallet_after.signer_b == wallet_before.signer_b - require wallet_after.threshold == wallet_before.threshold - require wallet_after.nonce == proposal_id - require wallet_after.created_at == wallet_before.created_at - - create proposal = Proposal { - wallet_id: wallet_before.wallet_id, - proposal_id: proposal_id, - proposer: proposer, - operation: 3, - target: Address::zero(), - amount: new_threshold as u64, - required_approvals: wallet_before.threshold, - approval_count: 0, - created_at: current_time, - expires_at: current_time + 1440 - } -} -""", - "execute_proposal": """ -action execute_proposal(proposal: Proposal, executor: Address, current_time: u64) -> record: ExecutionRecord { - verification - require current_time < proposal.expires_at - require proposal.approval_count >= proposal.required_approvals - - create record = ExecutionRecord { - proposal_id: proposal.proposal_id, - executor: executor, - executed_at: current_time, - success: 1 - } - - destroy proposal -} -""", - "cancel_proposal": """ -action cancel_proposal(proposal: Proposal, canceller: Address) { - verification - require proposal.proposer == canceller - destroy proposal -} -""", -} - -for action, source in MULTISIG_ACTION_SOURCES.items(): - (multisig_action_source_root / f"multisig_{action}.cell").write_text( - f"module acceptance::multisig_{action}\n\n" + MULTISIG_TYPES_SOURCE + "\n" + source, - encoding="utf-8", - ) - -LAUNCH_TYPES_SOURCE = """const U64_MAX: u64 = 18446744073709551615 - -resource Token has store, create, consume, replace, burn, relock { - amount: u64 - symbol: [u8; 8] -} - -resource MintAuthority has store, create, replace { - token_symbol: [u8; 8] - max_supply: u64 - minted: u64 -} - -receipt LPReceipt has store, create, consume { - pool_id: Hash - lp_amount: u64 - provider: Address -} - -shared Pool has store, create, replace { - token_a_symbol: [u8; 8] - token_b_symbol: [u8; 8] - reserve_a: u64 - reserve_b: u64 - total_lp: u64 - fee_rate_bps: u16 -} -""" - -LAUNCH_ACTION_SOURCES = { - "launch_token": """ -action launch_token(symbol: [u8; 8], max_supply: u64, initial_mint: u64, pool_seed_amount: u64, pool_paired_token: Token, fee_rate_bps: u16, creator: Address, distribution: [(Address, u64); 4]) -> (auth: MintAuthority, dist0: Token, dist1: Token, dist2: Token, dist3: Token, pool: Pool, lp_receipt: LPReceipt, change: Token) { - verification - require initial_mint <= max_supply, "initial exceeds max" - require pool_seed_amount > 0, "zero pool seed" - require pool_paired_token.amount > 0, "zero paired seed" - require symbol != pool_paired_token.symbol, "same token" - require fee_rate_bps <= 10000, "fee too high" - require pool_seed_amount <= initial_mint, "pool seed exceeds mint" - require distribution[1].1 <= U64_MAX - distribution[0].1, "distribution overflow" - let dist01 = distribution[0].1 + distribution[1].1 - require distribution[2].1 <= U64_MAX - dist01, "distribution overflow" - let dist012 = dist01 + distribution[2].1 - require distribution[3].1 <= U64_MAX - dist012, "distribution overflow" - let dist_total = dist012 + distribution[3].1 - require pool_seed_amount <= U64_MAX - dist_total, "allocation overflow" - require dist_total + pool_seed_amount <= initial_mint, "allocation exceeds mint" - - create auth = MintAuthority { - token_symbol: symbol, - max_supply: max_supply, - minted: initial_mint - } with_lock(creator) - create dist0 = Token { amount: distribution[0].1, symbol: symbol } with_lock(distribution[0].0) - create dist1 = Token { amount: distribution[1].1, symbol: symbol } with_lock(distribution[1].0) - create dist2 = Token { amount: distribution[2].1, symbol: symbol } with_lock(distribution[2].0) - create dist3 = Token { amount: distribution[3].1, symbol: symbol } with_lock(distribution[3].0) - - let initial_lp = pool_seed_amount - consume pool_paired_token - create pool = Pool { - token_a_symbol: symbol, - token_b_symbol: pool_paired_token.symbol, - reserve_a: pool_seed_amount, - reserve_b: pool_paired_token.amount, - total_lp: initial_lp, - fee_rate_bps: fee_rate_bps - } - create lp_receipt = LPReceipt { - pool_id: pool.type_hash(), - lp_amount: initial_lp, - provider: creator - } with_lock(creator) - let remaining = initial_mint - dist_total - pool_seed_amount - create change = Token { amount: remaining, symbol: symbol } with_lock(creator) -} -""", - "bootstrap_token": """ -action bootstrap_token(symbol: [u8; 8], max_supply: u64, initial_mint: u64, creator: Address, recipients: [(Address, u64); 2]) -> (auth: MintAuthority, rec0: Token, rec1: Token, change: Token) { - verification - require initial_mint <= max_supply, "initial exceeds max" - require recipients[1].1 <= U64_MAX - recipients[0].1, "distribution overflow" - let total_distributed = recipients[0].1 + recipients[1].1 - require total_distributed <= initial_mint, "distribution exceeds mint" - - create auth = MintAuthority { - token_symbol: symbol, - max_supply: max_supply, - minted: initial_mint - } with_lock(creator) - create rec0 = Token { amount: recipients[0].1, symbol: symbol } with_lock(recipients[0].0) - create rec1 = Token { amount: recipients[1].1, symbol: symbol } with_lock(recipients[1].0) - let remaining = initial_mint - total_distributed - create change = Token { amount: remaining, symbol: symbol } with_lock(creator) -} -""", -} - -for action, source in LAUNCH_ACTION_SOURCES.items(): - (launch_action_source_root / f"launch_{action}.cell").write_text( - f"module acceptance::launch_{action}\n\n" + LAUNCH_TYPES_SOURCE + "\n" + source, - encoding="utf-8", - ) - -ORIGINAL_SCOPED_ACTIONS = { - "nft.cell": [ - "create_collection", - "mint", - "transfer", - "create_listing", - "cancel_listing", - "buy_from_listing", - "create_offer", - "accept_offer", - "burn", - "batch_mint", - ], - "timelock.cell": [ - "create_absolute_lock", - "create_relative_lock", - "lock_asset", - "request_release", - "request_emergency_release", - "approve_emergency_release", - "execute_release", - "execute_emergency_release", - "extend_lock", - "batch_create_locks", - ], - "multisig.cell": [ - "create_wallet", - "propose_transfer", - "record_approval", - "propose_add_signer", - "propose_change_threshold", - "propose_remove_signer", - "execute_proposal", - "cancel_proposal", - ], - "vesting.cell": ["create_vesting_config", "grant_vesting", "claim_vested", "claim_fully_vested", "revoke_grant"], - "token.cell": ["mint_with_authority", "transfer_token", "burn", "merge"], - "amm_pool.cell": ["seed_pool", "swap_a_for_b", "add_liquidity", "remove_liquidity"], - "launch.cell": ["launch_token", "bootstrap_token"], -} - -ORIGINAL_SCOPED_LOCKS = { - "nft.cell": ["nft_ownership", "listing_seller", "offer_buyer", "valid_royalty", "collection_creator"], - "timelock.cell": ["can_unlock_lock", "is_owner", "lock_id_commitment", "asset_matches", "not_expired", "emergency_approved"], - "multisig.cell": ["is_signer_lock", "can_execute", "can_cancel", "has_enough_approvals", "not_expired"], - "vesting.cell": ["vesting_admin"], -} - -ORIGINAL_SCOPED_ACTION_FAIL_CLOSED = {} - -ORIGINAL_SCOPED_LOCK_FAIL_CLOSED = {} - -EXPECTED_SOURCE_ACTIONS = { - "token.cell": ["mint_with_authority", "transfer_token", "burn", "merge"], - "nft.cell": [ - "create_collection", - "mint", - "transfer", - "create_listing", - "cancel_listing", - "buy_from_listing", - "create_offer", - "accept_offer", - "burn", - "batch_mint", - ], - "timelock.cell": [ - "create_absolute_lock", - "create_relative_lock", - "lock_asset", - "request_release", - "execute_release", - "request_emergency_release", - "approve_emergency_release", - "execute_emergency_release", - "extend_lock", - "batch_create_locks", - ], - "multisig.cell": [ - "create_wallet", - "propose_transfer", - "record_approval", - "execute_proposal", - "cancel_proposal", - "propose_add_signer", - "propose_remove_signer", - "propose_change_threshold", - ], - "vesting.cell": ["create_vesting_config", "grant_vesting", "claim_vested", "claim_fully_vested", "revoke_grant"], - "amm_pool.cell": ["seed_pool", "swap_a_for_b", "add_liquidity", "remove_liquidity"], - "launch.cell": ["launch_token", "bootstrap_token"], -} - -EXPECTED_SOURCE_LOCKS = { - "token.cell": [], - "nft.cell": ["nft_ownership", "listing_seller", "offer_buyer", "valid_royalty", "collection_creator"], - "timelock.cell": ["can_unlock_lock", "is_owner", "lock_id_commitment", "asset_matches", "emergency_approved", "not_expired"], - "multisig.cell": ["is_signer_lock", "can_execute", "can_cancel", "has_enough_approvals", "not_expired"], - "vesting.cell": ["vesting_admin"], - "amm_pool.cell": [], - "launch.cell": [], -} - -CKB_ONCHAIN_ACTION_HARNESSES = { - "token.cell": list(TOKEN_ACTION_SOURCES.keys()), - "nft.cell": list(NFT_ACTION_SOURCES.keys()), - "timelock.cell": list(TIMELOCK_ACTION_SOURCES.keys()), - "multisig.cell": list(MULTISIG_ACTION_SOURCES.keys()), - "vesting.cell": ["create_vesting_config", "grant_vesting", "claim_vested", "claim_fully_vested", "revoke_grant"], - "amm_pool.cell": list(AMM_ACTION_SOURCES.keys()), - "launch.cell": ["launch_token", "bootstrap_token"], -} - -def clipped(text): - if len(text) <= TRUNCATE: - return text - return text[:TRUNCATE] + f"\n... truncated {len(text) - TRUNCATE} bytes ..." - -def run(args, *, env=None, timeout=180): - completed = subprocess.run(args, env=env, text=True, capture_output=True, timeout=timeout) - return { - "command": [str(arg) for arg in args], - "returncode": completed.returncode, - "stdout": clipped(completed.stdout), - "stderr": clipped(completed.stderr), - } - -def load_json(path): - return json.loads(path.read_text(encoding="utf-8")) - -def git_stdout(args): - return subprocess.check_output(["git", *args], cwd=repo_root, text=True).strip() - -def tracked_source_files(): - output = git_stdout(["ls-files", "--", *SOURCE_PROVENANCE_PATHS]) - return [ - line - for line in output.splitlines() - if line and (repo_root / line).is_file() - ] - -def file_sha256(path): - h = hashlib.sha256() - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - h.update(chunk) - return h.hexdigest() - -def sha256_hex(data): - return "0x" + hashlib.sha256(data).hexdigest() - -def ckb_data_hash_hex(data): - return "0x" + hashlib.blake2b(data, digest_size=32, person=b"ckb-default-hash").hexdigest() - -def tracked_source_sha256(files): - h = hashlib.sha256() - for rel in files: - h.update(rel.encode("utf-8")) - h.update(b"\0") - h.update(file_sha256(repo_root / rel).encode("ascii")) - h.update(b"\n") - return "0x" + h.hexdigest() - -def source_provenance_report(): - files = tracked_source_files() - return { - "schema": SOURCE_PROVENANCE_SCHEMA, - "generated_at_utc": datetime.datetime.now(datetime.timezone.utc) - .replace(microsecond=0) - .isoformat() - .replace("+00:00", "Z"), - "repo_commit": git_stdout(["rev-parse", "HEAD"]), - "git_dirty": bool(git_stdout(["status", "--porcelain", "--untracked-files=all"])), - "tracked_source_paths": SOURCE_PROVENANCE_PATHS, - "tracked_source_files": files, - "tracked_source_file_count": len(files), - "tracked_source_sha256": tracked_source_sha256(files), - "acceptance_script_sha256": "0x" + file_sha256(repo_root / "scripts/ckb_cellscript_acceptance.sh"), - "validator_script_sha256": "0x" + file_sha256(repo_root / "scripts/validate_ckb_cellscript_production_evidence.py"), - } - -def source_entries(name, keyword): - text = production_example_path(name).read_text(encoding="utf-8") - pattern = re.compile(rf"^\s*{keyword}\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(", re.MULTILINE) - return pattern.findall(text) - -def validate_source_coverage_matrix(): - action_mismatches = {} - lock_mismatches = {} - for name in EXAMPLES: - actual_actions = source_entries(name, "action") - expected_actions = EXPECTED_SOURCE_ACTIONS.get(name, []) - if actual_actions != expected_actions: - action_mismatches[name] = { - "expected": expected_actions, - "actual": actual_actions, - } - actual_locks = source_entries(name, "lock") - expected_locks = EXPECTED_SOURCE_LOCKS.get(name, []) - if actual_locks != expected_locks: - lock_mismatches[name] = { - "expected": expected_locks, - "actual": actual_locks, - } - if action_mismatches or lock_mismatches: - raise RuntimeError( - "source coverage matrix is stale: " - + json.dumps( - { - "action_mismatches": action_mismatches, - "lock_mismatches": lock_mismatches, - }, - sort_keys=True, - ) - ) - -def build_ckb_business_coverage(onchain_actions=None): - onchain_actions = onchain_actions or {} - rows = [] - for example in EXAMPLES: - source_actions = EXPECTED_SOURCE_ACTIONS.get(example, []) - source_locks = EXPECTED_SOURCE_LOCKS.get(example, []) - strict_actions = ORIGINAL_SCOPED_ACTIONS.get(example, []) - strict_locks = ORIGINAL_SCOPED_LOCKS.get(example, []) - fail_closed_actions = ORIGINAL_SCOPED_ACTION_FAIL_CLOSED.get(example, []) - fail_closed_locks = ORIGINAL_SCOPED_LOCK_FAIL_CLOSED.get(example, []) - ckb_onchain_actions = onchain_actions.get(example, []) - - missing_strict_actions = sorted(set(source_actions) - set(strict_actions) - set(fail_closed_actions)) - missing_strict_locks = sorted(set(source_locks) - set(strict_locks) - set(fail_closed_locks)) - missing_onchain_actions = sorted(set(strict_actions) - set(ckb_onchain_actions)) - - rows.append({ - "example": example, - "source_actions": source_actions, - "source_locks": source_locks, - "strict_ckb_actions": strict_actions, - "strict_ckb_locks": strict_locks, - "expected_fail_closed_actions": fail_closed_actions, - "expected_fail_closed_locks": fail_closed_locks, - "ckb_onchain_actions": ckb_onchain_actions, - "missing_strict_ckb_actions": missing_strict_actions, - "missing_strict_ckb_locks": missing_strict_locks, - "missing_ckb_onchain_actions": missing_onchain_actions, - "strict_action_coverage_complete": not missing_strict_actions, - "strict_lock_coverage_complete": not missing_strict_locks, - "ckb_onchain_action_coverage_complete": not missing_onchain_actions, - }) - - strict_complete = all( - row["strict_action_coverage_complete"] and row["strict_lock_coverage_complete"] - for row in rows - ) - onchain_complete = all(row["ckb_onchain_action_coverage_complete"] for row in rows) - return { - "status": "complete" if strict_complete and onchain_complete else "incomplete", - "strict_compile_coverage_complete": strict_complete, - "onchain_action_coverage_complete": onchain_complete, - "source_action_count": sum(len(row["source_actions"]) for row in rows), - "source_lock_count": sum(len(row["source_locks"]) for row in rows), - "strict_ckb_action_count": sum(len(row["strict_ckb_actions"]) for row in rows), - "strict_ckb_lock_count": sum(len(row["strict_ckb_locks"]) for row in rows), - "expected_fail_closed_action_count": sum(len(row["expected_fail_closed_actions"]) for row in rows), - "expected_fail_closed_lock_count": sum(len(row["expected_fail_closed_locks"]) for row in rows), - "ckb_onchain_action_count": sum(len(row["ckb_onchain_actions"]) for row in rows), - "missing_strict_ckb_actions": { - row["example"]: row["missing_strict_ckb_actions"] - for row in rows - if row["missing_strict_ckb_actions"] - }, - "missing_strict_ckb_locks": { - row["example"]: row["missing_strict_ckb_locks"] - for row in rows - if row["missing_strict_ckb_locks"] - }, - "missing_ckb_onchain_actions": { - row["example"]: row["missing_ckb_onchain_actions"] - for row in rows - if row["missing_ckb_onchain_actions"] - }, - "rows": rows, - } - -def verify_artifact(artifact): - completed = subprocess.run( - [cellc, "verify-artifact", artifact, "--expect-target-profile", "ckb", "--json"], - text=True, - capture_output=True, - timeout=180, - ) - if completed.returncode != 0: - raise RuntimeError(f"verify-artifact failed for {artifact}: {clipped(completed.stderr)}") - try: - return json.loads(completed.stdout) - except json.JSONDecodeError as error: - raise RuntimeError(f"verify-artifact did not return JSON for {artifact}: {clipped(completed.stdout)}") from error - -def internal_assembler_env(): - env = os.environ.copy() - for key in ("CELLSCRIPT_RISCV_CC", "CELLSCRIPT_RISCV_AS", "CELLSCRIPT_RISCV_LD"): - env.pop(key, None) - return env - -def read_u16_le(data, offset): - return struct.unpack_from(" len(artifact_bytes): - raise RuntimeError(f"{name} ELF program headers exceed artifact size") - - executable_headers = [] - for index in range(program_header_count): - offset = program_header_offset + index * program_header_entry_size - p_type = read_u32_le(artifact_bytes, offset) - flags = read_u32_le(artifact_bytes, offset + 4) - if p_type != ELF_PT_LOAD or flags & ELF_PF_X == 0: - continue - file_offset = read_u64_le(artifact_bytes, offset + 8) - virtual_address = read_u64_le(artifact_bytes, offset + 16) - file_size = read_u64_le(artifact_bytes, offset + 32) - memory_size = read_u64_le(artifact_bytes, offset + 40) - executable_headers.append({ - "index": index, - "flags": flags, - "file_offset": file_offset, - "virtual_address": virtual_address, - "file_size": file_size, - "memory_size": memory_size, - }) - - if not executable_headers: - raise RuntimeError(f"{name} ELF does not contain an executable PT_LOAD segment") - - header = executable_headers[0] - flags = header["flags"] - if flags != (ELF_PF_R | ELF_PF_X): - raise RuntimeError(f"{name} executable PT_LOAD flags must be RX-only, got 0x{flags:x}") - if flags & ELF_PF_W: - raise RuntimeError(f"{name} executable PT_LOAD segment must not be writable") - if header["file_size"] != header["memory_size"]: - raise RuntimeError( - f"{name} executable PT_LOAD must not fake stack memory: " - f"filesz={header['file_size']} memsz={header['memory_size']}" - ) - if not (header["virtual_address"] <= entry < header["virtual_address"] + header["file_size"]): - raise RuntimeError(f"{name} ELF entry point is outside the executable PT_LOAD segment") - - entry_file_offset = header["file_offset"] + (entry - header["virtual_address"]) - if entry_file_offset + ENTRY_TRAMPOLINE_SIZE > len(artifact_bytes): - raise RuntimeError(f"{name} ELF entry trampoline exceeds artifact size") - instructions = [ - read_u32_le(artifact_bytes, entry_file_offset + index * 4) - for index in range(ENTRY_TRAMPOLINE_SIZE // 4) - ] - first_instruction, call_instruction, exit_lui, exit_addi, exit_ecall = instructions - first_opcode = first_instruction & 0x7f - first_rd = (first_instruction >> 7) & 0x1f - if first_opcode != 0x17 or first_rd != 1: - raise RuntimeError( - f"{name} ELF entry trampoline must start with auipc ra, not instruction 0x{first_instruction:08x}" - ) - if ( - call_instruction & 0x7f != 0x67 - or (call_instruction >> 7) & 0x1f != 1 - or (call_instruction >> 12) & 0x7 != 0 - or (call_instruction >> 15) & 0x1f != 1 - ): - raise RuntimeError( - f"{name} ELF entry trampoline second instruction must be jalr ra, imm(ra), got 0x{call_instruction:08x}" - ) - - def sign_extend(value, bits): - sign = 1 << (bits - 1) - return (value ^ sign) - sign - - call_hi = sign_extend(first_instruction & 0xfffff000, 32) - call_lo = sign_extend(call_instruction >> 20, 12) - call_target = (entry + call_hi + call_lo) & ~1 - expected_call_target = entry + ENTRY_TRAMPOLINE_SIZE - if call_target != expected_call_target: - raise RuntimeError( - f"{name} ELF entry trampoline must call the first instruction after the trampoline: " - f"target=0x{call_target:x}, expected=0x{expected_call_target:x}" - ) - if ( - exit_lui & 0x7f != 0x37 - or (exit_lui >> 7) & 0x1f != 17 - or exit_lui >> 12 != 0 - or exit_addi & 0x7f != 0x13 - or (exit_addi >> 7) & 0x1f != 17 - or (exit_addi >> 12) & 0x7 != 0 - or (exit_addi >> 15) & 0x1f != 17 - or sign_extend(exit_addi >> 20, 12) != 93 - or exit_ecall != 0x00000073 - ): - raise RuntimeError( - f"{name} ELF entry trampoline must end with exact li a7, 93; ecall sequence, got " - + ", ".join(f"0x{instruction:08x}" for instruction in instructions[2:]) - ) - written_registers = [(instruction >> 7) & 0x1f for instruction in instructions[:-1]] - if 2 in written_registers: - raise RuntimeError(f"{name} ELF entry trampoline writes the CKB VM stack pointer") - - return { - "schema": ELF_ENTRY_ABI_SCHEMA, - "status": "passed", - "entry_point": f"0x{entry:x}", - "executable_load_segment": { - "index": header["index"], - "flags": flags, - "flags_symbolic": "R|X", - "writable": False, - "file_offset": header["file_offset"], - "virtual_address": f"0x{header['virtual_address']:x}", - "file_size": header["file_size"], - "memory_size": header["memory_size"], - "file_size_equals_memory_size": True, - }, - "trampoline": { - "size_bytes": ENTRY_TRAMPOLINE_SIZE, - "entry_file_offset": entry_file_offset, - "bytes_hex": artifact_bytes[entry_file_offset:entry_file_offset + ENTRY_TRAMPOLINE_SIZE].hex(), - "instructions_le_hex": [f"0x{instruction:08x}" for instruction in instructions], - "first_instruction_le_hex": f"0x{first_instruction:08x}", - "first_instruction_opcode": "auipc", - "first_instruction_rd": "ra", - "call_instruction_opcode": "jalr", - "call_target": f"0x{call_target:x}", - "expected_call_target": f"0x{expected_call_target:x}", - "exit_syscall_number": 93, - "exit_sequence_exact": True, - "calls_entry_with_ra": True, - "preserves_ckb_vm_stack_pointer": 2 not in written_registers, - "forbidden_sp_initialisation": False, - }, - } - -def compile_artifact(name, kind, source, artifact, *, entry_args=None): - entry_args = entry_args or [] - env = internal_assembler_env() - result = run([cellc, source, "--target-profile", "ckb", "--target", "riscv64-elf", *entry_args, "-o", artifact], env=env) - if result["returncode"] != 0: - raise RuntimeError(f"CKB artifact compile failed for {name}: {result['stderr']}") - if not artifact.exists(): - raise RuntimeError(f"CKB artifact compile did not produce artifact for {name}: {artifact}") - - metadata_path = pathlib.Path(str(artifact) + ".meta.json") - if not metadata_path.exists(): - raise RuntimeError(f"CKB artifact compile did not produce metadata sidecar for {name}: {metadata_path}") - - artifact_bytes = artifact.read_bytes() - artifact_has_unexpected_profile_trailer = UNEXPECTED_PROFILE_TRAILER in artifact_bytes[-64:] - if not artifact_bytes.startswith(b"\x7fELF"): - raise RuntimeError(f"{name} artifact is not an ELF") - if artifact_has_unexpected_profile_trailer: - raise RuntimeError(f"{name} CKB artifact still contains an unexpected non-CKB ABI trailer") - elf_entry_abi = audit_ckb_elf_entry_abi(name, artifact_bytes) - - metadata = load_json(metadata_path) - verify = verify_artifact(artifact) - if metadata.get("target_profile", {}).get("name") != "ckb" or verify.get("target_profile") != "ckb": - raise RuntimeError(f"{name} metadata/verify did not pin target_profile=ckb") - - return { - "name": name, - "kind": kind, - "source": str(source), - "artifact": str(artifact), - "metadata": str(metadata_path), - "artifact_size_bytes": len(artifact_bytes), - "artifact_starts_with_elf_magic": True, - "artifact_has_unexpected_profile_trailer": False, - "elf_entry_abi": elf_entry_abi, - "target_profile": "ckb", - "artifact_packaging": metadata.get("target_profile", {}).get("artifact_packaging"), - "entry_args": [str(arg) for arg in entry_args], - "compile": result, - "verify": verify, - } - -validate_source_coverage_matrix() - -def strict_policy_fail_closed(stderr): - return ( - "target profile policy failed for 'ckb'" in stderr - or ( - "ProofPlan soundness check failed" in stderr - and "PP0150" in stderr - and "strict v0.16 ProofPlan mode rejects metadata-only or runtime-required obligations" in stderr - ) - ) - -def strict_original_compile(name): - source = production_example_build_path(name) - artifact = strict_root / f"{name}.strict.elf" - result = run( - [cellc, source, "--target-profile", "ckb", "--target", "riscv64-elf", "--primitive-strict", "0.16", "-o", artifact], - env=internal_assembler_env(), - ) - policy_fail_closed = result["returncode"] != 0 and strict_policy_fail_closed(result["stderr"]) - unexpected_failure = result["returncode"] != 0 and not policy_fail_closed - verify = None - elf_entry_abi = None - if result["returncode"] == 0: - verify = verify_artifact(artifact) - elf_entry_abi = audit_ckb_elf_entry_abi(name, artifact.read_bytes()) - return { - "source": str(source), - "artifact": str(artifact), - "status": "passed" if result["returncode"] == 0 else "failed", - "policy_fail_closed": policy_fail_closed, - "unexpected_failure": unexpected_failure, - "verify": verify, - "elf_entry_abi": elf_entry_abi, - "returncode": result["returncode"], - "stdout": result["stdout"], - "stderr": result["stderr"], - } - -def strict_scoped_compile(name, source, entry_flag, entry_name): - artifact = strict_root / f"{name}.{entry_name}.strict-scoped.elf" - result = run( - [cellc, source, "--target-profile", "ckb", "--target", "riscv64-elf", "--primitive-strict", "0.16", entry_flag, entry_name, "-o", artifact], - env=internal_assembler_env(), - ) - policy_fail_closed = result["returncode"] != 0 and strict_policy_fail_closed(result["stderr"]) - unexpected_failure = result["returncode"] != 0 and not policy_fail_closed - verify = None - elf_entry_abi = None - if result["returncode"] == 0: - verify = verify_artifact(artifact) - elf_entry_abi = audit_ckb_elf_entry_abi(name, artifact.read_bytes()) - return { - "source": str(source), - "artifact": str(artifact), - "entry_flag": entry_flag, - "entry": entry_name, - "status": "passed" if result["returncode"] == 0 else "failed", - "policy_fail_closed": policy_fail_closed, - "unexpected_failure": unexpected_failure, - "verify": verify, - "elf_entry_abi": elf_entry_abi, - "returncode": result["returncode"], - "stdout": result["stdout"], - "stderr": result["stderr"], - } - -artifacts = [] -baseline = compile_artifact( - "ckb_noop.cell", - "pure-baseline", - baseline_source, - artifact_root / "ckb_noop.elf", -) -artifacts.append(baseline) - -bundled_examples = [] -bundled_example_deployment_artifacts = [] -for name in EXAMPLES: - strict = strict_original_compile(name) - if strict["unexpected_failure"]: - raise RuntimeError( - f"primitive-strict original CKB compile for {name} failed for a non-policy reason: {strict['stderr']}" - ) - record = { - "name": name, - "kind": "bundled-example-strict-original", - "source": str(production_example_path(name)), - "strict_original_ckb_compile": strict, - } - bundled_examples.append(record) - if strict["status"] == "passed": - bundled_example_deployment_artifacts.append({ - "name": name, - "kind": "bundled-example-strict-original", - "source": str(production_example_path(name)), - "artifact": strict["artifact"], - }) - -token_action_artifacts = [] -for action in TOKEN_ACTION_SOURCES: - source = token_action_source_root / f"token_{action}.cell" - record = compile_artifact( - f"token.{action}.cell", - "token-action-strict", - source, - artifact_root / f"token_{action}.elf", - ) - record["action"] = action - record["original_source"] = str(production_example_path("token.cell")) - token_action_artifacts.append(record) - -nft_action_artifacts = [] -for action in NFT_ACTION_SOURCES: - source = nft_action_source_root / f"nft_{action}.cell" - record = compile_artifact( - f"nft.{action}.cell", - "nft-action-strict", - source, - artifact_root / f"nft_{action}.elf", - ) - record["action"] = action - record["original_source"] = str(production_example_path("nft.cell")) - nft_action_artifacts.append(record) - -timelock_action_artifacts = [] -for action in TIMELOCK_ACTION_SOURCES: - source = timelock_action_source_root / f"timelock_{action}.cell" - record = compile_artifact( - f"timelock.{action}.cell", - "timelock-action-strict", - source, - artifact_root / f"timelock_{action}.elf", - ) - record["action"] = action - record["original_source"] = str(production_example_path("timelock.cell")) - timelock_action_artifacts.append(record) - -amm_action_artifacts = [] -for action in AMM_ACTION_SOURCES: - source = amm_action_source_root / f"amm_{action}.cell" - record = compile_artifact( - f"amm.{action}.cell", - "amm-action-strict", - source, - artifact_root / f"amm_{action}.elf", - ) - record["action"] = action - record["original_source"] = str(production_example_path("amm_pool.cell")) - amm_action_artifacts.append(record) - -multisig_action_artifacts = [] -for action in MULTISIG_ACTION_SOURCES: - source = multisig_action_source_root / f"multisig_{action}.cell" - record = compile_artifact( - f"multisig.{action}.cell", - "multisig-action-strict", - source, - artifact_root / f"multisig_{action}.elf", - ) - record["action"] = action - record["original_source"] = str(production_example_path("multisig.cell")) - multisig_action_artifacts.append(record) - -launch_action_artifacts = [] -for action in LAUNCH_ACTION_SOURCES: - source = launch_action_source_root / f"launch_{action}.cell" - record = compile_artifact( - f"launch.{action}.cell", - "launch-action-strict", - source, - artifact_root / f"launch_{action}.elf", - ) - record["action"] = action - record["original_source"] = str(production_example_path("launch.cell")) - launch_action_artifacts.append(record) - -original_scoped_action_artifacts = [] -for example_name, actions in ORIGINAL_SCOPED_ACTIONS.items(): - for action in actions: - record = compile_artifact( - f"{example_name}:{action}", - "original-scoped-action-strict", - production_example_build_path(example_name), - artifact_root / f"original_{example_name.removesuffix('.cell')}_{action}.elf", - entry_args=["--primitive-strict", "0.16", "--entry-action", action], - ) - record["example"] = example_name - record["action"] = action - record["original_source"] = str(production_example_path(example_name)) - original_scoped_action_artifacts.append(record) - -def original_scoped_action_or(record, example_name): - return next( - ( - original - for original in original_scoped_action_artifacts - if original["example"] == example_name and original["action"] == record["action"] - ), - record, - ) - -launch_action_artifacts = [ - original_scoped_action_or(record, "launch.cell") - for record in launch_action_artifacts -] - -token_action_artifacts = [ - original_scoped_action_or(record, "token.cell") - for record in token_action_artifacts -] - -nft_action_artifacts = [ - original_scoped_action_or(record, "nft.cell") - for record in nft_action_artifacts -] - -timelock_action_artifacts = [ - next( - ( - original - for original in original_scoped_action_artifacts - if original["example"] == "timelock.cell" and original["action"] == record["action"] - ), - record, - ) - if record["action"] in ( - "create_absolute_lock", - "create_relative_lock", - "lock_asset", - "request_release", - "request_emergency_release", - "approve_emergency_release", - "execute_release", - "execute_emergency_release", - "extend_lock", - "batch_create_locks", - ) else record - for record in timelock_action_artifacts -] - -amm_action_artifacts = [ - original_scoped_action_or(record, "amm_pool.cell") - for record in amm_action_artifacts -] - -multisig_action_artifacts = [ - next( - ( - original - for original in original_scoped_action_artifacts - if original["example"] == "multisig.cell" and original["action"] == record["action"] - ), - record, - ) - if record["action"] in ( - "create_wallet", - "propose_transfer", - "record_approval", - "propose_add_signer", - "propose_remove_signer", - "propose_change_threshold", - "execute_proposal", - "cancel_proposal", - ) else record - for record in multisig_action_artifacts -] - -original_scoped_lock_artifacts = [] -for example_name, locks in ORIGINAL_SCOPED_LOCKS.items(): - for lock in locks: - record = compile_artifact( - f"{example_name}:{lock}", - "original-scoped-lock-strict", - production_example_build_path(example_name), - artifact_root / f"original_{example_name.removesuffix('.cell')}_{lock}.elf", - entry_args=["--primitive-strict", "0.16", "--entry-lock", lock], - ) - record["example"] = example_name - record["lock"] = lock - record["original_source"] = str(production_example_path(example_name)) - original_scoped_lock_artifacts.append(record) - -original_scoped_action_fail_closed = [] -for example_name, actions in ORIGINAL_SCOPED_ACTION_FAIL_CLOSED.items(): - for action in actions: - record = strict_scoped_compile( - f"{example_name}:{action}", - production_example_build_path(example_name), - "--entry-action", - action, - ) - record["example"] = example_name - record["action"] = action - record["original_source"] = str(production_example_path(example_name)) - original_scoped_action_fail_closed.append(record) - -original_scoped_lock_fail_closed = [] -for example_name, locks in ORIGINAL_SCOPED_LOCK_FAIL_CLOSED.items(): - for lock in locks: - record = strict_scoped_compile( - f"{example_name}:{lock}", - production_example_build_path(example_name), - "--entry-lock", - lock, - ) - record["example"] = example_name - record["lock"] = lock - record["original_source"] = str(production_example_path(example_name)) - original_scoped_lock_fail_closed.append(record) - -expected_original_scoped_action_count = sum(len(actions) for actions in ORIGINAL_SCOPED_ACTIONS.values()) -expected_original_scoped_lock_count = sum(len(locks) for locks in ORIGINAL_SCOPED_LOCKS.values()) -expected_original_scoped_action_fail_closed_count = sum( - len(actions) for actions in ORIGINAL_SCOPED_ACTION_FAIL_CLOSED.values() -) -expected_original_scoped_lock_fail_closed_count = sum( - len(locks) for locks in ORIGINAL_SCOPED_LOCK_FAIL_CLOSED.values() -) -if len(original_scoped_action_artifacts) != expected_original_scoped_action_count: - raise RuntimeError( - f"original scoped action coverage mismatch: expected {expected_original_scoped_action_count}, " - f"compiled {len(original_scoped_action_artifacts)}" - ) -if len(original_scoped_lock_artifacts) != expected_original_scoped_lock_count: - raise RuntimeError( - f"original scoped lock coverage mismatch: expected {expected_original_scoped_lock_count}, " - f"compiled {len(original_scoped_lock_artifacts)}" - ) -if len(original_scoped_action_fail_closed) != expected_original_scoped_action_fail_closed_count: - raise RuntimeError( - "original scoped action fail-closed coverage mismatch: " - f"expected {expected_original_scoped_action_fail_closed_count}, " - f"checked {len(original_scoped_action_fail_closed)}" - ) -if len(original_scoped_lock_fail_closed) != expected_original_scoped_lock_fail_closed_count: - raise RuntimeError( - "original scoped lock fail-closed coverage mismatch: " - f"expected {expected_original_scoped_lock_fail_closed_count}, " - f"checked {len(original_scoped_lock_fail_closed)}" - ) - -unexpected_scoped_admissions = [ - f"{record['example']}:{record.get('action') or record.get('lock')}" - for record in [*original_scoped_action_fail_closed, *original_scoped_lock_fail_closed] - if record["status"] == "passed" -] -if unexpected_scoped_admissions: - raise RuntimeError( - "expected fail-closed original scoped entries were admitted; " - "move them into the strict scoped pass matrix only after reviewing coverage: " - + ", ".join(unexpected_scoped_admissions) - ) - -unexpected_scoped_failures = [ - f"{record['example']}:{record.get('action') or record.get('lock')}" - for record in [*original_scoped_action_fail_closed, *original_scoped_lock_fail_closed] - if record["unexpected_failure"] -] -if unexpected_scoped_failures: - raise RuntimeError( - "expected fail-closed original scoped entries failed for non-policy reasons: " - + ", ".join(unexpected_scoped_failures) - ) - -non_policy_fail_closed = [ - f"{record['example']}:{record.get('action') or record.get('lock')}" - for record in [*original_scoped_action_fail_closed, *original_scoped_lock_fail_closed] - if not record["policy_fail_closed"] -] -if non_policy_fail_closed: - raise RuntimeError( - "expected fail-closed original scoped entries were not rejected by strict CKB/ProofPlan policy: " - + ", ".join(non_policy_fail_closed) - ) - -strict_original_policy_fail_closed = [ - record["name"] - for record in bundled_examples - if record["strict_original_ckb_compile"]["policy_fail_closed"] -] -strict_original_unexpected_failures = [ - record["name"] - for record in bundled_examples - if record["strict_original_ckb_compile"]["unexpected_failure"] -] - -def elf_entry_abi_source_example(record): - example = record.get("example") - if isinstance(example, str) and example: - return example - original_source = record.get("original_source") or record.get("source") - if isinstance(original_source, str): - source_name = pathlib.Path(original_source).name - if source_name in EXAMPLES: - return source_name - return None - -def collect_elf_entry_abi_gate(): - rows = [] - seen_artifacts = set() - - def add_record(record, *, fallback_name=None, fallback_kind=None, source_example=None): - artifact = record.get("artifact") - if not artifact or artifact in seen_artifacts: - return - seen_artifacts.add(artifact) - audit = record.get("elf_entry_abi") - row = { - "name": record.get("name") or fallback_name or pathlib.Path(artifact).name, - "kind": record.get("kind") or fallback_kind or "unknown", - "source": record.get("source"), - "original_source": record.get("original_source"), - "example": source_example or elf_entry_abi_source_example(record), - "artifact": artifact, - "status": audit.get("status") if isinstance(audit, dict) else "missing", - "preserves_ckb_vm_stack_pointer": False, - "entry_trampoline_calls_with_ra": False, - "executable_segment_rx_only": False, - "executable_segment_file_size_equals_memory_size": False, - } - if isinstance(audit, dict): - trampoline = audit.get("trampoline") or {} - executable = audit.get("executable_load_segment") or {} - row.update({ - "preserves_ckb_vm_stack_pointer": trampoline.get("preserves_ckb_vm_stack_pointer") is True, - "entry_trampoline_calls_with_ra": trampoline.get("calls_entry_with_ra") is True, - "executable_segment_rx_only": executable.get("flags_symbolic") == "R|X" and executable.get("writable") is False, - "executable_segment_file_size_equals_memory_size": executable.get("file_size_equals_memory_size") is True, - "first_instruction_le_hex": trampoline.get("first_instruction_le_hex"), - "trampoline_bytes_hex": trampoline.get("bytes_hex"), - "trampoline_instructions_le_hex": trampoline.get("instructions_le_hex"), - "call_target": trampoline.get("call_target"), - "expected_call_target": trampoline.get("expected_call_target"), - "exit_syscall_number": trampoline.get("exit_syscall_number"), - "exit_sequence_exact": trampoline.get("exit_sequence_exact") is True, - "entry_point": audit.get("entry_point"), - }) - rows.append(row) - - for record in artifacts: - add_record(record) - for record in bundled_examples: - strict = record["strict_original_ckb_compile"] - if strict["status"] == "passed": - strict = {**strict, "name": record["name"], "kind": "bundled-example-strict-original", "source": record["source"], "example": record["name"]} - add_record(strict, source_example=record["name"]) - for group in ( - token_action_artifacts, - nft_action_artifacts, - timelock_action_artifacts, - amm_action_artifacts, - multisig_action_artifacts, - launch_action_artifacts, - original_scoped_action_artifacts, - original_scoped_lock_artifacts, - ): - for record in group: - add_record(record) - - failures = [ - row["name"] - for row in rows - if row["status"] != "passed" - or not row["preserves_ckb_vm_stack_pointer"] - or not row["entry_trampoline_calls_with_ra"] - or not row["executable_segment_rx_only"] - or not row["executable_segment_file_size_equals_memory_size"] - ] - - critical = {} - for example in CRITICAL_0_20_DEVNET_EXAMPLES: - example_rows = [row for row in rows if row.get("example") == example] - missing = not example_rows - failed = [row["name"] for row in example_rows if row["status"] != "passed"] - critical[example] = { - "status": "passed" if example_rows and not failed else "failed", - "artifact_count": len(example_rows), - "audited_artifacts": [row["name"] for row in example_rows], - "missing": missing, - "failures": failed, - } - if missing: - failures.append(f"{example}:missing") - failures.extend(f"{example}:{name}" for name in failed) - - unique_failures = sorted(set(failures)) - return { - "schema": "cellscript-ckb-elf-entry-abi-gate-v0.22", - "status": "passed" if not unique_failures else "failed", - "requires_ckb_vm_stack_pointer_preserved": True, - "requires_entry_trampoline_call_sequence": True, - "requires_rx_only_executable_segment": True, - "requires_no_fake_stack_load_segment": True, - "critical_examples": CRITICAL_0_20_DEVNET_EXAMPLES, - "critical_example_gate": critical, - "audited_artifact_count": len(rows), - "failures": unique_failures, - "rows": rows, - } - -def collect_build_reports(): - rows = [] - seen_artifacts = set() - - def add_record(record, *, fallback_name=None, fallback_kind=None, source_example=None): - artifact = record.get("artifact") - if not artifact or artifact in seen_artifacts: - return - seen_artifacts.add(artifact) - artifact_path = pathlib.Path(artifact) - artifact_bytes = artifact_path.read_bytes() - verify = record.get("verify") or {} - elf_entry_abi = record.get("elf_entry_abi") or {} - metadata_sidecar = record.get("metadata") - row = { - "schema": BUILD_REPORT_SCHEMA, - "name": record.get("name") or fallback_name or artifact_path.name, - "kind": record.get("kind") or fallback_kind or "unknown", - "source": record.get("source"), - "original_source": record.get("original_source"), - "example": source_example or elf_entry_abi_source_example(record), - "entry_flag": record.get("entry_flag"), - "entry": record.get("entry"), - "target_profile": "ckb", - "vm_profile": "ckb-vm", - "artifact_format": "riscv64-elf", - "artifact_path": str(artifact_path), - "metadata_sidecar": metadata_sidecar, - "artifact_packaging": record.get("artifact_packaging"), - "artifact_size_bytes": len(artifact_bytes), - "artifact_hash_algorithm": "ckb-blake2b256", - "deployable_elf_hash": ckb_data_hash_hex(artifact_bytes), - "artifact_sha256": sha256_hex(artifact_bytes), - "deployment_hash_type_used_by_gate": "data1", - "verify_artifact_status": "passed" if isinstance(verify, dict) else "missing", - "verify_target_profile": verify.get("target_profile") if isinstance(verify, dict) else None, - "elf_entry_abi_status": elf_entry_abi.get("status") if isinstance(elf_entry_abi, dict) else "missing", - "abi_trailer_stripped": UNEXPECTED_PROFILE_TRAILER not in artifact_bytes[-64:], - "onchain_deployments": [], - } - rows.append(row) - - for record in artifacts: - add_record(record) - for record in bundled_examples: - strict = record["strict_original_ckb_compile"] - if strict["status"] == "passed": - strict = { - **strict, - "name": record["name"], - "kind": "bundled-example-strict-original", - "source": record["source"], - "example": record["name"], - } - add_record(strict, source_example=record["name"]) - for group in ( - token_action_artifacts, - nft_action_artifacts, - timelock_action_artifacts, - amm_action_artifacts, - multisig_action_artifacts, - launch_action_artifacts, - original_scoped_action_artifacts, - original_scoped_lock_artifacts, - ): - for record in group: - add_record(record) - - return { - "schema": "cellscript-ckb-build-report-index-v0.20", - "status": "passed", - "artifact_count": len(rows), - "artifact_hash_algorithm": "ckb-blake2b256", - "artifact_format": "riscv64-elf", - "target_profile": "ckb", - "vm_profile": "ckb-vm", - "requires_exact_artifact_hash": True, - "requires_elf_entry_abi_gate": True, - "requires_live_code_cell_data_hash_match": True, - "reports": rows, - } - -def generate_public_builder_contracts(): - builder_root = run_dir / "public-builders" - contracts = [] - for example_name in EXAMPLES: - source = production_example_path(example_name) - output_dir = builder_root / example_name.removesuffix(".cell") - result = run([ - cellc, - "gen-builder", - source, - "--target", - "typescript", - "--target-profile", - "ckb", - "--output", - output_dir, - "--package-name", - f"@cellscript-acceptance/{example_name.removesuffix('.cell')}", - "--json", - ]) - if result["returncode"] != 0: - raise RuntimeError(f"public gen-builder failed for {example_name}: {result['stderr']}") - summary = json.loads(result["stdout"]) - manifest_path = output_dir / "cellscript-builder-manifest.json" - manifest = load_json(manifest_path) - expected_actions = source_entries(example_name, "action") - manifest_actions = [action["name"] for action in manifest["actions"]] - if summary.get("status") != "ok" or summary.get("actions") != expected_actions or manifest_actions != expected_actions: - raise RuntimeError( - f"public generated builder action mismatch for {example_name}: " - f"summary={summary.get('actions')}, manifest={manifest_actions}, expected={expected_actions}" - ) - - action_plans = [] - action_plan_dir = output_dir / "action-plans" - action_plan_dir.mkdir(parents=True, exist_ok=True) - for action in expected_actions: - plan_path = action_plan_dir / f"{action}.json" - plan_result = run([ - cellc, - "action", - "build", - source, - "--action", - action, - "--target-profile", - "ckb", - "--output", - plan_path, - ]) - if plan_result["returncode"] != 0: - raise RuntimeError(f"public action build failed for {example_name}:{action}: {plan_result['stderr']}") - plan = load_json(plan_path) - if ( - plan.get("status") != "ok" - or plan.get("policy") != "cellscript-action-builder-plan-v1" - or plan.get("action") != action - or plan.get("target_profile") != "ckb" - ): - raise RuntimeError(f"invalid public action build plan for {example_name}:{action}") - action_plans.append({ - "action": action, - "contract_id": f"{example_name}:{action}", - "policy": plan["policy"], - "artifact_hash": plan.get("artifact_hash"), - "plan_path": str(plan_path), - "plan_sha256": sha256_hex(plan_path.read_bytes()), - "status": "passed", - }) - - generated_files = sorted(path for path in output_dir.rglob("*") if path.is_file()) - tree_hash = hashlib.sha256() - for path in generated_files: - relative = path.relative_to(output_dir).as_posix() - tree_hash.update(relative.encode("utf-8")) - tree_hash.update(b"\0") - tree_hash.update(hashlib.sha256(path.read_bytes()).digest()) - contracts.append({ - "example": example_name, - "source": str(source), - "status": "passed", - "generator_schema": summary.get("schema"), - "builder_manifest_schema": manifest.get("schema"), - "target": summary.get("target"), - "target_profile": manifest.get("target_profile"), - "actions": expected_actions, - "action_count": len(expected_actions), - "manifest_path": str(manifest_path), - "manifest_sha256": sha256_hex(manifest_path.read_bytes()), - "generated_tree_sha256": "0x" + tree_hash.hexdigest(), - "generated_file_count": len(generated_files), - "action_plans": action_plans, - "runtime_adapter_execution": "not-proven-by-this-contract-gate", - }) - return { - "schema": "cellscript-public-builder-contract-gate-v0.22", - "status": "passed", - "example_count": len(contracts), - "action_count": sum(contract["action_count"] for contract in contracts), - "requires_gen_builder": True, - "requires_action_build": True, - "transaction_origin_claim": "acceptance-python-harness-not-generated-builder", - "contracts": contracts, - } - -ckb_elf_entry_abi_gate = collect_elf_entry_abi_gate() -if ckb_elf_entry_abi_gate["status"] != "passed": - raise RuntimeError("CKB ELF entry ABI gate failed: " + json.dumps(ckb_elf_entry_abi_gate["failures"], sort_keys=True)) -build_reports = collect_build_reports() -public_builder_contracts = generate_public_builder_contracts() - -report = { - "status": "artifact-verified", - "acceptance_mode": acceptance_mode, - "ckb_acceptance_scope": ( - "Production mode is a hard gate and must not depend on synthetic harnesses, " - "expected fail-closed entries, or non-original artifacts. Bounded mode is a development coverage matrix only." - ), - "cellc": str(cellc), - "source_provenance": source_provenance_report(), - "bundled_examples_exact_order": EXAMPLES, - "bundled_examples_count": len(EXAMPLES), - "non_production_examples": NON_PRODUCTION_EXAMPLES, - "language_examples_exact_order": LANGUAGE_EXAMPLES, - "language_examples_count": len(LANGUAGE_EXAMPLES), - "example_scope": EXAMPLE_SCOPE, - "example_source_layout": { - "canonical_bundled_examples": str(examples_dir), - "language_examples": str(language_examples_dir), - "canonical_examples_note": ( - "Production acceptance compiles the checked-in top-level examples/*.cell directly. " - "examples/business and examples/acceptance are intentionally not part of the checked-in source layout." - ), - }, - "lock_acceptance_scope": LOCK_ACCEPTANCE_SCOPE, - "ckb_elf_entry_abi_gate": ckb_elf_entry_abi_gate, - "cellscript_build_reports": build_reports, - "public_builder_contracts": public_builder_contracts, - "bundled_examples_strict_admitted": [ - record["name"] - for record in bundled_examples - if record["strict_original_ckb_compile"]["status"] == "passed" - ], - "strict_original_ckb_compile_policy_fail_closed": strict_original_policy_fail_closed, - "strict_original_ckb_compile_unexpected_failures": strict_original_unexpected_failures, - "pure_baseline": baseline, - "bundled_examples": bundled_examples, - "bundled_example_deployment_artifacts": bundled_example_deployment_artifacts, - "token_action_artifacts": token_action_artifacts, - "nft_action_artifacts": nft_action_artifacts, - "timelock_action_artifacts": timelock_action_artifacts, - "amm_action_artifacts": amm_action_artifacts, - "multisig_action_artifacts": multisig_action_artifacts, - "launch_action_artifacts": launch_action_artifacts, - "original_scoped_actions_expected": ORIGINAL_SCOPED_ACTIONS, - "original_scoped_locks_expected": ORIGINAL_SCOPED_LOCKS, - "original_scoped_action_fail_closed_expected": ORIGINAL_SCOPED_ACTION_FAIL_CLOSED, - "original_scoped_lock_fail_closed_expected": ORIGINAL_SCOPED_LOCK_FAIL_CLOSED, - "original_scoped_action_count": len(original_scoped_action_artifacts), - "original_scoped_lock_count": len(original_scoped_lock_artifacts), - "original_scoped_action_fail_closed_count": len(original_scoped_action_fail_closed), - "original_scoped_lock_fail_closed_count": len(original_scoped_lock_fail_closed), - "original_scoped_action_artifacts": original_scoped_action_artifacts, - "original_scoped_lock_artifacts": original_scoped_lock_artifacts, - "original_scoped_action_fail_closed": original_scoped_action_fail_closed, - "original_scoped_lock_fail_closed": original_scoped_lock_fail_closed, - "ckb_business_coverage": build_ckb_business_coverage(), - "production_ready": False, - "artifacts": artifacts, -} - -def production_gate_failures(report): - failures = [] - builder_contracts = report.get("public_builder_contracts") or {} - if ( - builder_contracts.get("status") != "passed" - or builder_contracts.get("example_count") != len(EXAMPLES) - or builder_contracts.get("action_count") != sum(len(actions) for actions in ORIGINAL_SCOPED_ACTIONS.values()) - ): - failures.append("public action-build/gen-builder contract coverage is incomplete") - if report.get("strict_original_ckb_compile_policy_fail_closed"): - failures.append( - "primitive-strict original bundled examples still fail strict CKB/ProofPlan policy: " - + ", ".join(report["strict_original_ckb_compile_policy_fail_closed"]) - ) - if report.get("strict_original_ckb_compile_unexpected_failures"): - failures.append( - "primitive-strict original bundled examples have unexpected compile failures: " - + ", ".join(report["strict_original_ckb_compile_unexpected_failures"]) - ) - fail_closed_actions = [ - f"{record['example']}:{record.get('action')}" - for record in report.get("original_scoped_action_fail_closed", []) - ] - fail_closed_locks = [ - f"{record['example']}:{record.get('lock')}" - for record in report.get("original_scoped_lock_fail_closed", []) - ] - if fail_closed_actions or fail_closed_locks: - failures.append( - "original scoped entries still intentionally fail closed: " - + ", ".join([*fail_closed_actions, *fail_closed_locks]) - ) - non_original_harnesses = [ - record["name"] - for key in ( - "token_action_artifacts", - "nft_action_artifacts", - "timelock_action_artifacts", - "amm_action_artifacts", - "multisig_action_artifacts", - "launch_action_artifacts", - ) - for record in report.get(key, []) - if record.get("kind") != "original-scoped-action-strict" - ] - if non_original_harnesses: - failures.append( - "on-chain action harnesses still use synthetic or non-original sources: " - + ", ".join(non_original_harnesses) - ) - coverage = report.get("ckb_business_coverage") or {} - if coverage.get("expected_fail_closed_action_count", 0) or coverage.get("expected_fail_closed_lock_count", 0): - failures.append( - "source coverage matrix still includes expected fail-closed entries" - ) - return failures - -production_failures = production_gate_failures(report) -report["production_gate"] = { - "status": "passed" if not production_failures else "failed", - "failures": production_failures, - "requires_original_scoped_harnesses": True, - "requires_no_expected_fail_closed_entries": True, - "requires_all_bundled_examples_strict_original_ckb": True, - "requires_ckb_elf_entry_abi_gate": True, - "requires_cellscript_build_reports": True, - "requires_public_builder_contracts": True, -} -if acceptance_mode == "production" and production_failures: - report["status"] = "failed-production-gate" - report["production_ready"] = False - report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") - raise SystemExit( - "CKB production gate failed; rerun with --bounded only for development coverage. " - + "Failures: " - + " | ".join(production_failures) - ) -report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") -PY - -if [[ "$RUN_ONCHAIN" != "1" ]]; then - python3 - "$REPORT_JSON" "$CKB_REPO" "$CKB_BIN" "$RPC_URL" <<'PY' -import json -import pathlib -import sys - -report_path = pathlib.Path(sys.argv[1]) -report = json.loads(report_path.read_text(encoding="utf-8")) -report.update({ - "status": "passed", - "ckb_repo": sys.argv[2], - "ckb_bin": sys.argv[3], - "rpc_url": sys.argv[4], - "onchain": {"status": "skipped", "reason": "compile-only"}, -}) -report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") -PY - if [[ "$ACCEPTANCE_MODE" == "production" ]]; then - python3 "$REPO_ROOT/scripts/validate_ckb_cellscript_production_evidence.py" "$REPORT_JSON" --compile-only - echo "CKB compile-only production evidence is not sufficient for external release; run without --compile-only for final hardening." >&2 - fi - echo "CKB CellScript $ACCEPTANCE_MODE compile-only acceptance passed: $REPORT_JSON" - exit 0 -fi - -"$CKB_BIN" -C "$CKB_DIR" run --ba-advanced > "$CKB_LOG" 2>&1 & -CKB_PID="$!" - -ready=0 -for _ in $(seq 1 120); do - if curl -sS --noproxy '*' \ - -H 'Content-Type: application/json' \ - -d '{"id":1,"jsonrpc":"2.0","method":"get_tip_header","params":[]}' \ - "$RPC_URL" > "$RUN_DIR/rpc-ready.json" 2>/dev/null; then - if python3 - "$RUN_DIR/rpc-ready.json" <<'PY' -import json -import pathlib -import sys - -payload = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")) -raise SystemExit(0 if payload.get("result") and not payload.get("error") else 1) -PY - then - ready=1 - break - fi - fi - if ! kill -0 "$CKB_PID" >/dev/null 2>&1; then - echo "CKB process exited before RPC became ready. Log: $CKB_LOG" >&2 - tail -100 "$CKB_LOG" >&2 || true - exit 1 - fi - sleep 1 -done - -if [[ "$ready" != "1" ]]; then - echo "CKB RPC did not become ready at $RPC_URL. Log: $CKB_LOG" >&2 - tail -100 "$CKB_LOG" >&2 || true - exit 1 -fi - -python3 - "$RPC_URL" "$REPORT_JSON" "$CKB_REPO" "$CKB_BIN" "$CKB_LOG" "$REPO_ROOT" "$RUN_STATEFUL_SCENARIOS" "$CKB_DIR" "$CKB_PIN_FILE" <<'PY' -import hashlib -import json -import math -import os -import pathlib -import re -import shutil -import subprocess -import sys -import time -import urllib.error -import urllib.request - -rpc_url, report_path, ckb_repo, ckb_bin, ckb_log, repo_root, run_stateful_scenarios, ckb_dir, ckb_pin_file = sys.argv[1:] -report_path = pathlib.Path(report_path) -ckb_repo = pathlib.Path(ckb_repo).resolve() -repo_root = pathlib.Path(repo_root) -ckb_dir = pathlib.Path(ckb_dir) -ckb_pin_file = pathlib.Path(ckb_pin_file) -run_stateful_scenarios = run_stateful_scenarios == "1" - -ALWAYS_SUCCESS_CODE_HASH = "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5" -ALWAYS_SUCCESS_INDEX = "0x5" -UNEXPECTED_PROFILE_TRAILER = bytes.fromhex("53504f5241424900") -LOCK_BEHAVIOR_ACCEPTANCE_SCOPE = { - "strict_compile_only": False, - "onchain_lock_spend_matrix": True, - "onchain_lock_spend_matrix_scope": { - "multisig.cell": ["is_signer_lock", "can_execute", "can_cancel", "has_enough_approvals", "not_expired"], - "nft.cell": ["nft_ownership", "listing_seller", "offer_buyer", "valid_royalty", "collection_creator"], - "timelock.cell": ["can_unlock_lock", "is_owner", "lock_id_commitment", "asset_matches", "not_expired", "emergency_approved"], - "vesting.cell": ["vesting_admin"], - }, - "required_cases_per_lock": ["valid_spend", "invalid_spend"], - "scope_note": ( - "Scoped lock entries are strict-compiled under the CKB profile and each lock is exercised " - "through handwritten Python acceptance-harness valid-spend and invalid-spend transactions." - ), -} - -report = json.loads(report_path.read_text(encoding="utf-8")) -ckb_pin = json.loads(ckb_pin_file.read_text(encoding="utf-8")) - -def file_sha256(path): - return "0x" + hashlib.sha256(path.read_bytes()).hexdigest() - -ckb_version_output = subprocess.check_output([ckb_bin, "--version"], text=True).strip() -ckb_repo_head = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ckb_repo, text=True).strip() -ckb_repo_dirty = bool(subprocess.check_output( - ["git", "status", "--porcelain", "--untracked-files=all"], - cwd=ckb_repo, - text=True, -).strip()) -ckb_runtime_provenance = { - "schema": "cellscript-ckb-runtime-provenance-v0.22", - "pin_schema": ckb_pin["schema"], - "pin_file_sha256": file_sha256(ckb_pin_file), - "repository": ckb_pin["repository"], - "revision": ckb_pin["revision"], - "repo_head": ckb_repo_head, - "repo_dirty": ckb_repo_dirty, - "version": ckb_pin["version"], - "version_output": ckb_version_output, - "build_mode": ( - "fresh-dedicated-cargo-target" - if report.get("acceptance_mode") == "production" - else "bounded-provided-cached-or-on-demand" - ), - "binary_path": ckb_bin, - "binary_archived_with_report": pathlib.Path(ckb_bin).resolve().is_relative_to(report_path.parent.resolve()), - "binary_sha256": file_sha256(pathlib.Path(ckb_bin)), - "source_template_path": str(ckb_repo / ckb_pin["template_paths"][0]), - "source_template_sha256": file_sha256(ckb_repo / ckb_pin["template_paths"][0]), - "source_spec_path": str(ckb_repo / ckb_pin["template_paths"][1]), - "source_spec_sha256": file_sha256(ckb_repo / ckb_pin["template_paths"][1]), - "effective_config_path": str(ckb_dir / "ckb.toml"), - "effective_config_sha256": file_sha256(ckb_dir / "ckb.toml"), - "effective_spec_path": str(ckb_dir / "specs" / "integration.toml"), - "effective_spec_sha256": file_sha256(ckb_dir / "specs" / "integration.toml"), -} -artifacts = report.get("artifacts", []) -if not artifacts: - raise RuntimeError("acceptance report does not contain artifacts") -bundled_example_deployment_artifacts = report.get("bundled_example_deployment_artifacts", []) -token_action_artifacts = report.get("token_action_artifacts", []) -nft_action_artifacts = report.get("nft_action_artifacts", []) -timelock_action_artifacts = report.get("timelock_action_artifacts", []) -amm_action_artifacts = report.get("amm_action_artifacts", []) -multisig_action_artifacts = report.get("multisig_action_artifacts", []) -vesting_action_artifacts = [ - record - for record in report.get("original_scoped_action_artifacts", []) - if record.get("example") == "vesting.cell" - and record.get("action") in {"create_vesting_config", "grant_vesting", "claim_vested", "claim_fully_vested", "revoke_grant"} -] -launch_action_artifacts = report.get("launch_action_artifacts", []) -original_scoped_lock_artifacts = report.get("original_scoped_lock_artifacts", []) - -report.update({ - "status": "running-onchain", - "lock_acceptance_scope": LOCK_BEHAVIOR_ACCEPTANCE_SCOPE, - "ckb_repo": str(ckb_repo), - "ckb_bin": ckb_bin, - "ckb_log": ckb_log, - "rpc_url": rpc_url, - "ckb_runtime_provenance": ckb_runtime_provenance, - "onchain": { - "status": "running", - "chain_template": "ckb/test/template integration devnet", - "always_success_system_cell_index": ALWAYS_SUCCESS_INDEX, - "artifact_runs": [], - "bundled_example_deployment_runs": [], - "token_action_runs": [], - "nft_action_runs": [], - "timelock_action_runs": [], - "multisig_action_runs": [], - "vesting_action_runs": [], - "amm_action_runs": [], - "launch_action_runs": [], - "lock_spend_matrix_runs": [], - "stateful_scenario_runs": [], - }, -}) - -def refresh_build_report_deployments(): - build_index = report.get("cellscript_build_reports") or {} - reports = build_index.get("reports") or [] - by_artifact = { - row.get("artifact_path"): row - for row in reports - if isinstance(row, dict) and isinstance(row.get("artifact_path"), str) - } - for row in reports: - if isinstance(row, dict): - row["onchain_deployments"] = [] - - unexpected_artifacts = [] - - def add_deployment(run, *, name=None, kind=None, code=None): - code = code or run - artifact = code.get("artifact") - row = by_artifact.get(artifact) - if row is None: - unexpected_artifacts.append(artifact) - return - deploy = code.get("code_cell_deploy") or {} - code_dep = code.get("code_cell_dep") or {} - out_point_value = code_dep.get("out_point") - artifact_hash = code.get("artifact_ckb_data_hash_blake2b") - live_hash = code.get("live_code_cell_data_hash") - row["onchain_deployments"].append({ - "run_name": name or run.get("name") or row.get("name"), - "run_kind": kind or run.get("kind") or row.get("kind"), - "out_point": out_point_value, - "tx_hash": deploy.get("tx_hash"), - "output_index": "0x0", - "artifact_ckb_data_hash_blake2b": artifact_hash, - "live_code_cell_data_hash": live_hash, - "live_code_cell_data_hash_matches_artifact": live_hash == artifact_hash, - "code_cell_live": code.get("code_cell_live") is True, - }) - - for run in report["onchain"].get("artifact_runs", []): - add_deployment(run, kind="artifact-spend") - for run in report["onchain"].get("bundled_example_deployment_runs", []): - add_deployment(run, kind="bundled-example-deployment") - for key in ( - "token_action_runs", - "nft_action_runs", - "timelock_action_runs", - "multisig_action_runs", - "vesting_action_runs", - "amm_action_runs", - "launch_action_runs", - "lock_spend_matrix_runs", - ): - for run in report["onchain"].get(key, []): - code = run.get("code") - if isinstance(code, dict): - add_deployment(run, kind=key.removesuffix("_runs"), code=code) - - missing = [ - row.get("name") - for row in reports - if isinstance(row, dict) and not row.get("onchain_deployments") - ] - mismatches = [ - f"{row.get('name')}:{deployment.get('run_name')}" - for row in reports - if isinstance(row, dict) - for deployment in row.get("onchain_deployments", []) - if deployment.get("live_code_cell_data_hash_matches_artifact") is not True - or deployment.get("code_cell_live") is not True - ] - build_index.update({ - "onchain_deployed_artifact_count": sum( - 1 for row in reports if isinstance(row, dict) and row.get("onchain_deployments") - ), - "live_code_cell_data_hash_match_count": sum( - 1 - for row in reports - if isinstance(row, dict) - and row.get("onchain_deployments") - and all( - deployment.get("live_code_cell_data_hash_matches_artifact") is True - and deployment.get("code_cell_live") is True - for deployment in row.get("onchain_deployments", []) - ) - ), - "missing_onchain_deployments": missing, - "live_code_cell_data_hash_mismatches": mismatches, - "unexpected_onchain_artifacts": [value for value in unexpected_artifacts if value], - "status": "passed" if not missing and not mismatches and not unexpected_artifacts else "failed", - }) - return build_index - -def write_report(): - report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") - -def update_ckb_business_coverage(onchain_actions): - coverage = report.get("ckb_business_coverage") or {} - rows = coverage.get("rows") or [] - for row in rows: - example = row["example"] - strict_actions = row.get("strict_ckb_actions") or [] - ckb_onchain_actions = onchain_actions.get(example, []) - row["ckb_onchain_actions"] = ckb_onchain_actions - row["missing_ckb_onchain_actions"] = sorted(set(strict_actions) - set(ckb_onchain_actions)) - row["ckb_onchain_action_coverage_complete"] = not row["missing_ckb_onchain_actions"] - - strict_complete = all( - row.get("strict_action_coverage_complete") and row.get("strict_lock_coverage_complete") - for row in rows - ) - onchain_complete = all(row.get("ckb_onchain_action_coverage_complete") for row in rows) - coverage.update({ - "status": "complete" if strict_complete and onchain_complete else "incomplete", - "strict_compile_coverage_complete": strict_complete, - "onchain_action_coverage_complete": onchain_complete, - "ckb_onchain_action_count": sum(len(row.get("ckb_onchain_actions") or []) for row in rows), - "missing_ckb_onchain_actions": { - row["example"]: row["missing_ckb_onchain_actions"] - for row in rows - if row.get("missing_ckb_onchain_actions") - }, - "rows": rows, - }) - report["ckb_business_coverage"] = coverage - report["production_ready"] = ( - report.get("acceptance_mode") == "production" - and coverage["status"] == "complete" - and (report.get("production_gate") or {}).get("status") == "passed" - and (report.get("final_production_hardening_gate") or {}).get("ready") is True - and (report.get("ckb_runtime_provenance") or {}).get("repo_dirty") is False - ) - -RPC_OPENER = urllib.request.build_opener(urllib.request.ProxyHandler({})) - -def rpc(method, params=None): - body = json.dumps({"id": 42, "jsonrpc": "2.0", "method": method, "params": params or []}).encode("utf-8") - last_error = None - for attempt in range(6): - request = urllib.request.Request(rpc_url, data=body, headers={"Content-Type": "application/json"}) - try: - with RPC_OPENER.open(request, timeout=20) as response: - payload = json.loads(response.read().decode("utf-8")) - break - except urllib.error.HTTPError as error: - if error.code not in {502, 503, 504}: - raise RuntimeError(f"RPC {method} failed to connect: {error}") from error - last_error = error - except urllib.error.URLError as error: - last_error = error - if attempt == 5: - raise RuntimeError(f"RPC {method} failed to connect after retries: {last_error}") from last_error - time.sleep(0.25 * (attempt + 1)) - if payload.get("error"): - raise RuntimeError(f"RPC {method} returned error: {payload['error']}") - return payload.get("result") - -def hex_u64(value): - if isinstance(value, str): - value = int(value, 16) - return hex(value) - -def out_point(tx_hash, index): - return {"tx_hash": tx_hash, "index": hex_u64(index)} - -def wait_live_cell(tx_hash, index, attempts=20, delay_seconds=0.05): - last_result = None - for _ in range(attempts): - last_result = rpc("get_live_cell", [out_point(tx_hash, index), True]) - if last_result and last_result.get("status") == "live": - return last_result - time.sleep(delay_seconds) - return last_result - -def always_success_lock(args="0x"): - return {"code_hash": ALWAYS_SUCCESS_CODE_HASH, "hash_type": "data", "args": args} - -def data_hash(data): - return "0x" + hashlib.blake2b(data, digest_size=32, person=b"ckb-default-hash").hexdigest() - -def live_cell_data_hash(live_cell): - cell = (live_cell or {}).get("cell") or {} - data = cell.get("data") or {} - if isinstance(data, dict): - reported_hash = data.get("hash") - if isinstance(reported_hash, str) and reported_hash.startswith("0x"): - return reported_hash - content = data.get("content") - else: - content = data - if isinstance(content, str) and content.startswith("0x"): - return data_hash(bytes.fromhex(content[2:])) - raise RuntimeError(f"live cell does not expose code data hash/content: {live_cell}") - -def ckb_hash(data): - return hashlib.blake2b(data, digest_size=32, person=b"ckb-default-hash").digest() - -def molecule_u32(value): - return int(value).to_bytes(4, "little") - -def molecule_bytes(data): - return molecule_u32(len(data)) + data - -def molecule_string_witness(data): - return molecule_bytes(molecule_bytes(data)) - -def molecule_fixvec(items): - out = bytearray(molecule_u32(len(items))) - for item in items: - out.extend(item) - return bytes(out) - -def molecule_table(fields): - header_size = 4 + 4 * len(fields) - offsets = [] - cursor = header_size - for field in fields: - offsets.append(cursor) - cursor += len(field) - out = bytearray() - out.extend(molecule_u32(cursor)) - for offset in offsets: - out.extend(molecule_u32(offset)) - for field in fields: - out.extend(field) - return bytes(out) - -def hash_type_byte(hash_type): - values = {"data": 0, "type": 1, "data1": 2, "data2": 4} - if hash_type not in values: - raise RuntimeError(f"unsupported hash_type for packed Script hash: {hash_type}") - return bytes([values[hash_type]]) - -def decode_hex(value, expected_len=None): - if not isinstance(value, str) or not value.startswith("0x"): - raise RuntimeError(f"expected 0x-prefixed hex string, got {value!r}") - data = bytes.fromhex(value[2:]) - if expected_len is not None and len(data) != expected_len: - raise RuntimeError(f"expected {expected_len} bytes, got {len(data)}") - return data - -def script_molecule(script): - return molecule_table([ - decode_hex(script["code_hash"], 32), - hash_type_byte(script["hash_type"]), - molecule_bytes(decode_hex(script.get("args", "0x"))), - ]) - -def script_hash(script): - return "0x" + ckb_hash(script_molecule(script)).hex() - -def token_data(amount, symbol=b"TOKEN001"): - if len(symbol) != 8: - raise RuntimeError(f"token symbol must be exactly 8 bytes, got {len(symbol)}") - return amount.to_bytes(8, "little") + symbol - -def pool_data(token_a_symbol, token_b_symbol, reserve_a, reserve_b, total_lp, fee_rate_bps, token_a_type, token_b_type): - if len(token_a_type) != 32 or len(token_b_type) != 32: - raise RuntimeError("Pool token TypeHashes must each be exactly 32 bytes") - return token_a_type + token_b_type + token_a_symbol + token_b_symbol + reserve_a.to_bytes(8, "little") + reserve_b.to_bytes(8, "little") + total_lp.to_bytes(8, "little") + fee_rate_bps.to_bytes(2, "little") - -def lp_receipt_data(pool_id, lp_amount, provider): - return pool_id + lp_amount.to_bytes(8, "little") + provider - -def mint_authority_data(token_symbol=b"TOKEN001", max_supply=1000, minted=0): - if len(token_symbol) != 8: - raise RuntimeError(f"mint authority symbol must be exactly 8 bytes, got {len(token_symbol)}") - return token_symbol + max_supply.to_bytes(8, "little") + minted.to_bytes(8, "little") - -def fixed_recipient_tuple_array(recipients): - if len(recipients) != 2: - raise RuntimeError(f"launch recipients must contain exactly 2 entries, got {len(recipients)}") - out = bytearray() - for address, amount in recipients: - if len(address) != 32: - raise RuntimeError(f"launch recipient address must be exactly 32 bytes, got {len(address)}") - out.extend(address) - out.extend(int(amount).to_bytes(8, "little")) - return bytes(out) - -def fixed_recipient_tuple_array4(recipients): - if len(recipients) != 4: - raise RuntimeError(f"launch recipients must contain exactly 4 entries, got {len(recipients)}") - out = bytearray() - for address, amount in recipients: - if len(address) != 32: - raise RuntimeError(f"launch recipient address must be exactly 32 bytes, got {len(address)}") - out.extend(address) - out.extend(int(amount).to_bytes(8, "little")) - return bytes(out) - -def fixed_address_array4(addresses): - if len(addresses) != 4: - raise RuntimeError(f"address array must contain exactly 4 entries, got {len(addresses)}") - out = bytearray() - for address in addresses: - if len(address) != 32: - raise RuntimeError(f"address array entry must be exactly 32 bytes, got {len(address)}") - out.extend(address) - return bytes(out) - -def fixed_hash_array4(hashes): - if len(hashes) != 4: - raise RuntimeError(f"hash array must contain exactly 4 entries, got {len(hashes)}") - out = bytearray() - for value in hashes: - if len(value) != 32: - raise RuntimeError(f"hash array entry must be exactly 32 bytes, got {len(value)}") - out.extend(value) - return bytes(out) - -def fixed_u64_array4(values): - if len(values) != 4: - raise RuntimeError(f"u64 array must contain exactly 4 entries, got {len(values)}") - out = bytearray() - for value in values: - out.extend(int(value).to_bytes(8, "little")) - return bytes(out) - -def nft_data(token_id, owner, metadata_hash, royalty_recipient, royalty_bps, collection_id=bytes(32)): - if len(collection_id) != 32: - raise RuntimeError(f"NFT collection_id must be exactly 32 bytes, got {len(collection_id)}") - if len(owner) != 32: - raise RuntimeError(f"NFT owner must be exactly 32 bytes, got {len(owner)}") - if len(metadata_hash) != 32: - raise RuntimeError(f"NFT metadata hash must be exactly 32 bytes, got {len(metadata_hash)}") - if len(royalty_recipient) != 32: - raise RuntimeError(f"NFT royalty recipient must be exactly 32 bytes, got {len(royalty_recipient)}") - return ( - collection_id - + token_id.to_bytes(8, "little") - + owner - + metadata_hash - + royalty_recipient - + royalty_bps.to_bytes(2, "little") - ) - -def collection_data(creator, total_supply, max_supply): - if len(creator) != 32: - raise RuntimeError(f"Collection creator must be exactly 32 bytes, got {len(creator)}") - return creator + total_supply.to_bytes(8, "little") + max_supply.to_bytes(8, "little") - -def collection_molecule_data(creator, total_supply, max_supply, name=b"Acceptance Collection", symbol=b"ACPT", base_uri=b"ckb://cellscript/nft/"): - if len(creator) != 32: - raise RuntimeError(f"Collection creator must be exactly 32 bytes, got {len(creator)}") - return molecule_table([ - molecule_bytes(name), - molecule_bytes(symbol), - creator, - total_supply.to_bytes(8, "little"), - max_supply.to_bytes(8, "little"), - molecule_bytes(base_uri), - ]) - -def listing_data(token_id, seller, price, created_at, state=None, collection_id=bytes(32)): - if len(collection_id) != 32: - raise RuntimeError(f"Listing collection_id must be exactly 32 bytes, got {len(collection_id)}") - if len(seller) != 32: - raise RuntimeError(f"Listing seller must be exactly 32 bytes, got {len(seller)}") - if state is not None and not 0 <= state <= 255: - raise RuntimeError(f"Listing state must fit in u8, got {state}") - payload = collection_id + token_id.to_bytes(8, "little") + seller + price.to_bytes(8, "little") + created_at.to_bytes(8, "little") - return payload if state is None else payload + bytes([state]) - -def offer_data(token_id, buyer, price, expires_at, state=None, collection_id=bytes(32), payment_symbol=b"PAYM0001"): - if len(collection_id) != 32: - raise RuntimeError(f"Offer collection_id must be exactly 32 bytes, got {len(collection_id)}") - if len(buyer) != 32: - raise RuntimeError(f"Offer buyer must be exactly 32 bytes, got {len(buyer)}") - if state is not None and not 0 <= state <= 255: - raise RuntimeError(f"Offer state must fit in u8, got {state}") - if len(payment_symbol) != 8: - raise RuntimeError(f"Offer payment_symbol must be exactly 8 bytes, got {len(payment_symbol)}") - payload = collection_id + token_id.to_bytes(8, "little") + buyer + price.to_bytes(8, "little") + expires_at.to_bytes(8, "little") + payment_symbol - return payload if state is None else payload + bytes([state]) - -def timelock_data(owner, lock_type, unlock_height, created_at, lock_id=None): - if lock_id is not None and len(lock_id) != 32: - raise RuntimeError(f"TimeLock lock_id must be exactly 32 bytes, got {len(lock_id)}") - if len(owner) != 32: - raise RuntimeError(f"TimeLock owner must be exactly 32 bytes, got {len(owner)}") - if not 0 <= lock_type <= 255: - raise RuntimeError(f"TimeLock lock_type must fit in u8, got {lock_type}") - payload = owner + bytes([lock_type]) + unlock_height.to_bytes(8, "little") + created_at.to_bytes(8, "little") - return payload if lock_id is None else lock_id + payload - -def locked_asset_data(token_symbol, amount, lock_id): - if len(token_symbol) != 8: - raise RuntimeError(f"LockedAsset token_symbol must be exactly 8 bytes, got {len(token_symbol)}") - if len(lock_id) != 32: - raise RuntimeError(f"LockedAsset lock_id must be exactly 32 bytes, got {len(lock_id)}") - return token_symbol + amount.to_bytes(8, "little") + lock_id - -def release_request_data(lock_hash, requester, requested_at, state=None): - if len(lock_hash) != 32: - raise RuntimeError(f"ReleaseRequest lock_hash must be exactly 32 bytes, got {len(lock_hash)}") - if len(requester) != 32: - raise RuntimeError(f"ReleaseRequest requester must be exactly 32 bytes, got {len(requester)}") - if state is not None and not 0 <= state <= 255: - raise RuntimeError(f"ReleaseRequest state must fit in u8, got {state}") - payload = lock_hash + requester + requested_at.to_bytes(8, "little") - return payload if state is None else payload + bytes([state]) - -def emergency_release_data(lock_hash, requester, requested_at, approvals): - if len(lock_hash) != 32: - raise RuntimeError(f"EmergencyRelease lock_hash must be exactly 32 bytes, got {len(lock_hash)}") - if len(requester) != 32: - raise RuntimeError(f"EmergencyRelease requester must be exactly 32 bytes, got {len(requester)}") - if not 0 <= approvals <= 255: - raise RuntimeError(f"EmergencyRelease approvals must fit in u8, got {approvals}") - return lock_hash + requester + requested_at.to_bytes(8, "little") + bytes([approvals]) - -def emergency_release_molecule_data(lock_hash, requester, reason, requested_at, approvers, state=0): - if len(lock_hash) != 32: - raise RuntimeError(f"EmergencyRelease lock_hash must be exactly 32 bytes, got {len(lock_hash)}") - if len(requester) != 32: - raise RuntimeError(f"EmergencyRelease requester must be exactly 32 bytes, got {len(requester)}") - if not 0 <= state <= 255: - raise RuntimeError(f"EmergencyRelease state must fit in u8, got {state}") - for approver in approvers: - if len(approver) != 32: - raise RuntimeError(f"EmergencyRelease approver must be exactly 32 bytes, got {len(approver)}") - return molecule_table([ - lock_hash, - requester, - reason, - requested_at.to_bytes(8, "little"), - molecule_fixvec(approvers), - bytes([state]), - ]) - -def release_record_data(lock_hash, released_at, released_by): - if len(lock_hash) != 32: - raise RuntimeError(f"ReleaseRecord lock_hash must be exactly 32 bytes, got {len(lock_hash)}") - if len(released_by) != 32: - raise RuntimeError(f"ReleaseRecord released_by must be exactly 32 bytes, got {len(released_by)}") - return lock_hash + released_at.to_bytes(8, "little") + released_by - -def multisig_wallet_data(wallet_id, signer_a, signer_b, threshold, nonce, created_at): - if len(wallet_id) != 32: - raise RuntimeError(f"MultisigWallet wallet_id must be exactly 32 bytes, got {len(wallet_id)}") - if len(signer_a) != 32: - raise RuntimeError(f"MultisigWallet signer_a must be exactly 32 bytes, got {len(signer_a)}") - if len(signer_b) != 32: - raise RuntimeError(f"MultisigWallet signer_b must be exactly 32 bytes, got {len(signer_b)}") - if not 0 <= threshold <= 255: - raise RuntimeError(f"MultisigWallet threshold must fit in u8, got {threshold}") - return wallet_id + signer_a + signer_b + bytes([threshold]) + nonce.to_bytes(8, "little") + created_at.to_bytes(8, "little") - -def multisig_wallet_molecule_data(wallet_id, signers, threshold, nonce, created_at): - if len(wallet_id) != 32: - raise RuntimeError(f"MultisigWallet wallet_id must be exactly 32 bytes, got {len(wallet_id)}") - if len(signers) < 2: - raise RuntimeError(f"MultisigWallet signers must contain at least 2 entries, got {len(signers)}") - for signer in signers: - if len(signer) != 32: - raise RuntimeError(f"MultisigWallet signer must be exactly 32 bytes, got {len(signer)}") - if not 0 <= threshold <= 255: - raise RuntimeError(f"MultisigWallet threshold must fit in u8, got {threshold}") - return molecule_table([ - wallet_id, - molecule_fixvec(signers), - bytes([threshold]), - nonce.to_bytes(8, "little"), - created_at.to_bytes(8, "little"), - ]) - -def multisig_proposal_molecule_data(wallet_id, proposal_id, proposer, operation, target, amount, data, approvals, required_approvals, created_at, expires_at, state=0): - if len(wallet_id) != 32: - raise RuntimeError(f"Proposal wallet_id must be exactly 32 bytes, got {len(wallet_id)}") - if len(proposer) != 32: - raise RuntimeError(f"Proposal proposer must be exactly 32 bytes, got {len(proposer)}") - if len(target) != 32: - raise RuntimeError(f"Proposal target must be exactly 32 bytes, got {len(target)}") - if not 0 <= operation <= 255: - raise RuntimeError(f"Proposal operation must fit in u8, got {operation}") - if not 0 <= required_approvals <= 255: - raise RuntimeError(f"Proposal required_approvals must fit in u8, got {required_approvals}") - if not 0 <= state <= 255: - raise RuntimeError(f"Proposal state must fit in u8, got {state}") - for approver in approvals: - if len(approver) != 32: - raise RuntimeError(f"Proposal approver must be exactly 32 bytes, got {len(approver)}") - return molecule_table([ - wallet_id, - proposal_id.to_bytes(8, "little"), - proposer, - bytes([operation]), - target, - amount.to_bytes(8, "little"), - molecule_fixvec([bytes([byte]) for byte in data]), - bytes([required_approvals]), - molecule_fixvec(approvals), - created_at.to_bytes(8, "little"), - expires_at.to_bytes(8, "little"), - bytes([state]), - ]) - -def multisig_proposal_data(wallet_id, proposal_id, proposer, operation, target, amount, required_approvals, approval_count, created_at, expires_at): - if len(wallet_id) != 32: - raise RuntimeError(f"Proposal wallet_id must be exactly 32 bytes, got {len(wallet_id)}") - if len(proposer) != 32: - raise RuntimeError(f"Proposal proposer must be exactly 32 bytes, got {len(proposer)}") - if len(target) != 32: - raise RuntimeError(f"Proposal target must be exactly 32 bytes, got {len(target)}") - if not 0 <= operation <= 255: - raise RuntimeError(f"Proposal operation must fit in u8, got {operation}") - if not 0 <= required_approvals <= 255: - raise RuntimeError(f"Proposal required_approvals must fit in u8, got {required_approvals}") - if not 0 <= approval_count <= 255: - raise RuntimeError(f"Proposal approval_count must fit in u8, got {approval_count}") - return ( - wallet_id - + proposal_id.to_bytes(8, "little") - + proposer - + bytes([operation]) - + target - + amount.to_bytes(8, "little") - + bytes([required_approvals]) - + bytes([approval_count]) - + created_at.to_bytes(8, "little") - + expires_at.to_bytes(8, "little") - ) - -def approval_confirmation_data(proposal_id, approver, reported_at): - if len(approver) != 32: - raise RuntimeError(f"ApprovalConfirmation approver must be exactly 32 bytes, got {len(approver)}") - return proposal_id.to_bytes(8, "little") + approver + reported_at.to_bytes(8, "little") - -def execution_record_data(proposal_id, executor, executed_at, success): - if len(executor) != 32: - raise RuntimeError(f"ExecutionRecord executor must be exactly 32 bytes, got {len(executor)}") - if not 0 <= success <= 255: - raise RuntimeError(f"ExecutionRecord success must fit in u8, got {success}") - return proposal_id.to_bytes(8, "little") + executor + executed_at.to_bytes(8, "little") + bytes([success]) - -def vesting_config_data(admin, symbol, cliff_period, total_period, revocable): - if len(admin) != 32: - raise RuntimeError(f"VestingConfig admin must be exactly 32 bytes, got {len(admin)}") - if len(symbol) != 8: - raise RuntimeError(f"VestingConfig token_symbol must be exactly 8 bytes, got {len(symbol)}") - if revocable not in (0, 1, False, True): - raise RuntimeError(f"VestingConfig revocable must be boolean-like, got {revocable!r}") - return ( - admin - + symbol - + cliff_period.to_bytes(8, "little") - + total_period.to_bytes(8, "little") - + bytes([1 if revocable else 0]) - ) - -def vesting_grant_data(state, beneficiary, total_amount, claimed_amount, grant_timepoint, cliff_timepoint, end_timepoint, symbol): - if len(beneficiary) != 32: - raise RuntimeError(f"VestingGrant beneficiary must be exactly 32 bytes, got {len(beneficiary)}") - if len(symbol) != 8: - raise RuntimeError(f"VestingGrant token_symbol must be exactly 8 bytes, got {len(symbol)}") - return ( - bytes([state]) - + beneficiary - + total_amount.to_bytes(8, "little") - + claimed_amount.to_bytes(8, "little") - + grant_timepoint.to_bytes(8, "little") - + cliff_timepoint.to_bytes(8, "little") - + end_timepoint.to_bytes(8, "little") - + symbol - ) - -def entry_witness(*args): - out = bytearray(b"CSARGv1\0") - for arg in args: - if isinstance(arg, int): - out.extend(arg.to_bytes(8, "little")) - elif isinstance(arg, bytes): - out.extend(arg) - else: - raise RuntimeError(f"unsupported entry witness arg: {arg!r}") - return "0x" + bytes(out).hex() - -def get_block(block_hash, attempts=20, delay_seconds=0.05): - block = None - for _ in range(attempts): - block = rpc("get_block", [block_hash]) - if block is not None: - return block - time.sleep(delay_seconds) - raise RuntimeError(f"block not found: {block_hash}") - -def get_block_by_number(number, attempts=20, delay_seconds=0.05): - block = None - for _ in range(attempts): - block = rpc("get_block_by_number", [hex_u64(number)]) - if block is not None: - return block - time.sleep(delay_seconds) - raise RuntimeError(f"block number not found: {number}") - -def epoch_number_from_header(header): - return int(header["epoch"], 16) & ((1 << 24) - 1) - -CKB_CONSENSUS_MAX_EPOCH_LENGTH = 1800 - -def wait_header_epoch_at_least(min_epoch, max_blocks=None): - last_header = rpc("get_tip_header") - initial_epoch = epoch_number_from_header(last_header) - if max_blocks is None: - remaining_epochs = max(0, min_epoch - initial_epoch) - max_blocks = (remaining_epochs + 1) * CKB_CONSENSUS_MAX_EPOCH_LENGTH - for generated in range(max_blocks + 1): - if generated > 0: - last_header = rpc("get_tip_header") - epoch_number = epoch_number_from_header(last_header) - if epoch_number >= min_epoch: - return { - "hash": last_header["hash"], - "epoch": last_header["epoch"], - "epoch_number": epoch_number, - "generated_blocks": generated, - } - if generated < max_blocks: - rpc("generate_block") - time.sleep(0.01) - raise RuntimeError( - f"tip epoch did not reach {min_epoch} after {max_blocks} generated blocks; " - f"last_header={last_header}" - ) - -RESERVED_SPENDABLE_OUTPOINTS = set() - -def spendable_outpoint_key(tx_hash, index): - return (tx_hash, int(index)) - -def reserve_spendable_outpoint(tx_hash, index): - key = spendable_outpoint_key(tx_hash, index) - if key in RESERVED_SPENDABLE_OUTPOINTS: - return False - RESERVED_SPENDABLE_OUTPOINTS.add(key) - return True - -def find_spendable_cellbase(max_blocks=64): - generated = [] - for _ in range(max_blocks): - block_hash = rpc("generate_block") - generated.append(block_hash) - block = get_block(block_hash) - cellbase = block["transactions"][0] - outputs = cellbase.get("outputs", []) - if outputs: - for index, output in enumerate(outputs): - capacity = int(output["capacity"], 16) - if capacity > 0: - if spendable_outpoint_key(cellbase["hash"], index) in RESERVED_SPENDABLE_OUTPOINTS: - continue - live_status = wait_live_cell(cellbase["hash"], index) - if ( - live_status - and live_status.get("status") == "live" - and reserve_spendable_outpoint(cellbase["hash"], index) - ): - return { - "block_hash": block_hash, - "tx_hash": cellbase["hash"], - "index": index, - "capacity": capacity, - "generated_blocks": generated, - } - raise RuntimeError(f"no spendable cellbase output found after {max_blocks} generated blocks") - -def collect_spendable_cellbases(min_capacity, max_cells=256): - cells = [] - total_capacity = 0 - generated_blocks = [] - while total_capacity < min_capacity and len(cells) < max_cells: - cell = find_spendable_cellbase() - cells.append(cell) - total_capacity += cell["capacity"] - generated_blocks.extend(cell["generated_blocks"]) - if total_capacity < min_capacity: - raise RuntimeError( - f"collected {total_capacity:#x} capacity from {len(cells)} cellbase cells, " - f"need at least {min_capacity:#x}" - ) - return { - "cells": cells, - "total_capacity": total_capacity, - "generated_blocks": generated_blocks, - } - -def transaction(input_cells, outputs, outputs_data, cell_deps, witnesses=None, header_deps=None): - if isinstance(input_cells, dict) and "cells" in input_cells: - input_cells = input_cells["cells"] - elif isinstance(input_cells, dict): - input_cells = [input_cells] - return { - "version": "0x0", - "cell_deps": cell_deps, - "header_deps": header_deps or [], - "inputs": [ - { - "previous_output": out_point(input_cell["tx_hash"], input_cell["index"]), - "since": "0x0", - } - for input_cell in input_cells - ], - "outputs": outputs, - "outputs_data": outputs_data, - "witnesses": witnesses or [], - } - -def cell_dep_for(cell): - return {"out_point": out_point(cell["tx_hash"], cell["index"]), "dep_type": "code"} - -def parse_hex_u64(value): - if value is None: - return None - if isinstance(value, int): - return value - if isinstance(value, str): - return int(value, 16) if value.startswith("0x") else int(value) - raise RuntimeError(f"unsupported numeric value: {value!r}") - -def json_serialized_size_bytes(value): - return len(json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")) - -def ensure_ckb_tx_measure_bin(): - import pathlib - import subprocess - helper_root = report_path.parent / "ckb-tx-measure-helper" - tx_measure_manifest = helper_root / "Cargo.toml" - tx_measure_lock = helper_root / "Cargo.lock" - tx_measure_target = helper_root / "target" - tx_measure_bin = tx_measure_target / "debug" / "cellscript-ckb-tx-measure" - if tx_measure_bin.exists(): - return tx_measure_bin - cargo_env = os.environ.copy() - helper_root.mkdir(parents=True, exist_ok=True) - source_bin = repo_root / "src" / "bin" / "ckb_tx_measure.rs" - lock_src = repo_root / "tools" / "ckb-tx-measure" / "Cargo.lock" - shutil.copyfile(lock_src, tx_measure_lock) - tx_measure_manifest.write_text( - f"""[package] -name = "cellscript-ckb-tx-measure" -version = "0.1.0" -edition = "2024" -rust-version = "1.97.1" -publish = false - -[workspace] -resolver = "3" - -[[bin]] -name = "cellscript-ckb-tx-measure" -path = "{source_bin.as_posix()}" - -[dependencies] -ckb-jsonrpc-types = {{ path = "{(ckb_repo / "util" / "jsonrpc-types").as_posix()}" }} -ckb-types = {{ path = "{(ckb_repo / "util" / "types").as_posix()}" }} -serde = {{ version = "1.0", features = ["derive"] }} -serde_json = "1.0" -""", - encoding="utf-8", - ) - subprocess.run( - [ - "cargo", - "generate-lockfile", - "--manifest-path", - str(tx_measure_manifest), - ], - check=True, - cwd=helper_root, - env=cargo_env, - ) - subprocess.run( - [ - "cargo", - "build", - "--locked", - "--manifest-path", - str(tx_measure_manifest), - "--target-dir", - str(tx_measure_target), - ], - check=True, - cwd=helper_root, - env=cargo_env, - ) - if not tx_measure_bin.exists(): - raise RuntimeError(f"ckb tx measure helper was not built at {tx_measure_bin}") - return tx_measure_bin - -def measure_ckb_transaction_shape(valid_tx): - import json - import subprocess - helper = ensure_ckb_tx_measure_bin() - proc = subprocess.run( - [str(helper)], - input=json.dumps(valid_tx, separators=(",", ":")), - text=True, - capture_output=True, - ) - if proc.returncode != 0: - stderr = (proc.stderr or "").strip() - stdout = (proc.stdout or "").strip() - raise RuntimeError( - f"cellscript-ckb-tx-measure failed with exit {proc.returncode}; stderr={stderr!r}; stdout={stdout!r}" - ) - return json.loads(proc.stdout) - -def measure_release_constraints(valid_tx, valid_dry_run): - outputs = valid_tx.get("outputs") or [] - outputs_data = valid_tx.get("outputs_data") or [] - witnesses = valid_tx.get("witnesses") or [] - input_count = len(valid_tx.get("inputs") or []) - cell_dep_count = len(valid_tx.get("cell_deps") or []) - header_dep_count = len(valid_tx.get("header_deps") or []) - output_capacity_shannons = sum(parse_hex_u64(output.get("capacity")) or 0 for output in outputs) - output_data_bytes = sum(len(decode_hex(data)) for data in outputs_data) - witness_bytes = sum(len(decode_hex(witness)) for witness in witnesses) - measured_cycles = None - cycles_status = "dry-run-missing-cycles" - if isinstance(valid_dry_run, dict): - measured_cycles = parse_hex_u64(valid_dry_run.get("cycles")) - if measured_cycles is not None: - cycles_status = "dry-run-measured" - tx_shape = None - tx_size_status = "not-measured-by-acceptance" - occupied_capacity_status = "not-derived-by-acceptance" - tx_measure_error = None - try: - tx_shape = measure_ckb_transaction_shape(valid_tx) - tx_size_status = "measured-by-cellscript-ckb-tx-measure" - occupied_capacity_status = "derived-by-cellscript-ckb-tx-measure" - except Exception as error: - tx_shape = None - tx_measure_error = str(error) - - return { - "measured_cycles": measured_cycles, - "cycles_status": cycles_status, - "consensus_serialized_tx_size_bytes": None if tx_shape is None else tx_shape.get("consensus_serialized_tx_size_bytes"), - "tx_size_status": tx_size_status, - "tx_measure_error": tx_measure_error, - "json_envelope_size_bytes": json_serialized_size_bytes(valid_tx), - "witness_bytes": witness_bytes, - "output_capacity_shannons": output_capacity_shannons, - "output_data_bytes": output_data_bytes, - "occupied_capacity_shannons": None if tx_shape is None else tx_shape.get("occupied_capacity_shannons"), - "output_occupied_capacity_shannons": [] if tx_shape is None else tx_shape.get("output_occupied_capacity_shannons", []), - "measured_output_capacity_shannons": [] if tx_shape is None else tx_shape.get("output_capacity_shannons", []), - "capacity_is_sufficient": None if tx_shape is None else tx_shape.get("capacity_is_sufficient"), - "under_capacity_output_indexes": [] if tx_shape is None else tx_shape.get("under_capacity_output_indexes", []), - "occupied_capacity_status": occupied_capacity_status, - "input_count": input_count, - "output_count": len(outputs), - "cell_dep_count": cell_dep_count, - "header_dep_count": header_dep_count, - "witness_count": len(witnesses), - } - -def submit_and_commit(tx, label, max_blocks=64): - tx_hash = rpc("send_test_transaction", [tx, "passthrough"]) - last_status = None - for generated in range(max_blocks + 1): - status = rpc("get_transaction", [tx_hash]) - tx_status = (status or {}).get("tx_status", {}) - last_status = tx_status - if tx_status.get("status") == "committed": - return {"tx_hash": tx_hash, "generated_blocks_after_submit": generated, "status": tx_status} - if tx_status.get("status") == "rejected": - raise RuntimeError(f"{label} was rejected while waiting for commit: {tx_hash}; last_status={tx_status}") - rpc("generate_block") - time.sleep(0.05) - raise RuntimeError(f"{label} was not committed after {max_blocks} generated blocks: {tx_hash}; last_status={last_status}") - -def expect_dry_run_rejected(tx, label, expected_fragments): - try: - estimate = rpc("dry_run_transaction", [tx]) - except RuntimeError as error: - message = str(error) - if not any(fragment in message for fragment in expected_fragments): - raise RuntimeError(f"{label} was rejected for an unexpected reason: {message}") from error - forbidden_fragments = ( - "InsufficientCellCapacity", - "ExceededMaximumAncestorsCount", - "ExceededMaximumCycles", - "MaxBlockCycles", - "MaxBlockBytes", - "Duplicated", - "PoolIsFull", - ) - if any(fragment in message for fragment in forbidden_fragments): - raise RuntimeError(f"{label} was rejected by a policy/capacity reason: {message}") from error - return { - "status": "rejected", - "check": "dry_run_transaction", - "reason": message, - "expected_reason_matched": True, - "policy_or_capacity_reason": False, - } - raise RuntimeError(f"{label} was unexpectedly accepted by dry-run: {estimate}") - -def assert_live(tx_hash, index, label): - result = wait_live_cell(tx_hash, index) - if not result or result.get("status") != "live": - raise RuntimeError(f"{label} is not live: {result}") - return result - -def is_transient_dead_outpoint_error(error): - message = str(error) - return ( - "Resolve failed Dead(OutPoint" in message - or "Dead(OutPoint" in message - or "Resolve failed Unknown(OutPoint" in message - or "Unknown(OutPoint" in message - ) - -def code_cell_deploy_transaction(deploy_input, artifact, always_success_dep): - return transaction( - deploy_input, - [ - { - "capacity": hex_u64(deploy_input["total_capacity"]), - "lock": always_success_lock(), - "type": None, - } - ], - ["0x" + artifact.hex()], - [always_success_dep], - ) - -def submit_code_cell_deploy_with_fresh_funding( - name, - artifact, - always_success_dep, - label_suffix, - measure_dry_run=False, - max_attempts=4, -): - deploy_min_capacity = (len(artifact) + 1_000) * 100_000_000 - last_error = None - for attempt in range(1, max_attempts + 1): - deploy_input = collect_spendable_cellbases(deploy_min_capacity) - deploy_tx = code_cell_deploy_transaction(deploy_input, artifact, always_success_dep) - try: - valid_deploy_dry_run = rpc("dry_run_transaction", [deploy_tx]) if measure_dry_run else None - deploy_result = submit_and_commit(deploy_tx, f"{name} {label_suffix}") - return { - "deploy_input": deploy_input, - "deploy_tx": deploy_tx, - "valid_deploy_dry_run": valid_deploy_dry_run, - "code_cell_deploy": deploy_result, - "deploy_attempts": attempt, - } - except RuntimeError as error: - last_error = error - if is_transient_dead_outpoint_error(error): - continue - raise - raise RuntimeError(f"{name} {label_suffix} failed after {max_attempts} attempts: {last_error}") - -def run_artifact(artifact_record, always_success_dep): - name = artifact_record["name"] - artifact_path = pathlib.Path(artifact_record["artifact"]) - artifact = artifact_path.read_bytes() - artifact_ckb_data_hash = data_hash(artifact) - - result = { - "name": name, - "kind": artifact_record["kind"], - "harness_origin": "handwritten-python-transaction", - "builder_backed": False, - "artifact": str(artifact_path), - "artifact_size_bytes": len(artifact), - "artifact_ckb_data_hash_blake2b": artifact_ckb_data_hash, - "artifact_has_unexpected_profile_trailer": UNEXPECTED_PROFILE_TRAILER in artifact[-64:], - } - if result["artifact_has_unexpected_profile_trailer"]: - raise RuntimeError(f"{name} CKB artifact still contains an unexpected non-CKB ABI trailer") - - deploy = submit_code_cell_deploy_with_fresh_funding(name, artifact, always_success_dep, "code-cell deploy") - deploy_input = deploy["deploy_input"] - deploy_result = deploy["code_cell_deploy"] - deploy_live = assert_live(deploy_result["tx_hash"], 0, f"{name} code cell") - live_data_hash = live_cell_data_hash(deploy_live) - code_dep = {"out_point": out_point(deploy_result["tx_hash"], 0), "dep_type": "code"} - result.update({ - "deploy_input": deploy_input, - "code_cell_deploy": deploy_result, - "code_cell_live": deploy_live.get("status") == "live", - "live_code_cell_data_hash": live_data_hash, - "live_code_cell_data_hash_matches_artifact": live_data_hash == artifact_ckb_data_hash, - "code_cell_dep": code_dep, - "deploy_attempts": deploy["deploy_attempts"], - }) - if not result["live_code_cell_data_hash_matches_artifact"]: - raise RuntimeError( - f"{name} live code cell data hash mismatch: " - f"live={live_data_hash} artifact={artifact_ckb_data_hash}" - ) - - create_input = collect_spendable_cellbases(100 * 100_000_000, max_cells=1) - cellscript_lock = {"code_hash": artifact_ckb_data_hash, "hash_type": "data1", "args": "0x"} - create_tx = transaction( - create_input, - [ - { - "capacity": hex_u64(create_input["total_capacity"]), - "lock": cellscript_lock, - "type": None, - } - ], - ["0x"], - [always_success_dep], - ) - create_result = submit_and_commit(create_tx, f"{name} locked-cell create") - create_live = assert_live(create_result["tx_hash"], 0, f"{name} locked cell") - result.update({ - "create_input": create_input, - "locked_cell_create": create_result, - "locked_cell_live": create_live.get("status") == "live", - }) - - spend_input = {"tx_hash": create_result["tx_hash"], "index": 0, "capacity": create_input["total_capacity"]} - missing_dep_spend_tx = transaction( - spend_input, - [ - { - "capacity": hex_u64(spend_input["capacity"]), - "lock": always_success_lock(), - "type": None, - } - ], - ["0x"], - [], - ) - missing_dep_rejection = expect_dry_run_rejected( - missing_dep_spend_tx, - f"{name} locked-cell spend without code cell dep", - ("Resolve", "resolve", "Script", "script", "CellDep", "cell_dep", "code hash"), - ) - still_live_after_reject = assert_live(create_result["tx_hash"], 0, f"{name} locked cell after malformed spend") - result.update({ - "malformed_spend_without_code_dep": missing_dep_rejection, - "locked_cell_live_after_malformed_spend": still_live_after_reject.get("status") == "live", - }) - - spend_tx = transaction( - spend_input, - [ - { - "capacity": hex_u64(spend_input["capacity"]), - "lock": always_success_lock(), - "type": None, - } - ], - ["0x"], - [code_dep], - ) - valid_spend_dry_run = rpc("dry_run_transaction", [spend_tx]) - spend_result = submit_and_commit(spend_tx, f"{name} locked-cell spend") - spend_live = assert_live(spend_result["tx_hash"], 0, f"{name} spend recipient") - result.update({ - "valid_spend_dry_run": valid_spend_dry_run, - "measured_constraints": measure_release_constraints(spend_tx, valid_spend_dry_run), - "locked_cell_spend": spend_result, - "spend_recipient_live": spend_live.get("status") == "live", - "status": "passed", - }) - return result - -def run_bundled_example_deployment(artifact_record, always_success_dep): - name = artifact_record["name"] - artifact_path = pathlib.Path(artifact_record["artifact"]) - artifact = artifact_path.read_bytes() - artifact_ckb_data_hash = data_hash(artifact) - - result = { - "name": name, - "kind": artifact_record["kind"], - "source": artifact_record["source"], - "artifact": str(artifact_path), - "artifact_size_bytes": len(artifact), - "artifact_ckb_data_hash_blake2b": artifact_ckb_data_hash, - "artifact_has_unexpected_profile_trailer": UNEXPECTED_PROFILE_TRAILER in artifact[-64:], - } - if result["artifact_has_unexpected_profile_trailer"]: - raise RuntimeError(f"{name} CKB artifact still contains an unexpected non-CKB ABI trailer") - - deploy = submit_code_cell_deploy_with_fresh_funding( - name, - artifact, - always_success_dep, - "bundled-example code-cell deploy", - measure_dry_run=True, - ) - deploy_result = deploy["code_cell_deploy"] - deploy_live = assert_live(deploy_result["tx_hash"], 0, f"{name} bundled-example code cell") - live_data_hash = live_cell_data_hash(deploy_live) - result.update({ - "deploy_input": deploy["deploy_input"], - "valid_deploy_dry_run": deploy["valid_deploy_dry_run"], - "measured_constraints": measure_release_constraints(deploy["deploy_tx"], deploy["valid_deploy_dry_run"]), - "code_cell_deploy": deploy_result, - "code_cell_live": deploy_live.get("status") == "live", - "live_code_cell_data_hash": live_data_hash, - "live_code_cell_data_hash_matches_artifact": live_data_hash == artifact_ckb_data_hash, - "code_cell_dep": {"out_point": out_point(deploy_result["tx_hash"], 0), "dep_type": "code"}, - "deploy_attempts": deploy["deploy_attempts"], - "status": "passed", - }) - if not result["live_code_cell_data_hash_matches_artifact"]: - raise RuntimeError( - f"{name} live bundled-example code cell data hash mismatch: " - f"live={live_data_hash} artifact={artifact_ckb_data_hash}" - ) - return result - -def deploy_code_cell(name, artifact_path, always_success_dep): - artifact = pathlib.Path(artifact_path).read_bytes() - artifact_ckb_data_hash = data_hash(artifact) - deploy = submit_code_cell_deploy_with_fresh_funding(name, artifact, always_success_dep, "action code-cell deploy") - deploy_result = deploy["code_cell_deploy"] - deploy_live = assert_live(deploy_result["tx_hash"], 0, f"{name} action code cell") - live_data_hash = live_cell_data_hash(deploy_live) - result = { - "artifact": str(artifact_path), - "artifact_size_bytes": len(artifact), - "artifact_ckb_data_hash_blake2b": artifact_ckb_data_hash, - "deploy_input": deploy["deploy_input"], - "code_cell_deploy": deploy_result, - "code_cell_live": deploy_live.get("status") == "live", - "live_code_cell_data_hash": live_data_hash, - "live_code_cell_data_hash_matches_artifact": live_data_hash == artifact_ckb_data_hash, - "code_cell_dep": {"out_point": out_point(deploy_result["tx_hash"], 0), "dep_type": "code"}, - "deploy_attempts": deploy["deploy_attempts"], - } - if not result["live_code_cell_data_hash_matches_artifact"]: - raise RuntimeError( - f"{name} live action code cell data hash mismatch: " - f"live={live_data_hash} artifact={artifact_ckb_data_hash}" - ) - return result - -def create_script_locked_cells(label, cells, cell_deps, max_attempts=4): - total_capacity = sum(cell["capacity"] for cell in cells) - create_fee_capacity = 10 * 100_000_000 - last_error = None - for attempt in range(1, max_attempts + 1): - funding = collect_spendable_cellbases(total_capacity + create_fee_capacity) - tx = transaction( - funding, - [ - { - "capacity": hex_u64(cell["capacity"]), - "lock": cell["lock"], - "type": cell.get("type"), - } - for cell in cells - ], - ["0x" + cell.get("data", b"").hex() for cell in cells], - cell_deps, - ) - try: - result = submit_and_commit(tx, f"{label} input-cell create") - break - except RuntimeError as error: - last_error = error - if is_transient_dead_outpoint_error(error): - continue - raise - else: - raise RuntimeError(f"{label} input-cell create failed after {max_attempts} attempts: {last_error}") - live = [assert_live(result["tx_hash"], index, f"{label} input cell {index}").get("status") == "live" for index in range(len(cells))] - return { - "create_input": funding, - "create_fee_capacity": create_fee_capacity, - "create_tx": result, - "created_cells_live": live, - "cells": [ - { - "tx_hash": result["tx_hash"], - "index": index, - "capacity": cell["capacity"], - "lock": cell["lock"], - "type": cell.get("type"), - "data_hex": "0x" + cell.get("data", b"").hex(), - } - for index, cell in enumerate(cells) - ], - } - -SCRIPT_REJECTION_FRAGMENTS = ( - "Script", - "script", - "ValidationFailure", - "error code", - "VM", - "Run result", - "Invalid", -) -LOCK_PREDICATE_REJECTION_FRAGMENTS = ( - "TransactionFailedToVerify: Script(", - "source: Inputs[0].Lock", - "ValidationFailure", - "error code 5", -) - -def lock_spend_case_specs(example, lock_name, lock_script): - addr_a = bytes([0x11]) * 32 - addr_b = bytes([0x22]) * 32 - addr_c = bytes([0x33]) * 32 - hash_a = bytes([0x44]) * 32 - hash_b = bytes([0x55]) * 32 - zero_hash = bytes(32) - cell_capacity = 1_000 * 100_000_000 - genesis_header = get_block_by_number(0)["header"]["hash"] - - def cell(data): - return { - "capacity": cell_capacity, - "lock": lock_script, - "type": None, - "data": data, - } - - proposal_valid = multisig_proposal_molecule_data( - hash_a, 1, addr_a, 0, addr_c, 500, b"", [addr_a, addr_b], 2, 10, 2000 - ) - proposal_missing_approval = multisig_proposal_molecule_data( - hash_a, 1, addr_a, 0, addr_c, 500, b"", [addr_a], 2, 10, 2000 - ) - nft_valid = nft_data(1, addr_a, hash_a, addr_b, 250) - time_lock_valid = timelock_data(addr_a, 0, 100, 10, lock_id=hash_a) - lock_seed = bytes([0x66]) * 32 - committed_lock_id = hashlib.blake2b(lock_seed, digest_size=32, person=b"ckb-default-hash").digest() - time_lock_committed = timelock_data(addr_a, 0, 100, 10, lock_id=committed_lock_id) - emergency_valid = emergency_release_molecule_data(hash_a, addr_a, b"operator review", 10, [addr_a, addr_b]) - emergency_insufficient = emergency_release_molecule_data(hash_a, addr_a, b"operator review", 10, [addr_a]) - - cases = { - ("multisig.cell", "is_signer_lock"): { - "valid_cells": [cell(multisig_wallet_molecule_data(hash_a, [addr_a, addr_b], 2, 0, 10))], - "valid_witnesses": [entry_witness(addr_a)], - "invalid_cells": [cell(multisig_wallet_molecule_data(hash_a, [addr_a, addr_b], 2, 0, 10))], - "invalid_witnesses": [entry_witness(addr_c)], - }, - ("multisig.cell", "can_execute"): { - "valid_cells": [cell(proposal_valid)], - "valid_witnesses": [entry_witness(100)], - "invalid_cells": [cell(proposal_valid)], - "invalid_witnesses": [entry_witness(2500)], - }, - ("multisig.cell", "can_cancel"): { - "valid_cells": [cell(proposal_valid)], - "valid_witnesses": [entry_witness(addr_a)], - "invalid_cells": [cell(proposal_valid)], - "invalid_witnesses": [entry_witness(addr_b)], - }, - ("multisig.cell", "has_enough_approvals"): { - "valid_cells": [cell(proposal_valid)], - "valid_witnesses": [entry_witness()], - "invalid_cells": [cell(proposal_missing_approval)], - "invalid_witnesses": [entry_witness()], - }, - ("multisig.cell", "not_expired"): { - "valid_cells": [cell(proposal_valid)], - "valid_witnesses": [entry_witness(100)], - "invalid_cells": [cell(proposal_valid)], - "invalid_witnesses": [entry_witness(2500)], - }, - ("nft.cell", "nft_ownership"): { - "valid_cells": [cell(nft_valid)], - "valid_witnesses": [entry_witness(addr_a)], - "invalid_cells": [cell(nft_valid)], - "invalid_witnesses": [entry_witness(addr_c)], - }, - ("nft.cell", "listing_seller"): { - "valid_cells": [cell(listing_data(1, addr_a, 500, 10, state=0))], - "valid_witnesses": [entry_witness(addr_a)], - "invalid_cells": [cell(listing_data(1, addr_a, 500, 10, state=0))], - "invalid_witnesses": [entry_witness(addr_c)], - }, - ("nft.cell", "offer_buyer"): { - "valid_cells": [cell(offer_data(1, addr_b, 500, 2000, state=0))], - "valid_witnesses": [entry_witness(addr_b)], - "invalid_cells": [cell(offer_data(1, addr_b, 500, 2000, state=0))], - "invalid_witnesses": [entry_witness(addr_c)], - }, - ("nft.cell", "valid_royalty"): { - "valid_cells": [cell(nft_valid)], - "valid_witnesses": [entry_witness()], - "invalid_cells": [cell(nft_data(1, addr_a, hash_a, addr_b, 1001))], - "invalid_witnesses": [entry_witness()], - }, - ("nft.cell", "collection_creator"): { - "valid_cells": [cell(collection_molecule_data(addr_a, 1, 1000))], - "valid_witnesses": [entry_witness(addr_a)], - "invalid_cells": [cell(collection_molecule_data(addr_a, 1, 1000))], - "invalid_witnesses": [entry_witness(addr_c)], - }, - ("timelock.cell", "can_unlock_lock"): { - "valid_cells": [cell(timelock_data(addr_a, 0, 0, 0, lock_id=hash_a))], - "valid_witnesses": [entry_witness()], - "valid_header_deps": [genesis_header], - "invalid_cells": [cell(timelock_data(addr_a, 0, 1, 0, lock_id=hash_a))], - "invalid_witnesses": [entry_witness()], - "invalid_header_deps": [genesis_header], - }, - ("timelock.cell", "is_owner"): { - "valid_cells": [cell(time_lock_valid)], - "valid_witnesses": [entry_witness(addr_a)], - "invalid_cells": [cell(time_lock_valid)], - "invalid_witnesses": [entry_witness(addr_c)], - }, - ("timelock.cell", "lock_id_commitment"): { - "valid_cells": [cell(time_lock_committed)], - "valid_witnesses": [entry_witness(lock_seed)], - "invalid_cells": [cell(time_lock_committed)], - "invalid_witnesses": [entry_witness(hash_b)], - }, - ("timelock.cell", "asset_matches"): { - "valid_cells": [cell(locked_asset_data(b"TOKEN001", 100, hash_a))], - "valid_read_deps": [cell(time_lock_valid)], - "valid_witnesses": [entry_witness(), "0x"], - "invalid_cells": [cell(locked_asset_data(b"TOKEN001", 100, hash_b))], - "invalid_read_deps": [cell(time_lock_valid)], - "invalid_witnesses": [entry_witness(), "0x"], - }, - ("timelock.cell", "not_expired"): { - "valid_cells": [cell(timelock_data(addr_a, 0, 1, 0, lock_id=hash_a))], - "valid_witnesses": [entry_witness()], - "valid_header_deps": [genesis_header], - "invalid_cells": [cell(timelock_data(addr_a, 0, 0, 0, lock_id=hash_a))], - "invalid_witnesses": [entry_witness()], - "invalid_header_deps": [genesis_header], - }, - ("timelock.cell", "emergency_approved"): { - "valid_cells": [cell(emergency_valid)], - "valid_witnesses": [entry_witness()], - "invalid_cells": [cell(emergency_insufficient)], - "invalid_witnesses": [entry_witness()], - }, - ("vesting.cell", "vesting_admin"): { - "valid_cells": [cell(vesting_config_data(addr_a, b"VEST0001", 10, 100, True))], - "valid_witnesses": [entry_witness(addr_a)], - "invalid_cells": [cell(vesting_config_data(addr_a, b"VEST0001", 10, 100, True))], - "invalid_witnesses": [entry_witness(addr_c)], - }, - } - try: - return cases[(example, lock_name)] - except KeyError as exc: - raise RuntimeError(f"missing lock spend matrix case for {example}:{lock_name}") from exc - -def run_lock_spend_case(label, cells, witnesses, cell_deps, commit_valid, read_deps=None, header_deps=None): - read_deps = read_deps or [] - initial = create_script_locked_cells(label, cells + read_deps, cell_deps) - input_cells = initial["cells"][:len(cells)] - dep_cells = initial["cells"][len(cells):] - action_cell_deps = [cell_dep_for(cell) for cell in dep_cells] + cell_deps - total_capacity = sum(cell["capacity"] for cell in input_cells) - tx = transaction( - input_cells, - [ - { - "capacity": hex_u64(total_capacity), - "lock": always_success_lock(), - "type": None, - } - ], - ["0x"], - action_cell_deps, - witnesses, - header_deps, - ) - if not commit_valid: - rejection = expect_dry_run_rejected(tx, f"{label} invalid lock spend", LOCK_PREDICATE_REJECTION_FRAGMENTS) - live_after_reject = [ - assert_live(cell["tx_hash"], cell["index"], f"{label} invalid input {index} after rejection").get("status") == "live" - for index, cell in enumerate(initial["cells"]) - ] - return { - "input_create": initial, - "tx": tx, - "rejection": rejection, - "input_cells_live_after_rejection": live_after_reject, - "status": "rejected", - } - - valid_dry_run = rpc("dry_run_transaction", [tx]) - commit = submit_and_commit(tx, f"{label} valid lock spend") - output_live = assert_live(commit["tx_hash"], 0, f"{label} valid spend output").get("status") == "live" - return { - "input_create": initial, - "tx": tx, - "dry_run": valid_dry_run, - "commit": commit, - "output_live": output_live, - "measured_constraints": measure_release_constraints(tx, valid_dry_run), - "status": "passed", - } - -def run_lock_spend_matrix(lock_record, always_success_dep): - example = lock_record["example"] - lock_name = lock_record["lock"] - name = lock_record["name"] - code = deploy_code_cell(name, lock_record["artifact"], always_success_dep) - lock_script = { - "code_hash": code["artifact_ckb_data_hash_blake2b"], - "hash_type": "data1", - "args": "0x", - } - cell_deps = [always_success_dep, code["code_cell_dep"]] - specs = lock_spend_case_specs(example, lock_name, lock_script) - invalid_spend = run_lock_spend_case( - f"{name} invalid-spend", - specs["invalid_cells"], - specs["invalid_witnesses"], - cell_deps, - False, - specs.get("invalid_read_deps"), - specs.get("invalid_header_deps"), - ) - valid_spend = run_lock_spend_case( - f"{name} valid-spend", - specs["valid_cells"], - specs["valid_witnesses"], - cell_deps, - True, - specs.get("valid_read_deps"), - specs.get("valid_header_deps"), - ) - return { - "name": name, - "example": example, - "lock": lock_name, - "kind": lock_record["kind"], - "harness_origin": "builder-backed-local-ckb-lock-spend-matrix", - "builder_backed": True, - "builder_name": "cellscript-lock-spend-matrix-builder-v1", - "source": lock_record["source"], - "artifact": lock_record["artifact"], - "code": code, - "valid_spend": valid_spend, - "invalid_spend": invalid_spend, - "measured_constraints": valid_spend["measured_constraints"], - "status": "passed", - } - -def build_token_action_case(action, cellscript_lock, cellscript_type, destination_lock, destination_lock_hash, token_symbol, cell_deps): - def normalized_outputs(outputs): - return [ - { - "capacity": hex_u64(output["capacity"]), - "lock": output["lock"], - "type": output.get("type"), - } - for output in outputs - ] - - if action == "mint_with_authority": - initial_specs = [ - { - "capacity": 1000 * 100_000_000, - "lock": cellscript_lock, - "type": cellscript_type, - "data": mint_authority_data(token_symbol, 1000, 10), - } - ] - valid_outputs = [ - {"capacity": 200 * 100_000_000, "lock": cellscript_lock, "type": cellscript_type}, - {"capacity": 100 * 100_000_000, "lock": destination_lock, "type": cellscript_type}, - ] - valid_outputs_data = [ - "0x" + mint_authority_data(token_symbol, 1000, 15).hex(), - "0x" + token_data(5, token_symbol).hex(), - ] - malformed_outputs = valid_outputs - malformed_outputs_data = [ - "0x" + mint_authority_data(token_symbol, 1000, 15).hex(), - "0x" + token_data(6, token_symbol).hex(), - ] - witnesses = [entry_witness(destination_lock_hash, 5)] - elif action == "transfer_token": - initial_specs = [ - { - "capacity": 200 * 100_000_000, - "lock": cellscript_lock, - "type": cellscript_type, - "data": token_data(42, token_symbol), - } - ] - valid_outputs = [{"capacity": 200 * 100_000_000, "lock": destination_lock, "type": cellscript_type}] - valid_outputs_data = ["0x" + token_data(42, token_symbol).hex()] - malformed_outputs = valid_outputs - malformed_outputs_data = ["0x" + token_data(41, token_symbol).hex()] - witnesses = [entry_witness(destination_lock_hash)] - elif action == "burn": - initial_specs = [ - { - "capacity": 100 * 100_000_000, - "lock": cellscript_lock, - "type": cellscript_type, - "data": token_data(7, token_symbol), - } - ] - valid_outputs = [{"capacity": 100 * 100_000_000, "lock": cellscript_lock, "type": None}] - valid_outputs_data = ["0x"] - malformed_outputs = [{"capacity": 100 * 100_000_000, "lock": cellscript_lock, "type": cellscript_type}] - malformed_outputs_data = ["0x" + token_data(7, token_symbol).hex()] - witnesses = [entry_witness()] - elif action == "merge": - initial_specs = [ - { - "capacity": 300 * 100_000_000, - "lock": cellscript_lock, - "type": cellscript_type, - "data": token_data(40, token_symbol), - }, - { - "capacity": 150 * 100_000_000, - "lock": cellscript_lock, - "type": cellscript_type, - "data": token_data(2, token_symbol), - }, - ] - valid_outputs = [{"capacity": 300 * 100_000_000, "lock": destination_lock, "type": cellscript_type}] - valid_outputs_data = ["0x" + token_data(42, token_symbol).hex()] - malformed_outputs = valid_outputs - malformed_outputs_data = ["0x" + token_data(41, token_symbol).hex()] - witnesses = [entry_witness(destination_lock_hash), "0x"] - else: - raise RuntimeError(f"unsupported token action harness: {action}") - - initial = create_script_locked_cells(f"token.{action}", initial_specs, cell_deps) - inputs = initial["cells"] if action == "merge" else initial["cells"][0] - return { - "builder_name": "token-action-builder-v1", - "initial": initial, - "valid_tx": transaction( - inputs, - normalized_outputs(valid_outputs), - valid_outputs_data, - cell_deps, - witnesses, - ), - "malformed_tx": transaction( - inputs, - normalized_outputs(malformed_outputs), - malformed_outputs_data, - cell_deps, - witnesses, - ), - } - -def run_token_action(action_record, always_success_dep): - action = action_record["action"] - name = action_record["name"] - code = deploy_code_cell(name, action_record["artifact"], always_success_dep) - cellscript_lock = {"code_hash": code["artifact_ckb_data_hash_blake2b"], "hash_type": "data1", "args": "0x"} - cellscript_type = always_success_lock() - destination_lock = always_success_lock() - destination_lock_hash = decode_hex(script_hash(destination_lock), 32) - token_symbol = b"TOKEN001" - cell_deps = [always_success_dep, code["code_cell_dep"]] - - result = { - "action": action, - "name": name, - "harness_origin": "token-action-builder-v1", - "builder_backed": True, - "artifact": action_record["artifact"], - "code": code, - "cellscript_lock_hash": script_hash(cellscript_lock), - "destination_lock_hash": "0x" + destination_lock_hash.hex(), - } - token_case = build_token_action_case( - action, - cellscript_lock, - cellscript_type, - destination_lock, - destination_lock_hash, - token_symbol, - cell_deps, - ) - initial = token_case["initial"] - valid_tx = token_case["valid_tx"] - malformed_tx = token_case["malformed_tx"] - result["builder_name"] = token_case["builder_name"] - - malformed_rejection = expect_dry_run_rejected( - malformed_tx, - f"{name} malformed action transaction", - ("Script", "script", "ValidationFailure", "error code", "VM", "Run result", "Invalid"), - ) - for index, cell in enumerate(initial["cells"]): - assert_live(cell["tx_hash"], cell["index"], f"{name} input cell {index} after malformed transaction") - - valid_dry_run = rpc("dry_run_transaction", [valid_tx]) - commit = submit_and_commit(valid_tx, f"{name} valid action transaction") - output_live = [assert_live(commit["tx_hash"], index, f"{name} valid output {index}").get("status") == "live" for index in range(len(valid_tx["outputs"]))] - result.update({ - "initial_cells": initial, - "malformed_transaction": malformed_rejection, - "valid_dry_run": valid_dry_run, - "measured_constraints": measure_release_constraints(valid_tx, valid_dry_run), - "valid_commit": commit, - "valid_outputs_live": output_live, - "status": "passed", - }) - return result - -def action_runtime_input_bindings(action_record): - metadata = json.loads(pathlib.Path(action_record["metadata"]).read_text(encoding="utf-8")) - indexed_bindings = [] - for access in (metadata.get("runtime") or {}).get("ckb_runtime_accesses", []): - if access.get("source") == "Input": - indexed_bindings.append((int(access["index"]), access["binding"])) - indexed_bindings.sort() - indexes = [index for index, _ in indexed_bindings] - if indexes != list(range(len(indexes))): - raise RuntimeError( - f"{action_record['name']} metadata has non-contiguous CKB input bindings: {indexed_bindings}" - ) - return [binding for _, binding in indexed_bindings] - -def build_nft_action_case(action_record, cellscript_lock, cellscript_type, destination_lock, current_owner, destination_owner, metadata_hash, royalty_recipient, nft_type, listing_type, offer_type, royalty_payment_type, cell_deps): - action = action_record["action"] - original_scoped = action_record.get("kind") == "original-scoped-action-strict" - flow_state = 0 if original_scoped else None - input_bindings = None - - if action == "create_collection": - name = b"Acceptance Collection" - symbol = b"ACPT" - base_uri = b"ckb://cellscript/nft/" - max_supply = 200 - valid_collection_payload = ( - collection_molecule_data(current_owner, 0, max_supply, name, symbol, base_uri) - if original_scoped - else collection_data(current_owner, 0, max_supply) - ) - malformed_collection_payload = ( - collection_molecule_data(current_owner, 1, max_supply, name, symbol, base_uri) - if original_scoped - else collection_data(current_owner, 1, max_supply) - ) - witness = ( - entry_witness(current_owner, max_supply, molecule_string_witness(name), molecule_string_witness(symbol), molecule_string_witness(base_uri)) - if original_scoped - else entry_witness(current_owner, max_supply) - ) - initial = create_script_locked_cells( - "nft.create_collection", - [{"capacity": 1000 * 100_000_000, "lock": cellscript_lock, "type": None, "data": b""}], - cell_deps, - ) - input_cell = initial["cells"][0] - outputs = [{"capacity": hex_u64(1000 * 100_000_000), "lock": cellscript_lock, "type": cellscript_type}] - valid_tx = transaction(input_cell, outputs, ["0x" + valid_collection_payload.hex()], cell_deps, [witness]) - malformed_tx = transaction(input_cell, outputs, ["0x" + malformed_collection_payload.hex()], cell_deps, [witness]) - elif action == "mint": - collection_id = decode_hex(script_hash(cellscript_type), 32) - input_collection_payload = ( - collection_molecule_data(current_owner, 10, 1000) - if original_scoped - else collection_data(current_owner, 10, 1000) - ) - output_collection_payload = ( - collection_molecule_data(current_owner, 11, 1000) - if original_scoped - else collection_data(current_owner, 11, 1000) - ) - initial = create_script_locked_cells( - "nft.mint", - [{"capacity": 1000 * 100_000_000, "lock": cellscript_lock, "type": cellscript_type, "data": input_collection_payload}], - cell_deps, - ) - input_cell = initial["cells"][0] - outputs = [ - {"capacity": hex_u64(300 * 100_000_000), "lock": cellscript_lock, "type": cellscript_type}, - {"capacity": hex_u64(300 * 100_000_000), "lock": destination_lock, "type": cellscript_type}, - ] - witness = [entry_witness(destination_owner, metadata_hash)] - valid_tx = transaction( - input_cell, - outputs, - [ - "0x" + output_collection_payload.hex(), - "0x" + nft_data(11, destination_owner, metadata_hash, current_owner, 250, collection_id).hex(), - ], - cell_deps, - witness, - ) - malformed_tx = transaction( - input_cell, - outputs, - [ - "0x" + output_collection_payload.hex(), - "0x" + nft_data(12, destination_owner, metadata_hash, current_owner, 250, collection_id).hex(), - ], - cell_deps, - witness, - ) - elif action == "transfer": - initial = create_script_locked_cells( - "nft.transfer", - [{"capacity": 1000 * 100_000_000, "lock": cellscript_lock, "type": cellscript_type, "data": nft_data(1, current_owner, metadata_hash, royalty_recipient, 250)}], - cell_deps, - ) - input_cell = initial["cells"][0] - outputs = [{"capacity": hex_u64(1000 * 100_000_000), "lock": cellscript_lock, "type": cellscript_type}] - witness = [entry_witness(destination_owner)] - valid_tx = transaction(input_cell, outputs, ["0x" + nft_data(1, destination_owner, metadata_hash, royalty_recipient, 250).hex()], cell_deps, witness) - malformed_tx = transaction(input_cell, outputs, ["0x" + nft_data(1, current_owner, metadata_hash, royalty_recipient, 250).hex()], cell_deps, witness) - elif action == "create_listing": - price = 100 - current_time = 0 - header_dep = get_block_by_number(0)["header"]["hash"] - token_id = 3 - nft_payload = nft_data(token_id, current_owner, metadata_hash, royalty_recipient, 250) - initial = create_script_locked_cells( - "nft.create_listing", - [ - {"capacity": 1000 * 100_000_000, "lock": cellscript_lock, "type": None, "data": b""}, - {"capacity": 300 * 100_000_000, "lock": always_success_lock(), "type": nft_type, "data": nft_payload}, - ], - cell_deps, - ) - input_cell = initial["cells"][0] - action_cell_deps = [cell_dep_for(initial["cells"][1])] + cell_deps - outputs = [ - {"capacity": hex_u64(300 * 100_000_000), "lock": cellscript_lock, "type": listing_type}, - {"capacity": hex_u64(700 * 100_000_000), "lock": always_success_lock(), "type": None}, - ] - witness = [entry_witness(price)] - valid_tx = transaction(input_cell, outputs, ["0x" + listing_data(token_id, current_owner, price, current_time, state=flow_state).hex(), "0x"], action_cell_deps, witness, [header_dep]) - malformed_tx = transaction(input_cell, outputs, ["0x" + listing_data(token_id, current_owner, price + 1, current_time, state=flow_state).hex(), "0x"], action_cell_deps, witness, [header_dep]) - elif action == "cancel_listing": - token_id = 4 - price = 120 - created_at = 60 - initial = create_script_locked_cells( - "nft.cancel_listing", - [{"capacity": 300 * 100_000_000, "lock": cellscript_lock, "type": listing_type, "data": listing_data(token_id, current_owner, price, created_at, state=flow_state)}], - cell_deps, - ) - input_cell = initial["cells"][0] - outputs = [{"capacity": hex_u64(300 * 100_000_000), "lock": cellscript_lock, "type": None}] - witness = [entry_witness()] - valid_tx = transaction(input_cell, outputs, ["0x"], cell_deps, witness) - malformed_tx = transaction(input_cell, [{"capacity": hex_u64(300 * 100_000_000), "lock": cellscript_lock, "type": listing_type}], ["0x" + listing_data(token_id, current_owner, price, created_at, state=flow_state).hex()], cell_deps, witness) - elif action == "buy_from_listing": - token_id = 6 - price = 10_000 - royalty_amount = 250 - seller_amount = price - royalty_amount - payment_symbol = b"PAYM0001" - created_at = 70 - nft_payload = nft_data(token_id, current_owner, metadata_hash, royalty_recipient, 250) - nft_input = {"capacity": 1000 * 100_000_000, "lock": cellscript_lock, "type": nft_type, "data": nft_payload} - listing_input = {"capacity": 500 * 100_000_000, "lock": cellscript_lock, "type": listing_type, "data": listing_data(token_id, current_owner, price, created_at, state=flow_state)} - royalty_input = {"capacity": 200 * 100_000_000, "lock": cellscript_lock, "type": royalty_payment_type, "data": token_data(royalty_amount, payment_symbol)} - seller_input = {"capacity": 200 * 100_000_000, "lock": cellscript_lock, "type": royalty_payment_type, "data": token_data(seller_amount, payment_symbol)} - input_specs = ( - [nft_input, royalty_input, seller_input, listing_input] - if original_scoped - else [nft_input, listing_input, royalty_input, seller_input] - ) - input_bindings = ( - ["nft_before", "royalty_payment", "seller_payment", "listing"] - if original_scoped - else ["nft_before", "listing", "royalty_payment", "seller_payment"] - ) - initial = create_script_locked_cells("nft.buy_from_listing", input_specs, cell_deps) - outputs = [ - {"capacity": hex_u64(1000 * 100_000_000), "lock": cellscript_lock, "type": nft_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": destination_lock, "type": royalty_payment_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": cellscript_lock, "type": royalty_payment_type}, - ] - witness = [entry_witness(destination_owner), "0x", "0x", "0x"] - valid_tx = transaction(initial["cells"], outputs, [ - "0x" + nft_data(token_id, destination_owner, metadata_hash, royalty_recipient, 250).hex(), - "0x" + token_data(royalty_amount, payment_symbol).hex(), - "0x" + token_data(seller_amount, payment_symbol).hex(), - ], cell_deps, witness) - malformed_tx = transaction(initial["cells"], outputs, [ - "0x" + nft_data(token_id, destination_owner, metadata_hash, royalty_recipient, 250).hex(), - "0x" + token_data(royalty_amount, payment_symbol).hex(), - "0x" + token_data(seller_amount + 1, payment_symbol).hex(), - ], cell_deps, witness) - elif action == "create_offer": - collection_id = bytes(32) - token_id = 5 - price = 150 - payment_symbol = b"PAYM0001" - expires_at = 200 - header_dep = get_block_by_number(0)["header"]["hash"] - initial = create_script_locked_cells( - "nft.create_offer", - [{"capacity": 1000 * 100_000_000, "lock": cellscript_lock, "type": None, "data": b""}], - cell_deps, - ) - input_cell = initial["cells"][0] - outputs = [{"capacity": hex_u64(300 * 100_000_000), "lock": destination_lock, "type": offer_type}] - witness = [entry_witness(collection_id, token_id, destination_owner, price, payment_symbol, expires_at)] - valid_tx = transaction(input_cell, outputs, ["0x" + offer_data(token_id, destination_owner, price, expires_at, state=flow_state, collection_id=collection_id, payment_symbol=payment_symbol).hex()], cell_deps, witness, [header_dep]) - malformed_tx = transaction(input_cell, outputs, ["0x" + offer_data(token_id, destination_owner, price + 1, expires_at, state=flow_state, collection_id=collection_id, payment_symbol=payment_symbol).hex()], cell_deps, witness, [header_dep]) - elif action == "accept_offer": - token_id = 7 - price = 10_000 - royalty_amount = 250 - seller_amount = price - royalty_amount - payment_symbol = b"PAYM0001" - expires_at = 200 - header_dep = get_block_by_number(0)["header"]["hash"] - nft_payload = nft_data(token_id, current_owner, metadata_hash, royalty_recipient, 250) - nft_input = {"capacity": 1000 * 100_000_000, "lock": cellscript_lock, "type": nft_type, "data": nft_payload} - offer_input = {"capacity": 500 * 100_000_000, "lock": cellscript_lock, "type": offer_type, "data": offer_data(token_id, destination_owner, price, expires_at, state=flow_state, payment_symbol=payment_symbol)} - royalty_input = {"capacity": 200 * 100_000_000, "lock": cellscript_lock, "type": royalty_payment_type, "data": token_data(royalty_amount, payment_symbol)} - seller_input = {"capacity": 200 * 100_000_000, "lock": cellscript_lock, "type": royalty_payment_type, "data": token_data(seller_amount, payment_symbol)} - input_specs = ( - [nft_input, royalty_input, seller_input, offer_input] - if original_scoped - else [nft_input, offer_input, royalty_input, seller_input] - ) - input_bindings = ( - ["nft_before", "royalty_payment", "seller_payment", "offer"] - if original_scoped - else ["nft_before", "offer", "royalty_payment", "seller_payment"] - ) - initial = create_script_locked_cells("nft.accept_offer", input_specs, cell_deps) - outputs = [ - {"capacity": hex_u64(1000 * 100_000_000), "lock": cellscript_lock, "type": nft_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": destination_lock, "type": royalty_payment_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": cellscript_lock, "type": royalty_payment_type}, - ] - witness = [entry_witness(), "0x", "0x", "0x"] - valid_tx = transaction(initial["cells"], outputs, [ - "0x" + nft_data(token_id, destination_owner, metadata_hash, royalty_recipient, 250).hex(), - "0x" + token_data(royalty_amount, payment_symbol).hex(), - "0x" + token_data(seller_amount, payment_symbol).hex(), - ], cell_deps, witness, [header_dep]) - malformed_tx = transaction(initial["cells"], outputs, [ - "0x" + nft_data(token_id, destination_owner, metadata_hash, royalty_recipient, 250).hex(), - "0x" + token_data(royalty_amount, payment_symbol).hex(), - "0x" + token_data(seller_amount + 1, payment_symbol).hex(), - ], cell_deps, witness, [header_dep]) - elif action == "burn": - initial = create_script_locked_cells( - "nft.burn", - [{"capacity": 1000 * 100_000_000, "lock": cellscript_lock, "type": cellscript_type, "data": nft_data(2, current_owner, metadata_hash, royalty_recipient, 250)}], - cell_deps, - ) - input_cell = initial["cells"][0] - witness = [entry_witness()] - valid_tx = transaction(input_cell, [{"capacity": hex_u64(1000 * 100_000_000), "lock": cellscript_lock, "type": None}], ["0x"], cell_deps, witness) - malformed_tx = transaction(input_cell, [{"capacity": hex_u64(1000 * 100_000_000), "lock": cellscript_lock, "type": cellscript_type}], ["0x" + nft_data(2, current_owner, metadata_hash, royalty_recipient, 250).hex()], cell_deps, witness) - elif action == "batch_mint": - collection_type = always_success_lock("0x25") - collection_id = decode_hex(script_hash(collection_type), 32) - recipients = [destination_owner, bytes([0x31]) * 32, bytes([0x32]) * 32, bytes([0x33]) * 32] - metadata_hashes = [bytes(range(32)), bytes([0x41]) * 32, bytes([0x42]) * 32, bytes([0x43]) * 32] - input_collection_payload = collection_molecule_data(current_owner, 20, 1000) - output_collection_payload = collection_molecule_data(current_owner, 24, 1000) - initial = create_script_locked_cells( - "nft.batch_mint", - [{"capacity": 2500 * 100_000_000, "lock": cellscript_lock, "type": collection_type, "data": input_collection_payload}], - cell_deps, - ) - input_cell = initial["cells"][0] - outputs = [ - {"capacity": hex_u64(1000 * 100_000_000), "lock": cellscript_lock, "type": collection_type}, - {"capacity": hex_u64(250 * 100_000_000), "lock": cellscript_lock, "type": nft_type}, - {"capacity": hex_u64(250 * 100_000_000), "lock": cellscript_lock, "type": nft_type}, - {"capacity": hex_u64(250 * 100_000_000), "lock": cellscript_lock, "type": nft_type}, - {"capacity": hex_u64(250 * 100_000_000), "lock": cellscript_lock, "type": nft_type}, - ] - outputs_data = [ - "0x" + output_collection_payload.hex(), - "0x" + nft_data(21, recipients[0], metadata_hashes[0], current_owner, 250, collection_id).hex(), - "0x" + nft_data(22, recipients[1], metadata_hashes[1], current_owner, 250, collection_id).hex(), - "0x" + nft_data(23, recipients[2], metadata_hashes[2], current_owner, 250, collection_id).hex(), - "0x" + nft_data(24, recipients[3], metadata_hashes[3], current_owner, 250, collection_id).hex(), - ] - witness = [entry_witness(fixed_address_array4(recipients), fixed_hash_array4(metadata_hashes))] - valid_tx = transaction(input_cell, outputs, outputs_data, cell_deps, witness) - malformed_outputs_data = list(outputs_data) - malformed_outputs_data[3] = "0x" + nft_data(99, recipients[2], metadata_hashes[2], current_owner, 250, collection_id).hex() - malformed_tx = transaction(input_cell, outputs, malformed_outputs_data, cell_deps, witness) - else: - raise RuntimeError(f"unsupported NFT action harness: {action}") - - return { - "builder_name": "nft-action-builder-v1", - "initial": initial, - "input_bindings": input_bindings, - "valid_tx": valid_tx, - "malformed_tx": malformed_tx, - } - -def run_nft_action(action_record, always_success_dep): - action = action_record["action"] - name = action_record["name"] - code = deploy_code_cell(name, action_record["artifact"], always_success_dep) - cellscript_lock = {"code_hash": code["artifact_ckb_data_hash_blake2b"], "hash_type": "data1", "args": "0x"} - cellscript_type = always_success_lock() - destination_lock = always_success_lock() - current_owner = decode_hex(script_hash(cellscript_lock), 32) - destination_owner = decode_hex(script_hash(destination_lock), 32) - metadata_hash = bytes(range(32)) - royalty_recipient = destination_owner - nft_type = always_success_lock("0x21") - listing_type = always_success_lock("0x22") - offer_type = always_success_lock("0x23") - royalty_payment_type = always_success_lock("0x24") - cell_deps = [always_success_dep, code["code_cell_dep"]] - - result = { - "action": action, - "name": name, - "harness_origin": "nft-action-builder-v1", - "builder_backed": True, - "artifact": action_record["artifact"], - "code": code, - "cellscript_lock_hash": script_hash(cellscript_lock), - "destination_owner": "0x" + destination_owner.hex(), - } - nft_case = build_nft_action_case( - action_record, - cellscript_lock, - cellscript_type, - destination_lock, - current_owner, - destination_owner, - metadata_hash, - royalty_recipient, - nft_type, - listing_type, - offer_type, - royalty_payment_type, - cell_deps, - ) - initial = nft_case["initial"] - valid_tx = nft_case["valid_tx"] - malformed_tx = nft_case["malformed_tx"] - actual_input_bindings = nft_case["input_bindings"] - result["builder_name"] = nft_case["builder_name"] - if actual_input_bindings is not None: - expected_input_bindings = action_runtime_input_bindings(action_record) - if actual_input_bindings[:len(expected_input_bindings)] != expected_input_bindings: - raise RuntimeError( - f"{name} builder input bindings do not match compiler metadata: " - f"builder={actual_input_bindings} metadata={expected_input_bindings}" - ) - result["builder_input_bindings"] = actual_input_bindings - result["metadata_input_bindings"] = expected_input_bindings - - malformed_rejection = expect_dry_run_rejected( - malformed_tx, - f"{name} malformed action transaction", - ("Script", "script", "ValidationFailure", "error code", "VM", "Run result", "Invalid"), - ) - for index, cell in enumerate(initial["cells"]): - assert_live(cell["tx_hash"], cell["index"], f"{name} input cell {index} after malformed transaction") - - valid_dry_run = rpc("dry_run_transaction", [valid_tx]) - commit = submit_and_commit(valid_tx, f"{name} valid action transaction") - output_live = [ - assert_live(commit["tx_hash"], index, f"{name} valid output {index}").get("status") == "live" - for index in range(len(valid_tx["outputs"])) - ] - result.update({ - "initial_cells": initial, - "malformed_transaction": malformed_rejection, - "valid_dry_run": valid_dry_run, - "measured_constraints": measure_release_constraints(valid_tx, valid_dry_run), - "valid_commit": commit, - "valid_outputs_live": output_live, - "status": "passed", - }) - return result - -def run_amm_action(action_record, always_success_dep): - action = action_record["action"] - name = action_record["name"] - code = deploy_code_cell(name, action_record["artifact"], always_success_dep) - cellscript_lock = {"code_hash": code["artifact_ckb_data_hash_blake2b"], "hash_type": "data1", "args": "0x"} - destination_lock = always_success_lock() - cell_deps = [always_success_dep, code["code_cell_dep"]] - - result = { - "action": action, - "name": name, - "harness_origin": "amm-action-builder-v1", - "builder_backed": True, - "artifact": action_record["artifact"], - "code": code, - "cellscript_lock_hash": script_hash(cellscript_lock), - } - amm_case = build_amm_action_case(action_record, cellscript_lock, destination_lock, cell_deps) - initial = amm_case["initial"] - input_cells_to_check = amm_case["input_cells_to_check"] - valid_tx = amm_case["valid_tx"] - malformed_tx = amm_case["malformed_tx"] - result["builder_name"] = amm_case["builder_name"] - malformed_rejection = expect_dry_run_rejected( - malformed_tx, - f"{name} malformed action transaction", - ("Script", "script", "ValidationFailure", "error code", "VM", "Run result", "Invalid"), - ) - for index, input_cell in enumerate(input_cells_to_check): - assert_live(input_cell["tx_hash"], input_cell["index"], f"{name} input cell {index} after malformed transaction") - - valid_dry_run = rpc("dry_run_transaction", [valid_tx]) - commit = submit_and_commit(valid_tx, f"{name} valid action transaction") - output_live = [ - assert_live(commit["tx_hash"], index, f"{name} valid output {index}").get("status") == "live" - for index in range(len(valid_tx["outputs"])) - ] - result.update({ - "initial_cells": initial, - "malformed_transaction": malformed_rejection, - "valid_dry_run": valid_dry_run, - "measured_constraints": measure_release_constraints(valid_tx, valid_dry_run), - "valid_commit": commit, - "valid_outputs_live": output_live, - "status": "passed", - }) - return result - -def build_amm_action_case(action_record, cellscript_lock, destination_lock, cell_deps): - action = action_record["action"] - - if action == "seed_pool": - token_a_symbol = b"AMMA0001" - token_b_symbol = b"AMMB0001" - token_a_amount = 4 - token_b_amount = 9 - fee_rate_bps = 30 - initial_lp = 6 - provider_lock = always_success_lock("0x61") - provider = decode_hex(script_hash(provider_lock), 32) - token_a_type = always_success_lock("0x62") - token_b_type = always_success_lock("0x63") - token_a_type_hash = decode_hex(script_hash(token_a_type), 32) - token_b_type_hash = decode_hex(script_hash(token_b_type), 32) - pool_type = always_success_lock("0x64") - lp_type = always_success_lock("0x65") - pool_id = decode_hex(script_hash(pool_type), 32) - initial = create_script_locked_cells("amm.seed_pool", [ - {"capacity": 200 * 100_000_000, "lock": cellscript_lock, "type": token_a_type, "data": token_data(token_a_amount, token_a_symbol)}, - {"capacity": 200 * 100_000_000, "lock": cellscript_lock, "type": token_b_type, "data": token_data(token_b_amount, token_b_symbol)}, - ], cell_deps) - valid_tx = transaction(initial["cells"], [ - {"capacity": hex_u64(200 * 100_000_000), "lock": destination_lock, "type": pool_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": provider_lock, "type": lp_type}, - ], [ - "0x" + pool_data(token_a_symbol, token_b_symbol, token_a_amount, token_b_amount, initial_lp, fee_rate_bps, token_a_type_hash, token_b_type_hash).hex(), - "0x" + lp_receipt_data(pool_id, initial_lp, provider).hex(), - ], cell_deps, [entry_witness(fee_rate_bps.to_bytes(2, "little"), provider), "0x"]) - malformed_tx = transaction(initial["cells"], [ - {"capacity": hex_u64(200 * 100_000_000), "lock": destination_lock, "type": pool_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": provider_lock, "type": lp_type}, - ], [ - "0x" + pool_data(token_a_symbol, token_b_symbol, token_a_amount + 1, token_b_amount, initial_lp, fee_rate_bps, token_a_type_hash, token_b_type_hash).hex(), - "0x" + lp_receipt_data(pool_id, initial_lp, provider).hex(), - ], cell_deps, [entry_witness(fee_rate_bps.to_bytes(2, "little"), provider), "0x"]) - input_cells_to_check = initial["cells"] - elif action == "swap_a_for_b": - token_a_symbol = b"AMMA0001" - token_b_symbol = b"AMMB0001" - pool_reserve_a = 10_000 - pool_reserve_b = 20_000 - pool_total_lp = 10_000 - input_amount = 1_000 - fee_rate_bps = 30 - fee = input_amount * fee_rate_bps // 10_000 - net_input = input_amount - fee - output_amount = pool_reserve_b * net_input // (pool_reserve_a + net_input) - min_output = output_amount - 1 - to_lock = always_success_lock("0x70") - to = decode_hex(script_hash(to_lock), 32) - token_a_type = always_success_lock("0x71") - token_b_type = always_success_lock("0x72") - token_a_type_hash = decode_hex(script_hash(token_a_type), 32) - token_b_type_hash = decode_hex(script_hash(token_b_type), 32) - pool_type = always_success_lock("0x73") - initial = create_script_locked_cells("amm.swap_a_for_b", [ - {"capacity": 400 * 100_000_000, "lock": cellscript_lock, "type": pool_type, "data": pool_data(token_a_symbol, token_b_symbol, pool_reserve_a, pool_reserve_b, pool_total_lp, fee_rate_bps, token_a_type_hash, token_b_type_hash)}, - {"capacity": 200 * 100_000_000, "lock": cellscript_lock, "type": token_a_type, "data": token_data(input_amount, token_a_symbol)}, - ], cell_deps) - valid_tx = transaction(initial["cells"], [ - {"capacity": hex_u64(400 * 100_000_000), "lock": cellscript_lock, "type": pool_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": to_lock, "type": token_b_type}, - ], [ - "0x" + pool_data(token_a_symbol, token_b_symbol, pool_reserve_a + input_amount, pool_reserve_b - output_amount, pool_total_lp, fee_rate_bps, token_a_type_hash, token_b_type_hash).hex(), - "0x" + token_data(output_amount, token_b_symbol).hex(), - ], cell_deps, [entry_witness(min_output, to), "0x"]) - malformed_tx = transaction(initial["cells"], [ - {"capacity": hex_u64(400 * 100_000_000), "lock": cellscript_lock, "type": pool_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": to_lock, "type": token_b_type}, - ], [ - "0x" + pool_data(token_a_symbol, token_b_symbol, pool_reserve_a + input_amount, pool_reserve_b - output_amount, pool_total_lp, fee_rate_bps, token_a_type_hash, token_b_type_hash).hex(), - "0x" + token_data(output_amount + 1, token_b_symbol).hex(), - ], cell_deps, [entry_witness(min_output, to), "0x"]) - input_cells_to_check = initial["cells"] - elif action == "add_liquidity": - token_a_symbol = b"AMMA0001" - token_b_symbol = b"AMMB0001" - pool_reserve_a = 100 - pool_reserve_b = 200 - pool_total_lp = 1000 - token_a_amount = 10 - token_b_amount = 20 - minted_lp = 100 - fee_rate_bps = 30 - provider_lock = always_success_lock("0x66") - provider = decode_hex(script_hash(provider_lock), 32) - token_a_type = always_success_lock("0x67") - token_b_type = always_success_lock("0x68") - token_a_type_hash = decode_hex(script_hash(token_a_type), 32) - token_b_type_hash = decode_hex(script_hash(token_b_type), 32) - pool_type = always_success_lock("0x69") - lp_type = always_success_lock("0x6a") - pool_id = decode_hex(script_hash(pool_type), 32) - initial = create_script_locked_cells("amm.add_liquidity", [ - {"capacity": 400 * 100_000_000, "lock": cellscript_lock, "type": pool_type, "data": pool_data(token_a_symbol, token_b_symbol, pool_reserve_a, pool_reserve_b, pool_total_lp, fee_rate_bps, token_a_type_hash, token_b_type_hash)}, - {"capacity": 200 * 100_000_000, "lock": cellscript_lock, "type": token_a_type, "data": token_data(token_a_amount, token_a_symbol)}, - {"capacity": 200 * 100_000_000, "lock": cellscript_lock, "type": token_b_type, "data": token_data(token_b_amount, token_b_symbol)}, - ], cell_deps) - valid_tx = transaction(initial["cells"], [ - {"capacity": hex_u64(400 * 100_000_000), "lock": cellscript_lock, "type": pool_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": provider_lock, "type": lp_type}, - ], [ - "0x" + pool_data(token_a_symbol, token_b_symbol, pool_reserve_a + token_a_amount, pool_reserve_b + token_b_amount, pool_total_lp + minted_lp, fee_rate_bps, token_a_type_hash, token_b_type_hash).hex(), - "0x" + lp_receipt_data(pool_id, minted_lp, provider).hex(), - ], cell_deps, [entry_witness(provider), "0x", "0x"]) - malformed_tx = transaction(initial["cells"], [ - {"capacity": hex_u64(400 * 100_000_000), "lock": cellscript_lock, "type": pool_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": provider_lock, "type": lp_type}, - ], [ - "0x" + pool_data(token_a_symbol, token_b_symbol, pool_reserve_a + token_a_amount, pool_reserve_b + token_b_amount, pool_total_lp + minted_lp, fee_rate_bps, token_a_type_hash, token_b_type_hash).hex(), - "0x" + lp_receipt_data(pool_id, minted_lp + 1, provider).hex(), - ], cell_deps, [entry_witness(provider), "0x", "0x"]) - input_cells_to_check = initial["cells"] - elif action == "remove_liquidity": - token_a_symbol = b"AMMA0001" - token_b_symbol = b"AMMB0001" - pool_reserve_a = 100 - pool_reserve_b = 200 - pool_total_lp = 1000 - burned_lp = 100 - withdrawn_a = 10 - withdrawn_b = 20 - fee_rate_bps = 30 - provider_lock = always_success_lock("0x6b") - provider = decode_hex(script_hash(provider_lock), 32) - token_a_type = always_success_lock("0x6c") - token_b_type = always_success_lock("0x6d") - token_a_type_hash = decode_hex(script_hash(token_a_type), 32) - token_b_type_hash = decode_hex(script_hash(token_b_type), 32) - pool_type = always_success_lock("0x6e") - lp_type = always_success_lock("0x6f") - pool_id = decode_hex(script_hash(pool_type), 32) - initial = create_script_locked_cells("amm.remove_liquidity", [ - {"capacity": 400 * 100_000_000, "lock": cellscript_lock, "type": pool_type, "data": pool_data(token_a_symbol, token_b_symbol, pool_reserve_a, pool_reserve_b, pool_total_lp, fee_rate_bps, token_a_type_hash, token_b_type_hash)}, - {"capacity": 600 * 100_000_000, "lock": cellscript_lock, "type": lp_type, "data": lp_receipt_data(pool_id, burned_lp, provider)}, - ], cell_deps) - valid_tx = transaction(initial["cells"], [ - {"capacity": hex_u64(400 * 100_000_000), "lock": cellscript_lock, "type": pool_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": provider_lock, "type": token_a_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": provider_lock, "type": token_b_type}, - ], [ - "0x" + pool_data(token_a_symbol, token_b_symbol, pool_reserve_a - withdrawn_a, pool_reserve_b - withdrawn_b, pool_total_lp - burned_lp, fee_rate_bps, token_a_type_hash, token_b_type_hash).hex(), - "0x" + token_data(withdrawn_a, token_a_symbol).hex(), - "0x" + token_data(withdrawn_b, token_b_symbol).hex(), - ], cell_deps, [entry_witness(provider), "0x"]) - malformed_tx = transaction(initial["cells"], [ - {"capacity": hex_u64(400 * 100_000_000), "lock": cellscript_lock, "type": pool_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": provider_lock, "type": token_a_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": provider_lock, "type": token_b_type}, - ], [ - "0x" + pool_data(token_a_symbol, token_b_symbol, pool_reserve_a - withdrawn_a, pool_reserve_b - withdrawn_b, pool_total_lp - burned_lp, fee_rate_bps, token_a_type_hash, token_b_type_hash).hex(), - "0x" + token_data(withdrawn_a + 1, token_a_symbol).hex(), - "0x" + token_data(withdrawn_b, token_b_symbol).hex(), - ], cell_deps, [entry_witness(provider), "0x"]) - input_cells_to_check = initial["cells"] - else: - raise RuntimeError(f"unsupported AMM action harness: {action}") - - return { - "builder_name": "amm-action-builder-v1", - "initial": initial, - "input_cells_to_check": input_cells_to_check, - "valid_tx": valid_tx, - "malformed_tx": malformed_tx, - } - -def run_multisig_action(action_record, always_success_dep): - action = action_record["action"] - name = action_record["name"] - code = deploy_code_cell(name, action_record["artifact"], always_success_dep) - cellscript_lock = {"code_hash": code["artifact_ckb_data_hash_blake2b"], "hash_type": "data1", "args": "0x"} - cellscript_type = always_success_lock() - wallet_type = always_success_lock("0x51") - proposal_type = always_success_lock("0x52") - confirmation_type = always_success_lock("0x53") - execution_type = always_success_lock("0x54") - signer_a = decode_hex(script_hash(cellscript_lock), 32) - signer_b = decode_hex(script_hash(always_success_lock("0x55")), 32) - signer_c = decode_hex(script_hash(always_success_lock("0x56")), 32) - target = decode_hex(script_hash(always_success_lock("0x57")), 32) - wallet_id = decode_hex(script_hash(always_success_lock("0x58")), 32) - cell_deps = [always_success_dep, code["code_cell_dep"]] - - result = { - "action": action, - "name": name, - "harness_origin": "multisig-action-builder-v1", - "builder_backed": True, - "artifact": action_record["artifact"], - "code": code, - "cellscript_lock_hash": script_hash(cellscript_lock), - } - multisig_case = build_multisig_action_case( - action_record, - cellscript_lock, - wallet_type, - proposal_type, - confirmation_type, - execution_type, - signer_a, - signer_b, - signer_c, - target, - wallet_id, - cell_deps, - ) - initial = multisig_case["initial"] - valid_tx = multisig_case["valid_tx"] - malformed_tx = multisig_case["malformed_tx"] - result["builder_name"] = multisig_case["builder_name"] - - malformed_rejection = expect_dry_run_rejected( - malformed_tx, - f"{name} malformed action transaction", - ("Script", "script", "ValidationFailure", "error code", "VM", "Run result", "Invalid"), - ) - for index, cell in enumerate(initial["cells"]): - assert_live(cell["tx_hash"], cell["index"], f"{name} input cell {index} after malformed transaction") - - valid_dry_run = rpc("dry_run_transaction", [valid_tx]) - commit = submit_and_commit(valid_tx, f"{name} valid action transaction") - output_live = [ - assert_live(commit["tx_hash"], index, f"{name} valid output {index}").get("status") == "live" - for index in range(len(valid_tx["outputs"])) - ] - result.update({ - "initial_cells": initial, - "malformed_transaction": malformed_rejection, - "valid_dry_run": valid_dry_run, - "measured_constraints": measure_release_constraints(valid_tx, valid_dry_run), - "valid_commit": commit, - "valid_outputs_live": output_live, - "status": "passed", - }) - return result - -def build_multisig_action_case(action_record, cellscript_lock, wallet_type, proposal_type, confirmation_type, execution_type, signer_a, signer_b, signer_c, target, wallet_id, cell_deps): - action = action_record["action"] - original_scoped = action_record.get("kind") == "original-scoped-action-strict" - - if action == "create_wallet": - current_time = 10 - signers = [signer_a, signer_b] - signers_payload = molecule_fixvec(signers) - wallet_payload = multisig_wallet_molecule_data(wallet_id, signers, 2, 0, current_time) if original_scoped else multisig_wallet_data(wallet_id, signer_a, signer_b, 2, 0, current_time) - malformed_wallet_payload = multisig_wallet_molecule_data(wallet_id, signers, 1, 0, current_time) if original_scoped else multisig_wallet_data(wallet_id, signer_a, signer_b, 1, 0, current_time) - witness = entry_witness(wallet_id, molecule_bytes(signers_payload), bytes([2]), current_time) if original_scoped else entry_witness(wallet_id, signer_a, signer_b, bytes([2]), current_time) - initial = create_script_locked_cells( - "multisig.create_wallet", - [{"capacity": 1000 * 100_000_000, "lock": cellscript_lock, "type": None, "data": b""}], - cell_deps, - ) - input_cell = initial["cells"][0] - outputs = [{"capacity": hex_u64(1000 * 100_000_000), "lock": cellscript_lock, "type": wallet_type}] - valid_tx = transaction(input_cell, outputs, ["0x" + wallet_payload.hex()], cell_deps, [witness]) - malformed_tx = transaction(input_cell, outputs, ["0x" + malformed_wallet_payload.hex()], cell_deps, [witness]) - elif action in ("propose_transfer", "propose_add_signer", "propose_remove_signer", "propose_change_threshold"): - current_time = 20 - threshold = 1 if action == "propose_remove_signer" else 2 - initial_nonce = 0 - proposal_id = 1 - signers = [signer_a, signer_b] - wallet_payload = multisig_wallet_molecule_data(wallet_id, signers, threshold, initial_nonce, 10) if original_scoped else multisig_wallet_data(wallet_id, signer_a, signer_b, threshold, initial_nonce, 10) - initial = create_script_locked_cells( - f"multisig.{action}", - [{"capacity": 1000 * 100_000_000, "lock": cellscript_lock, "type": wallet_type, "data": wallet_payload}], - cell_deps, - ) - input_cell = initial["cells"][0] - if action == "propose_transfer": - operation = 0 - proposal_target = target - amount = 500 - data_payload = b"" - witness = entry_witness(signer_a, target, amount, current_time) - malformed_witness = entry_witness(signer_b, target, 0, current_time) - elif action == "propose_add_signer": - operation = 1 - proposal_target = signer_c - amount = 0 - data_payload = signer_c - witness = entry_witness(signer_a, signer_c, current_time) - malformed_witness = entry_witness(signer_a, signer_a, current_time) - elif action == "propose_remove_signer": - operation = 2 - proposal_target = signer_b - amount = 0 - data_payload = b"" - witness = entry_witness(signer_a, signer_b, current_time) - malformed_witness = entry_witness(signer_a, signer_c, current_time) - else: - operation = 3 - proposal_target = bytes(32) - new_threshold = 2 if original_scoped else 1 - amount = new_threshold - data_payload = bytes([new_threshold]) - witness = entry_witness(signer_a, bytes([new_threshold]), current_time) - malformed_witness = entry_witness(signer_a, bytes([3]), current_time) - output_wallet_payload = multisig_wallet_molecule_data(wallet_id, signers, threshold, proposal_id, 10) if original_scoped else multisig_wallet_data(wallet_id, signer_a, signer_b, threshold, proposal_id, 10) - proposal_payload = ( - multisig_proposal_molecule_data(wallet_id, proposal_id, signer_a, operation, proposal_target, amount, data_payload, [], threshold, current_time, current_time + 1440) - if original_scoped - else multisig_proposal_data(wallet_id, proposal_id, signer_a, operation, proposal_target, amount, threshold, 0, current_time, current_time + 1440) - ) - outputs = [ - {"capacity": hex_u64(700 * 100_000_000), "lock": cellscript_lock, "type": wallet_type}, - {"capacity": hex_u64(300 * 100_000_000), "lock": cellscript_lock, "type": proposal_type}, - ] - outputs_data = ["0x" + output_wallet_payload.hex(), "0x" + proposal_payload.hex()] - valid_tx = transaction(input_cell, outputs, outputs_data, cell_deps, [witness]) - malformed_tx = transaction(input_cell, outputs, outputs_data, cell_deps, [malformed_witness]) - elif action == "record_approval": - current_time = 30 - proposal_id = 7 - signers = [signer_a, signer_b] - wallet_payload = multisig_wallet_molecule_data(wallet_id, signers, 2, 0, 10) - proposal_payload = ( - multisig_proposal_molecule_data(wallet_id, proposal_id, signer_a, 0, target, 500, b"", [signer_a], 2, 20, 2000) - if original_scoped - else multisig_proposal_data(wallet_id, proposal_id, signer_a, 0, target, 500, 2, 1, 20, 2000) - ) - output_proposal_payload = ( - multisig_proposal_molecule_data(wallet_id, proposal_id, signer_a, 0, target, 500, b"", [signer_a, signer_b], 2, 20, 2000) - if original_scoped - else multisig_proposal_data(wallet_id, proposal_id, signer_a, 0, target, 500, 2, 2, 20, 2000) - ) - malformed_output_proposal_payload = ( - multisig_proposal_molecule_data(wallet_id, proposal_id, signer_a, 0, target, 500, b"", [signer_b, signer_b], 2, 20, 2000) - if original_scoped - else proposal_payload - ) - input_cells = [ - {"capacity": 1000 * 100_000_000, "lock": cellscript_lock, "type": proposal_type, "data": proposal_payload}, - {"capacity": 500 * 100_000_000, "lock": always_success_lock(), "type": wallet_type, "data": wallet_payload}, - ] - initial = create_script_locked_cells("multisig.record_approval", input_cells, cell_deps) - inputs = initial["cells"][0] - action_cell_deps = [cell_dep_for(initial["cells"][1])] + cell_deps - outputs = [ - {"capacity": hex_u64(600 * 100_000_000), "lock": cellscript_lock, "type": proposal_type}, - {"capacity": hex_u64(300 * 100_000_000), "lock": cellscript_lock, "type": confirmation_type}, - ] - valid_tx = transaction(inputs, outputs, ["0x" + output_proposal_payload.hex(), "0x" + approval_confirmation_data(proposal_id, signer_b, current_time).hex()], action_cell_deps, [entry_witness(signer_b, current_time)]) - malformed_tx = transaction(inputs, outputs, ["0x" + malformed_output_proposal_payload.hex(), "0x" + approval_confirmation_data(proposal_id, signer_b, current_time).hex()], action_cell_deps, [entry_witness(signer_b, current_time)]) - elif action == "execute_proposal": - current_time = 40 - proposal_id = 8 - signers = [signer_a, signer_b] - wallet_payload = multisig_wallet_molecule_data(wallet_id, signers, 2, 0, 10) - proposal_payload = ( - multisig_proposal_molecule_data(wallet_id, proposal_id, signer_a, 0, target, 500, b"", [signer_a, signer_b], 2, 20, 2000) - if original_scoped - else multisig_proposal_data(wallet_id, proposal_id, signer_a, 0, target, 500, 2, 2, 20, 2000) - ) - input_cells = [ - {"capacity": 500 * 100_000_000, "lock": cellscript_lock, "type": proposal_type, "data": proposal_payload}, - {"capacity": 300 * 100_000_000, "lock": always_success_lock(), "type": wallet_type, "data": wallet_payload}, - ] - initial = create_script_locked_cells("multisig.execute_proposal", input_cells, cell_deps) - inputs = initial["cells"][0] - action_cell_deps = [cell_dep_for(initial["cells"][1])] + cell_deps - outputs = [{"capacity": hex_u64(200 * 100_000_000), "lock": cellscript_lock, "type": execution_type}] - valid_tx = transaction(inputs, outputs, ["0x" + execution_record_data(proposal_id, signer_a, current_time, 1).hex()], action_cell_deps, [entry_witness(signer_a, current_time)]) - malformed_tx = transaction(inputs, outputs, ["0x" + execution_record_data(proposal_id, signer_a, current_time + 1, 1).hex()], action_cell_deps, [entry_witness(signer_a, current_time)]) - elif action == "cancel_proposal": - proposal_id = 9 - signers = [signer_a, signer_b] - wallet_payload = multisig_wallet_molecule_data(wallet_id, signers, 2, 0, 10) - proposal_payload = multisig_proposal_molecule_data(wallet_id, proposal_id, signer_a, 0, target, 500, b"", [], 2, 20, 2000) if original_scoped else multisig_proposal_data(wallet_id, proposal_id, signer_a, 0, target, 500, 2, 0, 20, 2000) - input_cells = [ - {"capacity": 500 * 100_000_000, "lock": cellscript_lock, "type": proposal_type, "data": proposal_payload}, - {"capacity": 300 * 100_000_000, "lock": always_success_lock(), "type": wallet_type, "data": wallet_payload}, - ] - initial = create_script_locked_cells("multisig.cancel_proposal", input_cells, cell_deps) - inputs = initial["cells"][0] - action_cell_deps = [cell_dep_for(initial["cells"][1])] + cell_deps - outputs = [{"capacity": hex_u64(490 * 100_000_000), "lock": cellscript_lock, "type": None}] - valid_tx = transaction(inputs, outputs, ["0x"], action_cell_deps, [entry_witness(signer_a)]) - malformed_tx = transaction(inputs, outputs, ["0x"], action_cell_deps, [entry_witness(signer_b)]) - else: - raise RuntimeError(f"unsupported multisig action harness: {action}") - - return { - "builder_name": "multisig-action-builder-v1", - "initial": initial, - "valid_tx": valid_tx, - "malformed_tx": malformed_tx, - } - -def run_launch_action(action_record, always_success_dep): - action = action_record["action"] - name = action_record["name"] - if action != "bootstrap_token": - if action != "launch_token": - raise RuntimeError(f"unsupported launch action harness: {action}") - code = deploy_code_cell(name, action_record["artifact"], always_success_dep) - cellscript_lock = {"code_hash": code["artifact_ckb_data_hash_blake2b"], "hash_type": "data1", "args": "0x"} - auth_type = always_success_lock("0x61") - token_type = always_success_lock("0x62") - pool_paired_type = always_success_lock("0x63") - pool_type = always_success_lock("0x64") - lp_type = always_success_lock("0x65") - symbol = b"LAUNCH01" - max_supply = 10_000 - initial_mint = 1_000 - pool_seed_amount = 500 - paired_amount = 250 - paired_symbol = b"PAIR0001" - fee_rate_bps = 30 - creator_lock = always_success_lock("0x60") - recipient_count = 4 if action == "launch_token" else 2 - recipient_locks = [always_success_lock("0x7" + format(index, "x")) for index in range(recipient_count)] - creator = decode_hex(script_hash(creator_lock), 32) - recipients = [ - (decode_hex(script_hash(lock), 32), amount) - for lock, amount in zip(recipient_locks, [10, 20, 30, 40] if action == "launch_token" else [10, 20]) - ] - recipient_payload = fixed_recipient_tuple_array4(recipients) if action == "launch_token" else fixed_recipient_tuple_array(recipients) - total_distributed = sum(amount for _, amount in recipients) - cell_deps = [always_success_dep, code["code_cell_dep"]] - - result = { - "action": action, - "name": name, - "harness_origin": "launch-action-builder-v1", - "builder_backed": True, - "artifact": action_record["artifact"], - "code": code, - "cellscript_lock_hash": script_hash(cellscript_lock), - } - launch_case = build_launch_action_case( - action_record, - cellscript_lock, - auth_type, - token_type, - pool_paired_type, - pool_type, - lp_type, - symbol, - max_supply, - initial_mint, - pool_seed_amount, - paired_amount, - paired_symbol, - fee_rate_bps, - creator_lock, - creator, - recipient_locks, - recipients, - recipient_payload, - total_distributed, - cell_deps, - ) - initial = launch_case["initial"] - input_cell = launch_case["input_cell"] - valid_tx = launch_case["valid_tx"] - malformed_tx = launch_case["malformed_tx"] - result["builder_name"] = launch_case["builder_name"] - - malformed_rejection = expect_dry_run_rejected( - malformed_tx, - f"{name} malformed action transaction", - ("Script", "script", "ValidationFailure", "error code", "VM", "Run result", "Invalid"), - ) - assert_live(input_cell["tx_hash"], input_cell["index"], f"{name} input cell after malformed transaction") - - valid_dry_run = rpc("dry_run_transaction", [valid_tx]) - commit = submit_and_commit(valid_tx, f"{name} valid action transaction") - output_live = [ - assert_live(commit["tx_hash"], index, f"{name} valid output {index}").get("status") == "live" - for index in range(len(valid_tx["outputs"])) - ] - result.update({ - "initial_cells": initial, - "malformed_transaction": malformed_rejection, - "valid_dry_run": valid_dry_run, - "measured_constraints": measure_release_constraints(valid_tx, valid_dry_run), - "valid_commit": commit, - "valid_outputs_live": output_live, - "status": "passed", - }) - return result - -def build_launch_action_case(action_record, cellscript_lock, auth_type, token_type, pool_paired_type, pool_type, lp_type, symbol, max_supply, initial_mint, pool_seed_amount, paired_amount, paired_symbol, fee_rate_bps, creator_lock, creator, recipient_locks, recipients, recipient_payload, total_distributed, cell_deps): - action = action_record["action"] - if action == "launch_token": - initial_lp = math.isqrt(pool_seed_amount * paired_amount) - remaining = initial_mint - total_distributed - pool_seed_amount - pool_id = decode_hex(script_hash(pool_type), 32) - token_type_hash = decode_hex(script_hash(token_type), 32) - paired_type_hash = decode_hex(script_hash(pool_paired_type), 32) - initial = create_script_locked_cells( - "launch.launch_token", - [{"capacity": 4000 * 100_000_000, "lock": cellscript_lock, "type": pool_paired_type, "data": token_data(paired_amount, paired_symbol)}], - cell_deps, - ) - input_cell = initial["cells"][0] - outputs = [{"capacity": hex_u64(400 * 100_000_000), "lock": creator_lock, "type": auth_type}] - outputs_data = ["0x" + mint_authority_data(symbol, max_supply, initial_mint).hex()] - for recipient_lock, (_, amount) in zip(recipient_locks, recipients): - outputs.append({"capacity": hex_u64(200 * 100_000_000), "lock": recipient_lock, "type": token_type}) - outputs_data.append("0x" + token_data(amount, symbol).hex()) - outputs.append({"capacity": hex_u64(400 * 100_000_000), "lock": creator_lock, "type": pool_type}) - outputs_data.append("0x" + pool_data(symbol, paired_symbol, pool_seed_amount, paired_amount, initial_lp, fee_rate_bps, token_type_hash, paired_type_hash).hex()) - outputs.append({"capacity": hex_u64(200 * 100_000_000), "lock": creator_lock, "type": lp_type}) - outputs_data.append("0x" + lp_receipt_data(pool_id, initial_lp, creator).hex()) - outputs.append({"capacity": hex_u64(200 * 100_000_000), "lock": creator_lock, "type": token_type}) - outputs_data.append("0x" + token_data(remaining, symbol).hex()) - witness = entry_witness(symbol, max_supply, initial_mint, pool_seed_amount, bytes([fee_rate_bps & 0xff, fee_rate_bps >> 8]), creator, recipient_payload) - valid_tx = transaction(input_cell, outputs, outputs_data, cell_deps, [witness]) - malformed_outputs_data = list(outputs_data) - malformed_outputs_data[-1] = "0x" + token_data(remaining - 1, symbol).hex() - malformed_tx = transaction(input_cell, outputs, malformed_outputs_data, cell_deps, [witness]) - else: - initial = create_script_locked_cells( - "launch.bootstrap_token", - [{"capacity": 4000 * 100_000_000, "lock": cellscript_lock, "type": None, "data": b""}], - cell_deps, - ) - input_cell = initial["cells"][0] - outputs = [{"capacity": hex_u64(400 * 100_000_000), "lock": creator_lock, "type": auth_type}] - outputs_data = ["0x" + mint_authority_data(symbol, max_supply, initial_mint).hex()] - for recipient_lock, (_, amount) in zip(recipient_locks, recipients): - outputs.append({"capacity": hex_u64(200 * 100_000_000), "lock": recipient_lock, "type": token_type}) - outputs_data.append("0x" + token_data(amount, symbol).hex()) - outputs.append({"capacity": hex_u64(200 * 100_000_000), "lock": creator_lock, "type": token_type}) - outputs_data.append("0x" + token_data(initial_mint - total_distributed, symbol).hex()) - witness = entry_witness(symbol, max_supply, initial_mint, creator, recipient_payload) - valid_tx = transaction(input_cell, outputs, outputs_data, cell_deps, [witness]) - malformed_outputs_data = list(outputs_data) - malformed_outputs_data[-1] = "0x" + token_data(initial_mint - total_distributed - 1, symbol).hex() - malformed_tx = transaction(input_cell, outputs, malformed_outputs_data, cell_deps, [witness]) - return { - "builder_name": "launch-action-builder-v1", - "initial": initial, - "input_cell": input_cell, - "valid_tx": valid_tx, - "malformed_tx": malformed_tx, - } - -def run_vesting_action(action_record, always_success_dep): - action = action_record["action"] - name = action_record["name"] - code = deploy_code_cell(name, action_record["artifact"], always_success_dep) - cellscript_lock = {"code_hash": code["artifact_ckb_data_hash_blake2b"], "hash_type": "data1", "args": "0x"} - admin_lock = always_success_lock() - config_type = always_success_lock("0x41") - admin = decode_hex(script_hash(admin_lock), 32) - symbol = b"VEST0001" - cliff_period = 10 - total_period = 100 - revocable = True - cell_deps = [always_success_dep, code["code_cell_dep"]] - - if action not in {"create_vesting_config", "grant_vesting", "claim_vested", "claim_fully_vested", "revoke_grant"}: - raise RuntimeError(f"unsupported vesting action harness: {action}") - - result = { - "action": action, - "name": name, - "harness_origin": "vesting-action-builder-v1", - "builder_backed": True, - "artifact": action_record["artifact"], - "code": code, - "cellscript_lock_hash": script_hash(cellscript_lock), - "admin_lock_hash": "0x" + admin.hex(), - } - vesting_case = build_vesting_action_case( - action_record, - cellscript_lock, - admin_lock, - config_type, - admin, - symbol, - cliff_period, - total_period, - revocable, - cell_deps, - ) - initial = vesting_case["initial"] - input_cells_to_check = vesting_case["input_cells_to_check"] - valid_tx = vesting_case["valid_tx"] - malformed_tx = vesting_case["malformed_tx"] - result["builder_name"] = vesting_case["builder_name"] - if vesting_case.get("timepoint_header") is not None: - result["timepoint_header"] = vesting_case["timepoint_header"] - malformed_rejection = expect_dry_run_rejected( - malformed_tx, - f"{name} malformed action transaction", - ("Script", "script", "ValidationFailure", "error code", "VM", "Run result", "Invalid"), - ) - for index, input_cell in enumerate(input_cells_to_check): - assert_live(input_cell["tx_hash"], input_cell["index"], f"{name} input cell {index} after malformed transaction") - - valid_dry_run = rpc("dry_run_transaction", [valid_tx]) - commit = submit_and_commit(valid_tx, f"{name} valid action transaction") - output_live = [ - assert_live(commit["tx_hash"], index, f"{name} valid output {index}").get("status") == "live" - for index in range(len(valid_tx["outputs"])) - ] - result.update({ - "initial_cells": initial, - "malformed_transaction": malformed_rejection, - "valid_dry_run": valid_dry_run, - "measured_constraints": measure_release_constraints(valid_tx, valid_dry_run), - "valid_commit": commit, - "valid_outputs_live": output_live, - "status": "passed", - }) - return result - -def build_vesting_action_case(action_record, cellscript_lock, admin_lock, config_type, admin, symbol, cliff_period, total_period, revocable, cell_deps): - action = action_record["action"] - timepoint_header = None - - if action == "create_vesting_config": - initial = create_script_locked_cells( - "vesting.create_vesting_config", - [{"capacity": 1000 * 100_000_000, "lock": cellscript_lock, "type": None, "data": b""}], - cell_deps, - ) - input_cells_to_check = [initial["cells"][0]] - valid_tx = transaction( - initial["cells"][0], - [{"capacity": hex_u64(300 * 100_000_000), "lock": admin_lock, "type": config_type}], - ["0x" + vesting_config_data(admin, symbol, cliff_period, total_period, revocable).hex()], - cell_deps, - [entry_witness(admin, symbol, cliff_period, total_period, bytes([1]))], - ) - malformed_tx = transaction( - initial["cells"][0], - [{"capacity": hex_u64(300 * 100_000_000), "lock": admin_lock, "type": config_type}], - ["0x" + vesting_config_data(admin, symbol, cliff_period, total_period + 1, revocable).hex()], - cell_deps, - [entry_witness(admin, symbol, cliff_period, total_period, bytes([1]))], - ) - elif action == "grant_vesting": - beneficiary_lock = always_success_lock("0x42") - beneficiary = decode_hex(script_hash(beneficiary_lock), 32) - grant_type = always_success_lock("0x43") - amount = 77 - now = 0 - header_dep = get_block_by_number(0)["header"]["hash"] - initial = create_script_locked_cells( - "vesting.grant_vesting", - [ - {"capacity": 200 * 100_000_000, "lock": cellscript_lock, "type": always_success_lock("0x44"), "data": token_data(amount, symbol)}, - {"capacity": 200 * 100_000_000, "lock": admin_lock, "type": config_type, "data": vesting_config_data(admin, symbol, cliff_period, total_period, revocable)}, - ], - cell_deps, - ) - funding_input = find_spendable_cellbase() - change_capacity = initial["cells"][0]["capacity"] + funding_input["capacity"] - (300 * 100_000_000) - input_cells_to_check = initial["cells"] + [funding_input] - config_dep = {"out_point": out_point(initial["cells"][1]["tx_hash"], initial["cells"][1]["index"]), "dep_type": "code"} - action_cell_deps = [config_dep] + cell_deps - valid_tx = transaction( - [initial["cells"][0], funding_input], - [ - {"capacity": hex_u64(300 * 100_000_000), "lock": beneficiary_lock, "type": grant_type}, - {"capacity": hex_u64(change_capacity), "lock": always_success_lock(), "type": None}, - ], - [ - "0x" + vesting_grant_data(0, beneficiary, amount, 0, now, now + cliff_period, now + total_period, symbol).hex(), - "0x", - ], - action_cell_deps, - [entry_witness(beneficiary)], - [header_dep], - ) - malformed_tx = transaction( - [initial["cells"][0], funding_input], - [ - {"capacity": hex_u64(300 * 100_000_000), "lock": beneficiary_lock, "type": grant_type}, - {"capacity": hex_u64(change_capacity), "lock": always_success_lock(), "type": None}, - ], - [ - "0x" + vesting_grant_data(0, beneficiary, amount + 1, 0, now, now + cliff_period, now + total_period, symbol).hex(), - "0x", - ], - action_cell_deps, - [entry_witness(beneficiary)], - [header_dep], - ) - elif action == "claim_vested": - beneficiary_lock = cellscript_lock - beneficiary = decode_hex(script_hash(beneficiary_lock), 32) - grant_type = always_success_lock("0x43") - token_type = always_success_lock("0x45") - total_amount = 100 - claimed_amount = 20 - timepoint_header = wait_header_epoch_at_least(1) - grant_timepoint = 0 - cliff_timepoint = 0 - now = timepoint_header["epoch_number"] - end_timepoint = now * 2 - vested_total = total_amount * now // end_timepoint - claimable = vested_total - claimed_amount - header_dep = timepoint_header["hash"] - initial = create_script_locked_cells( - "vesting.claim_vested", - [{"capacity": 500 * 100_000_000, "lock": beneficiary_lock, "type": grant_type, "data": vesting_grant_data(0, beneficiary, total_amount, claimed_amount, grant_timepoint, cliff_timepoint, end_timepoint, symbol)}], - cell_deps, - ) - input_cells_to_check = initial["cells"] - valid_tx = transaction( - initial["cells"], - [ - {"capacity": hex_u64(200 * 100_000_000), "lock": beneficiary_lock, "type": token_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": beneficiary_lock, "type": grant_type}, - ], - [ - "0x" + token_data(claimable, symbol).hex(), - "0x" + vesting_grant_data(0, beneficiary, total_amount, vested_total, grant_timepoint, cliff_timepoint, end_timepoint, symbol).hex(), - ], - cell_deps, - [entry_witness()], - [header_dep], - ) - malformed_tx = transaction( - initial["cells"], - [ - {"capacity": hex_u64(200 * 100_000_000), "lock": beneficiary_lock, "type": token_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": beneficiary_lock, "type": grant_type}, - ], - [ - "0x" + token_data(claimable - 1, symbol).hex(), - "0x" + vesting_grant_data(0, beneficiary, total_amount, vested_total, grant_timepoint, cliff_timepoint, end_timepoint, symbol).hex(), - ], - cell_deps, - [entry_witness()], - [header_dep], - ) - elif action == "claim_fully_vested": - beneficiary_lock = cellscript_lock - beneficiary = decode_hex(script_hash(beneficiary_lock), 32) - grant_type = always_success_lock("0x43") - token_type = always_success_lock("0x45") - total_amount = 100 - claimed_amount = 20 - timepoint_header = wait_header_epoch_at_least(1) - grant_timepoint = 0 - cliff_timepoint = 0 - end_timepoint = timepoint_header["epoch_number"] - header_dep = timepoint_header["hash"] - claimable = total_amount - claimed_amount - initial = create_script_locked_cells( - "vesting.claim_fully_vested", - [{"capacity": 500 * 100_000_000, "lock": beneficiary_lock, "type": grant_type, "data": vesting_grant_data(0, beneficiary, total_amount, claimed_amount, grant_timepoint, cliff_timepoint, end_timepoint, symbol)}], - cell_deps, - ) - input_cells_to_check = initial["cells"] - valid_tx = transaction( - initial["cells"], - [ - {"capacity": hex_u64(200 * 100_000_000), "lock": beneficiary_lock, "type": token_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": beneficiary_lock, "type": grant_type}, - ], - [ - "0x" + token_data(claimable, symbol).hex(), - "0x" + vesting_grant_data(1, beneficiary, total_amount, total_amount, grant_timepoint, cliff_timepoint, end_timepoint, symbol).hex(), - ], - cell_deps, - [entry_witness()], - [header_dep], - ) - malformed_tx = transaction( - initial["cells"], - [ - {"capacity": hex_u64(200 * 100_000_000), "lock": beneficiary_lock, "type": token_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": beneficiary_lock, "type": grant_type}, - ], - [ - "0x" + token_data(claimable - 1, symbol).hex(), - "0x" + vesting_grant_data(1, beneficiary, total_amount, total_amount, grant_timepoint, cliff_timepoint, end_timepoint, symbol).hex(), - ], - cell_deps, - [entry_witness()], - [header_dep], - ) - elif action == "revoke_grant": - beneficiary_lock = always_success_lock("0x42") - beneficiary = decode_hex(script_hash(beneficiary_lock), 32) - grant_type = always_success_lock("0x43") - token_type = always_success_lock("0x45") - total_amount = 100 - claimed_amount = 20 - timepoint_header = wait_header_epoch_at_least(1) - grant_timepoint = 0 - cliff_timepoint = 0 - end_timepoint = timepoint_header["epoch_number"] - header_dep = timepoint_header["hash"] - unclaimed_vested = total_amount - claimed_amount - unvested = 0 - initial = create_script_locked_cells( - "vesting.revoke_grant", - [ - {"capacity": 500 * 100_000_000, "lock": cellscript_lock, "type": grant_type, "data": vesting_grant_data(0, beneficiary, total_amount, claimed_amount, grant_timepoint, cliff_timepoint, end_timepoint, symbol)}, - {"capacity": 200 * 100_000_000, "lock": admin_lock, "type": config_type, "data": vesting_config_data(admin, symbol, cliff_period, total_period, revocable)}, - ], - cell_deps, - ) - input_cells_to_check = initial["cells"] - config_dep = {"out_point": out_point(initial["cells"][1]["tx_hash"], initial["cells"][1]["index"]), "dep_type": "code"} - action_cell_deps = [config_dep] + cell_deps - valid_tx = transaction( - initial["cells"][0], - [ - {"capacity": hex_u64(200 * 100_000_000), "lock": beneficiary_lock, "type": token_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": admin_lock, "type": token_type}, - ], - [ - "0x" + token_data(unclaimed_vested, symbol).hex(), - "0x" + token_data(unvested, symbol).hex(), - ], - action_cell_deps, - [entry_witness(admin), "0x"], - [header_dep], - ) - malformed_tx = transaction( - initial["cells"][0], - [ - {"capacity": hex_u64(200 * 100_000_000), "lock": beneficiary_lock, "type": token_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": admin_lock, "type": token_type}, - ], - [ - "0x" + token_data(unclaimed_vested - 1, symbol).hex(), - "0x" + token_data(unvested, symbol).hex(), - ], - action_cell_deps, - [entry_witness(admin), "0x"], - [header_dep], - ) - else: - raise RuntimeError(f"unsupported vesting action harness: {action}") - - return { - "builder_name": "vesting-action-builder-v1", - "initial": initial, - "input_cells_to_check": input_cells_to_check, - "valid_tx": valid_tx, - "malformed_tx": malformed_tx, - "timepoint_header": timepoint_header, - } - -def build_timelock_action_case(action_record, cellscript_lock, cellscript_type, owner, cell_deps): - action = action_record["action"] - original_scoped = action_record.get("kind") == "original-scoped-action-strict" - flow_state = 0 if original_scoped else None - lock_id = decode_hex(script_hash(cellscript_type), 32) - timepoint_header = get_block_by_number(0)["header"]["hash"] - - def scoped_lock_id(): - return lock_id if original_scoped else bytes(32) - - def scoped_timelock_data(owner_value, lock_type, unlock_height, created_at): - return timelock_data( - owner_value, - lock_type, - unlock_height, - created_at, - lock_id=lock_id if original_scoped else None, - ) - - if action == "create_absolute_lock": - current_height = 0 - unlock_height = 100 - initial = create_script_locked_cells( - "timelock.create_absolute_lock", - [{"capacity": 1000 * 100_000_000, "lock": cellscript_lock, "type": None, "data": b""}], - cell_deps, - ) - input_cell = initial["cells"][0] - witness = [entry_witness(lock_id, owner, unlock_height)] if original_scoped else [entry_witness(owner, unlock_height)] - outputs = [{"capacity": hex_u64(300 * 100_000_000), "lock": cellscript_lock, "type": cellscript_type}] - valid_tx = transaction(input_cell, outputs, ["0x" + scoped_timelock_data(owner, 0, unlock_height, current_height).hex()], cell_deps, witness, [timepoint_header]) - malformed_tx = transaction(input_cell, outputs, ["0x" + scoped_timelock_data(owner, 0, unlock_height + 1, current_height).hex()], cell_deps, witness, [timepoint_header]) - elif action == "create_relative_lock": - current_height = 0 - lock_period = 25 - initial = create_script_locked_cells( - "timelock.create_relative_lock", - [{"capacity": 1000 * 100_000_000, "lock": cellscript_lock, "type": None, "data": b""}], - cell_deps, - ) - input_cell = initial["cells"][0] - witness = [entry_witness(lock_id, owner, lock_period)] if original_scoped else [entry_witness(owner, lock_period)] - outputs = [{"capacity": hex_u64(300 * 100_000_000), "lock": cellscript_lock, "type": cellscript_type}] - valid_tx = transaction(input_cell, outputs, ["0x" + scoped_timelock_data(owner, 1, current_height + lock_period, current_height).hex()], cell_deps, witness, [timepoint_header]) - malformed_tx = transaction(input_cell, outputs, ["0x" + scoped_timelock_data(owner, 1, current_height + lock_period + 1, current_height).hex()], cell_deps, witness, [timepoint_header]) - elif action == "lock_asset": - unlock_height = 500 - created_at = 1 - amount = 42 - token_symbol = b"TOKEN001" - lock_hash = scoped_lock_id() - locked_asset_payload = locked_asset_data(token_symbol, amount, lock_hash) - malformed_locked_asset_payload = locked_asset_data(token_symbol, amount + 1, lock_hash) - token_type = always_success_lock("0x1f") - locked_asset_type = always_success_lock("0x20") - initial = create_script_locked_cells( - "timelock.lock_asset", - [ - {"capacity": 1000 * 100_000_000, "lock": cellscript_lock, "type": token_type, "data": token_data(amount, token_symbol)}, - {"capacity": 300 * 100_000_000, "lock": always_success_lock(), "type": cellscript_type, "data": scoped_timelock_data(owner, 0, unlock_height, created_at)}, - ], - cell_deps, - ) - inputs = initial["cells"][0] - action_cell_deps = [cell_dep_for(initial["cells"][1])] + cell_deps - outputs = [ - {"capacity": hex_u64(300 * 100_000_000), "lock": cellscript_lock, "type": locked_asset_type}, - {"capacity": hex_u64(700 * 100_000_000), "lock": always_success_lock(), "type": None}, - ] - witness = [entry_witness()] - valid_tx = transaction(inputs, outputs, ["0x" + locked_asset_payload.hex(), "0x"], action_cell_deps, witness) - malformed_tx = transaction(inputs, outputs, ["0x" + malformed_locked_asset_payload.hex(), "0x"], action_cell_deps, witness) - elif action == "request_release": - unlock_height = 0 - current_height = 0 - created_at = 0 - lock_hash = scoped_lock_id() - request_type = always_success_lock("0x21") - initial = create_script_locked_cells( - "timelock.request_release", - [ - {"capacity": 1000 * 100_000_000, "lock": cellscript_lock, "type": None, "data": b""}, - {"capacity": 300 * 100_000_000, "lock": always_success_lock(), "type": cellscript_type, "data": scoped_timelock_data(owner, 0, unlock_height, created_at)}, - ], - cell_deps, - ) - input_cell = initial["cells"][0] - action_cell_deps = [cell_dep_for(initial["cells"][1])] + cell_deps - outputs = [ - {"capacity": hex_u64(300 * 100_000_000), "lock": cellscript_lock, "type": request_type}, - {"capacity": hex_u64(700 * 100_000_000), "lock": always_success_lock(), "type": None}, - ] - witness = [entry_witness(owner)] - valid_tx = transaction(input_cell, outputs, ["0x" + release_request_data(lock_hash, owner, current_height, state=flow_state).hex(), "0x"], action_cell_deps, witness, [timepoint_header]) - malformed_tx = transaction(input_cell, outputs, ["0x" + release_request_data(lock_hash, owner, current_height + 1, state=flow_state).hex(), "0x"], action_cell_deps, witness, [timepoint_header]) - elif action == "request_emergency_release": - unlock_height = 500 - current_height = 0 - created_at = 0 - lock_hash = scoped_lock_id() - reason_payload = molecule_bytes(b"emergency release") - emergency_type = always_success_lock("0x22") - initial = create_script_locked_cells( - "timelock.request_emergency_release", - [ - {"capacity": 1000 * 100_000_000, "lock": cellscript_lock, "type": None, "data": b""}, - {"capacity": 300 * 100_000_000, "lock": always_success_lock(), "type": cellscript_type, "data": scoped_timelock_data(owner, 0, unlock_height, created_at)}, - ], - cell_deps, - ) - emergency_payload = emergency_release_molecule_data(lock_hash, owner, reason_payload, current_height, []) if original_scoped else emergency_release_data(lock_hash, owner, current_height, 0) - malformed_emergency_payload = emergency_release_molecule_data(lock_hash, owner, reason_payload, current_height + 1, []) if original_scoped else emergency_release_data(lock_hash, owner, current_height, 1) - inputs = initial["cells"][0] - action_cell_deps = [cell_dep_for(initial["cells"][1])] + cell_deps - outputs = [ - {"capacity": hex_u64(300 * 100_000_000), "lock": cellscript_lock, "type": emergency_type}, - {"capacity": hex_u64(700 * 100_000_000), "lock": always_success_lock(), "type": None}, - ] - witness = [entry_witness(owner, molecule_bytes(reason_payload))] if original_scoped else [entry_witness(lock_hash, owner)] - valid_tx = transaction(inputs, outputs, ["0x" + emergency_payload.hex(), "0x"], action_cell_deps, witness, [timepoint_header]) - malformed_tx = transaction(inputs, outputs, ["0x" + malformed_emergency_payload.hex(), "0x"], action_cell_deps, witness, [timepoint_header]) - elif action == "approve_emergency_release": - lock_hash = scoped_lock_id() - requester = bytes([0x41]) * 32 - requested_at = 120 - initial_approvals = 1 - existing_approver = bytes([0x42]) * 32 - reason_payload = molecule_bytes(b"emergency release") - emergency_type = always_success_lock("0x23") - input_payload = emergency_release_molecule_data(lock_hash, requester, reason_payload, requested_at, [existing_approver]) if original_scoped else emergency_release_data(lock_hash, requester, requested_at, initial_approvals) - output_payload = emergency_release_molecule_data(lock_hash, requester, reason_payload, requested_at, [existing_approver, owner]) if original_scoped else emergency_release_data(lock_hash, requester, requested_at, initial_approvals + 1) - malformed_output_payload = emergency_release_molecule_data(lock_hash, requester, reason_payload, requested_at, [existing_approver]) if original_scoped else emergency_release_data(lock_hash, requester, requested_at, initial_approvals) - initial = create_script_locked_cells( - "timelock.approve_emergency_release", - [{"capacity": 1000 * 100_000_000, "lock": cellscript_lock, "type": emergency_type, "data": input_payload}], - cell_deps, - ) - input_cell = initial["cells"][0] - outputs = [{"capacity": hex_u64(1000 * 100_000_000), "lock": cellscript_lock, "type": emergency_type}] - witness = [entry_witness(owner)] - valid_tx = transaction(input_cell, outputs, ["0x" + output_payload.hex()], cell_deps, witness) - malformed_tx = transaction(input_cell, outputs, ["0x" + malformed_output_payload.hex()], cell_deps, witness) - elif action == "extend_lock": - current_height = 0 - initial_unlock_height = 100 - additional_period = 10 - created_at = 0 - initial = create_script_locked_cells( - "timelock.extend_lock", - [{"capacity": 1000 * 100_000_000, "lock": cellscript_lock, "type": cellscript_type, "data": scoped_timelock_data(owner, 0, initial_unlock_height, created_at)}], - cell_deps, - ) - input_cell = initial["cells"][0] - outputs = [{"capacity": hex_u64(1000 * 100_000_000), "lock": cellscript_lock, "type": cellscript_type}] - witness = [entry_witness(additional_period, owner)] - valid_tx = transaction(input_cell, outputs, ["0x" + scoped_timelock_data(owner, 0, initial_unlock_height + additional_period, created_at).hex()], cell_deps, witness, [timepoint_header]) - malformed_tx = transaction(input_cell, outputs, ["0x" + scoped_timelock_data(owner, 0, initial_unlock_height + additional_period + 1, created_at).hex()], cell_deps, witness, [timepoint_header]) - elif action == "execute_release": - unlock_height = 0 - current_height = 0 - created_at = 0 - lock_hash = scoped_lock_id() - token_symbol = b"TOKEN001" - time_lock_type = always_success_lock("0x01") - locked_asset_type = always_success_lock("0x02") - release_request_type = always_success_lock("0x03") - release_record_type = always_success_lock("0x04") - released_token_type = always_success_lock("0x05") - locked_asset_payload = locked_asset_data(token_symbol, 42, lock_hash) - initial = create_script_locked_cells( - "timelock.execute_release", - [ - {"capacity": 300 * 100_000_000, "lock": cellscript_lock, "type": time_lock_type, "data": scoped_timelock_data(owner, 0, unlock_height, created_at)}, - {"capacity": 300 * 100_000_000, "lock": cellscript_lock, "type": locked_asset_type, "data": locked_asset_payload}, - {"capacity": 300 * 100_000_000, "lock": cellscript_lock, "type": release_request_type, "data": release_request_data(lock_hash, owner, 0, state=flow_state)}, - ], - cell_deps, - ) - outputs = [ - {"capacity": hex_u64(300 * 100_000_000), "lock": cellscript_lock, "type": released_token_type}, - {"capacity": hex_u64(300 * 100_000_000), "lock": cellscript_lock, "type": release_record_type}, - ] - witness = [entry_witness(owner), "0x", "0x"] - valid_tx = transaction(initial["cells"], outputs, ["0x" + token_data(42, token_symbol).hex(), "0x" + release_record_data(lock_hash, current_height, owner).hex()], cell_deps, witness, [timepoint_header]) - malformed_tx = transaction(initial["cells"], outputs, ["0x" + token_data(43, token_symbol).hex(), "0x" + release_record_data(lock_hash, current_height, owner).hex()], cell_deps, witness, [timepoint_header]) - elif action == "execute_emergency_release": - unlock_height = 500 - current_height = 0 - created_at = 0 - lock_hash = scoped_lock_id() - token_symbol = b"TOKEN001" - time_lock_type = always_success_lock("0x11") - locked_asset_type = always_success_lock("0x12") - emergency_type = always_success_lock("0x13") - release_record_type = always_success_lock("0x14") - released_token_type = always_success_lock("0x15") - reason_payload = molecule_bytes(b"emergency release") - locked_asset_payload = locked_asset_data(token_symbol, 42, lock_hash) - emergency_payload = emergency_release_molecule_data(lock_hash, owner, reason_payload, 0, [bytes([0x42]) * 32, bytes([0x43]) * 32]) if original_scoped else emergency_release_data(lock_hash, owner, 0, 2) - initial = create_script_locked_cells( - "timelock.execute_emergency_release", - [ - {"capacity": 300 * 100_000_000, "lock": cellscript_lock, "type": time_lock_type, "data": scoped_timelock_data(owner, 0, unlock_height, created_at)}, - {"capacity": 300 * 100_000_000, "lock": cellscript_lock, "type": locked_asset_type, "data": locked_asset_payload}, - {"capacity": 300 * 100_000_000, "lock": cellscript_lock, "type": emergency_type, "data": emergency_payload}, - ], - cell_deps, - ) - outputs = [ - {"capacity": hex_u64(300 * 100_000_000), "lock": cellscript_lock, "type": released_token_type}, - {"capacity": hex_u64(300 * 100_000_000), "lock": cellscript_lock, "type": release_record_type}, - ] - witness = [entry_witness(owner), "0x", "0x"] - valid_tx = transaction(initial["cells"], outputs, ["0x" + token_data(42, token_symbol).hex(), "0x" + release_record_data(lock_hash, current_height, owner).hex()], cell_deps, witness, [timepoint_header]) - malformed_tx = transaction(initial["cells"], outputs, ["0x" + token_data(43, token_symbol).hex(), "0x" + release_record_data(lock_hash, current_height, owner).hex()], cell_deps, witness, [timepoint_header]) - elif action == "batch_create_locks": - current_height = 0 - owners = [owner, bytes([0x51]) * 32, bytes([0x52]) * 32, bytes([0x53]) * 32] - lock_ids = [lock_id, bytes([0x61]) * 32, bytes([0x62]) * 32, bytes([0x63]) * 32] - unlock_heights = [100, 110, 120, 130] - initial = create_script_locked_cells( - "timelock.batch_create_locks", - [{"capacity": 1500 * 100_000_000, "lock": cellscript_lock, "type": None, "data": b""}], - cell_deps, - ) - input_cell = initial["cells"][0] - outputs = [ - {"capacity": hex_u64(300 * 100_000_000), "lock": cellscript_lock, "type": cellscript_type}, - {"capacity": hex_u64(300 * 100_000_000), "lock": cellscript_lock, "type": cellscript_type}, - {"capacity": hex_u64(300 * 100_000_000), "lock": cellscript_lock, "type": cellscript_type}, - {"capacity": hex_u64(300 * 100_000_000), "lock": cellscript_lock, "type": cellscript_type}, - ] - outputs_data = [ - "0x" + timelock_data(owners[0], 0, unlock_heights[0], current_height, lock_id=lock_ids[0] if original_scoped else None).hex(), - "0x" + timelock_data(owners[1], 0, unlock_heights[1], current_height, lock_id=lock_ids[1] if original_scoped else None).hex(), - "0x" + timelock_data(owners[2], 0, unlock_heights[2], current_height, lock_id=lock_ids[2] if original_scoped else None).hex(), - "0x" + timelock_data(owners[3], 0, unlock_heights[3], current_height, lock_id=lock_ids[3] if original_scoped else None).hex(), - ] - witness = [entry_witness(fixed_hash_array4(lock_ids), fixed_address_array4(owners), fixed_u64_array4(unlock_heights))] if original_scoped else [entry_witness(fixed_address_array4(owners), fixed_u64_array4(unlock_heights))] - valid_tx = transaction(input_cell, outputs, outputs_data, cell_deps, witness, [timepoint_header]) - malformed_outputs_data = list(outputs_data) - malformed_outputs_data[1] = "0x" + timelock_data(owners[1], 0, unlock_heights[1] + 1, current_height, lock_id=lock_ids[1] if original_scoped else None).hex() - malformed_tx = transaction(input_cell, outputs, malformed_outputs_data, cell_deps, witness, [timepoint_header]) - else: - raise RuntimeError(f"unsupported TimeLock action harness: {action}") - - return { - "builder_name": "timelock-action-builder-v1", - "initial": initial, - "valid_tx": valid_tx, - "malformed_tx": malformed_tx, - } - -def run_timelock_action(action_record, always_success_dep): - action = action_record["action"] - name = action_record["name"] - code = deploy_code_cell(name, action_record["artifact"], always_success_dep) - cellscript_lock = {"code_hash": code["artifact_ckb_data_hash_blake2b"], "hash_type": "data1", "args": "0x"} - cellscript_type = always_success_lock() - owner = decode_hex(script_hash(cellscript_lock), 32) - cell_deps = [always_success_dep, code["code_cell_dep"]] - - result = { - "action": action, - "name": name, - "harness_origin": "timelock-action-builder-v1", - "builder_backed": True, - "artifact": action_record["artifact"], - "code": code, - "cellscript_lock_hash": script_hash(cellscript_lock), - "owner": "0x" + owner.hex(), - } - timelock_case = build_timelock_action_case(action_record, cellscript_lock, cellscript_type, owner, cell_deps) - initial = timelock_case["initial"] - valid_tx = timelock_case["valid_tx"] - malformed_tx = timelock_case["malformed_tx"] - result["builder_name"] = timelock_case["builder_name"] - - malformed_rejection = expect_dry_run_rejected( - malformed_tx, - f"{name} malformed action transaction", - ("Script", "script", "ValidationFailure", "error code", "VM", "Run result", "Invalid"), - ) - for index, cell in enumerate(initial["cells"]): - assert_live(cell["tx_hash"], cell["index"], f"{name} input cell {index} after malformed transaction") - - valid_dry_run = rpc("dry_run_transaction", [valid_tx]) - commit = submit_and_commit(valid_tx, f"{name} valid action transaction") - output_live = [ - assert_live(commit["tx_hash"], index, f"{name} valid output {index}").get("status") == "live" - for index in range(len(valid_tx["outputs"])) - ] - result.update({ - "initial_cells": initial, - "malformed_transaction": malformed_rejection, - "valid_dry_run": valid_dry_run, - "measured_constraints": measure_release_constraints(valid_tx, valid_dry_run), - "valid_commit": commit, - "valid_outputs_live": output_live, - "status": "passed", - }) - return result - -def action_record_by(records, action): - for record in records: - if record.get("action") == action: - return record - raise RuntimeError(f"missing action artifact for stateful scenario: {action}") - -def deploy_stateful_action(record, always_success_dep): - code = deploy_code_cell(f"stateful.{record['name']}", record["artifact"], always_success_dep) - lock_script = { - "code_hash": code["artifact_ckb_data_hash_blake2b"], - "hash_type": "data1", - "args": "0x", - } - return { - "action": record["action"], - "name": record["name"], - "record": record, - "code": code, - "lock": lock_script, - "lock_hash": decode_hex(script_hash(lock_script), 32), - "cell_deps": [always_success_dep, code["code_cell_dep"]], - } - -def output_cell_from_tx(commit, tx, index): - output = tx["outputs"][index] - return { - "tx_hash": commit["tx_hash"], - "index": index, - "capacity": parse_hex_u64(output["capacity"]), - "lock": output["lock"], - "type": output.get("type"), - "data_hex": tx["outputs_data"][index], - } - -def assert_not_live(tx_hash, index, label): - result = rpc("get_live_cell", [out_point(tx_hash, index), True]) - if result and result.get("status") == "live": - raise RuntimeError(f"{label} is still live after stateful spend: {result}") - return result - -def assert_stateful_step_constraints(label, constraints): - failures = [] - if constraints.get("consensus_serialized_tx_size_bytes") is None: - failures.append("consensus tx size was not measured") - if constraints.get("occupied_capacity_shannons") is None: - failures.append("occupied capacity was not derived") - if constraints.get("capacity_is_sufficient") is not True: - failures.append( - "outputs are under-capacity" - if constraints.get("capacity_is_sufficient") is False - else "capacity sufficiency was not measured" - ) - if failures: - detail = { - "label": label, - "failures": failures, - "tx_measure_error": constraints.get("tx_measure_error"), - "under_capacity_output_indexes": constraints.get("under_capacity_output_indexes"), - } - raise RuntimeError("stateful step constraint measurement failed: " + json.dumps(detail, sort_keys=True)) - -def run_stateful_step(scenario, step, tx, consumed_cells=None, live_output_indexes=None): - consumed_cells = consumed_cells or [] - live_output_indexes = list(range(len(tx["outputs"]))) if live_output_indexes is None else live_output_indexes - dry_run = rpc("dry_run_transaction", [tx]) - constraints = measure_release_constraints(tx, dry_run) - assert_stateful_step_constraints(f"{scenario}.{step}", constraints) - commit = submit_and_commit(tx, f"stateful {scenario}.{step}") - consumed = [ - assert_not_live(cell["tx_hash"], cell["index"], f"stateful {scenario}.{step} consumed input {index}") - for index, cell in enumerate(consumed_cells) - ] - outputs_live = { - str(index): assert_live(commit["tx_hash"], index, f"stateful {scenario}.{step} output {index}").get("status") == "live" - for index in live_output_indexes - } - return { - "step": step, - "dry_run": dry_run, - "commit": commit, - "measured_constraints": constraints, - "consumed_inputs": consumed, - "outputs_live": outputs_live, - "status": "passed", - } - -def action_example(record): - example = record.get("example") - if example: - return pathlib.Path(example).name - original_source = record.get("original_source") or record.get("source") - if original_source: - return pathlib.Path(original_source).name - name = record.get("name", "") - for row in (report.get("ckb_business_coverage") or {}).get("rows", []): - candidate = row.get("example", "") - if candidate.removesuffix(".cell") in name: - return candidate - raise RuntimeError(f"cannot determine example for action record: {record}") - -def action_id(record_or_action): - record = record_or_action.get("record", record_or_action) - return f"{action_example(record)}:{record['action']}" - -def action_ids(records_or_actions): - return [action_id(record_or_action) for record_or_action in records_or_actions] - -def expected_stateful_action_ids(): - coverage_rows = (report.get("ckb_business_coverage") or {}).get("rows", []) - if not coverage_rows: - raise RuntimeError("acceptance report does not contain CKB business coverage rows") - return sorted( - f"{example}:{action}" - for row in coverage_rows - for example in [row["example"]] - for action in (row.get("strict_ckb_actions") or row.get("source_actions") or []) - ) - -def all_stateful_action_records(): - records = ( - token_action_artifacts - + nft_action_artifacts - + timelock_action_artifacts - + multisig_action_artifacts - + vesting_action_artifacts - + amm_action_artifacts - + launch_action_artifacts - ) - by_id = {} - for record in records: - by_id.setdefault(action_id(record), record) - return [by_id[action] for action in sorted(by_id)] - -def consumed_cells_from_tx(tx): - consumed = [] - for tx_input in tx.get("inputs", []): - previous_output = tx_input["previous_output"] - consumed.append({ - "tx_hash": previous_output["tx_hash"], - "index": parse_hex_u64(previous_output["index"]), - }) - return consumed - -def build_stateful_action_branch_case(record, always_success_dep): - deployed = deploy_stateful_action(record, always_success_dep) - cellscript_lock = deployed["lock"] - cell_deps = deployed["cell_deps"] - example = action_example(record) - - if example == "token.cell": - cellscript_type = always_success_lock() - destination_lock = always_success_lock() - case = build_token_action_case( - record["action"], - cellscript_lock, - cellscript_type, - destination_lock, - decode_hex(script_hash(destination_lock), 32), - b"TOKEN001", - cell_deps, - ) - elif example == "nft.cell": - destination_lock = always_success_lock() - destination_owner = decode_hex(script_hash(destination_lock), 32) - case = build_nft_action_case( - record, - cellscript_lock, - always_success_lock(), - destination_lock, - decode_hex(script_hash(cellscript_lock), 32), - destination_owner, - bytes(range(32)), - destination_owner, - always_success_lock("0x21"), - always_success_lock("0x22"), - always_success_lock("0x23"), - always_success_lock("0x24"), - cell_deps, - ) - elif example == "timelock.cell": - owner = decode_hex(script_hash(cellscript_lock), 32) - case = build_timelock_action_case(record, cellscript_lock, always_success_lock(), owner, cell_deps) - elif example == "multisig.cell": - case = build_multisig_action_case( - record, - cellscript_lock, - always_success_lock("0x51"), - always_success_lock("0x52"), - always_success_lock("0x53"), - always_success_lock("0x54"), - decode_hex(script_hash(cellscript_lock), 32), - decode_hex(script_hash(always_success_lock("0x55")), 32), - decode_hex(script_hash(always_success_lock("0x56")), 32), - decode_hex(script_hash(always_success_lock("0x57")), 32), - bytes(32), - cell_deps, - ) - elif example == "vesting.cell": - admin_lock = always_success_lock() - case = build_vesting_action_case( - record, - cellscript_lock, - admin_lock, - always_success_lock("0x41"), - decode_hex(script_hash(admin_lock), 32), - b"VEST0001", - 10, - 100, - True, - cell_deps, - ) - elif example == "amm_pool.cell": - case = build_amm_action_case(record, cellscript_lock, always_success_lock(), cell_deps) - elif example == "launch.cell": - action = record["action"] - symbol = b"LAUNCH01" - max_supply = 10_000 - initial_mint = 1_000 - pool_seed_amount = 500 - paired_amount = 250 - paired_symbol = b"PAIR0001" - fee_rate_bps = 30 - creator_lock = always_success_lock("0x60") - recipient_amounts = [10, 20, 30, 40] if action == "launch_token" else [10, 20] - recipient_locks = [always_success_lock("0x7" + format(index, "x")) for index in range(len(recipient_amounts))] - recipients = [ - (decode_hex(script_hash(lock), 32), amount) - for lock, amount in zip(recipient_locks, recipient_amounts) - ] - case = build_launch_action_case( - record, - cellscript_lock, - always_success_lock("0x61"), - always_success_lock("0x62"), - always_success_lock("0x63"), - always_success_lock("0x64"), - always_success_lock("0x65"), - symbol, - max_supply, - initial_mint, - pool_seed_amount, - paired_amount, - paired_symbol, - fee_rate_bps, - creator_lock, - decode_hex(script_hash(creator_lock), 32), - recipient_locks, - recipients, - fixed_recipient_tuple_array4(recipients) if action == "launch_token" else fixed_recipient_tuple_array(recipients), - sum(amount for _, amount in recipients), - cell_deps, - ) - else: - raise RuntimeError(f"unsupported stateful action branch example: {example}") - - return { - "record": record, - "deployed_action": deployed, - "initial": case["initial"], - "builder_name": case["builder_name"], - "valid_tx": case["valid_tx"], - } - -def run_stateful_action_branch(record, always_success_dep): - case = build_stateful_action_branch_case(record, always_success_dep) - coverage_id = action_id(record) - scenario = coverage_id.replace(":", ".") + ".stateful-branch" - try: - step = run_stateful_step( - scenario, - "valid_action_branch", - case["valid_tx"], - consumed_cells_from_tx(case["valid_tx"]), - ) - except Exception as error: - raise RuntimeError(f"stateful action branch failed for {coverage_id}: {error}") from error - return { - "name": scenario, - "kind": "stateful-action-branch", - "builder_backed": True, - "builder_name": case["builder_name"], - "actions": [record["action"]], - "action_ids": [coverage_id], - "initial_cells": case["initial"], - "steps": [step], - "status": "passed", - } - -def run_stateful_action_branch_coverage(always_success_dep, required_records, already_covered): - branch_runs = [] - for record in required_records: - if action_id(record) in already_covered: - continue - branch_runs.append(run_stateful_action_branch(record, always_success_dep)) - return branch_runs - -def run_stateful_token_lifecycle(always_success_dep): - scenario = "token.mint-with-authority-transfer-mint-with-authority-merge-burn" - actions = { - name: deploy_stateful_action(action_record_by(token_action_artifacts, name), always_success_dep) - for name in ("mint_with_authority", "transfer_token", "merge", "burn") - } - token_type = always_success_lock("0xa1") - token_symbol = b"STATE001" - steps = [] - - initial = create_script_locked_cells( - "stateful.token.auth", - [{ - "capacity": 700 * 100_000_000, - "lock": actions["mint_with_authority"]["lock"], - "type": token_type, - "data": mint_authority_data(token_symbol, 1000, 0), - }], - actions["mint_with_authority"]["cell_deps"], - ) - auth0 = initial["cells"][0] - tx1 = transaction( - auth0, - [ - {"capacity": hex_u64(600 * 100_000_000), "lock": actions["mint_with_authority"]["lock"], "type": token_type}, - {"capacity": hex_u64(100 * 100_000_000), "lock": actions["transfer_token"]["lock"], "type": token_type}, - ], - [ - "0x" + mint_authority_data(token_symbol, 1000, 5).hex(), - "0x" + token_data(5, token_symbol).hex(), - ], - actions["mint_with_authority"]["cell_deps"], - [entry_witness(actions["transfer_token"]["lock_hash"], 5)], - ) - step = run_stateful_step(scenario, "mint_first_token_to_transfer", tx1, [auth0]) - steps.append(step) - auth1 = output_cell_from_tx(step["commit"], tx1, 0) - token_a = output_cell_from_tx(step["commit"], tx1, 1) - - tx2 = transaction( - token_a, - [{"capacity": hex_u64(100 * 100_000_000), "lock": actions["merge"]["lock"], "type": token_type}], - ["0x" + token_data(5, token_symbol).hex()], - actions["transfer_token"]["cell_deps"], - [entry_witness(actions["merge"]["lock_hash"])], - ) - step = run_stateful_step(scenario, "transfer_first_token_to_merge", tx2, [token_a]) - steps.append(step) - token_a_for_merge = output_cell_from_tx(step["commit"], tx2, 0) - - tx3 = transaction( - auth1, - [ - {"capacity": hex_u64(500 * 100_000_000), "lock": actions["mint_with_authority"]["lock"], "type": token_type}, - {"capacity": hex_u64(100 * 100_000_000), "lock": actions["merge"]["lock"], "type": token_type}, - ], - [ - "0x" + mint_authority_data(token_symbol, 1000, 12).hex(), - "0x" + token_data(7, token_symbol).hex(), - ], - actions["mint_with_authority"]["cell_deps"], - [entry_witness(actions["merge"]["lock_hash"], 7)], - ) - step = run_stateful_step(scenario, "mint_second_token_to_merge", tx3, [auth1]) - steps.append(step) - auth2 = output_cell_from_tx(step["commit"], tx3, 0) - token_b_for_merge = output_cell_from_tx(step["commit"], tx3, 1) - - tx4 = transaction( - [token_a_for_merge, token_b_for_merge], - [{"capacity": hex_u64(200 * 100_000_000), "lock": actions["burn"]["lock"], "type": token_type}], - ["0x" + token_data(12, token_symbol).hex()], - actions["merge"]["cell_deps"], - [entry_witness(actions["burn"]["lock_hash"]), "0x"], - ) - step = run_stateful_step(scenario, "merge_tokens_to_burn", tx4, [token_a_for_merge, token_b_for_merge]) - steps.append(step) - merged_token = output_cell_from_tx(step["commit"], tx4, 0) - - tx5 = transaction( - merged_token, - [{"capacity": hex_u64(200 * 100_000_000), "lock": always_success_lock(), "type": None}], - ["0x"], - actions["burn"]["cell_deps"], - [entry_witness()], - ) - step = run_stateful_step(scenario, "burn_merged_token", tx5, [merged_token]) - steps.append(step) - - auth2_live = assert_live(auth2["tx_hash"], auth2["index"], f"stateful {scenario} final mint authority").get("status") == "live" - return { - "name": scenario, - "kind": "stateful-scenario", - "builder_backed": True, - "builder_name": "cellscript-stateful-scenario-builder-v1", - "actions": list(actions.keys()), - "action_ids": action_ids(actions.values()), - "steps": steps, - "final_live_cells": {"mint_authority": auth2_live}, - "status": "passed", - } - -def run_stateful_timelock_release(always_success_dep): - scenario = "timelock.create-lock-lock-asset-request-release-execute" - actions = { - name: deploy_stateful_action(action_record_by(timelock_action_artifacts, name), always_success_dep) - for name in ("create_absolute_lock", "lock_asset", "request_release", "execute_release") - } - time_lock_type = always_success_lock("0xb1") - locked_asset_type = always_success_lock("0xb2") - request_type = always_success_lock("0xb3") - record_type = always_success_lock("0xb4") - token_type = always_success_lock("0xb5") - owner = actions["execute_release"]["lock_hash"] - lock_id = decode_hex(script_hash(time_lock_type), 32) - token_symbol = b"TOKEN001" - current_height = 0 - unlock_height = 11 - create_header = get_block_by_number(0)["header"]["hash"] - steps = [] - - initial = create_script_locked_cells( - "stateful.timelock.create", - [{"capacity": 500 * 100_000_000, "lock": actions["create_absolute_lock"]["lock"], "type": None, "data": b""}], - actions["create_absolute_lock"]["cell_deps"], - ) - create_input = initial["cells"][0] - tx1 = transaction( - create_input, - [{"capacity": hex_u64(300 * 100_000_000), "lock": actions["execute_release"]["lock"], "type": time_lock_type}], - ["0x" + timelock_data(owner, 0, unlock_height, current_height, lock_id=lock_id).hex()], - actions["create_absolute_lock"]["cell_deps"], - [entry_witness(lock_id, owner, unlock_height)], - [create_header], - ) - step = run_stateful_step(scenario, "create_absolute_lock_for_release", tx1, [create_input]) - steps.append(step) - time_lock_cell = output_cell_from_tx(step["commit"], tx1, 0) - time_lock_dep = cell_dep_for(time_lock_cell) - - lock_asset_initial = create_script_locked_cells( - "stateful.timelock.lock_asset", - [{"capacity": 1000 * 100_000_000, "lock": actions["lock_asset"]["lock"], "type": token_type, "data": token_data(42, token_symbol)}], - actions["lock_asset"]["cell_deps"], - ) - lock_asset_input = lock_asset_initial["cells"][0] - tx2 = transaction( - lock_asset_input, - [ - {"capacity": hex_u64(300 * 100_000_000), "lock": actions["execute_release"]["lock"], "type": locked_asset_type}, - {"capacity": hex_u64(700 * 100_000_000), "lock": always_success_lock(), "type": None}, - ], - [ - "0x" + locked_asset_data(token_symbol, 42, lock_id).hex(), - "0x", - ], - [time_lock_dep] + actions["lock_asset"]["cell_deps"], - [entry_witness()], - ) - step = run_stateful_step(scenario, "lock_asset_against_live_lock", tx2, [lock_asset_input]) - steps.append(step) - locked_asset_cell = output_cell_from_tx(step["commit"], tx2, 0) - - request_initial = create_script_locked_cells( - "stateful.timelock.request_release", - [{"capacity": 1000 * 100_000_000, "lock": actions["request_release"]["lock"], "type": None, "data": b""}], - actions["request_release"]["cell_deps"], - ) - request_input = request_initial["cells"][0] - release_timepoint = wait_header_epoch_at_least(unlock_height) - release_height = release_timepoint["epoch_number"] - tx3 = transaction( - request_input, - [ - {"capacity": hex_u64(300 * 100_000_000), "lock": actions["execute_release"]["lock"], "type": request_type}, - {"capacity": hex_u64(700 * 100_000_000), "lock": always_success_lock(), "type": None}, - ], - [ - "0x" + release_request_data(lock_id, owner, release_height, state=0).hex(), - "0x", - ], - [time_lock_dep] + actions["request_release"]["cell_deps"], - [entry_witness(owner)], - [release_timepoint["hash"]], - ) - step = run_stateful_step(scenario, "request_release_from_live_lock", tx3, [request_input]) - steps.append(step) - request_cell = output_cell_from_tx(step["commit"], tx3, 0) - - tx4 = transaction( - [time_lock_cell, locked_asset_cell, request_cell], - [ - {"capacity": hex_u64(300 * 100_000_000), "lock": actions["execute_release"]["lock"], "type": token_type}, - {"capacity": hex_u64(300 * 100_000_000), "lock": always_success_lock(), "type": record_type}, - ], - [ - "0x" + token_data(42, token_symbol).hex(), - "0x" + release_record_data(lock_id, release_height, owner).hex(), - ], - actions["execute_release"]["cell_deps"], - [entry_witness(owner), "0x", "0x"], - [release_timepoint["hash"]], - ) - step = run_stateful_step(scenario, "execute_release_from_live_cells", tx4, [time_lock_cell, locked_asset_cell, request_cell]) - steps.append(step) - - return { - "name": scenario, - "kind": "stateful-scenario", - "builder_backed": True, - "builder_name": "cellscript-stateful-scenario-builder-v1", - "actions": list(actions.keys()), - "action_ids": action_ids(actions.values()), - "steps": steps, - "status": "passed", - } - -def run_stateful_nft_listing_sale(always_success_dep): - scenario = "nft.mint-list-transfer-by-listing" - actions = { - name: deploy_stateful_action(action_record_by(nft_action_artifacts, name), always_success_dep) - for name in ("create_collection", "mint", "create_listing", "buy_from_listing") - } - collection_type = always_success_lock("0xc1") - nft_type = always_success_lock("0xc2") - listing_type = always_success_lock("0xc3") - royalty_payment_type = always_success_lock("0xc4") - seller = actions["buy_from_listing"]["lock_hash"] - buyer_lock = always_success_lock("0xc5") - buyer = decode_hex(script_hash(buyer_lock), 32) - collection_creator = actions["mint"]["lock_hash"] - royalty_recipient = collection_creator - collection_id = decode_hex(script_hash(collection_type), 32) - collection_name = b"Stateful Collection" - collection_symbol = b"SNFT" - collection_base_uri = b"ckb://cellscript/stateful-nft/" - max_supply = 200 - metadata_hash = bytes([0x33]) * 32 - token_id = 1 - price = 10_000 - royalty_amount = 250 - seller_amount = price - royalty_amount - created_at = 0 - timepoint_header = get_block_by_number(0)["header"]["hash"] - payment_symbol = b"PAYM0001" - steps = [] - - initial = create_script_locked_cells( - "stateful.nft.collection_seed", - [{ - "capacity": 900 * 100_000_000, - "lock": actions["create_collection"]["lock"], - "type": None, - "data": b"", - }], - actions["create_collection"]["cell_deps"], - ) - collection_seed = initial["cells"][0] - tx1 = transaction( - collection_seed, - [{"capacity": hex_u64(800 * 100_000_000), "lock": actions["mint"]["lock"], "type": collection_type}], - ["0x" + collection_molecule_data(collection_creator, 0, max_supply, collection_name, collection_symbol, collection_base_uri).hex()], - actions["create_collection"]["cell_deps"], - [ - entry_witness( - collection_creator, - max_supply, - molecule_string_witness(collection_name), - molecule_string_witness(collection_symbol), - molecule_string_witness(collection_base_uri), - ) - ], - ) - step = run_stateful_step(scenario, "create_collection_for_live_mint", tx1, [collection_seed]) - steps.append(step) - collection0 = output_cell_from_tx(step["commit"], tx1, 0) - - tx2 = transaction( - collection0, - [ - {"capacity": hex_u64(500 * 100_000_000), "lock": actions["mint"]["lock"], "type": collection_type}, - {"capacity": hex_u64(300 * 100_000_000), "lock": actions["buy_from_listing"]["lock"], "type": nft_type}, - ], - [ - "0x" + collection_molecule_data(collection_creator, token_id, max_supply, collection_name, collection_symbol, collection_base_uri).hex(), - "0x" + nft_data(token_id, seller, metadata_hash, royalty_recipient, 250, collection_id).hex(), - ], - actions["mint"]["cell_deps"], - [entry_witness(seller, metadata_hash)], - ) - step = run_stateful_step(scenario, "mint_nft_for_listing_sale", tx2, [collection0]) - steps.append(step) - nft_for_sale = output_cell_from_tx(step["commit"], tx2, 1) - nft_dep = cell_dep_for(nft_for_sale) - - listing_initial = create_script_locked_cells( - "stateful.nft.create_listing", - [{"capacity": 500 * 100_000_000, "lock": actions["create_listing"]["lock"], "type": None, "data": b""}], - actions["create_listing"]["cell_deps"], - ) - listing_input = listing_initial["cells"][0] - tx3 = transaction( - listing_input, - [ - {"capacity": hex_u64(300 * 100_000_000), "lock": actions["buy_from_listing"]["lock"], "type": listing_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": always_success_lock(), "type": None}, - ], - [ - "0x" + listing_data(token_id, seller, price, created_at, state=0, collection_id=collection_id).hex(), - "0x", - ], - [nft_dep] + actions["create_listing"]["cell_deps"], - [entry_witness(price)], - [timepoint_header], - ) - step = run_stateful_step(scenario, "create_listing_from_live_nft_dep", tx3, [listing_input]) - steps.append(step) - listing = output_cell_from_tx(step["commit"], tx3, 0) - - payment_initial = create_script_locked_cells( - "stateful.nft.listing_payment_tokens", - [ - {"capacity": 200 * 100_000_000, "lock": actions["buy_from_listing"]["lock"], "type": royalty_payment_type, "data": token_data(royalty_amount, payment_symbol)}, - {"capacity": 200 * 100_000_000, "lock": actions["buy_from_listing"]["lock"], "type": royalty_payment_type, "data": token_data(seller_amount, payment_symbol)}, - ], - actions["buy_from_listing"]["cell_deps"], - ) - sale_cells_by_binding = { - "nft_before": nft_for_sale, - "listing": listing, - "royalty_payment": payment_initial["cells"][0], - "seller_payment": payment_initial["cells"][1], - } - sale_input_bindings = action_runtime_input_bindings(actions["buy_from_listing"]["record"]) - if set(sale_input_bindings) != set(sale_cells_by_binding): - raise RuntimeError( - "stateful NFT listing-sale inputs do not match compiler metadata: " - f"builder={sorted(sale_cells_by_binding)} metadata={sale_input_bindings}" - ) - sale_inputs = [sale_cells_by_binding[binding] for binding in sale_input_bindings] - tx4 = transaction( - sale_inputs, - [ - {"capacity": hex_u64(300 * 100_000_000), "lock": buyer_lock, "type": nft_type}, - {"capacity": hex_u64(150 * 100_000_000), "lock": actions["mint"]["lock"], "type": royalty_payment_type}, - {"capacity": hex_u64(150 * 100_000_000), "lock": actions["buy_from_listing"]["lock"], "type": royalty_payment_type}, - ], - [ - "0x" + nft_data(token_id, buyer, metadata_hash, royalty_recipient, 250, collection_id).hex(), - "0x" + token_data(royalty_amount, payment_symbol).hex(), - "0x" + token_data(seller_amount, payment_symbol).hex(), - ], - actions["buy_from_listing"]["cell_deps"], - [entry_witness(buyer), "0x", "0x", "0x"], - ) - step = run_stateful_step(scenario, "buy_listing_from_live_nft_and_listing", tx4, sale_inputs) - steps.append(step) - - return { - "name": scenario, - "kind": "stateful-scenario", - "builder_backed": True, - "builder_name": "cellscript-stateful-scenario-builder-v1", - "actions": list(actions.keys()), - "action_ids": action_ids(actions.values()), - "steps": steps, - "status": "passed", - } - -def run_stateful_launch_to_token_mint(always_success_dep): - scenario = "launch.launch-token-then-mint-with-authority" - launch = deploy_stateful_action(action_record_by(launch_action_artifacts, "launch_token"), always_success_dep) - mint = deploy_stateful_action(action_record_by(token_action_artifacts, "mint_with_authority"), always_success_dep) - actions = {"launch_token": launch, "mint_with_authority": mint} - auth_type = always_success_lock("0x91") - token_type = always_success_lock("0x92") - pool_paired_type = always_success_lock("0x93") - pool_type = always_success_lock("0x94") - lp_type = always_success_lock("0x95") - symbol = b"LAUNCH01" - paired_symbol = b"PAIR0001" - max_supply = 10_000 - initial_mint = 1_000 - extra_mint = 25 - pool_seed_amount = 500 - paired_amount = 250 - fee_rate_bps = 30 - creator = mint["lock_hash"] - recipient_locks = [always_success_lock("0xa" + format(index, "x")) for index in range(4)] - recipients = [ - (decode_hex(script_hash(lock), 32), amount) - for lock, amount in zip(recipient_locks, [10, 20, 30, 40]) - ] - recipient_payload = fixed_recipient_tuple_array4(recipients) - total_distributed = sum(amount for _, amount in recipients) - remaining = initial_mint - total_distributed - pool_seed_amount - pool_id = decode_hex(script_hash(pool_type), 32) - token_type_hash = decode_hex(script_hash(token_type), 32) - paired_type_hash = decode_hex(script_hash(pool_paired_type), 32) - initial_lp = math.isqrt(pool_seed_amount * paired_amount) - steps = [] - - initial = create_script_locked_cells( - "stateful.launch.paired_token", - [{ - "capacity": 4000 * 100_000_000, - "lock": launch["lock"], - "type": pool_paired_type, - "data": token_data(paired_amount, paired_symbol), - }], - launch["cell_deps"], - ) - paired_input = initial["cells"][0] - outputs = [{"capacity": hex_u64(400 * 100_000_000), "lock": mint["lock"], "type": auth_type}] - outputs_data = ["0x" + mint_authority_data(symbol, max_supply, initial_mint).hex()] - for recipient_lock, (_, amount) in zip(recipient_locks, recipients): - outputs.append({"capacity": hex_u64(200 * 100_000_000), "lock": recipient_lock, "type": token_type}) - outputs_data.append("0x" + token_data(amount, symbol).hex()) - outputs.append({"capacity": hex_u64(400 * 100_000_000), "lock": always_success_lock(), "type": pool_type}) - outputs_data.append("0x" + pool_data(symbol, paired_symbol, pool_seed_amount, paired_amount, initial_lp, fee_rate_bps, token_type_hash, paired_type_hash).hex()) - outputs.append({"capacity": hex_u64(200 * 100_000_000), "lock": mint["lock"], "type": lp_type}) - outputs_data.append("0x" + lp_receipt_data(pool_id, initial_lp, creator).hex()) - outputs.append({"capacity": hex_u64(200 * 100_000_000), "lock": mint["lock"], "type": token_type}) - outputs_data.append("0x" + token_data(remaining, symbol).hex()) - - tx1 = transaction( - paired_input, - outputs, - outputs_data, - launch["cell_deps"], - [entry_witness(symbol, max_supply, initial_mint, pool_seed_amount, bytes([fee_rate_bps & 0xff, fee_rate_bps >> 8]), creator, recipient_payload)], - ) - step = run_stateful_step(scenario, "launch_token_to_live_mint_authority", tx1, [paired_input]) - steps.append(step) - auth_for_mint = output_cell_from_tx(step["commit"], tx1, 0) - - to_lock = always_success_lock("0xa4") - to = decode_hex(script_hash(to_lock), 32) - tx2 = transaction( - auth_for_mint, - [ - {"capacity": hex_u64(300 * 100_000_000), "lock": mint["lock"], "type": auth_type}, - {"capacity": hex_u64(100 * 100_000_000), "lock": to_lock, "type": token_type}, - ], - [ - "0x" + mint_authority_data(symbol, max_supply, initial_mint + extra_mint).hex(), - "0x" + token_data(extra_mint, symbol).hex(), - ], - mint["cell_deps"], - [entry_witness(to, extra_mint)], - ) - step = run_stateful_step(scenario, "mint_with_authority_again_from_launched_authority", tx2, [auth_for_mint]) - steps.append(step) - - return { - "name": scenario, - "kind": "stateful-scenario", - "builder_backed": True, - "builder_name": "cellscript-stateful-scenario-builder-v1", - "actions": list(actions.keys()), - "action_ids": action_ids(actions.values()), - "steps": steps, - "status": "passed", - } - -def run_stateful_amm_pool_lifecycle(always_success_dep): - scenario = "amm.seed-add-swap-remove" - actions = { - name: deploy_stateful_action(action_record_by(amm_action_artifacts, name), always_success_dep) - for name in ("seed_pool", "add_liquidity", "swap_a_for_b", "remove_liquidity") - } - token_a_symbol = b"AMMA0001" - token_b_symbol = b"AMMB0001" - token_a_type = always_success_lock("0xd1") - token_b_type = always_success_lock("0xd2") - token_a_type_hash = decode_hex(script_hash(token_a_type), 32) - token_b_type_hash = decode_hex(script_hash(token_b_type), 32) - pool_type = always_success_lock("0xd3") - lp_type = always_success_lock("0xd4") - provider_lock = actions["remove_liquidity"]["lock"] - provider = actions["remove_liquidity"]["lock_hash"] - pool_id = decode_hex(script_hash(pool_type), 32) - fee_rate_bps = 30 - steps = [] - - seed_initial = create_script_locked_cells( - "stateful.amm.seed_inputs", - [ - {"capacity": 200 * 100_000_000, "lock": actions["seed_pool"]["lock"], "type": token_a_type, "data": token_data(4, token_a_symbol)}, - {"capacity": 200 * 100_000_000, "lock": actions["seed_pool"]["lock"], "type": token_b_type, "data": token_data(9, token_b_symbol)}, - ], - actions["seed_pool"]["cell_deps"], - ) - tx1 = transaction( - seed_initial["cells"], - [ - {"capacity": hex_u64(200 * 100_000_000), "lock": actions["add_liquidity"]["lock"], "type": pool_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": provider_lock, "type": lp_type}, - ], - [ - "0x" + pool_data(token_a_symbol, token_b_symbol, 4, 9, 6, fee_rate_bps, token_a_type_hash, token_b_type_hash).hex(), - "0x" + lp_receipt_data(pool_id, 6, provider).hex(), - ], - actions["seed_pool"]["cell_deps"], - [entry_witness(fee_rate_bps.to_bytes(2, "little"), provider), "0x"], - ) - step = run_stateful_step(scenario, "seed_pool_for_add_liquidity", tx1, seed_initial["cells"]) - steps.append(step) - pool_for_add = output_cell_from_tx(step["commit"], tx1, 0) - - add_tokens = create_script_locked_cells( - "stateful.amm.add_liquidity_tokens", - [ - {"capacity": 200 * 100_000_000, "lock": actions["add_liquidity"]["lock"], "type": token_a_type, "data": token_data(4, token_a_symbol)}, - {"capacity": 200 * 100_000_000, "lock": actions["add_liquidity"]["lock"], "type": token_b_type, "data": token_data(9, token_b_symbol)}, - ], - actions["add_liquidity"]["cell_deps"], - ) - add_inputs = [pool_for_add, *add_tokens["cells"]] - tx2 = transaction( - add_inputs, - [ - {"capacity": hex_u64(200 * 100_000_000), "lock": actions["swap_a_for_b"]["lock"], "type": pool_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": actions["remove_liquidity"]["lock"], "type": lp_type}, - ], - [ - "0x" + pool_data(token_a_symbol, token_b_symbol, 8, 18, 12, fee_rate_bps, token_a_type_hash, token_b_type_hash).hex(), - "0x" + lp_receipt_data(pool_id, 6, provider).hex(), - ], - actions["add_liquidity"]["cell_deps"], - [entry_witness(provider), "0x", "0x"], - ) - step = run_stateful_step(scenario, "add_liquidity_to_live_pool", tx2, add_inputs) - steps.append(step) - pool_for_swap = output_cell_from_tx(step["commit"], tx2, 0) - receipt_for_remove = output_cell_from_tx(step["commit"], tx2, 1) - - swap_token = create_script_locked_cells( - "stateful.amm.swap_token", - [{"capacity": 200 * 100_000_000, "lock": actions["swap_a_for_b"]["lock"], "type": token_a_type, "data": token_data(2, token_a_symbol)}], - actions["swap_a_for_b"]["cell_deps"], - ) - swap_inputs = [pool_for_swap, swap_token["cells"][0]] - to_lock = always_success_lock("0xd5") - to = decode_hex(script_hash(to_lock), 32) - tx3 = transaction( - swap_inputs, - [ - {"capacity": hex_u64(200 * 100_000_000), "lock": actions["remove_liquidity"]["lock"], "type": pool_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": to_lock, "type": token_b_type}, - ], - [ - "0x" + pool_data(token_a_symbol, token_b_symbol, 10, 15, 12, fee_rate_bps, token_a_type_hash, token_b_type_hash).hex(), - "0x" + token_data(3, token_b_symbol).hex(), - ], - actions["swap_a_for_b"]["cell_deps"], - [entry_witness(2, to), "0x"], - ) - step = run_stateful_step(scenario, "swap_against_live_pool", tx3, swap_inputs) - steps.append(step) - pool_for_remove = output_cell_from_tx(step["commit"], tx3, 0) - - remove_funding = find_spendable_cellbase() - remove_change_capacity = remove_funding["capacity"] - 200 * 100_000_000 - tx4 = transaction( - [pool_for_remove, receipt_for_remove, remove_funding], - [ - {"capacity": hex_u64(200 * 100_000_000), "lock": always_success_lock(), "type": pool_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": provider_lock, "type": token_a_type}, - {"capacity": hex_u64(200 * 100_000_000), "lock": provider_lock, "type": token_b_type}, - {"capacity": hex_u64(remove_change_capacity), "lock": always_success_lock(), "type": None}, - ], - [ - "0x" + pool_data(token_a_symbol, token_b_symbol, 5, 8, 6, fee_rate_bps, token_a_type_hash, token_b_type_hash).hex(), - "0x" + token_data(5, token_a_symbol).hex(), - "0x" + token_data(7, token_b_symbol).hex(), - "0x", - ], - actions["remove_liquidity"]["cell_deps"], - [entry_witness(provider), "0x", "0x"], - ) - step = run_stateful_step(scenario, "remove_liquidity_from_live_pool", tx4, [pool_for_remove, receipt_for_remove, remove_funding]) - steps.append(step) - - return { - "name": scenario, - "kind": "stateful-scenario", - "builder_backed": True, - "builder_name": "cellscript-stateful-scenario-builder-v1", - "actions": list(actions.keys()), - "action_ids": action_ids(actions.values()), - "steps": steps, - "status": "passed", - } - -def run_stateful_vesting_revoke(always_success_dep): - scenario = "vesting.create-config-grant-revoke" - actions = { - name: deploy_stateful_action(action_record_by(vesting_action_artifacts, name), always_success_dep) - for name in ("create_vesting_config", "grant_vesting", "revoke_grant") - } - symbol = b"VEST0001" - cliff_period = 10 - total_period = 100 - amount = 77 - config_type = always_success_lock("0x41") - token_type = always_success_lock("0x44") - grant_type = always_success_lock("0x43") - admin_lock = always_success_lock() - admin = decode_hex(script_hash(admin_lock), 32) - beneficiary = actions["revoke_grant"]["lock_hash"] - header_dep = get_block_by_number(0)["header"]["hash"] - steps = [] - - config_initial = create_script_locked_cells( - "stateful.vesting.config_input", - [{"capacity": 1000 * 100_000_000, "lock": actions["create_vesting_config"]["lock"], "type": None, "data": b""}], - actions["create_vesting_config"]["cell_deps"], - ) - config_input = config_initial["cells"][0] - tx1 = transaction( - config_input, - [{"capacity": hex_u64(300 * 100_000_000), "lock": admin_lock, "type": config_type}], - ["0x" + vesting_config_data(admin, symbol, cliff_period, total_period, True).hex()], - actions["create_vesting_config"]["cell_deps"], - [entry_witness(admin, symbol, cliff_period, total_period, bytes([1]))], - ) - step = run_stateful_step(scenario, "create_config_for_grant", tx1, [config_input]) - steps.append(step) - config_cell = output_cell_from_tx(step["commit"], tx1, 0) - config_dep = cell_dep_for(config_cell) - - grant_initial = create_script_locked_cells( - "stateful.vesting.grant_tokens", - [{"capacity": 200 * 100_000_000, "lock": actions["grant_vesting"]["lock"], "type": token_type, "data": token_data(amount, symbol)}], - actions["grant_vesting"]["cell_deps"], - ) - grant_input = grant_initial["cells"][0] - funding_input = find_spendable_cellbase() - grant_change_capacity = grant_input["capacity"] + funding_input["capacity"] - 300 * 100_000_000 - tx2 = transaction( - [grant_input, funding_input], - [ - {"capacity": hex_u64(300 * 100_000_000), "lock": actions["revoke_grant"]["lock"], "type": grant_type}, - {"capacity": hex_u64(grant_change_capacity), "lock": always_success_lock(), "type": None}, - ], - [ - "0x" + vesting_grant_data(0, beneficiary, amount, 0, 0, cliff_period, total_period, symbol).hex(), - "0x", - ], - [config_dep] + actions["grant_vesting"]["cell_deps"], - [entry_witness(beneficiary), "0x"], - [header_dep], - ) - step = run_stateful_step(scenario, "grant_vesting_from_live_config", tx2, [grant_input, funding_input]) - steps.append(step) - grant_cell = output_cell_from_tx(step["commit"], tx2, 0) - - tx3 = transaction( - grant_cell, - [ - {"capacity": hex_u64(150 * 100_000_000), "lock": actions["revoke_grant"]["lock"], "type": token_type}, - {"capacity": hex_u64(150 * 100_000_000), "lock": admin_lock, "type": token_type}, - ], - [ - "0x" + token_data(0, symbol).hex(), - "0x" + token_data(amount, symbol).hex(), - ], - [config_dep] + actions["revoke_grant"]["cell_deps"], - [entry_witness(admin)], - [header_dep], - ) - step = run_stateful_step(scenario, "revoke_live_grant", tx3, [grant_cell]) - steps.append(step) - - return { - "name": scenario, - "kind": "stateful-scenario", - "builder_backed": True, - "builder_name": "cellscript-stateful-scenario-builder-v1", - "actions": list(actions.keys()), - "action_ids": action_ids(actions.values()), - "steps": steps, - "status": "passed", - } - -def run_stateful_multisig_execution(always_success_dep): - scenario = "multisig.create-propose-approve-approve-execute" - actions = { - name: deploy_stateful_action(action_record_by(multisig_action_artifacts, name), always_success_dep) - for name in ("create_wallet", "propose_transfer", "record_approval", "execute_proposal") - } - wallet_type = always_success_lock("0xf1") - proposal_type = always_success_lock("0xf2") - confirmation_type = always_success_lock("0xf3") - execution_type = always_success_lock("0xf4") - signer_a = actions["propose_transfer"]["lock_hash"] - signer_b = decode_hex(script_hash(always_success_lock("0xf5")), 32) - target = decode_hex(script_hash(always_success_lock("0xf6")), 32) - wallet_id = decode_hex(script_hash(wallet_type), 32) - signers = [signer_a, signer_b] - proposal_id = 1 - created_at = 20 - expires_at = created_at + 1440 - steps = [] - - wallet_initial = create_script_locked_cells( - "stateful.multisig.wallet_input", - [{"capacity": 2000 * 100_000_000, "lock": actions["create_wallet"]["lock"], "type": None, "data": b""}], - actions["create_wallet"]["cell_deps"], - ) - wallet_input = wallet_initial["cells"][0] - tx1 = transaction( - wallet_input, - [{"capacity": hex_u64(2000 * 100_000_000), "lock": actions["propose_transfer"]["lock"], "type": wallet_type}], - ["0x" + multisig_wallet_molecule_data(wallet_id, signers, 2, 0, 10).hex()], - actions["create_wallet"]["cell_deps"], - [entry_witness(wallet_id, molecule_bytes(molecule_fixvec(signers)), bytes([2]), 10)], - ) - step = run_stateful_step(scenario, "create_wallet_for_proposal", tx1, [wallet_input]) - steps.append(step) - wallet_for_propose = output_cell_from_tx(step["commit"], tx1, 0) - - proposal_payload = multisig_proposal_molecule_data( - wallet_id, proposal_id, signer_a, 0, target, 500, b"", [], 2, created_at, expires_at - ) - wallet_after_payload = multisig_wallet_molecule_data(wallet_id, signers, 2, proposal_id, 10) - tx2 = transaction( - wallet_for_propose, - [ - {"capacity": hex_u64(500 * 100_000_000), "lock": actions["propose_transfer"]["lock"], "type": wallet_type}, - {"capacity": hex_u64(1500 * 100_000_000), "lock": actions["record_approval"]["lock"], "type": proposal_type}, - ], - ["0x" + wallet_after_payload.hex(), "0x" + proposal_payload.hex()], - actions["propose_transfer"]["cell_deps"], - [entry_witness(signer_a, target, 500, created_at)], - ) - step = run_stateful_step(scenario, "propose_transfer_from_live_wallet", tx2, [wallet_for_propose]) - steps.append(step) - wallet_dep_cell = output_cell_from_tx(step["commit"], tx2, 0) - wallet_dep = cell_dep_for(wallet_dep_cell) - proposal0 = output_cell_from_tx(step["commit"], tx2, 1) - - proposal1_payload = multisig_proposal_molecule_data( - wallet_id, proposal_id, signer_a, 0, target, 500, b"", [signer_a], 2, created_at, expires_at - ) - tx3 = transaction( - proposal0, - [ - {"capacity": hex_u64(1200 * 100_000_000), "lock": actions["record_approval"]["lock"], "type": proposal_type}, - {"capacity": hex_u64(300 * 100_000_000), "lock": always_success_lock(), "type": confirmation_type}, - ], - [ - "0x" + proposal1_payload.hex(), - "0x" + approval_confirmation_data(proposal_id, signer_a, 30).hex(), - ], - [wallet_dep] + actions["record_approval"]["cell_deps"], - [entry_witness(signer_a, 30)], - ) - step = run_stateful_step(scenario, "record_first_approval", tx3, [proposal0]) - steps.append(step) - proposal1 = output_cell_from_tx(step["commit"], tx3, 0) - - proposal2_payload = multisig_proposal_molecule_data( - wallet_id, proposal_id, signer_a, 0, target, 500, b"", [signer_a, signer_b], 2, created_at, expires_at - ) - tx4 = transaction( - proposal1, - [ - {"capacity": hex_u64(900 * 100_000_000), "lock": actions["execute_proposal"]["lock"], "type": proposal_type}, - {"capacity": hex_u64(300 * 100_000_000), "lock": always_success_lock(), "type": confirmation_type}, - ], - [ - "0x" + proposal2_payload.hex(), - "0x" + approval_confirmation_data(proposal_id, signer_b, 31).hex(), - ], - [wallet_dep] + actions["record_approval"]["cell_deps"], - [entry_witness(signer_b, 31)], - ) - step = run_stateful_step(scenario, "record_second_approval", tx4, [proposal1]) - steps.append(step) - proposal2 = output_cell_from_tx(step["commit"], tx4, 0) - - tx5 = transaction( - proposal2, - [{"capacity": hex_u64(400 * 100_000_000), "lock": always_success_lock(), "type": execution_type}], - ["0x" + execution_record_data(proposal_id, signer_a, 40, 1).hex()], - [wallet_dep] + actions["execute_proposal"]["cell_deps"], - [entry_witness(signer_a, 40)], - ) - step = run_stateful_step(scenario, "execute_approved_proposal", tx5, [proposal2]) - steps.append(step) - - return { - "name": scenario, - "kind": "stateful-scenario", - "builder_backed": True, - "builder_name": "cellscript-stateful-scenario-builder-v1", - "actions": list(actions.keys()), - "action_ids": action_ids(actions.values()), - "steps": steps, - "status": "passed", - } - -def run_stateful_scenario_suite(always_success_dep): - required_records = all_stateful_action_records() - required_ids = sorted(action_id(record) for record in required_records) - expected_ids = expected_stateful_action_ids() - missing_artifact_ids = sorted(set(expected_ids) - set(required_ids)) - unexpected_artifact_ids = sorted(set(required_ids) - set(expected_ids)) - if missing_artifact_ids: - raise RuntimeError("stateful action artifacts missing: " + ", ".join(missing_artifact_ids)) - - main_runs = [ - run_stateful_token_lifecycle(always_success_dep), - run_stateful_nft_listing_sale(always_success_dep), - run_stateful_timelock_release(always_success_dep), - run_stateful_launch_to_token_mint(always_success_dep), - run_stateful_amm_pool_lifecycle(always_success_dep), - run_stateful_vesting_revoke(always_success_dep), - run_stateful_multisig_execution(always_success_dep), - ] - covered_ids = set() - for run in main_runs: - covered_ids.update(run.get("action_ids", [])) - branch_runs = run_stateful_action_branch_coverage(always_success_dep, required_records, covered_ids) - runs = main_runs + branch_runs - for run in runs: - run["acceptance_harness_name"] = run.get("builder_name") - run["harness_origin"] = "handwritten-python-acceptance-transaction" - run["transaction_origin"] = "acceptance-python-harness" - run["builder_backed"] = False - for run in branch_runs: - covered_ids.update(run.get("action_ids", [])) - missing_stateful_action_ids = sorted(set(required_ids) - covered_ids) - if missing_stateful_action_ids: - raise RuntimeError("stateful action coverage missing: " + ", ".join(missing_stateful_action_ids)) - - return { - "status": "passed", - "scope": ( - "Strict stateful local CKB scenarios. End-to-end flows commit live output handoffs between " - "related actions; branch scenarios then commit every remaining production acceptance action." - ), - "scenario_count": len(runs), - "step_count": sum(len(run.get("steps", [])) for run in runs), - "end_to_end_scenario_count": len(main_runs), - "action_branch_scenario_count": len(branch_runs), - "stateful_action_coverage": { - "status": "passed", - "required_action_count": len(required_ids), - "covered_action_count": len(covered_ids), - "required_action_ids": required_ids, - "covered_action_ids": sorted(covered_ids), - "missing_action_ids": missing_stateful_action_ids, - "missing_artifact_ids": missing_artifact_ids, - "unexpected_artifact_ids": unexpected_artifact_ids, - }, - "runs": runs, - } - -try: - tip_before = rpc("get_tip_header") - genesis = get_block_by_number(0) - genesis_cellbase_hash = genesis["transactions"][0]["hash"] - always_success_dep = { - "out_point": out_point(genesis_cellbase_hash, int(ALWAYS_SUCCESS_INDEX, 16)), - "dep_type": "code", - } - report["onchain"].update({ - "tip_before": tip_before, - "genesis_hash": genesis["header"]["hash"], - "genesis_cellbase_hash": genesis_cellbase_hash, - }) - report["ckb_runtime_provenance"]["genesis_hash"] = genesis["header"]["hash"] - write_report() - - for artifact_record in bundled_example_deployment_artifacts: - deployment_result = run_bundled_example_deployment(artifact_record, always_success_dep) - report["onchain"]["bundled_example_deployment_runs"].append(deployment_result) - report["onchain"]["completed_bundled_example_deployments"] = len( - report["onchain"]["bundled_example_deployment_runs"] - ) - write_report() - - for artifact_record in artifacts: - artifact_result = run_artifact(artifact_record, always_success_dep) - report["onchain"]["artifact_runs"].append(artifact_result) - report["onchain"]["completed_artifacts"] = len(report["onchain"]["artifact_runs"]) - write_report() - - for action_record in token_action_artifacts: - action_result = run_token_action(action_record, always_success_dep) - report["onchain"]["token_action_runs"].append(action_result) - report["onchain"]["completed_token_actions"] = len(report["onchain"]["token_action_runs"]) - write_report() - - for action_record in nft_action_artifacts: - action_result = run_nft_action(action_record, always_success_dep) - report["onchain"]["nft_action_runs"].append(action_result) - report["onchain"]["completed_nft_actions"] = len(report["onchain"]["nft_action_runs"]) - write_report() - - for action_record in timelock_action_artifacts: - action_result = run_timelock_action(action_record, always_success_dep) - report["onchain"]["timelock_action_runs"].append(action_result) - report["onchain"]["completed_timelock_actions"] = len(report["onchain"]["timelock_action_runs"]) - write_report() - - for action_record in multisig_action_artifacts: - action_result = run_multisig_action(action_record, always_success_dep) - report["onchain"]["multisig_action_runs"].append(action_result) - report["onchain"]["completed_multisig_actions"] = len(report["onchain"]["multisig_action_runs"]) - write_report() - - for action_record in vesting_action_artifacts: - action_result = run_vesting_action(action_record, always_success_dep) - report["onchain"]["vesting_action_runs"].append(action_result) - report["onchain"]["completed_vesting_actions"] = len(report["onchain"]["vesting_action_runs"]) - write_report() - - for action_record in amm_action_artifacts: - action_result = run_amm_action(action_record, always_success_dep) - report["onchain"]["amm_action_runs"].append(action_result) - report["onchain"]["completed_amm_actions"] = len(report["onchain"]["amm_action_runs"]) - write_report() - - for action_record in launch_action_artifacts: - action_result = run_launch_action(action_record, always_success_dep) - report["onchain"]["launch_action_runs"].append(action_result) - report["onchain"]["completed_launch_actions"] = len(report["onchain"]["launch_action_runs"]) - write_report() - - for lock_record in original_scoped_lock_artifacts: - lock_result = run_lock_spend_matrix(lock_record, always_success_dep) - report["onchain"]["lock_spend_matrix_runs"].append(lock_result) - report["onchain"]["completed_lock_spend_matrix"] = len(report["onchain"]["lock_spend_matrix_runs"]) - write_report() - - if run_stateful_scenarios: - stateful_result = run_stateful_scenario_suite(always_success_dep) - report["onchain"]["stateful_scenarios"] = stateful_result - report["onchain"]["stateful_scenario_runs"] = stateful_result["runs"] - write_report() - - tip_after = rpc("get_tip_header") - report["onchain"]["tip_after"] = tip_after - expected_artifact_count = len(artifacts) - completed_artifact_names = [ - run["name"] - for run in report["onchain"]["artifact_runs"] - if run.get("status") == "passed" - and run.get("code_cell_live") is True - and run.get("locked_cell_live") is True - and run.get("locked_cell_live_after_malformed_spend") is True - and run.get("spend_recipient_live") is True - ] - report["onchain"]["bundled_examples_deployed_and_spent"] = [ - run["name"] for run in report["onchain"]["artifact_runs"] if run["kind"].startswith("bundled-example-") - ] - report["onchain"]["bundled_examples_deployed"] = [ - run["name"] - for run in report["onchain"]["bundled_example_deployment_runs"] - if run.get("status") == "passed" and run.get("code_cell_live") is True - ] - report["onchain"]["all_bundled_examples_deployed"] = ( - report["onchain"]["bundled_examples_deployed"] == report["bundled_examples_exact_order"] - ) - report["onchain"]["all_artifacts_deployed_and_spent"] = ( - len(completed_artifact_names) == expected_artifact_count - and len(report["onchain"]["artifact_runs"]) == expected_artifact_count - ) - report["onchain"]["token_actions_exercised"] = [run["action"] for run in report["onchain"]["token_action_runs"]] - report["onchain"]["all_token_actions_exercised"] = sorted(report["onchain"]["token_actions_exercised"]) == [ - "burn", - "merge", - "mint_with_authority", - "transfer_token", - ] - report["onchain"]["nft_actions_exercised"] = [run["action"] for run in report["onchain"]["nft_action_runs"]] - report["onchain"]["all_nft_actions_exercised"] = sorted(report["onchain"]["nft_actions_exercised"]) == [ - "accept_offer", - "batch_mint", - "burn", - "buy_from_listing", - "cancel_listing", - "create_collection", - "create_listing", - "create_offer", - "mint", - "transfer", - ] - report["onchain"]["timelock_actions_exercised"] = [run["action"] for run in report["onchain"]["timelock_action_runs"]] - report["onchain"]["all_timelock_actions_exercised"] = report["onchain"]["timelock_actions_exercised"] == [ - "create_absolute_lock", - "create_relative_lock", - "lock_asset", - "request_release", - "request_emergency_release", - "approve_emergency_release", - "extend_lock", - "execute_release", - "execute_emergency_release", - "batch_create_locks", - ] - report["onchain"]["multisig_actions_exercised"] = [run["action"] for run in report["onchain"]["multisig_action_runs"]] - report["onchain"]["all_multisig_actions_exercised"] = sorted(report["onchain"]["multisig_actions_exercised"]) == [ - "cancel_proposal", - "create_wallet", - "execute_proposal", - "propose_add_signer", - "propose_change_threshold", - "propose_remove_signer", - "propose_transfer", - "record_approval", - ] - report["onchain"]["vesting_actions_exercised"] = [run["action"] for run in report["onchain"]["vesting_action_runs"]] - report["onchain"]["all_vesting_actions_exercised"] = report["onchain"]["vesting_actions_exercised"] == [ - "create_vesting_config", - "grant_vesting", - "claim_vested", - "claim_fully_vested", - "revoke_grant", - ] - report["onchain"]["amm_actions_exercised"] = [run["action"] for run in report["onchain"]["amm_action_runs"]] - report["onchain"]["all_amm_actions_exercised"] = sorted(report["onchain"]["amm_actions_exercised"]) == [ - "add_liquidity", - "remove_liquidity", - "seed_pool", - "swap_a_for_b", - ] - report["onchain"]["launch_actions_exercised"] = [run["action"] for run in report["onchain"]["launch_action_runs"]] - report["onchain"]["all_launch_actions_exercised"] = report["onchain"]["launch_actions_exercised"] == [ - "launch_token", - "bootstrap_token", - ] - all_action_runs = ( - report["onchain"]["token_action_runs"] - + report["onchain"]["nft_action_runs"] - + report["onchain"]["timelock_action_runs"] - + report["onchain"]["multisig_action_runs"] - + report["onchain"]["vesting_action_runs"] - + report["onchain"]["amm_action_runs"] - + report["onchain"]["launch_action_runs"] - ) - public_builder_action_ids = { - plan["contract_id"] - for contract in report["public_builder_contracts"]["contracts"] - for plan in contract["action_plans"] - if plan.get("status") == "passed" - } - for run in all_action_runs: - run["acceptance_harness_name"] = run.get("builder_name") - run["acceptance_harness_implementation"] = run.get("harness_origin") - run["harness_origin"] = "handwritten-python-acceptance-transaction" - run["transaction_origin"] = "acceptance-python-harness" - run["builder_backed"] = False - run["public_builder_contract_id"] = run["name"] - run["public_builder_contract_verified"] = run["name"] in public_builder_action_ids - report["onchain"]["builder_backed_action_count"] = 0 - report["onchain"]["acceptance_harness_action_count"] = len(all_action_runs) - report["onchain"]["public_builder_contract_action_count"] = sum( - 1 for run in all_action_runs if run.get("public_builder_contract_verified") - ) - report["onchain"]["measured_cycles_action_count"] = sum( - 1 - for run in all_action_runs - if ((run.get("measured_constraints") or {}).get("measured_cycles")) is not None - ) - report["onchain"]["tx_size_measured_action_count"] = sum( - 1 - for run in all_action_runs - if ((run.get("measured_constraints") or {}).get("consensus_serialized_tx_size_bytes")) is not None - ) - report["onchain"]["occupied_capacity_measured_action_count"] = sum( - 1 - for run in all_action_runs - if ((run.get("measured_constraints") or {}).get("occupied_capacity_shannons")) is not None - ) - all_lock_runs = report["onchain"]["lock_spend_matrix_runs"] - for run in all_lock_runs: - run["acceptance_harness_name"] = run.get("builder_name") - run["acceptance_harness_implementation"] = run.get("harness_origin") - run["harness_origin"] = "handwritten-python-acceptance-transaction" - run["transaction_origin"] = "acceptance-python-harness" - run["builder_backed"] = False - expected_lock_spend_count = len(original_scoped_lock_artifacts) - report["onchain"]["lock_spend_matrix_count"] = len(all_lock_runs) - report["onchain"]["builder_backed_lock_spend_matrix_count"] = 0 - report["onchain"]["acceptance_harness_lock_spend_matrix_count"] = len(all_lock_runs) - report["onchain"]["lock_valid_spend_count"] = sum( - 1 - for run in all_lock_runs - if (run.get("valid_spend") or {}).get("status") == "passed" - and (run.get("valid_spend") or {}).get("output_live") is True - ) - report["onchain"]["lock_invalid_spend_count"] = sum( - 1 - for run in all_lock_runs - if ((run.get("invalid_spend") or {}).get("rejection") or {}).get("expected_reason_matched") is True - and ((run.get("invalid_spend") or {}).get("rejection") or {}).get("policy_or_capacity_reason") is False - ) - report["onchain"]["measured_cycles_lock_count"] = sum( - 1 - for run in all_lock_runs - if ((run.get("measured_constraints") or {}).get("measured_cycles")) is not None - ) - report["onchain"]["tx_size_measured_lock_count"] = sum( - 1 - for run in all_lock_runs - if ((run.get("measured_constraints") or {}).get("consensus_serialized_tx_size_bytes")) is not None - ) - report["onchain"]["occupied_capacity_measured_lock_count"] = sum( - 1 - for run in all_lock_runs - if ((run.get("measured_constraints") or {}).get("occupied_capacity_shannons")) is not None - ) - report["onchain"]["locks_behavior_exercised"] = [run["name"] for run in all_lock_runs] - report["onchain"]["all_locks_behavior_exercised"] = ( - report["onchain"]["lock_spend_matrix_count"] == expected_lock_spend_count - and report["onchain"]["acceptance_harness_lock_spend_matrix_count"] == expected_lock_spend_count - and report["onchain"]["lock_valid_spend_count"] == expected_lock_spend_count - and report["onchain"]["lock_invalid_spend_count"] == expected_lock_spend_count - ) - report["onchain"]["resource_identity_evidence_scope"] = { - "status": "fixture-only", - "always_success_resource_types": True, - "production_resource_identity_proven": False, - "scope_note": ( - "Action/stateful harnesses use always_success fixture Type Scripts for resource cells. " - "They prove scoped verifier behavior and transaction shape, not production passive resource identity deployment." - ), - } - final_hardening_failures = [] - missing_public_builder_contracts = [ - run["name"] for run in all_action_runs if not run.get("public_builder_contract_verified") - ] - if missing_public_builder_contracts: - final_hardening_failures.append( - "public action-build/gen-builder contracts are missing for: " + ", ".join(missing_public_builder_contracts) - ) - missing_tx_size_actions = [ - run["name"] - for run in all_action_runs - if ((run.get("measured_constraints") or {}).get("consensus_serialized_tx_size_bytes")) is None - ] - if missing_tx_size_actions: - final_hardening_failures.append( - "consensus-serialized tx size is not yet measured for: " + ", ".join(missing_tx_size_actions) - ) - missing_occupied_capacity_actions = [ - run["name"] - for run in all_action_runs - if ((run.get("measured_constraints") or {}).get("occupied_capacity_shannons")) is None - ] - if missing_occupied_capacity_actions: - final_hardening_failures.append( - "exact occupied capacity is not yet derived for: " + ", ".join(missing_occupied_capacity_actions) - ) - under_capacity_actions = [ - f"{run['name']}@{(run.get('measured_constraints') or {}).get('under_capacity_output_indexes')}" - for run in all_action_runs - if ((run.get("measured_constraints") or {}).get("capacity_is_sufficient") is False) - ] - if under_capacity_actions: - final_hardening_failures.append( - "acceptance transactions contain under-capacity outputs: " + ", ".join(under_capacity_actions) - ) - missing_lock_matrix = [ - run["name"] - for run in all_lock_runs - if (run.get("valid_spend") or {}).get("status") != "passed" - or ((run.get("invalid_spend") or {}).get("rejection") or {}).get("expected_reason_matched") is not True - or ((run.get("invalid_spend") or {}).get("rejection") or {}).get("policy_or_capacity_reason") is not False - ] - if len(all_lock_runs) != expected_lock_spend_count or missing_lock_matrix: - final_hardening_failures.append( - "acceptance-harness lock valid/invalid spend matrix is incomplete: " - + ", ".join(missing_lock_matrix or [f"{len(all_lock_runs)}/{expected_lock_spend_count} locks"]) - ) - stateful_scenarios = report["onchain"].get("stateful_scenarios") - if run_stateful_scenarios: - stateful_coverage = (stateful_scenarios or {}).get("stateful_action_coverage") or {} - exact_stateful_action_ids = expected_stateful_action_ids() - if ( - not stateful_scenarios - or stateful_scenarios.get("status") != "passed" - or stateful_coverage.get("status") != "passed" - or stateful_coverage.get("required_action_ids") != exact_stateful_action_ids - or stateful_coverage.get("covered_action_ids") != exact_stateful_action_ids - or stateful_coverage.get("missing_action_ids") - or stateful_coverage.get("missing_artifact_ids") - or stateful_coverage.get("unexpected_artifact_ids") - ): - final_hardening_failures.append( - "stateful scenario coverage is incomplete: " - + json.dumps(stateful_coverage, sort_keys=True) - ) - missing_lock_tx_size = [ - run["name"] - for run in all_lock_runs - if ((run.get("measured_constraints") or {}).get("consensus_serialized_tx_size_bytes")) is None - ] - if missing_lock_tx_size: - final_hardening_failures.append( - "consensus-serialized tx size is not yet measured for lock spends: " + ", ".join(missing_lock_tx_size) - ) - under_capacity_locks = [ - f"{run['name']}@{(run.get('measured_constraints') or {}).get('under_capacity_output_indexes')}" - for run in all_lock_runs - if ((run.get("measured_constraints") or {}).get("capacity_is_sufficient") is False) - ] - if under_capacity_locks: - final_hardening_failures.append( - "acceptance lock spend transactions contain under-capacity outputs: " + ", ".join(under_capacity_locks) - ) - build_report_gate = refresh_build_report_deployments() - if build_report_gate.get("status") != "passed": - final_hardening_failures.append( - "build report live artifact linkage failed: " - + json.dumps( - { - "missing_onchain_deployments": build_report_gate.get("missing_onchain_deployments"), - "live_code_cell_data_hash_mismatches": build_report_gate.get("live_code_cell_data_hash_mismatches"), - "unexpected_onchain_artifacts": build_report_gate.get("unexpected_onchain_artifacts"), - }, - sort_keys=True, - ) - ) - report["final_production_hardening_gate"] = { - "status": "passed" if not final_hardening_failures else "blocked", - "ready": not final_hardening_failures, - "requires_builder_generated_transactions": False, - "requires_public_builder_contracts": True, - "requires_acceptance_harness_transactions": True, - "requires_measured_cycles": True, - "requires_consensus_serialized_tx_size": True, - "requires_exact_occupied_capacity": True, - "requires_stateful_action_coverage": report.get("acceptance_mode") == "production", - "production_resource_identity_claim": False, - "resource_identity_evidence_scope": "always-success-fixture-only", - "requires_build_report_live_artifact_linkage": True, - "failures": final_hardening_failures, - } - update_ckb_business_coverage({ - "token.cell": report["onchain"]["token_actions_exercised"], - "nft.cell": report["onchain"]["nft_actions_exercised"], - "timelock.cell": report["onchain"]["timelock_actions_exercised"], - "multisig.cell": report["onchain"]["multisig_actions_exercised"], - "vesting.cell": report["onchain"]["vesting_actions_exercised"], - "amm_pool.cell": report["onchain"]["amm_actions_exercised"], - "launch.cell": report["onchain"]["launch_actions_exercised"], - }) - missing_strict_original_deployments = sorted( - set(report["bundled_examples_exact_order"]) - set(report["onchain"]["bundled_examples_deployed"]) - ) - report["onchain"]["strict_original_bundled_deployment_gate"] = { - "status": "passed" if not missing_strict_original_deployments else "partial", - "deployed": report["onchain"]["bundled_examples_deployed"], - "missing": missing_strict_original_deployments, - "fatal_in_mode": report.get("acceptance_mode") == "production", - } - if report.get("acceptance_mode") == "production" and not report["onchain"]["all_bundled_examples_deployed"]: - raise RuntimeError( - "not all primitive-strict original bundled examples deployed: " - f"deployed={report['onchain']['bundled_examples_deployed']}, " - f"expected={report['bundled_examples_exact_order']}" - ) - if not report["onchain"]["all_artifacts_deployed_and_spent"]: - raise RuntimeError( - "not all CKB artifacts deployed and spent: " - f"completed={completed_artifact_names}, " - f"expected_artifact_count={expected_artifact_count}" - ) - if not report["onchain"]["all_token_actions_exercised"]: - raise RuntimeError(f"incomplete token action coverage: {report['onchain']['token_actions_exercised']}") - if not report["onchain"]["all_nft_actions_exercised"]: - raise RuntimeError(f"incomplete nft action coverage: {report['onchain']['nft_actions_exercised']}") - if not report["onchain"]["all_timelock_actions_exercised"]: - raise RuntimeError(f"incomplete timelock action coverage: {report['onchain']['timelock_actions_exercised']}") - if not report["onchain"]["all_multisig_actions_exercised"]: - raise RuntimeError(f"incomplete multisig action coverage: {report['onchain']['multisig_actions_exercised']}") - if not report["onchain"]["all_vesting_actions_exercised"]: - raise RuntimeError(f"incomplete vesting action coverage: {report['onchain']['vesting_actions_exercised']}") - if not report["onchain"]["all_amm_actions_exercised"]: - raise RuntimeError(f"incomplete AMM action coverage: {report['onchain']['amm_actions_exercised']}") - if not report["onchain"]["all_launch_actions_exercised"]: - raise RuntimeError(f"incomplete launch action coverage: {report['onchain']['launch_actions_exercised']}") - if not report["onchain"]["all_locks_behavior_exercised"]: - raise RuntimeError(f"incomplete lock behavior coverage: {report['onchain']['locks_behavior_exercised']}") - report["status"] = "passed" - report["onchain"]["status"] = "passed" - write_report() -except Exception as error: - report["status"] = "failed" - report["onchain"]["status"] = "failed" - report["onchain"]["error"] = str(error) - write_report() - raise -PY - -if [[ "$ACCEPTANCE_MODE" == "production" ]]; then - if [[ "$RUN_ONCHAIN" == "1" ]]; then - python3 "$REPO_ROOT/scripts/validate_ckb_cellscript_production_evidence.py" "$REPORT_JSON" - else - python3 "$REPO_ROOT/scripts/validate_ckb_cellscript_production_evidence.py" "$REPORT_JSON" --compile-only - echo "CKB compile-only production evidence is not sufficient for external release; run without --compile-only for final hardening." >&2 - fi -fi -echo "CKB CellScript $ACCEPTANCE_MODE acceptance passed: $REPORT_JSON" + -p cellscript-tools -- \ + --root "$REPO_ROOT" \ + ckb-acceptance "${args[@]}" diff --git a/scripts/evolving_dob_devnet_workflow.py b/scripts/evolving_dob_devnet_workflow.py deleted file mode 100644 index 1003bc28..00000000 --- a/scripts/evolving_dob_devnet_workflow.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -"""Run the evolving-DOB proposal devnet workflow gate.""" - -from __future__ import annotations - -import runpy -import sys -from pathlib import Path - - -SCRIPT = Path(__file__).resolve().parents[1] / "proposals/evolving-dob/evolving-dob-profile-v1/scripts/evolving_dob_devnet_workflow.py" - - -if __name__ == "__main__": - sys.argv[0] = str(SCRIPT) - runpy.run_path(str(SCRIPT), run_name="__main__") diff --git a/scripts/evolving_dob_registry_pressure.py b/scripts/evolving_dob_registry_pressure.py deleted file mode 100644 index efc7e688..00000000 --- a/scripts/evolving_dob_registry_pressure.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -"""Run the evolving-DOB proposal registry pressure gate.""" - -from __future__ import annotations - -import runpy -import sys -from pathlib import Path - - -SCRIPT = Path(__file__).resolve().parents[1] / "proposals/evolving-dob/evolving-dob-profile-v1/scripts/evolving_dob_registry_pressure.py" - - -if __name__ == "__main__": - sys.argv[0] = str(SCRIPT) - runpy.run_path(str(SCRIPT), run_name="__main__") diff --git a/scripts/novaseal_agreement_devnet_stateful_live.py b/scripts/novaseal_agreement_devnet_stateful_live.py deleted file mode 100644 index 9af11514..00000000 --- a/scripts/novaseal_agreement_devnet_stateful_live.py +++ /dev/null @@ -1,1476 +0,0 @@ -#!/usr/bin/env python3 -"""Run a live CKB devnet NovaSeal Agreement originate -> repay lifecycle.""" - -from __future__ import annotations - -import argparse -import json -import pathlib -import subprocess -import time -from typing import Any - -from novaseal_devnet_stateful_live import ( - RECEIPT_CAPACITY, - SHANNONS, - STATE_CAPACITY, - TEST_AUX_RAND, - TEST_SECRET_KEY, - ZERO_HASH, - CkbDevnet, - LiveAcceptanceError, - always_success_dep, - always_success_lock, - ckb_hash, - ckb_hash_hex, - cell_data_hash, - deploy_code_cell, - hex0x, - packed_hash, - resolve_ckb_bin, - schnorr_sign, - stateful_provenance, - transaction, - u8, - u16, - u32, - u64, - xonly_pubkey, -) - - -def packed_hash(type_name: str, packed: bytes) -> bytes: - del type_name - return cell_data_hash(packed) - - -AGREEMENT_VERSION = 0 -ASSET_KIND_CKB = 0 -EARLY_CLOSE_FIXED_FEE = 0 -STATUS_OFFERED = 0 -STATUS_ACTIVE = 1 -STATUS_REPAID = 2 -STATUS_DEFAULTED = 3 -PATH_ORIGINATE = 0 -PATH_REPAY_BEFORE_EXPIRY = 1 -PATH_CLAIM_AFTER_EXPIRY = 2 -PAYOUT_BORROWER_PRINCIPAL = 0 -PAYOUT_LENDER_REPAYMENT = 1 -PAYOUT_BORROWER_COLLATERAL_RETURN = 2 -PAYOUT_LENDER_DEFAULT_CLAIM = 3 -NATIVE_CKB_PAYOUT_OCCUPIED_CAPACITY = 300 * SHANNONS -LIVE_NATIVE_CKB_PAYOUT_CAPACITY_BASE = 300 * SHANNONS -LENDER_SECRET_KEY = bytes.fromhex("11" * 32) -LENDER_AUX_RAND = bytes([0x24]) * 32 - - -def parse_args() -> argparse.Namespace: - repo_root = pathlib.Path(__file__).resolve().parents[1] - default_ckb_repo = repo_root.parent / "ckb" - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--repo-root", type=pathlib.Path, default=repo_root) - parser.add_argument("--ckb-repo", type=pathlib.Path, default=default_ckb_repo) - parser.add_argument("--ckb-bin", type=pathlib.Path) - parser.add_argument( - "--output", - type=pathlib.Path, - default=repo_root / "target/novaseal-agreement-devnet-stateful-live.json", - ) - parser.add_argument("--run-dir", type=pathlib.Path) - parser.add_argument("--pretty", action="store_true") - parser.add_argument("--keep-node", action="store_true") - return parser.parse_args() - - -def epoch_number_from_header(header: dict[str, Any]) -> int: - # CKB encodes EpochNumberWithFraction as number:24 | index:16 | length:16. - return int(header["epoch"], 16) & ((1 << 24) - 1) - - -def pack_agreement_terms(terms: dict[str, Any]) -> bytes: - return ( - u16(terms["version"]) - + terms["agreement_id"] - + terms["terms_hash"] - + terms["borrower_authority_hash"] - + terms["lender_authority_hash"] - + u8(terms["collateral_asset_kind"]) - + terms["collateral_asset_hash"] - + u64(terms["collateral_amount"]) - + u8(terms["principal_asset_kind"]) - + terms["principal_asset_hash"] - + u64(terms["principal_amount"]) - + u64(terms["fixed_fee_amount"]) - + u64(terms["start_timepoint"]) - + u64(terms["expiry_timepoint"]) - + u8(terms["early_close_policy"]) - ) - - -def pack_agreement_cell(cell: dict[str, Any]) -> bytes: - return ( - u16(cell["version"]) - + cell["agreement_id"] - + cell["terms_hash"] - + cell["borrower_authority_hash"] - + cell["lender_authority_hash"] - + u8(cell["collateral_asset_kind"]) - + cell["collateral_asset_hash"] - + u64(cell["collateral_amount"]) - + u8(cell["principal_asset_kind"]) - + cell["principal_asset_hash"] - + u64(cell["principal_amount"]) - + u64(cell["fixed_fee_amount"]) - + u64(cell["expiry_timepoint"]) - + u8(cell["status"]) - + cell["latest_receipt_hash"] - + u64(cell["nonce"]) - ) - - -def pack_agreement_intent_core(core: dict[str, Any]) -> bytes: - return ( - u8(core["action"]) - + core["agreement_id"] - + core["terms_hash"] - + core["borrower_authority_hash"] - + core["lender_authority_hash"] - + u8(core["old_status"]) - + u8(core["new_status"]) - + u64(core["old_nonce"]) - + u64(core["new_nonce"]) - + u64(core["terminal_amount"]) - + core["payout_commitment_hash"] - + u64(core["expiry_timepoint"]) - ) - - -def pack_canonical_envelope(envelope: dict[str, Any]) -> bytes: - return ( - envelope["profile_id"] - + envelope["policy_hash"] - + u8(envelope["action"]) - + u8(envelope["terminal_path"]) - + envelope["subject_id"] - + envelope["old_state_commitment"] - + envelope["new_state_commitment"] - + u64(envelope["old_nonce"]) - + u64(envelope["new_nonce"]) - + u64(envelope["expiry"]) - + envelope["authority_hash"] - + envelope["profile_body_hash"] - + envelope["payout_commitment_hash"] - ) - - -def canonical_envelope_hash( - *, - action: int, - agreement_id: bytes, - terms_hash: bytes, - old_state_commitment: bytes, - new_state_commitment: bytes, - old_nonce: int, - new_nonce: int, - expiry: int, - authority_hash: bytes, - profile_body_hash: bytes, - payout_commitment_hash: bytes, -) -> bytes: - envelope = { - "profile_id": agreement_id, - "policy_hash": terms_hash, - "action": action, - "terminal_path": action, - "subject_id": agreement_id, - "old_state_commitment": old_state_commitment, - "new_state_commitment": new_state_commitment, - "old_nonce": old_nonce, - "new_nonce": new_nonce, - "expiry": expiry, - "authority_hash": authority_hash, - "profile_body_hash": profile_body_hash, - "payout_commitment_hash": payout_commitment_hash, - } - return packed_hash("NovaSealCanonicalEnvelopeV0", pack_canonical_envelope(envelope)) - - -def pack_agreement_signed_intent(core_bytes: bytes, canonical_hash: bytes, expected_receipt_hash: bytes) -> bytes: - return core_bytes + canonical_hash + expected_receipt_hash - - -def pack_agreement_receipt_commitment(commitment: dict[str, Any]) -> bytes: - return ( - u8(commitment["action"]) - + commitment["agreement_id"] - + u8(commitment["old_status"]) - + u8(commitment["new_status"]) - + commitment["terms_hash"] - + commitment["borrower_authority_hash"] - + commitment["lender_authority_hash"] - + u64(commitment["terminal_amount"]) - + u64(commitment["old_nonce"]) - + u64(commitment["new_nonce"]) - + commitment["intent_core_hash"] - + commitment["payout_commitment_hash"] - ) - - -def pack_repay_payout_commitment(lender_repayment_hash: bytes, borrower_collateral_return_hash: bytes) -> bytes: - return lender_repayment_hash + borrower_collateral_return_hash - - -def pack_agreement_receipt(receipt: dict[str, Any]) -> bytes: - return ( - u8(receipt["action"]) - + receipt["agreement_id"] - + u8(receipt["old_status"]) - + u8(receipt["new_status"]) - + receipt["terms_hash"] - + receipt["borrower_authority_hash"] - + receipt["lender_authority_hash"] - + u64(receipt["collateral_amount"]) - + u64(receipt["principal_amount"]) - + u64(receipt["fixed_fee_amount"]) - + u64(receipt["terminal_amount"]) - + receipt["previous_receipt_hash"] - + receipt["latest_receipt_hash"] - + receipt["intent_core_hash"] - + receipt["signed_intent_hash"] - + receipt["payout_commitment_hash"] - + u64(receipt["nonce"]) - + u64(receipt["timepoint"]) - ) - - -def pack_native_ckb_payout(payout: dict[str, Any]) -> bytes: - return ( - u8(payout["action"]) - + payout["agreement_id"] - + u8(payout["role"]) - + payout["recipient_authority_hash"] - + u8(payout["asset_kind"]) - + payout["asset_hash"] - + u64(payout["amount"]) - + payout["terms_hash"] - + u64(payout["nonce"]) - ) - - -def signature_payload(secret_key: bytes, message_hash: bytes, aux_rand: bytes) -> bytes: - pubkey, signature = schnorr_sign(message_hash, secret_key, aux_rand) - return pubkey + signature - - -def entry_witness( - op: int, - terms_data: bytes, - active_data: bytes, - signed_intent: bytes, - borrower_sig_payload: bytes, - lender_sig_payload: bytes, -) -> str: - payload = ( - b"CSARGv1\0" - + u8(op) - + u32(len(terms_data)) - + terms_data - + u32(len(active_data)) - + active_data - + u32(len(signed_intent)) - + signed_intent - + u32(len(borrower_sig_payload)) - + borrower_sig_payload - + u32(len(lender_sig_payload)) - + lender_sig_payload - ) - return hex0x(payload) - - -def make_terms(now: int, label: str, *, expiry_timepoint: int | None = None) -> dict[str, Any]: - borrower = xonly_pubkey(TEST_SECRET_KEY) - lender = xonly_pubkey(LENDER_SECRET_KEY) - agreement_id = ckb_hash(f"NovaSeal Agreement live devnet v0 {label}".encode("ascii")) - terms_hash = ckb_hash(f"NovaSeal Agreement live devnet terms v0 {label}".encode("ascii")) - return { - "version": AGREEMENT_VERSION, - "agreement_id": agreement_id, - "terms_hash": terms_hash, - "borrower_authority_hash": borrower, - "lender_authority_hash": lender, - "collateral_asset_kind": ASSET_KIND_CKB, - "collateral_asset_hash": ZERO_HASH, - "collateral_amount": 50 * SHANNONS, - "principal_asset_kind": ASSET_KIND_CKB, - "principal_asset_hash": ZERO_HASH, - "principal_amount": 20 * SHANNONS, - "fixed_fee_amount": 2 * SHANNONS, - "start_timepoint": 0, - "expiry_timepoint": expiry_timepoint if expiry_timepoint is not None else now + 1_000_000, - "early_close_policy": EARLY_CLOSE_FIXED_FEE, - } - - -def build_origin_material( - terms: dict[str, Any], - now: int, - *, - mutate_borrower_signature: bool = False, - mutate_lender_signature: bool = False, -) -> dict[str, Any]: - payout = { - "action": PATH_ORIGINATE, - "agreement_id": terms["agreement_id"], - "role": PAYOUT_BORROWER_PRINCIPAL, - "recipient_authority_hash": terms["borrower_authority_hash"], - "asset_kind": terms["principal_asset_kind"], - "asset_hash": terms["principal_asset_hash"], - "amount": terms["principal_amount"], - "terms_hash": terms["terms_hash"], - "nonce": 0, - } - payout_data = pack_native_ckb_payout(payout) - payout_commitment_hash = packed_hash("NativeCkbPayoutV0", payout_data) - core = { - "action": PATH_ORIGINATE, - "agreement_id": terms["agreement_id"], - "terms_hash": terms["terms_hash"], - "borrower_authority_hash": terms["borrower_authority_hash"], - "lender_authority_hash": terms["lender_authority_hash"], - "old_status": STATUS_OFFERED, - "new_status": STATUS_ACTIVE, - "old_nonce": 0, - "new_nonce": 0, - "terminal_amount": terms["principal_amount"], - "payout_commitment_hash": payout_commitment_hash, - "expiry_timepoint": terms["expiry_timepoint"], - } - core_data = pack_agreement_intent_core(core) - intent_core_hash = packed_hash("NovaAgreementIntentCoreV0", core_data) - receipt_commitment = { - "action": PATH_ORIGINATE, - "agreement_id": terms["agreement_id"], - "old_status": STATUS_OFFERED, - "new_status": STATUS_ACTIVE, - "terms_hash": terms["terms_hash"], - "borrower_authority_hash": terms["borrower_authority_hash"], - "lender_authority_hash": terms["lender_authority_hash"], - "terminal_amount": terms["principal_amount"], - "old_nonce": 0, - "new_nonce": 0, - "intent_core_hash": intent_core_hash, - "payout_commitment_hash": payout_commitment_hash, - } - receipt_commitment_data = pack_agreement_receipt_commitment(receipt_commitment) - materialized_receipt_hash = packed_hash("NovaAgreementReceiptCommitmentV0", receipt_commitment_data) - canonical_hash = canonical_envelope_hash( - action=PATH_ORIGINATE, - agreement_id=terms["agreement_id"], - terms_hash=terms["terms_hash"], - old_state_commitment=ZERO_HASH, - new_state_commitment=materialized_receipt_hash, - old_nonce=0, - new_nonce=0, - expiry=terms["expiry_timepoint"], - authority_hash=terms["borrower_authority_hash"], - profile_body_hash=intent_core_hash, - payout_commitment_hash=payout_commitment_hash, - ) - signed_intent = pack_agreement_signed_intent(core_data, canonical_hash, materialized_receipt_hash) - signed_intent_hash = packed_hash("NovaAgreementSignedIntentV0", signed_intent) - active_cell = { - "version": AGREEMENT_VERSION, - "agreement_id": terms["agreement_id"], - "terms_hash": terms["terms_hash"], - "borrower_authority_hash": terms["borrower_authority_hash"], - "lender_authority_hash": terms["lender_authority_hash"], - "collateral_asset_kind": terms["collateral_asset_kind"], - "collateral_asset_hash": terms["collateral_asset_hash"], - "collateral_amount": terms["collateral_amount"], - "principal_asset_kind": terms["principal_asset_kind"], - "principal_asset_hash": terms["principal_asset_hash"], - "principal_amount": terms["principal_amount"], - "fixed_fee_amount": terms["fixed_fee_amount"], - "expiry_timepoint": terms["expiry_timepoint"], - "status": STATUS_ACTIVE, - "latest_receipt_hash": materialized_receipt_hash, - "nonce": 0, - } - active_data = pack_agreement_cell(active_cell) - receipt = { - "action": PATH_ORIGINATE, - "agreement_id": terms["agreement_id"], - "old_status": STATUS_OFFERED, - "new_status": STATUS_ACTIVE, - "terms_hash": terms["terms_hash"], - "borrower_authority_hash": terms["borrower_authority_hash"], - "lender_authority_hash": terms["lender_authority_hash"], - "collateral_amount": terms["collateral_amount"], - "principal_amount": terms["principal_amount"], - "fixed_fee_amount": terms["fixed_fee_amount"], - "terminal_amount": terms["principal_amount"], - "previous_receipt_hash": ZERO_HASH, - "latest_receipt_hash": materialized_receipt_hash, - "intent_core_hash": intent_core_hash, - "signed_intent_hash": signed_intent_hash, - "payout_commitment_hash": payout_commitment_hash, - "nonce": 0, - "timepoint": now, - } - receipt_data = pack_agreement_receipt(receipt) - borrower_sig = bytearray(signature_payload(TEST_SECRET_KEY, signed_intent_hash, TEST_AUX_RAND)) - lender_sig = bytearray(signature_payload(LENDER_SECRET_KEY, signed_intent_hash, LENDER_AUX_RAND)) - if mutate_borrower_signature: - borrower_sig[-1] ^= 1 - if mutate_lender_signature: - lender_sig[-1] ^= 1 - return { - "terms_data": pack_agreement_terms(terms), - "active_cell": active_cell, - "active_data": active_data, - "payout_data": payout_data, - "receipt_data": receipt_data, - "signed_intent": signed_intent, - "signed_intent_hash": signed_intent_hash, - "intent_core_hash": intent_core_hash, - "latest_receipt_hash": materialized_receipt_hash, - "payout_commitment_hash": payout_commitment_hash, - "borrower_sig": bytes(borrower_sig), - "lender_sig": bytes(lender_sig), - } - - -def build_repay_material( - terms: dict[str, Any], - active_cell: dict[str, Any], - previous_receipt_hash: bytes, - now: int, - *, - mutate_borrower_signature: bool = False, -) -> dict[str, Any]: - repayment_amount = active_cell["principal_amount"] + active_cell["fixed_fee_amount"] - next_nonce = active_cell["nonce"] + 1 - lender_payout = { - "action": PATH_REPAY_BEFORE_EXPIRY, - "agreement_id": active_cell["agreement_id"], - "role": PAYOUT_LENDER_REPAYMENT, - "recipient_authority_hash": active_cell["lender_authority_hash"], - "asset_kind": active_cell["principal_asset_kind"], - "asset_hash": active_cell["principal_asset_hash"], - "amount": repayment_amount, - "terms_hash": active_cell["terms_hash"], - "nonce": next_nonce, - } - borrower_payout = { - "action": PATH_REPAY_BEFORE_EXPIRY, - "agreement_id": active_cell["agreement_id"], - "role": PAYOUT_BORROWER_COLLATERAL_RETURN, - "recipient_authority_hash": active_cell["borrower_authority_hash"], - "asset_kind": active_cell["collateral_asset_kind"], - "asset_hash": active_cell["collateral_asset_hash"], - "amount": active_cell["collateral_amount"], - "terms_hash": active_cell["terms_hash"], - "nonce": next_nonce, - } - lender_payout_data = pack_native_ckb_payout(lender_payout) - borrower_payout_data = pack_native_ckb_payout(borrower_payout) - payout_commitment_data = pack_repay_payout_commitment( - packed_hash("NativeCkbPayoutV0", lender_payout_data), - packed_hash("NativeCkbPayoutV0", borrower_payout_data), - ) - payout_commitment_hash = packed_hash("RepayPayoutCommitmentV0", payout_commitment_data) - core = { - "action": PATH_REPAY_BEFORE_EXPIRY, - "agreement_id": active_cell["agreement_id"], - "terms_hash": active_cell["terms_hash"], - "borrower_authority_hash": active_cell["borrower_authority_hash"], - "lender_authority_hash": active_cell["lender_authority_hash"], - "old_status": STATUS_ACTIVE, - "new_status": STATUS_REPAID, - "old_nonce": active_cell["nonce"], - "new_nonce": next_nonce, - "terminal_amount": repayment_amount, - "payout_commitment_hash": payout_commitment_hash, - "expiry_timepoint": active_cell["expiry_timepoint"], - } - core_data = pack_agreement_intent_core(core) - intent_core_hash = packed_hash("NovaAgreementIntentCoreV0", core_data) - receipt_commitment = { - "action": PATH_REPAY_BEFORE_EXPIRY, - "agreement_id": active_cell["agreement_id"], - "old_status": STATUS_ACTIVE, - "new_status": STATUS_REPAID, - "terms_hash": active_cell["terms_hash"], - "borrower_authority_hash": active_cell["borrower_authority_hash"], - "lender_authority_hash": active_cell["lender_authority_hash"], - "terminal_amount": repayment_amount, - "old_nonce": active_cell["nonce"], - "new_nonce": next_nonce, - "intent_core_hash": intent_core_hash, - "payout_commitment_hash": payout_commitment_hash, - } - materialized_receipt_hash = packed_hash( - "NovaAgreementReceiptCommitmentV0", - pack_agreement_receipt_commitment(receipt_commitment), - ) - canonical_hash = canonical_envelope_hash( - action=PATH_REPAY_BEFORE_EXPIRY, - agreement_id=active_cell["agreement_id"], - terms_hash=active_cell["terms_hash"], - old_state_commitment=previous_receipt_hash, - new_state_commitment=materialized_receipt_hash, - old_nonce=active_cell["nonce"], - new_nonce=next_nonce, - expiry=active_cell["expiry_timepoint"], - authority_hash=active_cell["borrower_authority_hash"], - profile_body_hash=intent_core_hash, - payout_commitment_hash=payout_commitment_hash, - ) - signed_intent = pack_agreement_signed_intent(core_data, canonical_hash, materialized_receipt_hash) - signed_intent_hash = packed_hash("NovaAgreementSignedIntentV0", signed_intent) - closed_cell = dict(active_cell) - closed_cell.update({"status": STATUS_REPAID, "latest_receipt_hash": materialized_receipt_hash, "nonce": next_nonce}) - closed_data = pack_agreement_cell(closed_cell) - receipt = { - "action": PATH_REPAY_BEFORE_EXPIRY, - "agreement_id": active_cell["agreement_id"], - "old_status": STATUS_ACTIVE, - "new_status": STATUS_REPAID, - "terms_hash": active_cell["terms_hash"], - "borrower_authority_hash": active_cell["borrower_authority_hash"], - "lender_authority_hash": active_cell["lender_authority_hash"], - "collateral_amount": active_cell["collateral_amount"], - "principal_amount": active_cell["principal_amount"], - "fixed_fee_amount": active_cell["fixed_fee_amount"], - "terminal_amount": repayment_amount, - "previous_receipt_hash": previous_receipt_hash, - "latest_receipt_hash": materialized_receipt_hash, - "intent_core_hash": intent_core_hash, - "signed_intent_hash": signed_intent_hash, - "payout_commitment_hash": payout_commitment_hash, - "nonce": next_nonce, - "timepoint": now, - } - receipt_data = pack_agreement_receipt(receipt) - borrower_sig = bytearray(signature_payload(TEST_SECRET_KEY, signed_intent_hash, TEST_AUX_RAND)) - if mutate_borrower_signature: - borrower_sig[-1] ^= 1 - lender_sig = signature_payload(LENDER_SECRET_KEY, signed_intent_hash, LENDER_AUX_RAND) - return { - "terms_data": pack_agreement_terms(terms), - "active_data": pack_agreement_cell(active_cell), - "closed_cell": closed_cell, - "closed_data": closed_data, - "lender_payout": lender_payout, - "borrower_payout": borrower_payout, - "lender_payout_data": lender_payout_data, - "borrower_payout_data": borrower_payout_data, - "receipt_data": receipt_data, - "signed_intent": signed_intent, - "signed_intent_hash": signed_intent_hash, - "intent_core_hash": intent_core_hash, - "latest_receipt_hash": materialized_receipt_hash, - "payout_commitment_hash": payout_commitment_hash, - "borrower_sig": bytes(borrower_sig), - "lender_sig": lender_sig, - "repayment_amount": repayment_amount, - } - - -def build_claim_material( - terms: dict[str, Any], - active_cell: dict[str, Any], - previous_receipt_hash: bytes, - now: int, - *, - mutate_lender_signature: bool = False, -) -> dict[str, Any]: - claim_amount = active_cell["collateral_amount"] - next_nonce = active_cell["nonce"] + 1 - claim_payout = { - "action": PATH_CLAIM_AFTER_EXPIRY, - "agreement_id": active_cell["agreement_id"], - "role": PAYOUT_LENDER_DEFAULT_CLAIM, - "recipient_authority_hash": active_cell["lender_authority_hash"], - "asset_kind": active_cell["collateral_asset_kind"], - "asset_hash": active_cell["collateral_asset_hash"], - "amount": claim_amount, - "terms_hash": active_cell["terms_hash"], - "nonce": next_nonce, - } - claim_payout_data = pack_native_ckb_payout(claim_payout) - payout_commitment_hash = packed_hash("NativeCkbPayoutV0", claim_payout_data) - core = { - "action": PATH_CLAIM_AFTER_EXPIRY, - "agreement_id": active_cell["agreement_id"], - "terms_hash": active_cell["terms_hash"], - "borrower_authority_hash": active_cell["borrower_authority_hash"], - "lender_authority_hash": active_cell["lender_authority_hash"], - "old_status": STATUS_ACTIVE, - "new_status": STATUS_DEFAULTED, - "old_nonce": active_cell["nonce"], - "new_nonce": next_nonce, - "terminal_amount": claim_amount, - "payout_commitment_hash": payout_commitment_hash, - "expiry_timepoint": active_cell["expiry_timepoint"], - } - core_data = pack_agreement_intent_core(core) - intent_core_hash = packed_hash("NovaAgreementIntentCoreV0", core_data) - receipt_commitment = { - "action": PATH_CLAIM_AFTER_EXPIRY, - "agreement_id": active_cell["agreement_id"], - "old_status": STATUS_ACTIVE, - "new_status": STATUS_DEFAULTED, - "terms_hash": active_cell["terms_hash"], - "borrower_authority_hash": active_cell["borrower_authority_hash"], - "lender_authority_hash": active_cell["lender_authority_hash"], - "terminal_amount": claim_amount, - "old_nonce": active_cell["nonce"], - "new_nonce": next_nonce, - "intent_core_hash": intent_core_hash, - "payout_commitment_hash": payout_commitment_hash, - } - materialized_receipt_hash = packed_hash( - "NovaAgreementReceiptCommitmentV0", - pack_agreement_receipt_commitment(receipt_commitment), - ) - canonical_hash = canonical_envelope_hash( - action=PATH_CLAIM_AFTER_EXPIRY, - agreement_id=active_cell["agreement_id"], - terms_hash=active_cell["terms_hash"], - old_state_commitment=previous_receipt_hash, - new_state_commitment=materialized_receipt_hash, - old_nonce=active_cell["nonce"], - new_nonce=next_nonce, - expiry=active_cell["expiry_timepoint"], - authority_hash=active_cell["lender_authority_hash"], - profile_body_hash=intent_core_hash, - payout_commitment_hash=payout_commitment_hash, - ) - signed_intent = pack_agreement_signed_intent(core_data, canonical_hash, materialized_receipt_hash) - signed_intent_hash = packed_hash("NovaAgreementSignedIntentV0", signed_intent) - closed_cell = dict(active_cell) - closed_cell.update({"status": STATUS_DEFAULTED, "latest_receipt_hash": materialized_receipt_hash, "nonce": next_nonce}) - closed_data = pack_agreement_cell(closed_cell) - receipt = { - "action": PATH_CLAIM_AFTER_EXPIRY, - "agreement_id": active_cell["agreement_id"], - "old_status": STATUS_ACTIVE, - "new_status": STATUS_DEFAULTED, - "terms_hash": active_cell["terms_hash"], - "borrower_authority_hash": active_cell["borrower_authority_hash"], - "lender_authority_hash": active_cell["lender_authority_hash"], - "collateral_amount": active_cell["collateral_amount"], - "principal_amount": active_cell["principal_amount"], - "fixed_fee_amount": active_cell["fixed_fee_amount"], - "terminal_amount": claim_amount, - "previous_receipt_hash": previous_receipt_hash, - "latest_receipt_hash": materialized_receipt_hash, - "intent_core_hash": intent_core_hash, - "signed_intent_hash": signed_intent_hash, - "payout_commitment_hash": payout_commitment_hash, - "nonce": next_nonce, - "timepoint": now, - } - receipt_data = pack_agreement_receipt(receipt) - borrower_sig = signature_payload(TEST_SECRET_KEY, signed_intent_hash, TEST_AUX_RAND) - lender_sig = bytearray(signature_payload(LENDER_SECRET_KEY, signed_intent_hash, LENDER_AUX_RAND)) - if mutate_lender_signature: - lender_sig[-1] ^= 1 - return { - "terms_data": pack_agreement_terms(terms), - "active_data": pack_agreement_cell(active_cell), - "closed_cell": closed_cell, - "closed_data": closed_data, - "claim_payout": claim_payout, - "claim_payout_data": claim_payout_data, - "receipt_data": receipt_data, - "signed_intent": signed_intent, - "signed_intent_hash": signed_intent_hash, - "intent_core_hash": intent_core_hash, - "latest_receipt_hash": materialized_receipt_hash, - "payout_commitment_hash": payout_commitment_hash, - "borrower_sig": borrower_sig, - "lender_sig": bytes(lender_sig), - "claim_amount": claim_amount, - } - - -def compile_agreement_lifecycle(repo_root: pathlib.Path, output: pathlib.Path) -> None: - cmd = [ - "cargo", - "run", - "--quiet", - "--bin", - "cellc", - "--", - "proposals/novaseal/agreement-profile-v0/src/nova_agreement_lifecycle_type.cell", - "--target-profile", - "ckb", - "--target", - "riscv64-elf", - "--entry-action", - "nova_agreement_lifecycle", - "-o", - str(output), - ] - subprocess.run(cmd, cwd=repo_root, check=True) - - -def lifecycle_type(lifecycle_data_hash: str) -> dict[str, str]: - return {"code_hash": lifecycle_data_hash, "hash_type": "data2", "args": "0x"} - - -def build_origin_tx( - funding: dict[str, Any], - lifecycle_data_hash: str, - cell_deps: list[dict[str, Any]], - header_hash: str, - terms: dict[str, Any], - material: dict[str, Any], -) -> dict[str, Any]: - principal_payout_capacity = LIVE_NATIVE_CKB_PAYOUT_CAPACITY_BASE + terms["principal_amount"] - change_capacity = funding["total_capacity"] - STATE_CAPACITY - principal_payout_capacity - RECEIPT_CAPACITY - if change_capacity <= 0: - raise LiveAcceptanceError("originate funding capacity is too small") - witness = entry_witness( - PATH_ORIGINATE, - material["terms_data"], - material["active_data"], - material["signed_intent"], - material["borrower_sig"], - material["lender_sig"], - ) - return transaction( - funding, - [ - {"capacity": hex(STATE_CAPACITY), "lock": always_success_lock(), "type": lifecycle_type(lifecycle_data_hash)}, - { - "capacity": hex(principal_payout_capacity), - "lock": always_success_lock(hex0x(terms["borrower_authority_hash"])), - "type": None, - }, - {"capacity": hex(RECEIPT_CAPACITY), "lock": always_success_lock(), "type": None}, - {"capacity": hex(change_capacity), "lock": always_success_lock(), "type": None}, - ], - [hex0x(material["active_data"]), hex0x(material["payout_data"]), hex0x(material["receipt_data"]), "0x"], - cell_deps, - [witness] + ["0x" for _ in funding["cells"][1:]], - [header_hash], - ) - - -def build_repay_tx( - *, - active_ref: dict[str, Any], - funding: dict[str, Any], - lifecycle_data_hash: str, - cell_deps: list[dict[str, Any]], - header_hash: str, - terms: dict[str, Any], - material: dict[str, Any], - repayment_capacity_delta: int = 0, - repayment_lock_args_override: bytes | None = None, - lender_payout_data_override: bytes | None = None, -) -> dict[str, Any]: - repayment_payout_capacity = LIVE_NATIVE_CKB_PAYOUT_CAPACITY_BASE + material["repayment_amount"] + repayment_capacity_delta - collateral_return_capacity = LIVE_NATIVE_CKB_PAYOUT_CAPACITY_BASE + terms["collateral_amount"] - change_capacity = ( - funding["total_capacity"] - + active_ref["capacity"] - - active_ref["capacity"] - - repayment_payout_capacity - - collateral_return_capacity - - RECEIPT_CAPACITY - ) - if change_capacity <= 0: - raise LiveAcceptanceError("repay funding capacity is too small") - witness = entry_witness( - PATH_REPAY_BEFORE_EXPIRY, - material["terms_data"], - material["active_data"], - material["signed_intent"], - material["borrower_sig"], - material["lender_sig"], - ) - return transaction( - [active_ref] + funding["cells"], - [ - {"capacity": hex(active_ref["capacity"]), "lock": always_success_lock(), "type": lifecycle_type(lifecycle_data_hash)}, - { - "capacity": hex(repayment_payout_capacity), - "lock": always_success_lock(hex0x(repayment_lock_args_override or terms["lender_authority_hash"])), - "type": None, - }, - { - "capacity": hex(collateral_return_capacity), - "lock": always_success_lock(hex0x(terms["borrower_authority_hash"])), - "type": None, - }, - {"capacity": hex(RECEIPT_CAPACITY), "lock": always_success_lock(), "type": None}, - {"capacity": hex(change_capacity), "lock": always_success_lock(), "type": None}, - ], - [ - hex0x(material["closed_data"]), - hex0x(lender_payout_data_override or material["lender_payout_data"]), - hex0x(material["borrower_payout_data"]), - hex0x(material["receipt_data"]), - "0x", - ], - cell_deps, - [witness] + ["0x" for _ in funding["cells"]], - [header_hash], - ) - - -def build_claim_tx( - *, - active_ref: dict[str, Any], - funding: dict[str, Any], - lifecycle_data_hash: str, - cell_deps: list[dict[str, Any]], - header_hash: str, - terms: dict[str, Any], - material: dict[str, Any], - claim_capacity_delta: int = 0, - claim_lock_args_override: bytes | None = None, - claim_payout_data_override: bytes | None = None, -) -> dict[str, Any]: - claim_payout_capacity = LIVE_NATIVE_CKB_PAYOUT_CAPACITY_BASE + material["claim_amount"] + claim_capacity_delta - change_capacity = funding["total_capacity"] - claim_payout_capacity - RECEIPT_CAPACITY - if change_capacity <= 0: - raise LiveAcceptanceError("claim funding capacity is too small") - witness = entry_witness( - PATH_CLAIM_AFTER_EXPIRY, - material["terms_data"], - material["active_data"], - material["signed_intent"], - material["borrower_sig"], - material["lender_sig"], - ) - return transaction( - [active_ref] + funding["cells"], - [ - {"capacity": hex(active_ref["capacity"]), "lock": always_success_lock(), "type": lifecycle_type(lifecycle_data_hash)}, - { - "capacity": hex(claim_payout_capacity), - "lock": always_success_lock(hex0x(claim_lock_args_override or terms["lender_authority_hash"])), - "type": None, - }, - {"capacity": hex(RECEIPT_CAPACITY), "lock": always_success_lock(), "type": None}, - {"capacity": hex(change_capacity), "lock": always_success_lock(), "type": None}, - ], - [ - hex0x(material["closed_data"]), - hex0x(claim_payout_data_override or material["claim_payout_data"]), - hex0x(material["receipt_data"]), - "0x", - ], - cell_deps, - [witness] + ["0x" for _ in funding["cells"]], - [header_hash], - ) - - -def wait_epoch_after(devnet: CkbDevnet, expiry_timepoint: int, *, max_blocks: int = 5000) -> dict[str, Any]: - last_header: dict[str, Any] | None = None - for _ in range(max_blocks): - header = devnet.rpc("get_tip_header") - last_header = header - if epoch_number_from_header(header) > expiry_timepoint: - return header - devnet.rpc("generate_block") - last_epoch = last_header.get("epoch") if last_header else "" - raise LiveAcceptanceError(f"devnet epoch did not advance past expiry {expiry_timepoint}; last epoch={last_epoch}") - - -def submit_origin( - devnet: CkbDevnet, - *, - lifecycle_data_hash: str, - cell_deps: list[dict[str, Any]], - terms: dict[str, Any], - label: str, -) -> dict[str, Any]: - header = devnet.rpc("get_tip_header") - now = epoch_number_from_header(header) - material = build_origin_material(terms, now) - required = STATE_CAPACITY + RECEIPT_CAPACITY + LIVE_NATIVE_CKB_PAYOUT_CAPACITY_BASE + terms["principal_amount"] - funding = devnet.collect_spendable(required + 100 * SHANNONS) - tx = build_origin_tx( - funding, - lifecycle_data_hash, - cell_deps, - header["hash"], - terms, - material, - ) - dry_run = devnet.rpc("dry_run_transaction", [tx]) - commit = devnet.submit_and_commit(tx, label) - active_live = devnet.assert_live_cell( - commit["tx_hash"], - 0, - label=f"{label} active", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle_data_hash), - expected_data=material["active_data"], - ) - principal_payout_live = devnet.assert_live_cell( - commit["tx_hash"], - 1, - label=f"{label} principal payout", - expected_capacity=LIVE_NATIVE_CKB_PAYOUT_CAPACITY_BASE + terms["principal_amount"], - expected_lock=always_success_lock(hex0x(terms["borrower_authority_hash"])), - expected_type=None, - expected_data=material["payout_data"], - ) - receipt_live = devnet.assert_live_cell( - commit["tx_hash"], - 2, - label=f"{label} receipt", - expected_capacity=RECEIPT_CAPACITY, - expected_lock=always_success_lock(), - expected_type=None, - expected_data=material["receipt_data"], - ) - return { - "header": header, - "timepoint": now, - "material": material, - "active_ref": {"tx_hash": commit["tx_hash"], "index": 0, "capacity": STATE_CAPACITY}, - "dry_run": dry_run, - "commit": commit, - "active_live": active_live, - "principal_payout_live": principal_payout_live, - "receipt_live": receipt_live, - } - - -def run_live(args: argparse.Namespace) -> dict[str, Any]: - repo_root = args.repo_root.resolve() - ckb_repo = args.ckb_repo.resolve() - ckb_bin = resolve_ckb_bin(ckb_repo, args.ckb_bin) - run_dir = (args.run_dir or (repo_root / "target/novaseal-agreement-devnet-stateful-live" / str(int(time.time())))).resolve() - run_dir.mkdir(parents=True, exist_ok=True) - lifecycle_elf = run_dir / "nova-agreement-lifecycle-type.elf" - compile_agreement_lifecycle(repo_root, lifecycle_elf) - verifier_elf = repo_root / "proposals/novaseal/v0-mvp-skeleton/target/novaseal-btc-verifier-riscv-shell-release.elf" - if not verifier_elf.is_file(): - raise LiveAcceptanceError(f"missing verifier ELF: {verifier_elf}") - - devnet = CkbDevnet(ckb_repo, ckb_bin, run_dir) - report: dict[str, Any] = { - "schema": "novaseal-agreement-devnet-stateful-live-v0.1", - "status": "running", - "scenario": "agreement_profile_originate_repay_and_claim", - "repo_root": str(repo_root), - "ckb_repo": str(ckb_repo), - "ckb_bin": str(ckb_bin), - "run_dir": str(run_dir), - } - stage = "initializing" - try: - stage = "start devnet" - devnet.start() - stage = "deploy artifacts" - genesis = devnet.get_block_by_number(0) - always_dep = always_success_dep(genesis["transactions"][0]["hash"]) - verifier = deploy_code_cell(devnet, "cellscript_btc_bip340_verifier_riscv", verifier_elf.read_bytes(), always_dep) - lifecycle = deploy_code_cell(devnet, "nova_agreement_lifecycle_type", lifecycle_elf.read_bytes(), always_dep) - cell_deps = [verifier["cell_dep"], lifecycle["cell_dep"], always_dep] - provenance = stateful_provenance( - repo_root, - [ - pathlib.Path("proposals/novaseal/agreement-profile-v0/Cell.toml"), - pathlib.Path("proposals/novaseal/agreement-profile-v0/src"), - pathlib.Path("proposals/novaseal/agreement-profile-v0/schemas"), - pathlib.Path("proposals/novaseal/v0-mvp-skeleton/verifier/novaseal_btc_verifier"), - pathlib.Path("scripts/novaseal_agreement_devnet_stateful_live.py"), - pathlib.Path("scripts/novaseal_devnet_stateful_live.py"), - ], - {"verifier": verifier_elf, "lifecycle": lifecycle_elf}, - ) - - stage = "negative originate wrong lender signature" - negative_origin_header = devnet.rpc("get_tip_header") - negative_origin_now = epoch_number_from_header(negative_origin_header) - wrong_lender_terms = make_terms(negative_origin_now, "wrong-lender-signature") - wrong_lender_origin_material = build_origin_material( - wrong_lender_terms, - negative_origin_now, - mutate_lender_signature=True, - ) - wrong_lender_origin_required = ( - STATE_CAPACITY - + RECEIPT_CAPACITY - + LIVE_NATIVE_CKB_PAYOUT_CAPACITY_BASE - + wrong_lender_terms["principal_amount"] - ) - wrong_lender_origin_funding = devnet.collect_spendable(wrong_lender_origin_required + 100 * SHANNONS) - wrong_lender_origin_tx = build_origin_tx( - wrong_lender_origin_funding, - lifecycle["data_hash"], - cell_deps, - negative_origin_header["hash"], - wrong_lender_terms, - wrong_lender_origin_material, - ) - wrong_lender_origin_reject = devnet.dry_run_rejects( - wrong_lender_origin_tx, - "wrong lender signature originate", - expected_source="Outputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=56, - ) - - stage = "negative originate non-CKB asset kind" - non_ckb_terms = make_terms(negative_origin_now, "non-ckb-asset-kind") - non_ckb_terms["principal_asset_kind"] = 1 - non_ckb_origin_material = build_origin_material(non_ckb_terms, negative_origin_now) - non_ckb_origin_funding = devnet.collect_spendable(wrong_lender_origin_required + 100 * SHANNONS) - non_ckb_origin_tx = build_origin_tx( - non_ckb_origin_funding, - lifecycle["data_hash"], - cell_deps, - negative_origin_header["hash"], - non_ckb_terms, - non_ckb_origin_material, - ) - non_ckb_asset_kind_reject = devnet.dry_run_rejects( - non_ckb_origin_tx, - "non-CKB asset kind originate", - expected_source="Outputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=5, - ) - - stage = "valid repay-path originate" - repay_seed_header = devnet.rpc("get_tip_header") - repay_terms = make_terms(epoch_number_from_header(repay_seed_header), "repay") - repay_origin = submit_origin( - devnet, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - terms=repay_terms, - label="agreement repay-path originate", - ) - origin_material = repay_origin["material"] - active_ref = repay_origin["active_ref"] - stage = "negative repay wrong borrower signature" - negative_header = devnet.rpc("get_tip_header") - negative_now = epoch_number_from_header(negative_header) - negative_material = build_repay_material( - repay_terms, - origin_material["active_cell"], - origin_material["latest_receipt_hash"], - negative_now, - mutate_borrower_signature=True, - ) - repay_required = ( - RECEIPT_CAPACITY - + LIVE_NATIVE_CKB_PAYOUT_CAPACITY_BASE - + negative_material["repayment_amount"] - + LIVE_NATIVE_CKB_PAYOUT_CAPACITY_BASE - + repay_terms["collateral_amount"] - ) - negative_funding = devnet.collect_spendable(repay_required + 100 * SHANNONS) - negative_tx = build_repay_tx( - active_ref=active_ref, - funding=negative_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=negative_header["hash"], - terms=repay_terms, - material=negative_material, - ) - wrong_borrower_signature_reject = devnet.dry_run_rejects( - negative_tx, - "wrong borrower signature repay", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=56, - ) - - stage = "negative repay payout capacity short" - repay_capacity_material = build_repay_material( - repay_terms, - origin_material["active_cell"], - origin_material["latest_receipt_hash"], - negative_now, - ) - repay_capacity_funding = devnet.collect_spendable(repay_required + 100 * SHANNONS) - repay_capacity_short_tx = build_repay_tx( - active_ref=active_ref, - funding=repay_capacity_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=negative_header["hash"], - terms=repay_terms, - material=repay_capacity_material, - repayment_capacity_delta=-1, - ) - repay_payout_capacity_short_reject = devnet.dry_run_rejects( - repay_capacity_short_tx, - "repay payout capacity short", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=5, - ) - - stage = "negative repay payout lock args mismatch" - repay_lock_funding = devnet.collect_spendable(repay_required + 100 * SHANNONS) - repay_lock_args_mismatch_tx = build_repay_tx( - active_ref=active_ref, - funding=repay_lock_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=negative_header["hash"], - terms=repay_terms, - material=repay_capacity_material, - repayment_lock_args_override=ckb_hash(b"wrong lender payout lock args"), - ) - repay_payout_lock_args_mismatch_reject = devnet.dry_run_rejects( - repay_lock_args_mismatch_tx, - "repay payout lock args mismatch", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=5, - ) - - stage = "negative repay wrong payout amount" - wrong_lender_payout = dict(repay_capacity_material["lender_payout"]) - wrong_lender_payout["amount"] += 1 - repay_wrong_payout_funding = devnet.collect_spendable(repay_required + 100 * SHANNONS) - repay_wrong_payout_amount_tx = build_repay_tx( - active_ref=active_ref, - funding=repay_wrong_payout_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=negative_header["hash"], - terms=repay_terms, - material=repay_capacity_material, - lender_payout_data_override=pack_native_ckb_payout(wrong_lender_payout), - ) - repay_wrong_payout_amount_reject = devnet.dry_run_rejects( - repay_wrong_payout_amount_tx, - "repay wrong payout amount", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=5, - ) - active_still_live = devnet.assert_live_cell( - active_ref["tx_hash"], - active_ref["index"], - label="post-negative repay active", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=origin_material["active_data"], - ) - - stage = "valid repay" - repay_header = devnet.rpc("get_tip_header") - repay_now = epoch_number_from_header(repay_header) - repay_material = build_repay_material(repay_terms, origin_material["active_cell"], origin_material["latest_receipt_hash"], repay_now) - repay_funding = devnet.collect_spendable(repay_required + 100 * SHANNONS) - repay_tx = build_repay_tx( - active_ref=active_ref, - funding=repay_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=repay_header["hash"], - terms=repay_terms, - material=repay_material, - ) - repay_dry_run = devnet.rpc("dry_run_transaction", [repay_tx]) - repay_commit = devnet.submit_and_commit(repay_tx, "agreement repay before expiry") - active_dead = devnet.wait_dead_cell(active_ref["tx_hash"], active_ref["index"]) - closed_live = devnet.assert_live_cell( - repay_commit["tx_hash"], - 0, - label="repay closed agreement", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=repay_material["closed_data"], - ) - lender_repayment_live = devnet.assert_live_cell( - repay_commit["tx_hash"], - 1, - label="repay lender repayment", - expected_capacity=LIVE_NATIVE_CKB_PAYOUT_CAPACITY_BASE + repay_material["repayment_amount"], - expected_lock=always_success_lock(hex0x(repay_terms["lender_authority_hash"])), - expected_type=None, - expected_data=repay_material["lender_payout_data"], - ) - borrower_collateral_return_live = devnet.assert_live_cell( - repay_commit["tx_hash"], - 2, - label="repay borrower collateral return", - expected_capacity=LIVE_NATIVE_CKB_PAYOUT_CAPACITY_BASE + repay_terms["collateral_amount"], - expected_lock=always_success_lock(hex0x(repay_terms["borrower_authority_hash"])), - expected_type=None, - expected_data=repay_material["borrower_payout_data"], - ) - repay_receipt_live = devnet.assert_live_cell( - repay_commit["tx_hash"], - 3, - label="repay receipt", - expected_capacity=RECEIPT_CAPACITY, - expected_lock=always_success_lock(), - expected_type=None, - expected_data=repay_material["receipt_data"], - ) - - stage = "valid claim-path originate" - claim_seed_header = devnet.rpc("get_tip_header") - claim_seed_now = epoch_number_from_header(claim_seed_header) - claim_terms = make_terms(claim_seed_now, "claim", expiry_timepoint=claim_seed_now + 1) - claim_origin = submit_origin( - devnet, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - terms=claim_terms, - label="agreement claim-path originate", - ) - claim_origin_material = claim_origin["material"] - claim_active_ref = claim_origin["active_ref"] - stage = "negative early claim" - early_claim_header = devnet.rpc("get_tip_header") - early_claim_now = epoch_number_from_header(early_claim_header) - early_claim_material = build_claim_material( - claim_terms, - claim_origin_material["active_cell"], - claim_origin_material["latest_receipt_hash"], - early_claim_now, - ) - claim_required = RECEIPT_CAPACITY + LIVE_NATIVE_CKB_PAYOUT_CAPACITY_BASE + early_claim_material["claim_amount"] - early_claim_funding = devnet.collect_spendable(claim_required + 100 * SHANNONS) - early_claim_tx = build_claim_tx( - active_ref=claim_active_ref, - funding=early_claim_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=early_claim_header["hash"], - terms=claim_terms, - material=early_claim_material, - ) - early_claim_reject = devnet.dry_run_rejects( - early_claim_tx, - "early claim before expiry", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=5, - ) - - stage = "wait claim expiry" - claim_header = wait_epoch_after(devnet, claim_terms["expiry_timepoint"]) - claim_now = epoch_number_from_header(claim_header) - stage = "negative claim wrong lender signature" - wrong_lender_claim_material = build_claim_material( - claim_terms, - claim_origin_material["active_cell"], - claim_origin_material["latest_receipt_hash"], - claim_now, - mutate_lender_signature=True, - ) - wrong_lender_claim_funding = devnet.collect_spendable(claim_required + 100 * SHANNONS) - wrong_lender_claim_tx = build_claim_tx( - active_ref=claim_active_ref, - funding=wrong_lender_claim_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=claim_header["hash"], - terms=claim_terms, - material=wrong_lender_claim_material, - ) - wrong_lender_claim_reject = devnet.dry_run_rejects( - wrong_lender_claim_tx, - "wrong lender signature claim", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=56, - ) - claim_active_still_live = devnet.assert_live_cell( - claim_active_ref["tx_hash"], - claim_active_ref["index"], - label="post-negative claim active", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=claim_origin_material["active_data"], - ) - - stage = "valid claim" - claim_material = build_claim_material( - claim_terms, - claim_origin_material["active_cell"], - claim_origin_material["latest_receipt_hash"], - claim_now, - ) - claim_funding = devnet.collect_spendable(claim_required + 100 * SHANNONS) - claim_tx = build_claim_tx( - active_ref=claim_active_ref, - funding=claim_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=claim_header["hash"], - terms=claim_terms, - material=claim_material, - ) - claim_dry_run = devnet.rpc("dry_run_transaction", [claim_tx]) - claim_commit = devnet.submit_and_commit(claim_tx, "agreement claim after expiry") - claim_active_dead = devnet.wait_dead_cell(claim_active_ref["tx_hash"], claim_active_ref["index"]) - claim_closed_live = devnet.assert_live_cell( - claim_commit["tx_hash"], - 0, - label="claim closed agreement", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=claim_material["closed_data"], - ) - lender_default_claim_live = devnet.assert_live_cell( - claim_commit["tx_hash"], - 1, - label="claim lender default claim", - expected_capacity=LIVE_NATIVE_CKB_PAYOUT_CAPACITY_BASE + claim_material["claim_amount"], - expected_lock=always_success_lock(hex0x(claim_terms["lender_authority_hash"])), - expected_type=None, - expected_data=claim_material["claim_payout_data"], - ) - claim_receipt_live = devnet.assert_live_cell( - claim_commit["tx_hash"], - 2, - label="claim receipt", - expected_capacity=RECEIPT_CAPACITY, - expected_lock=always_success_lock(), - expected_type=None, - expected_data=claim_material["receipt_data"], - ) - - report.update( - { - "status": "passed", - "live_devnet_rpc_executed": True, - "stateful_lifecycle_executed": True, - "ckb_log": str(devnet.log_path), - "rpc_url": devnet.rpc_url, - "artifacts": { - "verifier": verifier, - "lifecycle": lifecycle, - }, - "provenance": provenance, - "repay_terms": { - "agreement_id": hex0x(repay_terms["agreement_id"]), - "terms_hash": hex0x(repay_terms["terms_hash"]), - "borrower_authority_hash": hex0x(repay_terms["borrower_authority_hash"]), - "lender_authority_hash": hex0x(repay_terms["lender_authority_hash"]), - "principal_amount": repay_terms["principal_amount"], - "collateral_amount": repay_terms["collateral_amount"], - "fixed_fee_amount": repay_terms["fixed_fee_amount"], - "expiry_timepoint": repay_terms["expiry_timepoint"], - }, - "claim_terms": { - "agreement_id": hex0x(claim_terms["agreement_id"]), - "terms_hash": hex0x(claim_terms["terms_hash"]), - "borrower_authority_hash": hex0x(claim_terms["borrower_authority_hash"]), - "lender_authority_hash": hex0x(claim_terms["lender_authority_hash"]), - "principal_amount": claim_terms["principal_amount"], - "collateral_amount": claim_terms["collateral_amount"], - "fixed_fee_amount": claim_terms["fixed_fee_amount"], - "expiry_timepoint": claim_terms["expiry_timepoint"], - }, - "originate": { - "dry_run_cycles": repay_origin["dry_run"].get("cycles"), - "commit": repay_origin["commit"], - "active_live": repay_origin["active_live"].get("status") == "live", - "principal_payout_live": repay_origin["principal_payout_live"].get("status") == "live", - "receipt_live": repay_origin["receipt_live"].get("status") == "live", - "active_data_hash": hex0x(cell_data_hash(origin_material["active_data"])), - "principal_payout_data_hash": ckb_hash_hex(origin_material["payout_data"]), - "signed_intent_hash": hex0x(origin_material["signed_intent_hash"]), - "latest_receipt_hash": hex0x(origin_material["latest_receipt_hash"]), - }, - "repay": { - "dry_run_cycles": repay_dry_run.get("cycles"), - "commit": repay_commit, - "old_active_not_live": active_dead.get("status") != "live", - "closed_live": closed_live.get("status") == "live", - "lender_repayment_live": lender_repayment_live.get("status") == "live", - "borrower_collateral_return_live": borrower_collateral_return_live.get("status") == "live", - "receipt_live": repay_receipt_live.get("status") == "live", - "closed_data_hash": hex0x(cell_data_hash(repay_material["closed_data"])), - "lender_payout_data_hash": ckb_hash_hex(repay_material["lender_payout_data"]), - "borrower_payout_data_hash": ckb_hash_hex(repay_material["borrower_payout_data"]), - "signed_intent_hash": hex0x(repay_material["signed_intent_hash"]), - "latest_receipt_hash": hex0x(repay_material["latest_receipt_hash"]), - }, - "claim_originate": { - "dry_run_cycles": claim_origin["dry_run"].get("cycles"), - "commit": claim_origin["commit"], - "active_live": claim_origin["active_live"].get("status") == "live", - "principal_payout_live": claim_origin["principal_payout_live"].get("status") == "live", - "receipt_live": claim_origin["receipt_live"].get("status") == "live", - "latest_receipt_hash": hex0x(claim_origin_material["latest_receipt_hash"]), - }, - "claim": { - "dry_run_cycles": claim_dry_run.get("cycles"), - "commit": claim_commit, - "old_active_not_live": claim_active_dead.get("status") != "live", - "closed_live": claim_closed_live.get("status") == "live", - "lender_default_claim_live": lender_default_claim_live.get("status") == "live", - "receipt_live": claim_receipt_live.get("status") == "live", - "closed_data_hash": hex0x(cell_data_hash(claim_material["closed_data"])), - "claim_payout_data_hash": ckb_hash_hex(claim_material["claim_payout_data"]), - "signed_intent_hash": hex0x(claim_material["signed_intent_hash"]), - "latest_receipt_hash": hex0x(claim_material["latest_receipt_hash"]), - "timepoint": claim_now, - }, - "negative_cases": { - "wrong_lender_signature_dry_run": wrong_lender_origin_reject, - "non_ckb_asset_kind_dry_run": non_ckb_asset_kind_reject, - "wrong_borrower_signature_dry_run": wrong_borrower_signature_reject, - "repay_payout_capacity_short_dry_run": repay_payout_capacity_short_reject, - "repay_payout_lock_args_mismatch_dry_run": repay_payout_lock_args_mismatch_reject, - "repay_wrong_payout_amount_dry_run": repay_wrong_payout_amount_reject, - "early_claim_dry_run": early_claim_reject, - "wrong_lender_claim_signature_dry_run": wrong_lender_claim_reject, - "post_negative_active_still_live": active_still_live.get("status") == "live", - "post_claim_negative_active_still_live": claim_active_still_live.get("status") == "live", - }, - } - ) - return report - except Exception as error: - report.update( - { - "status": "failed", - "stage": stage, - "error": str(error), - "ckb_log": str(devnet.log_path), - "rpc_url": devnet.rpc_url, - } - ) - return report - finally: - if not args.keep_node: - devnet.stop() - - -def main() -> int: - args = parse_args() - report = run_live(args) - output = args.output if args.output.is_absolute() else args.repo_root.resolve() / args.output - output.parent.mkdir(parents=True, exist_ok=True) - output.write_text(json.dumps(report, indent=2 if args.pretty else None, sort_keys=True) + "\n", encoding="utf-8") - print( - f"wrote {output} status={report['status']} " - f"live_devnet_rpc_executed={report.get('live_devnet_rpc_executed', False)}" - ) - return 0 if report["status"] == "passed" else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/novaseal_bip340_tcb_review.py b/scripts/novaseal_bip340_tcb_review.py deleted file mode 100644 index 40a7e7c3..00000000 --- a/scripts/novaseal_bip340_tcb_review.py +++ /dev/null @@ -1,285 +0,0 @@ -#!/usr/bin/env python3 -"""Build the local NovaSeal BIP340 runtime-verifier TCB review bundle. - -This report is deliberately not an external audit attestation. It collects the -local facts needed before asking a reviewer to sign off on the runtime verifier -TCB: source hashes, artifact hash, vector coverage, IPC coverage, and CKB VM -harness coverage. -""" - -from __future__ import annotations - -import argparse -import hashlib -import json -import subprocess -from pathlib import Path -from typing import Any - - -ROOT = Path(__file__).resolve().parents[1] -CORE_ROOT = ROOT / "proposals/novaseal/v0-mvp-skeleton" -TARGET = ROOT / "target" -DEFAULT_OUTPUT = TARGET / "novaseal-bip340-tcb-review.json" - -VERIFIER_DIRS = [ - CORE_ROOT / "verifier/novaseal_btc_verifier_core", - CORE_ROOT / "verifier/novaseal_btc_verifier_riscv", - CORE_ROOT / "verifier/novaseal_btc_verifier", -] - -REPORTS = { - "reference_vectors": CORE_ROOT / "target/novaseal-btc-verifier-vectors.json", - "ipc_vectors": CORE_ROOT / "target/novaseal-btc-verifier-ipc-vectors.json", - "shell_report": CORE_ROOT / "target/novaseal-btc-verifier-shell-report.json", - "riscv_artifact": CORE_ROOT / "target/novaseal-riscv-shell-artifact.json", - "child_verifier_ckb_vm": CORE_ROOT / "target/novaseal-ckb-vm-child-verifier-report.json", - "parent_lock_ckb_vm": CORE_ROOT / "target/novaseal-parent-lock-ckb-vm-report.json", - "combined_tx_ckb_vm": CORE_ROOT / "target/novaseal-combined-tx-report.json", - "core_live_devnet": TARGET / "novaseal-devnet-stateful-live.json", - "agreement_live_devnet": TARGET / "novaseal-agreement-devnet-stateful-live.json", -} - - -def json_load(path: Path) -> dict[str, Any]: - if not path.exists(): - return {"missing": True, "path": str(path.relative_to(ROOT))} - return json.loads(path.read_text(encoding="utf-8")) - - -def sha256_file(path: Path) -> str: - h = hashlib.sha256() - with path.open("rb") as fh: - for chunk in iter(lambda: fh.read(1024 * 1024), b""): - h.update(chunk) - return "0x" + h.hexdigest() - - -def git_commit() -> str | None: - try: - return subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip() - except (OSError, subprocess.CalledProcessError): - return None - - -def source_files() -> tuple[list[Path], list[str]]: - files: list[Path] = [] - invalid_paths: list[str] = [] - for root in VERIFIER_DIRS: - for path in root.rglob("*"): - rel_parts = path.relative_to(root).parts - if any(part in {"target", "build", ".git", "__pycache__"} for part in rel_parts): - continue - if path.is_symlink(): - invalid_paths.append(path.relative_to(ROOT).as_posix()) - continue - if not path.is_file(): - continue - if path.suffix == ".rs" or path.name in {"Cargo.toml", "Cargo.lock", "README.md"}: - files.append(path) - return sorted(files), sorted(invalid_paths) - - -def source_inventory() -> dict[str, Any]: - files, invalid_paths = source_files() - file_rows = [] - tree_hash = hashlib.sha256() - unsafe_hits = [] - review_hits = [] - for path in files: - rel = path.relative_to(ROOT).as_posix() - data = path.read_bytes() - digest = hashlib.sha256(data).hexdigest() - text = data.decode("utf-8", errors="replace") - line_count = text.count("\n") + (0 if text.endswith("\n") else 1) - file_rows.append({"path": rel, "sha256": "0x" + digest, "lines": line_count}) - tree_hash.update(rel.encode("utf-8")) - tree_hash.update(b"\0") - tree_hash.update(bytes.fromhex(digest)) - for idx, line in enumerate(text.splitlines(), start=1): - stripped = line.strip() - if "unsafe" in stripped: - unsafe_hits.append({"path": rel, "line": idx, "text": stripped}) - if any(token in stripped for token in ("TODO", "todo!", "unimplemented!", "panic!")): - review_hits.append({"path": rel, "line": idx, "text": stripped}) - return { - "source_tree_sha256": "0x" + tree_hash.hexdigest(), - "files": file_rows, - "total_files": len(file_rows), - "total_lines": sum(row["lines"] for row in file_rows), - "valid": not invalid_paths, - "invalid_paths": invalid_paths, - "unsafe_hits": unsafe_hits, - "review_hits": review_hits, - } - - -def gate(name: str, passed: bool, evidence: str, detail: dict[str, Any] | None = None) -> dict[str, Any]: - return { - "name": name, - "status": "passed" if passed else "failed", - "evidence": evidence, - "detail": detail or {}, - } - - -def build_report() -> dict[str, Any]: - reports = {name: json_load(path) for name, path in REPORTS.items()} - vectors = reports["reference_vectors"].get("summary", {}) - ipc = reports["ipc_vectors"].get("summary", {}) - shell = reports["shell_report"].get("summary", {}) - artifact = reports["riscv_artifact"] - child = reports["child_verifier_ckb_vm"].get("summary", {}) - parent = reports["parent_lock_ckb_vm"].get("summary", {}) - combined = reports["combined_tx_ckb_vm"].get("summary", {}) - core_live = reports["core_live_devnet"] - agreement_live = reports["agreement_live_devnet"] - - artifact_sha = artifact.get("staged_release_elf", {}).get("sha256") - if artifact_sha and not artifact_sha.startswith("0x"): - artifact_sha = "0x" + artifact_sha - - gates = [ - gate( - "reference_bip340_vectors", - vectors.get("positive_self_verified", 0) > 0 - and vectors.get("positive_self_verified") == vectors.get("positive_vectors") - and vectors.get("negative_self_rejected") == vectors.get("negative_vectors"), - "target/novaseal-btc-verifier-vectors.json", - vectors, - ), - gate( - "fixed_ipc_vectors", - ipc.get("expected_accept", 0) > 0 - and ipc.get("expected_reject", 0) > 0 - and ipc.get("total_vectors") == ipc.get("expected_accept", 0) + ipc.get("expected_reject", 0), - "target/novaseal-btc-verifier-ipc-vectors.json", - ipc, - ), - gate( - "riscv_shell_spawn_word_report", - shell.get("all_expected_matched") is True and shell.get("matched_expected") == shell.get("total_vectors"), - "target/novaseal-btc-verifier-shell-report.json", - shell, - ), - gate( - "riscv_artifact_preflight", - artifact.get("staged_matches_release") is True - and artifact.get("status", {}).get("preflight_passed") is True - and artifact.get("status", {}).get("ready_for_ckb_vm_dry_run") is True, - "target/novaseal-riscv-shell-artifact.json", - { - "artifact_hash": artifact_sha, - "size_bytes": artifact.get("staged_release_elf", {}).get("size_bytes"), - "production_ready_claim": artifact.get("status", {}).get("production_ready"), - }, - ), - gate( - "child_verifier_ckb_vm", - child.get("child_verifier_ckb_vm_executed") is True - and child.get("matched_expected") == child.get("total_cases") - and child.get("mismatched") == 0, - "target/novaseal-ckb-vm-child-verifier-report.json", - child, - ), - gate( - "parent_lock_spawn_ckb_vm", - parent.get("parent_spawn_executed") is True - and parent.get("child_verifier_ckb_vm_executed") is True - and parent.get("full_transaction_verifier_matched_expected") is True - and parent.get("matched_expected") == parent.get("total_cases"), - "target/novaseal-parent-lock-ckb-vm-report.json", - parent, - ), - gate( - "combined_lock_type_node_stack", - ( - ( - combined.get("ckb_node_verification_stack_executed") is True - and combined.get("node_stack_matched_expected") == combined.get("total_cases") - ) - or ( - combined.get("combined_full_transaction_executed") is True - and combined.get("matched_expected") == combined.get("total_cases") - and combined.get("lock_and_type_script_groups_present") is True - ) - ) - and combined.get("child_spawn_target_cell_dep0_modelled") is True, - "target/novaseal-combined-tx-report.json", - combined, - ), - gate( - "live_local_devnet_core_and_agreement", - core_live.get("status") == "passed" - and core_live.get("live_devnet_rpc_executed") is True - and agreement_live.get("status") == "passed" - and agreement_live.get("live_devnet_rpc_executed") is True, - "target/novaseal-devnet-stateful-live.json + target/novaseal-agreement-devnet-stateful-live.json", - { - "core_status": core_live.get("status"), - "agreement_status": agreement_live.get("status"), - "core_verifier_data_hash": core_live.get("artifacts", {}).get("verifier", {}).get("data_hash"), - "agreement_verifier_data_hash": agreement_live.get("artifacts", {}).get("verifier", {}).get("data_hash"), - }, - ), - ] - - inventory = source_inventory() - local_passed = all(row["status"] == "passed" for row in gates) and inventory["valid"] - return { - "schema": "novaseal-bip340-tcb-review-v0.1", - "status": "passed_local_review_external_attestation_required" if local_passed else "failed", - "repo_commit": git_commit(), - "verifier_id": "btc.bip340.v0", - "ipc_abi": "cellscript-btc-bip340-ipc-v0", - "runtime_artifact": { - "name": "cellscript_btc_bip340_verifier_riscv", - "role": "runtime_verifier", - "artifact_hash": artifact_sha, - "artifact_hash_algorithm": "sha256", - "size_bytes": artifact.get("staged_release_elf", {}).get("size_bytes"), - }, - "local_review_gates": gates, - "source_inventory": inventory, - "tcb_boundary": { - "included": [ - "BIP340 verifier core", - "RISC-V spawn/pipe/wait shell", - "IPC envelope parser", - "artifact hash used by NovaSeal manifests", - ], - "excluded": [ - "NovaSeal .cell protocol code", - "CKB node implementation", - "test harness Rust used only to construct evidence", - "wallet UI implementation", - ], - }, - "external_review": { - "required_for_production": True, - "attestation_file": "proposals/novaseal/v0-mvp-skeleton/proofs/bip340_external_tcb_review_attestation.json", - "template": "proposals/novaseal/v0-mvp-skeleton/proofs/bip340_external_tcb_review_attestation.template.json", - "status": "missing_attestation", - }, - } - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) - parser.add_argument("--pretty", action="store_true") - args = parser.parse_args() - report = build_report() - args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") - if args.pretty: - print( - f"wrote {args.output} status={report['status']} " - f"artifact={report['runtime_artifact']['artifact_hash']} " - f"local_gates={len(report['local_review_gates'])}" - ) - return 0 if report["status"].startswith("passed_local_review") else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/novaseal_btc_anchor_contract.py b/scripts/novaseal_btc_anchor_contract.py deleted file mode 100644 index 3fad82b6..00000000 --- a/scripts/novaseal_btc_anchor_contract.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Shared NovaSeal BTC public-anchor shape checks.""" - -from __future__ import annotations - -from typing import Any - - -def _is_nonzero_hex32(value: Any) -> bool: - if not isinstance(value, str) or not value.startswith("0x") or len(value) != 66: - return False - try: - raw = bytes.fromhex(value[2:]) - except ValueError: - return False - return any(byte != 0 for byte in raw) - - -def _is_non_negative_int(value: Any) -> bool: - return isinstance(value, int) and not isinstance(value, bool) and value >= 0 - - -def _is_positive_int(value: Any) -> bool: - return isinstance(value, int) and not isinstance(value, bool) and value > 0 - - -def _exact_keys(value: dict[str, Any], keys: list[str]) -> bool: - return set(value.keys()) == set(keys) - - -def public_btc_anchor_shape_matches_profile(profile: str, anchor: Any) -> bool: - if not isinstance(anchor, dict): - return False - if profile == "btc-transaction-commitment-profile-v0": - return ( - _exact_keys( - anchor, - [ - "kind", - "anchor_source", - "btc_txid", - "btc_wtxid", - "btc_output_index", - "btc_amount_sats", - "ckb_btc_commitment_hash", - ], - ) - and anchor.get("kind") == "btc_transaction_commitment" - and isinstance(anchor.get("anchor_source"), str) - and bool(anchor.get("anchor_source")) - and _is_nonzero_hex32(anchor.get("btc_txid")) - and _is_nonzero_hex32(anchor.get("btc_wtxid")) - and _is_non_negative_int(anchor.get("btc_output_index")) - and _is_positive_int(anchor.get("btc_amount_sats")) - and _is_nonzero_hex32(anchor.get("ckb_btc_commitment_hash")) - ) - if profile in {"btc-utxo-seal-profile-v0", "dual-seal-profile-v0"}: - expected_kind = { - "btc-utxo-seal-profile-v0": "btc_utxo_spend", - "dual-seal-profile-v0": "dual_seal_btc_closure", - }[profile] - return ( - _exact_keys( - anchor, - [ - "kind", - "anchor_source", - "sealed_btc_txid", - "sealed_btc_vout_index", - "sealed_btc_amount_sats", - "script_pubkey_hash", - "btc_txid", - "btc_wtxid", - "spend_input_index", - "ckb_btc_commitment_hash", - "sealed_utxo_commitment_hash", - ], - ) - and anchor.get("kind") == expected_kind - and isinstance(anchor.get("anchor_source"), str) - and bool(anchor.get("anchor_source")) - and _is_nonzero_hex32(anchor.get("sealed_btc_txid")) - and _is_non_negative_int(anchor.get("sealed_btc_vout_index")) - and _is_positive_int(anchor.get("sealed_btc_amount_sats")) - and _is_nonzero_hex32(anchor.get("script_pubkey_hash")) - and _is_nonzero_hex32(anchor.get("btc_txid")) - and _is_nonzero_hex32(anchor.get("btc_wtxid")) - and _is_non_negative_int(anchor.get("spend_input_index")) - and _is_nonzero_hex32(anchor.get("ckb_btc_commitment_hash")) - and _is_nonzero_hex32(anchor.get("sealed_utxo_commitment_hash")) - ) - return False diff --git a/scripts/novaseal_btc_spv_evidence_adapter.py b/scripts/novaseal_btc_spv_evidence_adapter.py deleted file mode 100644 index 35a3df17..00000000 --- a/scripts/novaseal_btc_spv_evidence_adapter.py +++ /dev/null @@ -1,314 +0,0 @@ -#!/usr/bin/env python3 -"""Generate the NovaSeal public BTC SPV evidence adapter request. - -This report is not public BTC evidence. It is the deterministic request -contract that tells an external BTC SPV operator exactly which NovaSeal -profiles, local builder evidence, and production fields must be supplied before -`public_btc_spv_evidence.json` may pass the production gate. -""" - -from __future__ import annotations - -import argparse -import hashlib -import json -from pathlib import Path -from typing import Any - - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_SERVICE_BUILDER_FIXTURES = ROOT / "target/novaseal-service-builder-fixtures.json" -DEFAULT_TEMPLATE = ROOT / "proposals/novaseal/v0-mvp-skeleton/proofs/public_btc_spv_evidence.template.json" -DEFAULT_OUTPUT = ROOT / "target/novaseal-btc-spv-evidence-adapter.json" - -REPORT_PERSON = b"NovaBtcSpvReqV0" -REQUIRED_PROFILES = [ - "btc-transaction-commitment-profile-v0", - "btc-utxo-seal-profile-v0", - "dual-seal-profile-v0", -] -REQUIRED_SCENARIOS = { - "btc-transaction-commitment-profile-v0": "btc-transaction-commitment-transition", - "btc-utxo-seal-profile-v0": "btc-utxo-seal-closure", - "dual-seal-profile-v0": "dual-seal-finality", -} -PRODUCTION_ANCHOR_SOURCES = { - "btc-transaction-commitment-profile-v0": "external_public_btc_transaction", - "btc-utxo-seal-profile-v0": "external_public_btc_spend", - "dual-seal-profile-v0": "external_public_btc_spend", -} -REQUIRED_PUBLIC_FIELDS = [ - "network", - "generated_at", - "evidence_provider", - "required_profiles", - "profile", - "scenario", - "ckb_live_tx_hash", - "live_report_hash", - "service_builder_case_hash", - "service_builder_tx_skeleton_hash", - "service_builder_receipt_binding_hash", - "ckb_btc_commitment_hash", - "btc_txid", - "btc_wtxid", - "btc_tx_hex", - "btc_block_hash", - "btc_block_header", - "btc_merkle_proof.tx_index", - "btc_merkle_proof.merkle_branch", - "btc_merkle_proof.merkle_root", - "btc_merkle_proof.block_height", - "btc_merkle_proof.observed_tip_height", - "btc_transaction_binding.kind", - "btc_transaction_binding.btc_output_index", - "btc_transaction_binding.btc_amount_sats", - "btc_transaction_binding.spend_input_index", - "btc_transaction_binding.sealed_btc_txid", - "btc_transaction_binding.sealed_btc_vout_index", - "btc_transaction_binding.sealed_btc_amount_sats", - "btc_transaction_binding.script_pubkey_hash", - "btc_transaction_binding.sealed_btc_tx_hex", - "btc_transaction_binding.sealed_utxo_commitment_hash", - "spv_proof_hash", - "minimum_confirmations", - "confirmations", - "spv_client_cell_dep.out_point", - "spv_client_cell_dep.data_hash", - "spv_client_cell_dep.dep_type", - "spv_client_cell_dep.hash_type", - "source_service.name", - "source_service.commit", - "source_service.report_hash", - "request_handoff.bundle", - "request_handoff.bundle_hash", - "request_handoff.bundle_hash_algorithm", - "request_handoff.group", -] -FIELD_CONSTRAINTS = { - "network": "explicit public mainnet/testnet name; placeholders and local/devnet/regtest/simnet/private/fake labels are rejected", - "generated_at": "UTC timestamp in YYYY-MM-DDTHH:MM:SSZ form; future timestamps are rejected", - "evidence_provider": "real external provider identity; placeholder, first-party NovaSeal/CellScript/a19q3, local/devnet/fake/internal, example, and unknown tokens are rejected", - "ckb_live_tx_hash": "0x-prefixed 32-byte CKB live transaction hash matching the current NovaSeal service-builder case", - "live_report_hash": "0x-prefixed 32-byte hash of the current NovaSeal live devnet report for this profile", - "service_builder_case_hash": "0x-prefixed 32-byte hash of the current NovaSeal service-builder case for this profile", - "service_builder_tx_skeleton_hash": "0x-prefixed 32-byte service-builder transaction skeleton hash for this profile", - "service_builder_receipt_binding_hash": "0x-prefixed 32-byte service-builder receipt binding hash for this profile", - "ckb_btc_commitment_hash": "0x-prefixed 32-byte CKB-side BTC commitment hash from the current live profile report", - "btc_txid": "0x-prefixed 32-byte non-placeholder Bitcoin transaction id", - "btc_wtxid": "0x-prefixed 32-byte Bitcoin witness transaction id derived from btc_tx_hex", - "btc_tx_hex": "0x-prefixed raw Bitcoin transaction bytes whose txid/wtxid match the public evidence case", - "btc_block_hash": "0x-prefixed 32-byte non-placeholder Bitcoin block hash anchoring the SPV proof", - "btc_block_header": "0x-prefixed 80-byte Bitcoin block header whose double-SHA256 hash matches btc_block_hash", - "btc_merkle_proof.tx_index": "zero-based transaction index used to orient the Merkle branch", - "btc_merkle_proof.merkle_branch": ( - "array of 0x-prefixed 32-byte Bitcoin sibling hashes in display order; " - "empty only for tx_index 0 in a single-transaction block" - ), - "btc_merkle_proof.merkle_root": "0x-prefixed 32-byte Bitcoin Merkle root matching the block header", - "btc_merkle_proof.block_height": "public Bitcoin block height containing btc_txid", - "btc_merkle_proof.observed_tip_height": "public Bitcoin tip height used to compute confirmations", - "btc_transaction_binding.kind": "profile-specific binding kind: btc_transaction_output, btc_utxo_spend, or dual_seal_btc_closure", - "btc_transaction_binding.btc_output_index": "BTC transaction commitment output index; required for btc-transaction-commitment-profile-v0", - "btc_transaction_binding.btc_amount_sats": "BTC transaction commitment output amount in sats; required for btc-transaction-commitment-profile-v0", - "btc_transaction_binding.spend_input_index": "Bitcoin spend input index; required for UTXO and dual-seal closure profiles", - "btc_transaction_binding.sealed_btc_txid": "sealed Bitcoin transaction id whose output is spent; required for btc-utxo-seal-profile-v0 and dual-seal-profile-v0", - "btc_transaction_binding.sealed_btc_vout_index": "sealed Bitcoin output index; required for btc-utxo-seal-profile-v0 and dual-seal-profile-v0", - "btc_transaction_binding.sealed_btc_amount_sats": "sealed Bitcoin output amount in sats; required for btc-utxo-seal-profile-v0 and dual-seal-profile-v0", - "btc_transaction_binding.script_pubkey_hash": "0x-prefixed CKB Blake2b-256 hash of the sealed output scriptPubKey bytes; required for btc-utxo-seal-profile-v0 and dual-seal-profile-v0", - "btc_transaction_binding.sealed_btc_tx_hex": "0x-prefixed raw sealed Bitcoin transaction bytes; required for btc-utxo-seal-profile-v0 and dual-seal-profile-v0", - "btc_transaction_binding.sealed_utxo_commitment_hash": "0x-prefixed 32-byte CKB-side sealed UTXO commitment hash; required for btc-utxo-seal-profile-v0 and dual-seal-profile-v0", - "spv_proof_hash": "0x-prefixed SHA-256 hash of the canonical BTC SPV proof material carried in this case", - "minimum_confirmations": "integer confirmation floor; at least 6", - "confirmations": "integer observed confirmations meeting minimum_confirmations", - "spv_client_cell_dep.out_point": "0x-prefixed 32-byte CKB transaction hash plus numeric output index", - "spv_client_cell_dep.data_hash": "0x-prefixed 32-byte non-placeholder SPV client data hash", - "spv_client_cell_dep.dep_type": "code", - "spv_client_cell_dep.hash_type": "data, data1, or type CKB script hash type", - "source_service.name": "real external SPV service identity; placeholder, first-party NovaSeal/CellScript/a19q3, local/devnet/fake/internal, example, and unknown tokens are rejected", - "source_service.commit": "40-character hex service source commit", - "source_service.report_hash": "0x-prefixed 32-byte non-placeholder SPV service report hash", - "request_handoff.bundle": "target/novaseal-external-evidence-handoff-bundle.json", - "request_handoff.bundle_hash": "0x-prefixed 32-byte hash of the NovaSeal external evidence handoff bundle", - "request_handoff.bundle_hash_algorithm": "blake2b-256(person=NovaExtHandoff)", - "request_handoff.group": "public_btc_spv_evidence", -} - - -def hex0x(data: bytes) -> str: - return "0x" + data.hex() - - -def canonical_json(value: Any) -> bytes: - return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8") - - -def report_hash(label: str, value: Any) -> str: - h = hashlib.blake2b(digest_size=32, person=REPORT_PERSON) - h.update(label.encode("utf-8")) - h.update(b"\x00") - h.update(canonical_json(value)) - return hex0x(h.digest()) - - -def is_hex32(value: Any) -> bool: - return ( - isinstance(value, str) - and len(value) == 66 - and value.startswith("0x") - and all(char in "0123456789abcdefABCDEF" for char in value[2:]) - ) - - -def is_non_negative_int(value: Any) -> bool: - return type(value) is int and value >= 0 - - -def is_positive_int(value: Any) -> bool: - return type(value) is int and value > 0 - - -def anchor_source_production_eligible(profile: str, value: Any) -> bool: - return isinstance(value, str) and value == PRODUCTION_ANCHOR_SOURCES.get(profile) - - -def profile_cases(service_builder: dict[str, Any], template: dict[str, Any]) -> list[dict[str, Any]]: - builder_cases = service_builder.get("cases", []) - template_cases = template.get("cases", []) - cases = [] - for profile in REQUIRED_PROFILES: - builder_case = next((case for case in builder_cases if case.get("profile") == profile), None) - template_case = next((case for case in template_cases if case.get("profile") == profile), None) - external_inputs = builder_case.get("request", {}).get("production_external_inputs", []) if builder_case else [] - required_live_inputs = builder_case.get("request", {}).get("required_live_inputs", {}) if builder_case else {} - public_btc_anchor = required_live_inputs.get("public_btc_anchor", {}) if isinstance(required_live_inputs, dict) else {} - if not isinstance(public_btc_anchor, dict): - public_btc_anchor = {} - request = { - "profile": profile, - "scenario": template_case.get("scenario") if template_case else None, - "minimum_confirmations": template_case.get("minimum_confirmations") if template_case else 6, - "required_public_fields": REQUIRED_PUBLIC_FIELDS, - "field_constraints": FIELD_CONSTRAINTS, - "required_external_inputs": external_inputs, - "ckb_live_tx_hash": required_live_inputs.get("live_devnet_tx_hash"), - "live_report_hash": required_live_inputs.get("live_report_hash"), - "service_builder_case_hash": report_hash("service_builder_case", builder_case), - "service_builder_tx_skeleton_hash": builder_case.get("response", {}).get("tx_skeleton_hash") if builder_case else None, - "service_builder_receipt_binding_hash": builder_case.get("response", {}).get("receipt_binding_hash") if builder_case else None, - "local_anchor_source": public_btc_anchor.get("anchor_source"), - "expected_anchor_source": PRODUCTION_ANCHOR_SOURCES.get(profile), - "ckb_btc_commitment_hash": public_btc_anchor.get("ckb_btc_commitment_hash"), - "expected_btc_txid": public_btc_anchor.get("btc_txid"), - "expected_btc_wtxid": public_btc_anchor.get("btc_wtxid"), - "expected_btc_output_index": public_btc_anchor.get("btc_output_index"), - "expected_btc_amount_sats": public_btc_anchor.get("btc_amount_sats"), - "expected_sealed_btc_txid": public_btc_anchor.get("sealed_btc_txid"), - "expected_sealed_btc_vout_index": public_btc_anchor.get("sealed_btc_vout_index"), - "expected_sealed_btc_amount_sats": public_btc_anchor.get("sealed_btc_amount_sats"), - "expected_script_pubkey_hash": public_btc_anchor.get("script_pubkey_hash"), - "expected_spend_input_index": public_btc_anchor.get("spend_input_index"), - "expected_sealed_utxo_commitment_hash": public_btc_anchor.get("sealed_utxo_commitment_hash"), - "template_case_hash": report_hash("template_case", template_case), - } - tx_profile = profile == "btc-transaction-commitment-profile-v0" - utxo_profile = profile == "btc-utxo-seal-profile-v0" - dual_profile = profile == "dual-seal-profile-v0" - checks = { - "service_builder_case_present": builder_case is not None, - "template_case_present": template_case is not None, - "scenario_matches_required_profile": request["scenario"] == REQUIRED_SCENARIOS[profile], - "public_btc_spv_external_input_named": "public_btc_spv_evidence" in external_inputs, - "minimum_confirmations_at_least_six": is_non_negative_int(request["minimum_confirmations"]) - and request["minimum_confirmations"] >= 6, - "live_binding_hashes_present": is_hex32(request["ckb_live_tx_hash"]) and is_hex32(request["live_report_hash"]), - "service_builder_hashes_present": is_hex32(request["service_builder_tx_skeleton_hash"]) - and is_hex32(request["service_builder_receipt_binding_hash"]), - "expected_anchor_source_production_eligible": anchor_source_production_eligible( - profile, request["expected_anchor_source"] - ), - "local_anchor_source_present": bool(request["local_anchor_source"]), - "ckb_btc_commitment_hash_present": is_hex32(request["ckb_btc_commitment_hash"]), - "expected_btc_txid_present": is_hex32(request["expected_btc_txid"]), - "expected_btc_wtxid_present": is_hex32(request["expected_btc_wtxid"]), - "expected_output_fields_present": (not tx_profile) - or ( - is_non_negative_int(request["expected_btc_output_index"]) - and is_positive_int(request["expected_btc_amount_sats"]) - ), - "expected_utxo_fields_present": (not utxo_profile) - or ( - is_hex32(request["expected_sealed_btc_txid"]) - and is_non_negative_int(request["expected_sealed_btc_vout_index"]) - and is_positive_int(request["expected_sealed_btc_amount_sats"]) - and is_hex32(request["expected_script_pubkey_hash"]) - and is_non_negative_int(request["expected_spend_input_index"]) - and is_hex32(request["expected_sealed_utxo_commitment_hash"]) - ), - "expected_dual_sealed_utxo_fields_present": (not dual_profile) - or ( - is_hex32(request["expected_sealed_btc_txid"]) - and is_non_negative_int(request["expected_sealed_btc_vout_index"]) - and is_positive_int(request["expected_sealed_btc_amount_sats"]) - and is_hex32(request["expected_script_pubkey_hash"]) - and is_non_negative_int(request["expected_spend_input_index"]) - and is_hex32(request["expected_sealed_utxo_commitment_hash"]) - ), - "required_public_fields_complete": len(request["required_public_fields"]) == len(REQUIRED_PUBLIC_FIELDS), - } - cases.append( - { - "profile": profile, - "status": "passed" if all(checks.values()) else "failed", - "checks": checks, - "request": request, - } - ) - return cases - - -def build_report(service_builder: dict[str, Any], template: dict[str, Any]) -> dict[str, Any]: - cases = profile_cases(service_builder, template) - status = "passed" if all(case["status"] == "passed" for case in cases) else "failed" - return { - "schema": "novaseal-btc-spv-evidence-adapter-v0.1", - "status": status, - "adapter_status": "request_ready_external_evidence_required", - "source_service_builder_report": str(DEFAULT_SERVICE_BUILDER_FIXTURES.relative_to(ROOT)), - "source_service_builder_report_hash": report_hash("service_builder_report", service_builder), - "source_public_btc_spv_template": str(DEFAULT_TEMPLATE.relative_to(ROOT)), - "source_public_btc_spv_template_hash": report_hash("public_btc_spv_template", template), - "production_output": "proposals/novaseal/v0-mvp-skeleton/proofs/public_btc_spv_evidence.json", - "production_boundary": "This adapter proves the request contract is complete; it does not prove BTC inclusion, spend validity, confirmation depth, or public SPV client deployment.", - "summary": { - "total": len(cases), - "matched": len([case for case in cases if case["status"] == "passed"]), - "required_profiles": REQUIRED_PROFILES, - }, - "cases": cases, - } - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--service-builder-fixtures", type=Path, default=DEFAULT_SERVICE_BUILDER_FIXTURES) - parser.add_argument("--template", type=Path, default=DEFAULT_TEMPLATE) - parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) - parser.add_argument("--pretty", action="store_true") - args = parser.parse_args() - - service_builder = json.loads(args.service_builder_fixtures.read_text(encoding="utf-8")) - template = json.loads(args.template.read_text(encoding="utf-8")) - report = build_report(service_builder, template) - args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") - if args.pretty: - print( - f"wrote {args.output} status={report['status']} " - f"profiles={report['summary']['matched']}/{report['summary']['total']}" - ) - return 0 if report["status"] == "passed" else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/novaseal_devnet_stateful_acceptance.sh b/scripts/novaseal_devnet_stateful_acceptance.sh index c9865296..848f6ac6 100755 --- a/scripts/novaseal_devnet_stateful_acceptance.sh +++ b/scripts/novaseal_devnet_stateful_acceptance.sh @@ -66,33 +66,8 @@ if [[ ! -f "$REPORT" ]]; then exit 1 fi -summary="$(python3 - "$REPORT" <<'PY' -import json -import sys - -with open(sys.argv[1], "r", encoding="utf-8") as handle: - report = json.load(handle) - -def field(name): - value = report.get(name, "unknown") - if isinstance(value, bool): - return "true" if value else "false" - return str(value) - -print( - "\t".join( - [ - field("status"), - field("live_devnet_rpc_executed"), - field("local_blocker_count"), - field("acceptance_blocker_count"), - field("blocker_count"), - str(report.get("external_endpoint_coverage", {}).get("status", "unknown")), - ] - ) -) -PY -)" +summary="$(cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ + --root "$ROOT_DIR" novaseal-acceptance-summary "$REPORT")" IFS=$'\t' read -r status live_devnet_rpc_executed local_blockers acceptance_blockers blockers external_endpoint_status <<< "$summary" printf 'wrote %s status=%s live_devnet_rpc_executed=%s local_blockers=%s acceptance_blockers=%s blockers=%s external_endpoint_status=%s certifier_status=%s\n' \ "$REPORT" "$status" "$live_devnet_rpc_executed" "$local_blockers" "$acceptance_blockers" "$blockers" "$external_endpoint_status" "$certifier_status" diff --git a/scripts/novaseal_devnet_stateful_live.py b/scripts/novaseal_devnet_stateful_live.py deleted file mode 100644 index e3f2c8aa..00000000 --- a/scripts/novaseal_devnet_stateful_live.py +++ /dev/null @@ -1,1220 +0,0 @@ -#!/usr/bin/env python3 -"""Run a minimal live CKB devnet NovaSeal stateful lifecycle. - -This is intentionally narrow: it proves that the core NovaSeal lifecycle type -can be deployed as a live CellDep, create a bootstrap state cell, then consume -that exact outpoint in a signed transition that materializes the next state and -receipt outputs. -""" - -from __future__ import annotations - -import argparse -import hashlib -import json -import os -import pathlib -import re -import shutil -import socket -import subprocess -import time -import urllib.error -import urllib.request -from typing import Any - - -CKB_BLAKE2B_PERSONAL = b"ckb-default-hash" -PACKED_HASH_DOMAIN = b"CellScriptPackedHashV0\0" -ALWAYS_SUCCESS_CODE_HASH = "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5" -ALWAYS_SUCCESS_INDEX = "0x5" -SHANNONS = 100_000_000 -STATE_CAPACITY = 1_000 * SHANNONS -RECEIPT_CAPACITY = 1_000 * SHANNONS -VERSION = 0 -OP_BOOTSTRAP = 0 -OP_KEY_AUTH_TRANSITION = 1 -TEST_SECRET_KEY = bytes.fromhex("3e7490680639a2f7bbe8361dd3f34eb6429a9c924d8b342c015e555e628f94e5") -TEST_AUX_RAND = bytes([0x42]) * 32 -ZERO_HASH = bytes(32) -_UNSET = object() - -P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F -N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 -G = ( - 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, - 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8, -) - - -class LiveAcceptanceError(RuntimeError): - def __init__(self, message: str, *, rpc_error: dict[str, Any] | None = None) -> None: - super().__init__(message) - self.rpc_error = rpc_error - - -SCRIPT_ERROR_CODE_KEYS = { - "error_code", - "errorCode", - "exit_code", - "exitCode", - "script_error_code", - "scriptErrorCode", -} - - -def _script_error_code_from_rpc_error(value: Any) -> int | None: - if isinstance(value, dict): - for key, nested in value.items(): - if key in SCRIPT_ERROR_CODE_KEYS: - try: - return int(nested) - except (TypeError, ValueError): - continue - found = _script_error_code_from_rpc_error(nested) - if found is not None: - return found - if isinstance(value, list): - for nested in value: - found = _script_error_code_from_rpc_error(nested) - if found is not None: - return found - return None - - -def script_error_code_matches(reason: str, expected: int, rpc_error: dict[str, Any] | None = None) -> bool: - if _script_error_code_from_rpc_error(rpc_error) == expected: - return True - patterns = [ - rf"\berror code\s*[:#]?\s*{expected}\b", - rf"\berror_code\s*[:=]\s*{expected}\b", - rf"\bexit[_ ]?code\s*[:=]\s*{expected}\b", - rf"\bExitCode\(\s*{expected}\s*\)", - rf"#{expected}\b", - ] - return any(re.search(pattern, reason, re.IGNORECASE) for pattern in patterns) - - -def sha256_hex(data: bytes) -> str: - return "0x" + hashlib.sha256(data).hexdigest() - - -def file_sha256_hex(path: pathlib.Path) -> str: - return sha256_hex(path.read_bytes()) - - -def display_path(path: pathlib.Path, repo_root: pathlib.Path) -> str: - try: - return path.relative_to(repo_root).as_posix() - except ValueError: - return str(path) - - -def git_commit(repo_root: pathlib.Path) -> str | None: - try: - return subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=repo_root, text=True).strip() - except (OSError, subprocess.CalledProcessError): - return None - - -def source_tree_hash(repo_root: pathlib.Path, paths: list[pathlib.Path]) -> dict[str, Any]: - files: list[pathlib.Path] = [] - invalid_paths: list[str] = [] - for raw_path in paths: - path = raw_path if raw_path.is_absolute() else repo_root / raw_path - if path.is_symlink(): - invalid_paths.append(display_path(path, repo_root)) - continue - if path.is_file(): - files.append(path) - continue - if path.is_dir(): - for child in path.rglob("*"): - if any(part in {"target", "build", ".git", "__pycache__"} for part in child.relative_to(path).parts): - continue - if child.is_symlink(): - invalid_paths.append(display_path(child, repo_root)) - continue - if not child.is_file(): - continue - if child.suffix in {".cell", ".schema", ".toml", ".py", ".json", ".rs"} or child.name == "Cargo.lock": - files.append(child) - h = hashlib.sha256() - rows = [] - for path in sorted(set(files)): - rel = display_path(path, repo_root) - digest = hashlib.sha256(path.read_bytes()).digest() - h.update(rel.encode("utf-8")) - h.update(b"\0") - h.update(digest) - rows.append(rel) - return { - "sha256": None if invalid_paths else "0x" + h.hexdigest(), - "files": rows, - "file_count": len(rows), - "valid": not invalid_paths, - "invalid_paths": sorted(invalid_paths), - } - - -def stateful_provenance(repo_root: pathlib.Path, source_paths: list[pathlib.Path], artifacts: dict[str, pathlib.Path]) -> dict[str, Any]: - return { - "repo_commit": git_commit(repo_root), - "source_tree": source_tree_hash(repo_root, source_paths), - "artifacts": { - name: { - "path": display_path(path, repo_root), - "sha256": file_sha256_hex(path), - "ckb_data_hash": ckb_hash_hex(path.read_bytes()), - "size_bytes": path.stat().st_size, - } - for name, path in artifacts.items() - }, - } - - -def parse_args() -> argparse.Namespace: - repo_root = pathlib.Path(__file__).resolve().parents[1] - default_ckb_repo = repo_root.parent / "ckb" - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--repo-root", type=pathlib.Path, default=repo_root) - parser.add_argument("--ckb-repo", type=pathlib.Path, default=default_ckb_repo) - parser.add_argument("--ckb-bin", type=pathlib.Path) - parser.add_argument("--output", type=pathlib.Path, default=repo_root / "target/novaseal-devnet-stateful-live.json") - parser.add_argument("--run-dir", type=pathlib.Path) - parser.add_argument("--pretty", action="store_true") - parser.add_argument("--keep-node", action="store_true") - return parser.parse_args() - - -def ckb_hash(data: bytes) -> bytes: - return hashlib.blake2b(data, digest_size=32, person=CKB_BLAKE2B_PERSONAL).digest() - - -def ckb_hash_hex(data: bytes) -> str: - return "0x" + ckb_hash(data).hex() - - -def tagged_hash(tag: str, data: bytes) -> bytes: - tag_hash = hashlib.sha256(tag.encode("ascii")).digest() - return hashlib.sha256(tag_hash + tag_hash + data).digest() - - -def has_even_y(point: tuple[int, int]) -> bool: - return point[1] % 2 == 0 - - -def point_add(a: tuple[int, int] | None, b: tuple[int, int] | None) -> tuple[int, int] | None: - if a is None: - return b - if b is None: - return a - x1, y1 = a - x2, y2 = b - if x1 == x2 and (y1 + y2) % P == 0: - return None - if a == b: - lam = (3 * x1 * x1 * pow(2 * y1, -1, P)) % P - else: - lam = ((y2 - y1) * pow(x2 - x1, -1, P)) % P - x3 = (lam * lam - x1 - x2) % P - y3 = (lam * (x1 - x3) - y1) % P - return (x3, y3) - - -def point_mul(k: int, point: tuple[int, int] = G) -> tuple[int, int] | None: - result: tuple[int, int] | None = None - addend: tuple[int, int] | None = point - while k: - if k & 1: - result = point_add(result, addend) - addend = point_add(addend, addend) - k >>= 1 - return result - - -def lift_x(x: int) -> tuple[int, int] | None: - if x >= P: - return None - y_sq = (pow(x, 3, P) + 7) % P - y = pow(y_sq, (P + 1) // 4, P) - if (y * y) % P != y_sq: - return None - return (x, y if y % 2 == 0 else P - y) - - -def xonly_pubkey(secret_key: bytes) -> bytes: - d = int.from_bytes(secret_key, "big") - if not 1 <= d < N: - raise LiveAcceptanceError("test secret key is out of range") - point = point_mul(d) - if point is None: - raise LiveAcceptanceError("failed to derive test pubkey") - return point[0].to_bytes(32, "big") - - -def schnorr_sign(message32: bytes, secret_key: bytes, aux_rand32: bytes) -> tuple[bytes, bytes]: - if len(message32) != 32 or len(secret_key) != 32 or len(aux_rand32) != 32: - raise LiveAcceptanceError("BIP340 signer expects 32-byte message, secret, and aux rand") - d0 = int.from_bytes(secret_key, "big") - if not 1 <= d0 < N: - raise LiveAcceptanceError("secret key is out of range") - p0 = point_mul(d0) - if p0 is None: - raise LiveAcceptanceError("secret key produced infinity") - d = d0 if has_even_y(p0) else N - d0 - pubkey = p0[0].to_bytes(32, "big") - t = bytes(a ^ b for a, b in zip(d.to_bytes(32, "big"), tagged_hash("BIP0340/aux", aux_rand32))) - k0 = int.from_bytes(tagged_hash("BIP0340/nonce", t + pubkey + message32), "big") % N - if k0 == 0: - raise LiveAcceptanceError("BIP340 nonce was zero") - r0 = point_mul(k0) - if r0 is None: - raise LiveAcceptanceError("BIP340 nonce produced infinity") - k = k0 if has_even_y(r0) else N - k0 - rx = r0[0].to_bytes(32, "big") - e = int.from_bytes(tagged_hash("BIP0340/challenge", rx + pubkey + message32), "big") % N - sig = rx + ((k + e * d) % N).to_bytes(32, "big") - if not schnorr_verify(message32, pubkey, sig): - raise LiveAcceptanceError("self-generated BIP340 signature failed verification") - return pubkey, sig - - -def schnorr_verify(message32: bytes, pubkey32: bytes, signature64: bytes) -> bool: - if len(message32) != 32 or len(pubkey32) != 32 or len(signature64) != 64: - return False - px = int.from_bytes(pubkey32, "big") - r = int.from_bytes(signature64[:32], "big") - s = int.from_bytes(signature64[32:], "big") - if px >= P or r >= P or s >= N: - return False - point = lift_x(px) - if point is None: - return False - e = int.from_bytes(tagged_hash("BIP0340/challenge", signature64[:32] + pubkey32 + message32), "big") % N - r_point = point_add(point_mul(s), point_mul(N - e, point)) - return r_point is not None and has_even_y(r_point) and r_point[0] == r - - -def hex0x(data: bytes) -> str: - return "0x" + data.hex() - - -def decode_hex(value: str) -> bytes: - return bytes.fromhex(value[2:] if value.startswith("0x") else value) - - -def u8(value: int) -> bytes: - return int(value).to_bytes(1, "little") - - -def u16(value: int) -> bytes: - return int(value).to_bytes(2, "little") - - -def u32(value: int) -> bytes: - return int(value).to_bytes(4, "little") - - -def u64(value: int) -> bytes: - return int(value).to_bytes(8, "little") - - -def packed_hash(type_name: str, packed: bytes) -> bytes: - preimage = PACKED_HASH_DOMAIN + type_name.encode("ascii") + b"\0" + u32(len(packed)) + packed - return ckb_hash(preimage) - - -def cell_data_hash(packed: bytes) -> bytes: - return ckb_hash(packed) - - -def pack_out_point(tx_hash: str, index: int) -> bytes: - tx_hash_bytes = decode_hex(tx_hash) - if len(tx_hash_bytes) != 32: - raise LiveAcceptanceError(f"tx hash must be 32 bytes: {tx_hash}") - return tx_hash_bytes + u32(index) - - -def pack_novaseal_cell( - *, - authority_hash: bytes, - state_hash: bytes, - policy_hash: bytes, - latest_receipt_hash: bytes, - nonce: int, - expiry: int, -) -> bytes: - return ( - u16(VERSION) - + authority_hash - + state_hash - + policy_hash - + latest_receipt_hash - + u64(nonce) - + u64(expiry) - ) - - -def pack_cell_commitment(*, authority_hash: bytes, state_hash: bytes, policy_hash: bytes, nonce: int, expiry: int) -> bytes: - return u16(VERSION) + authority_hash + state_hash + policy_hash + u64(nonce) + u64(expiry) - - -def pack_intent_core( - *, - protocol_id: bytes, - package_hash: bytes, - policy_hash: bytes, - action: int, - terminal_path: int, - old_tx_hash: str, - old_index: int, - old_state_hash: bytes, - new_state_hash: bytes, - old_nonce: int, - new_nonce: int, - expiry: int, -) -> bytes: - return ( - protocol_id - + package_hash - + policy_hash - + u8(action) - + u8(terminal_path) - + pack_out_point(old_tx_hash, old_index) - + old_state_hash - + new_state_hash - + u64(old_nonce) - + u64(new_nonce) - + u64(expiry) - ) - - -def pack_receipt_commitment( - *, - protocol_id: bytes, - package_hash: bytes, - policy_hash: bytes, - action: int, - terminal_path: int, - old_tx_hash: str, - old_index: int, - new_cell_commitment: bytes, - old_state_hash: bytes, - new_state_hash: bytes, - old_nonce: int, - new_nonce: int, - intent_core_hash: bytes, - payout_commitment_hash: bytes, -) -> bytes: - return ( - protocol_id - + package_hash - + policy_hash - + u8(action) - + u8(terminal_path) - + pack_out_point(old_tx_hash, old_index) - + new_cell_commitment - + old_state_hash - + new_state_hash - + u64(old_nonce) - + u64(new_nonce) - + intent_core_hash - + payout_commitment_hash - ) - - -def pack_receipt( - *, - protocol_id: bytes, - package_hash: bytes, - policy_hash: bytes, - action: int, - terminal_path: int, - old_tx_hash: str, - old_index: int, - new_cell_commitment: bytes, - old_state_hash: bytes, - new_state_hash: bytes, - old_nonce: int, - new_nonce: int, - intent_core_hash: bytes, - signed_intent_hash: bytes, - payout_commitment_hash: bytes, - signer_authority_hash: bytes, - expiry: int, -) -> bytes: - return ( - protocol_id - + package_hash - + policy_hash - + u8(action) - + u8(terminal_path) - + pack_out_point(old_tx_hash, old_index) - + new_cell_commitment - + old_state_hash - + new_state_hash - + u64(old_nonce) - + u64(new_nonce) - + intent_core_hash - + signed_intent_hash - + payout_commitment_hash - + signer_authority_hash - + u64(expiry) - ) - - -def pack_flat_intent_header( - *, - protocol_id: bytes, - package_hash: bytes, - policy_hash: bytes, - old_cell_tx_hash: bytes, - old_state_hash: bytes, - new_state_hash: bytes, - old_nonce: int, - new_nonce: int, - expiry: int, -) -> bytes: - return ( - protocol_id - + package_hash - + policy_hash - + old_cell_tx_hash - + old_state_hash - + new_state_hash - + u64(old_nonce) - + u64(new_nonce) - + u64(expiry) - ) - - -def build_transition_material(old_tx_hash: str, old_index: int, old_cell: dict[str, Any], new_state_hash: bytes) -> dict[str, bytes]: - protocol_id = ckb_hash(b"NovaSeal/core/v0") - package_hash = ckb_hash(b"NovaSeal/devnet/stateful/live") - policy_hash = old_cell["policy_hash"] - authority_hash = old_cell["authority_hash"] - old_state_hash = old_cell["state_hash"] - old_nonce = old_cell["nonce"] - new_nonce = old_nonce + 1 - expiry = old_cell["expiry"] - new_cell_commitment = packed_hash( - "NovaSealCellCommitmentV0", - pack_cell_commitment( - authority_hash=authority_hash, - state_hash=new_state_hash, - policy_hash=policy_hash, - nonce=new_nonce, - expiry=expiry, - ), - ) - core = pack_intent_core( - protocol_id=protocol_id, - package_hash=package_hash, - policy_hash=policy_hash, - action=OP_KEY_AUTH_TRANSITION, - terminal_path=OP_KEY_AUTH_TRANSITION, - old_tx_hash=old_tx_hash, - old_index=old_index, - old_state_hash=old_state_hash, - new_state_hash=new_state_hash, - old_nonce=old_nonce, - new_nonce=new_nonce, - expiry=expiry, - ) - intent_core_hash = packed_hash("NovaSealIntentCoreV0", core) - receipt_commitment = pack_receipt_commitment( - protocol_id=protocol_id, - package_hash=package_hash, - policy_hash=policy_hash, - action=OP_KEY_AUTH_TRANSITION, - terminal_path=OP_KEY_AUTH_TRANSITION, - old_tx_hash=old_tx_hash, - old_index=old_index, - new_cell_commitment=new_cell_commitment, - old_state_hash=old_state_hash, - new_state_hash=new_state_hash, - old_nonce=old_nonce, - new_nonce=new_nonce, - intent_core_hash=intent_core_hash, - payout_commitment_hash=ZERO_HASH, - ) - materialized_receipt_hash = packed_hash("ProofReceiptCommitmentV0", receipt_commitment) - signed_intent = core + materialized_receipt_hash - signed_intent_hash = packed_hash("NovaSealSignedIntentV0", signed_intent) - state_hash_commitment = ckb_hash(new_state_hash) - pubkey, signature = schnorr_sign(state_hash_commitment, TEST_SECRET_KEY, TEST_AUX_RAND) - if pubkey != authority_hash: - raise LiveAcceptanceError("derived pubkey does not match old cell authority hash") - new_cell_data = pack_novaseal_cell( - authority_hash=authority_hash, - state_hash=new_state_hash, - policy_hash=policy_hash, - latest_receipt_hash=materialized_receipt_hash, - nonce=new_nonce, - expiry=expiry, - ) - receipt_data = pack_receipt( - protocol_id=protocol_id, - package_hash=package_hash, - policy_hash=policy_hash, - action=OP_KEY_AUTH_TRANSITION, - terminal_path=OP_KEY_AUTH_TRANSITION, - old_tx_hash=old_tx_hash, - old_index=old_index, - new_cell_commitment=new_cell_commitment, - old_state_hash=old_state_hash, - new_state_hash=new_state_hash, - old_nonce=old_nonce, - new_nonce=new_nonce, - intent_core_hash=intent_core_hash, - signed_intent_hash=signed_intent_hash, - payout_commitment_hash=ZERO_HASH, - signer_authority_hash=authority_hash, - expiry=expiry, - ) - return { - "flat_header": pack_flat_intent_header( - protocol_id=protocol_id, - package_hash=package_hash, - policy_hash=policy_hash, - old_cell_tx_hash=bytes.fromhex(old_tx_hash.removeprefix("0x")), - old_state_hash=old_state_hash, - new_state_hash=new_state_hash, - old_nonce=old_nonce, - new_nonce=new_nonce, - expiry=expiry, - ), - "core": core, - "signed_intent": signed_intent, - "signed_intent_hash": signed_intent_hash, - "state_hash_commitment": state_hash_commitment, - "signature_payload": pubkey + signature, - "new_cell_data": new_cell_data, - "receipt_data": receipt_data, - "materialized_receipt_hash": materialized_receipt_hash, - "new_state_hash": new_state_hash, - } - - -def entry_witness( - op: int, - old_cell_data: bytes, - signed_intent: bytes, - state_hash_commitment: bytes, - sig_payload: bytes, - *, - flat_header: bytes | None = None, -) -> str: - if len(sig_payload) != 96: - raise LiveAcceptanceError("entry witness expects 32-byte pubkey plus 64-byte signature") - if flat_header is None: - flat_header = bytes(216) - payload = ( - b"CSARGv1\0" - + u8(op) - + state_hash_commitment - + sig_payload - + u32(len(flat_header)) - + flat_header - + u32(len(old_cell_data)) - + old_cell_data - + u32(len(signed_intent)) - + signed_intent - ) - return hex0x(payload) - - -def pick_port() -> int: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: - sock.bind(("127.0.0.1", 0)) - return int(sock.getsockname()[1]) - - -def resolve_ckb_bin(ckb_repo: pathlib.Path, ckb_bin: pathlib.Path | None) -> pathlib.Path: - if ckb_bin is not None: - if not ckb_bin.exists() or not os.access(ckb_bin, os.X_OK): - raise LiveAcceptanceError(f"CKB binary is not executable: {ckb_bin}") - return ckb_bin.resolve() - for candidate in (ckb_repo / "target/debug/ckb", ckb_repo / "target/release/ckb"): - if candidate.exists() and os.access(candidate, os.X_OK): - return candidate.resolve() - raise LiveAcceptanceError(f"no CKB binary found under {ckb_repo}; pass --ckb-bin") - - -def patch_ckb_toml(path: pathlib.Path, rpc_port: int, p2p_port: int) -> None: - text = path.read_text(encoding="utf-8") - text = re.sub(r'listen_address = "127\.0\.0\.1:\d+"', f'listen_address = "127.0.0.1:{rpc_port}"', text, count=1) - text = re.sub( - r'listen_addresses = \["/ip4/0\.0\.0\.0/tcp/\d+"\]', - f'listen_addresses = ["/ip4/127.0.0.1/tcp/{p2p_port}"]', - text, - count=1, - ) - path.write_text(text, encoding="utf-8") - - -class CkbDevnet: - def __init__(self, ckb_repo: pathlib.Path, ckb_bin: pathlib.Path, run_dir: pathlib.Path): - self.ckb_repo = ckb_repo - self.ckb_bin = ckb_bin - self.run_dir = run_dir - self.ckb_dir = run_dir / "ckb-node" - self.log_path = run_dir / "ckb.log" - self.rpc_port = pick_port() - self.p2p_port = pick_port() - self.rpc_url = f"http://127.0.0.1:{self.rpc_port}" - self.proc: subprocess.Popen[bytes] | None = None - self.opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) - self.reserved: set[tuple[str, int]] = set() - - def start(self) -> None: - template = self.ckb_repo / "test/template" - if not template.is_dir(): - raise LiveAcceptanceError(f"CKB test template not found: {template}") - self.ckb_dir.parent.mkdir(parents=True, exist_ok=True) - shutil.copytree(template, self.ckb_dir) - patch_ckb_toml(self.ckb_dir / "ckb.toml", self.rpc_port, self.p2p_port) - log = self.log_path.open("wb") - self.proc = subprocess.Popen( - [str(self.ckb_bin), "-C", str(self.ckb_dir), "run", "--ba-advanced"], - stdout=log, - stderr=subprocess.STDOUT, - ) - for _ in range(80): - try: - self.rpc("get_tip_header") - return - except Exception: - if self.proc.poll() is not None: - raise LiveAcceptanceError(f"CKB process exited early; see {self.log_path}") - time.sleep(0.25) - raise LiveAcceptanceError(f"CKB RPC did not become ready at {self.rpc_url}; see {self.log_path}") - - def stop(self) -> None: - if self.proc and self.proc.poll() is None: - self.proc.terminate() - try: - self.proc.wait(timeout=5) - except subprocess.TimeoutExpired: - self.proc.kill() - self.proc.wait(timeout=5) - - def rpc(self, method: str, params: list[Any] | None = None) -> Any: - body = json.dumps({"id": 42, "jsonrpc": "2.0", "method": method, "params": params or []}).encode() - request = urllib.request.Request(self.rpc_url, data=body, headers={"Content-Type": "application/json"}) - last_error: Exception | None = None - for attempt in range(6): - try: - with self.opener.open(request, timeout=20) as response: - payload = json.loads(response.read().decode("utf-8")) - break - except (urllib.error.HTTPError, urllib.error.URLError) as error: - last_error = error - if attempt == 5: - raise LiveAcceptanceError(f"RPC {method} failed after retries: {last_error}") from error - time.sleep(0.25 * (attempt + 1)) - else: - raise LiveAcceptanceError(f"RPC {method} failed: {last_error}") - if payload.get("error"): - raise LiveAcceptanceError(f"RPC {method} returned error: {payload['error']}", rpc_error=payload["error"]) - return payload.get("result") - - def get_block(self, block_hash: str) -> dict[str, Any]: - for _ in range(20): - block = self.rpc("get_block", [block_hash]) - if block is not None: - return block - time.sleep(0.05) - raise LiveAcceptanceError(f"block not found: {block_hash}") - - def get_block_by_number(self, number: int) -> dict[str, Any]: - block = self.rpc("get_block_by_number", [hex(number)]) - if block is None: - raise LiveAcceptanceError(f"block number not found: {number}") - return block - - def wait_live_cell(self, tx_hash: str, index: int) -> dict[str, Any]: - last = None - for _ in range(40): - last = self.rpc("get_live_cell", [{"tx_hash": tx_hash, "index": hex(index)}, True]) - if last and last.get("status") == "live": - return last - time.sleep(0.05) - raise LiveAcceptanceError(f"cell is not live: {tx_hash}:{index}; last={last}") - - def assert_live_cell( - self, - tx_hash: str, - index: int, - *, - label: str, - expected_capacity: int | None = None, - expected_lock: dict[str, Any] | None = None, - expected_type: Any = _UNSET, - expected_data: bytes | None = None, - ) -> dict[str, Any]: - live = self.wait_live_cell(tx_hash, index) - cell = live.get("cell") or {} - output = cell.get("output") or {} - data = cell.get("data") or {} - if expected_capacity is not None and int(output.get("capacity", "0x0"), 16) != expected_capacity: - raise LiveAcceptanceError(f"{label} capacity mismatch: {output.get('capacity')} != {hex(expected_capacity)}") - if expected_lock is not None and output.get("lock") != expected_lock: - raise LiveAcceptanceError(f"{label} lock mismatch: {output.get('lock')} != {expected_lock}") - if expected_type is not _UNSET and output.get("type") != expected_type: - raise LiveAcceptanceError(f"{label} type mismatch: {output.get('type')} != {expected_type}") - if expected_data is not None: - expected_content = hex0x(expected_data) - expected_hash = ckb_hash_hex(expected_data) - if data.get("content") != expected_content: - raise LiveAcceptanceError(f"{label} data content mismatch") - if data.get("hash") != expected_hash: - raise LiveAcceptanceError(f"{label} data hash mismatch: {data.get('hash')} != {expected_hash}") - return live - - def wait_dead_cell(self, tx_hash: str, index: int) -> dict[str, Any]: - last = None - for _ in range(40): - last = self.rpc("get_live_cell", [{"tx_hash": tx_hash, "index": hex(index)}, False]) - if last and last.get("status") != "live": - return last - time.sleep(0.05) - raise LiveAcceptanceError(f"cell is still live: {tx_hash}:{index}; last={last}") - - def find_spendable_cellbase(self, max_blocks: int = 80) -> dict[str, Any]: - for _ in range(max_blocks): - block_hash = self.rpc("generate_block") - block = self.get_block(block_hash) - cellbase = block["transactions"][0] - for index, output in enumerate(cellbase.get("outputs", [])): - capacity = int(output["capacity"], 16) - key = (cellbase["hash"], index) - if capacity > 0 and key not in self.reserved: - self.wait_live_cell(cellbase["hash"], index) - self.reserved.add(key) - return {"tx_hash": cellbase["hash"], "index": index, "capacity": capacity} - raise LiveAcceptanceError("no spendable cellbase found") - - def collect_spendable(self, min_capacity: int) -> dict[str, Any]: - cells = [] - total = 0 - while total < min_capacity: - cell = self.find_spendable_cellbase() - cells.append(cell) - total += int(cell["capacity"]) - return {"cells": cells, "total_capacity": total} - - def submit_and_commit(self, tx: dict[str, Any], label: str) -> dict[str, Any]: - tx_hash = self.rpc("send_test_transaction", [tx, "passthrough"]) - last_status = None - for generated in range(80): - status = self.rpc("get_transaction", [tx_hash]) - tx_status = (status or {}).get("tx_status", {}) - last_status = tx_status - if tx_status.get("status") == "committed": - return {"tx_hash": tx_hash, "generated_blocks_after_submit": generated} - if tx_status.get("status") == "rejected": - raise LiveAcceptanceError(f"{label} rejected: {tx_hash}; status={tx_status}") - self.rpc("generate_block") - time.sleep(0.05) - raise LiveAcceptanceError(f"{label} not committed: {tx_hash}; last_status={last_status}") - - def dry_run_rejects( - self, - tx: dict[str, Any], - label: str, - *, - expected_source: str | None = None, - expected_data_hash: str | None = None, - expected_error_code: int | None = None, - ) -> dict[str, Any]: - try: - result = self.rpc("dry_run_transaction", [tx]) - except LiveAcceptanceError as error: - reason = str(error) - checks: dict[str, bool] = {} - if expected_source is not None: - checks["source"] = expected_source in reason - if expected_data_hash is not None: - checks["data_hash"] = expected_data_hash.lower().removeprefix("0x") in reason.lower() - if expected_error_code is not None: - checks["error_code"] = script_error_code_matches(reason, expected_error_code, error.rpc_error) - matched = all(checks.values()) if checks else True - if not matched: - raise LiveAcceptanceError(f"{label} rejected for unexpected reason: checks={checks} reason={reason}") from error - return { - "status": "rejected", - "label": label, - "reason": reason, - "expected": { - "source": expected_source, - "data_hash": expected_data_hash, - "error_code": expected_error_code, - }, - "matched_expected": matched, - } - raise LiveAcceptanceError(f"{label} unexpectedly passed dry-run: {result}") - - -def out_point(tx_hash: str, index: int) -> dict[str, str]: - return {"tx_hash": tx_hash, "index": hex(index)} - - -def always_success_dep(genesis_cellbase_hash: str) -> dict[str, Any]: - return {"out_point": out_point(genesis_cellbase_hash, int(ALWAYS_SUCCESS_INDEX, 16)), "dep_type": "code"} - - -def always_success_lock(args: str = "0x") -> dict[str, str]: - return {"code_hash": ALWAYS_SUCCESS_CODE_HASH, "hash_type": "data", "args": args} - - -def transaction( - input_cells: list[dict[str, Any]] | dict[str, Any], - outputs: list[dict[str, Any]], - outputs_data: list[str], - cell_deps: list[dict[str, Any]], - witnesses: list[str], - header_deps: list[str], -) -> dict[str, Any]: - if isinstance(input_cells, dict) and "cells" in input_cells: - input_cells = input_cells["cells"] - elif isinstance(input_cells, dict): - input_cells = [input_cells] - return { - "version": "0x0", - "cell_deps": cell_deps, - "header_deps": header_deps, - "inputs": [{"previous_output": out_point(cell["tx_hash"], cell["index"]), "since": "0x0"} for cell in input_cells], - "outputs": outputs, - "outputs_data": outputs_data, - "witnesses": witnesses, - } - - -def deploy_code_cell(devnet: CkbDevnet, name: str, artifact: bytes, always_dep: dict[str, Any]) -> dict[str, Any]: - min_capacity = (len(artifact) + 1_000) * SHANNONS - funding = devnet.collect_spendable(min_capacity) - tx = transaction( - funding, - [{"capacity": hex(funding["total_capacity"]), "lock": always_success_lock(), "type": None}], - [hex0x(artifact)], - [always_dep], - ["0x" for _ in funding["cells"]], - [], - ) - commit = devnet.submit_and_commit(tx, f"deploy {name}") - devnet.assert_live_cell( - commit["tx_hash"], - 0, - label=f"deploy {name}", - expected_capacity=funding["total_capacity"], - expected_lock=always_success_lock(), - expected_type=None, - expected_data=artifact, - ) - return { - "name": name, - "artifact_size_bytes": len(artifact), - "data_hash": ckb_hash_hex(artifact), - "cell_dep": {"out_point": out_point(commit["tx_hash"], 0), "dep_type": "code"}, - "commit": commit, - } - - -def compile_lifecycle(repo_root: pathlib.Path, output: pathlib.Path) -> None: - cmd = [ - "cargo", - "run", - "--quiet", - "--bin", - "cellc", - "--", - "proposals/novaseal/v0-mvp-skeleton/src/nova_state_lifecycle_type.cell", - "--target-profile", - "ckb", - "--target", - "riscv64-elf", - "--entry-action", - "novaseal_lifecycle", - "-o", - str(output), - ] - subprocess.run(cmd, cwd=repo_root, check=True) - - -def build_bootstrap_tx( - funding: dict[str, Any], - lifecycle_data_hash: str, - cell_deps: list[dict[str, Any]], - header_hash: str, - initial_cell_data: bytes, -) -> dict[str, Any]: - change_capacity = funding["total_capacity"] - STATE_CAPACITY - if change_capacity <= 0: - raise LiveAcceptanceError("bootstrap funding capacity is too small") - lifecycle_type = {"code_hash": lifecycle_data_hash, "hash_type": "data2", "args": "0x"} - witness = entry_witness(OP_BOOTSTRAP, initial_cell_data, bytes(254), ZERO_HASH, bytes(96)) - return transaction( - funding, - [ - {"capacity": hex(STATE_CAPACITY), "lock": always_success_lock(), "type": lifecycle_type}, - {"capacity": hex(change_capacity), "lock": always_success_lock(), "type": None}, - ], - [hex0x(initial_cell_data), "0x"], - cell_deps, - [witness] + ["0x" for _ in funding["cells"][1:]], - [header_hash], - ) - - -def build_transition_tx( - *, - old_cell_ref: dict[str, Any], - old_cell_state: dict[str, Any], - lifecycle_data_hash: str, - cell_deps: list[dict[str, Any]], - header_hash: str, - funding: dict[str, Any], - new_state_hash: bytes, - mutate_signature: bool = False, -) -> tuple[dict[str, Any], dict[str, Any]]: - old_cell_data = pack_novaseal_cell( - authority_hash=old_cell_state["authority_hash"], - state_hash=old_cell_state["state_hash"], - policy_hash=old_cell_state["policy_hash"], - latest_receipt_hash=old_cell_state["latest_receipt_hash"], - nonce=old_cell_state["nonce"], - expiry=old_cell_state["expiry"], - ) - material = build_transition_material(old_cell_ref["tx_hash"], old_cell_ref["index"], old_cell_state, new_state_hash) - sig_payload = bytearray(material["signature_payload"]) - if mutate_signature: - sig_payload[-1] ^= 1 - witness = entry_witness( - OP_KEY_AUTH_TRANSITION, - old_cell_data, - material["signed_intent"], - material["state_hash_commitment"], - bytes(sig_payload), - flat_header=material["flat_header"], - ) - change_capacity = funding["total_capacity"] - RECEIPT_CAPACITY - if change_capacity <= 0: - raise LiveAcceptanceError("transition funding capacity is too small") - lifecycle_type = {"code_hash": lifecycle_data_hash, "hash_type": "data2", "args": "0x"} - tx = transaction( - [old_cell_ref] + funding["cells"], - [ - {"capacity": hex(old_cell_ref["capacity"]), "lock": always_success_lock(), "type": lifecycle_type}, - {"capacity": hex(RECEIPT_CAPACITY), "lock": always_success_lock(), "type": None}, - {"capacity": hex(change_capacity), "lock": always_success_lock(), "type": None}, - ], - [hex0x(material["new_cell_data"]), hex0x(material["receipt_data"]), "0x"], - cell_deps, - [witness] + ["0x" for _ in funding["cells"]], - [header_hash], - ) - new_state = { - "authority_hash": old_cell_state["authority_hash"], - "state_hash": material["new_state_hash"], - "policy_hash": old_cell_state["policy_hash"], - "latest_receipt_hash": material["materialized_receipt_hash"], - "nonce": old_cell_state["nonce"] + 1, - "expiry": old_cell_state["expiry"], - } - return tx, {"new_state": new_state, "material": material} - - -def run_live(args: argparse.Namespace) -> dict[str, Any]: - repo_root = args.repo_root.resolve() - ckb_repo = args.ckb_repo.resolve() - ckb_bin = resolve_ckb_bin(ckb_repo, args.ckb_bin) - run_dir = (args.run_dir or (repo_root / "target/novaseal-devnet-stateful-live" / str(int(time.time())))).resolve() - run_dir.mkdir(parents=True, exist_ok=True) - lifecycle_elf = run_dir / "novaseal-lifecycle-type.elf" - compile_lifecycle(repo_root, lifecycle_elf) - verifier_elf = repo_root / "proposals/novaseal/v0-mvp-skeleton/target/novaseal-btc-verifier-riscv-shell-release.elf" - if not verifier_elf.is_file(): - raise LiveAcceptanceError(f"missing verifier ELF: {verifier_elf}") - - devnet = CkbDevnet(ckb_repo, ckb_bin, run_dir) - report: dict[str, Any] = { - "schema": "novaseal-devnet-stateful-live-v0.1", - "status": "running", - "scenario": "core_bootstrap_then_key_auth_transition", - "repo_root": str(repo_root), - "ckb_repo": str(ckb_repo), - "ckb_bin": str(ckb_bin), - "run_dir": str(run_dir), - } - try: - devnet.start() - genesis = devnet.get_block_by_number(0) - always_dep = always_success_dep(genesis["transactions"][0]["hash"]) - verifier_artifact = verifier_elf.read_bytes() - lifecycle_artifact = lifecycle_elf.read_bytes() - verifier = deploy_code_cell(devnet, "cellscript_btc_bip340_verifier_riscv", verifier_artifact, always_dep) - lifecycle = deploy_code_cell(devnet, "novaseal_lifecycle_type", lifecycle_artifact, always_dep) - cell_deps = [verifier["cell_dep"], lifecycle["cell_dep"], always_dep] - provenance = stateful_provenance( - repo_root, - [ - pathlib.Path("proposals/novaseal/v0-mvp-skeleton/Cell.toml"), - pathlib.Path("proposals/novaseal/v0-mvp-skeleton/src"), - pathlib.Path("proposals/novaseal/v0-mvp-skeleton/schemas"), - pathlib.Path("proposals/novaseal/v0-mvp-skeleton/verifier/novaseal_btc_verifier"), - pathlib.Path("scripts/novaseal_devnet_stateful_live.py"), - ], - {"verifier": verifier_elf, "lifecycle": lifecycle_elf}, - ) - - header_hash = devnet.rpc("get_tip_header")["hash"] - authority_hash = xonly_pubkey(TEST_SECRET_KEY) - initial_state = { - "authority_hash": authority_hash, - "state_hash": ckb_hash(b"novaseal devnet initial state"), - "policy_hash": ckb_hash(b"novaseal devnet policy"), - "latest_receipt_hash": ZERO_HASH, - "nonce": 0, - "expiry": (1 << 63) - 1, - } - initial_cell_data = pack_novaseal_cell(**initial_state) - bootstrap_funding = devnet.collect_spendable(STATE_CAPACITY + 100 * SHANNONS) - bootstrap_tx = build_bootstrap_tx(bootstrap_funding, lifecycle["data_hash"], cell_deps, header_hash, initial_cell_data) - (run_dir / "bootstrap-tx.json").write_text(json.dumps(bootstrap_tx, indent=2, sort_keys=True) + "\n") - bootstrap_dry_run = devnet.rpc("dry_run_transaction", [bootstrap_tx]) - bootstrap_commit = devnet.submit_and_commit(bootstrap_tx, "novaseal bootstrap") - bootstrap_live = devnet.assert_live_cell( - bootstrap_commit["tx_hash"], - 0, - label="bootstrap state", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type={"code_hash": lifecycle["data_hash"], "hash_type": "data2", "args": "0x"}, - expected_data=initial_cell_data, - ) - - state_ref = {"tx_hash": bootstrap_commit["tx_hash"], "index": 0, "capacity": STATE_CAPACITY} - transition_header = devnet.rpc("get_tip_header")["hash"] - transition_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - transition_tx, transition_material = build_transition_tx( - old_cell_ref=state_ref, - old_cell_state=initial_state, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=transition_header, - funding=transition_funding, - new_state_hash=ckb_hash(b"novaseal devnet state after transition"), - ) - (run_dir / "transition-tx.json").write_text(json.dumps(transition_tx, indent=2, sort_keys=True) + "\n") - transition_dry_run = devnet.rpc("dry_run_transaction", [transition_tx]) - transition_commit = devnet.submit_and_commit(transition_tx, "novaseal key-auth transition") - bootstrap_dead = devnet.wait_dead_cell(bootstrap_commit["tx_hash"], 0) - new_state_live = devnet.assert_live_cell( - transition_commit["tx_hash"], - 0, - label="transition new state", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type={"code_hash": lifecycle["data_hash"], "hash_type": "data2", "args": "0x"}, - expected_data=transition_material["material"]["new_cell_data"], - ) - receipt_live = devnet.assert_live_cell( - transition_commit["tx_hash"], - 1, - label="transition receipt", - expected_capacity=RECEIPT_CAPACITY, - expected_lock=always_success_lock(), - expected_type=None, - expected_data=transition_material["material"]["receipt_data"], - ) - - negative_header = devnet.rpc("get_tip_header")["hash"] - negative_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - negative_ref = {"tx_hash": transition_commit["tx_hash"], "index": 0, "capacity": STATE_CAPACITY} - negative_tx, _ = build_transition_tx( - old_cell_ref=negative_ref, - old_cell_state=transition_material["new_state"], - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=negative_header, - funding=negative_funding, - new_state_hash=ckb_hash(b"novaseal devnet rejected state"), - mutate_signature=True, - ) - (run_dir / "wrong-signature-tx.json").write_text(json.dumps(negative_tx, indent=2, sort_keys=True) + "\n") - wrong_signature_reject = devnet.dry_run_rejects( - negative_tx, - "wrong signature transition", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=56, - ) - still_live = devnet.assert_live_cell( - transition_commit["tx_hash"], - 0, - label="post-negative state", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type={"code_hash": lifecycle["data_hash"], "hash_type": "data2", "args": "0x"}, - expected_data=transition_material["material"]["new_cell_data"], - ) - - report.update( - { - "status": "passed", - "live_devnet_rpc_executed": True, - "stateful_lifecycle_executed": True, - "ckb_log": str(devnet.log_path), - "rpc_url": devnet.rpc_url, - "artifacts": { - "verifier": verifier, - "lifecycle": lifecycle, - }, - "provenance": provenance, - "bootstrap": { - "dry_run_cycles": bootstrap_dry_run.get("cycles"), - "commit": bootstrap_commit, - "state_cell_live": bootstrap_live.get("status") == "live", - "state_data_hash": hex0x(cell_data_hash(initial_cell_data)), - }, - "transition": { - "dry_run_cycles": transition_dry_run.get("cycles"), - "commit": transition_commit, - "old_state_not_live": bootstrap_dead.get("status") != "live", - "new_state_live": new_state_live.get("status") == "live", - "receipt_live": receipt_live.get("status") == "live", - "signed_intent_hash": hex0x(transition_material["material"]["signed_intent_hash"]), - "latest_receipt_hash": hex0x(transition_material["new_state"]["latest_receipt_hash"]), - }, - "negative_cases": { - "wrong_signature_dry_run": wrong_signature_reject, - "post_negative_state_still_live": still_live.get("status") == "live", - }, - } - ) - return report - except Exception as error: - report.update({"status": "failed", "error": str(error), "ckb_log": str(devnet.log_path), "rpc_url": devnet.rpc_url}) - return report - finally: - if not args.keep_node: - devnet.stop() - - -def main() -> int: - args = parse_args() - report = run_live(args) - output = args.output if args.output.is_absolute() else args.repo_root.resolve() / args.output - output.parent.mkdir(parents=True, exist_ok=True) - output.write_text(json.dumps(report, indent=2 if args.pretty else None, sort_keys=True) + "\n", encoding="utf-8") - print( - f"wrote {output} status={report['status']} " - f"live_devnet_rpc_executed={report.get('live_devnet_rpc_executed', False)}" - ) - return 0 if report["status"] == "passed" else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/novaseal_external_attestation_adapter.py b/scripts/novaseal_external_attestation_adapter.py deleted file mode 100644 index 3b8df7da..00000000 --- a/scripts/novaseal_external_attestation_adapter.py +++ /dev/null @@ -1,255 +0,0 @@ -#!/usr/bin/env python3 -"""Generate NovaSeal external attestation adapter requests. - -This report packages the public/shared CellDep and external BIP340 TCB review -requests from the current templates and local TCB review. It is deliberately -not an attestation; production still requires the real public/shared CellDep -attestation and external reviewer acceptance files. -""" - -from __future__ import annotations - -import argparse -import hashlib -import json -from pathlib import Path -from typing import Any - - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_TCB_REVIEW = ROOT / "target/novaseal-bip340-tcb-review.json" -DEFAULT_PUBLIC_TEMPLATE = ROOT / "proposals/novaseal/v0-mvp-skeleton/proofs/public_shared_cell_dep_attestation.template.json" -DEFAULT_EXTERNAL_TEMPLATE = ROOT / "proposals/novaseal/v0-mvp-skeleton/proofs/bip340_external_tcb_review_attestation.template.json" -DEFAULT_OUTPUT = ROOT / "target/novaseal-external-attestation-adapter.json" - -REPORT_PERSON = b"NovaExtAttReqV0" - - -def hex0x(data: bytes) -> str: - return "0x" + data.hex() - - -def canonical_json(value: Any) -> bytes: - return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8") - - -def report_hash(label: str, value: Any) -> str: - h = hashlib.blake2b(digest_size=32, person=REPORT_PERSON) - h.update(label.encode("utf-8")) - h.update(b"\x00") - h.update(canonical_json(value)) - return hex0x(h.digest()) - - -def is_present(value: Any) -> bool: - return value is not None and value != "" and value != [] and value != {} - - -def public_celldep_case(template: dict[str, Any], tcb: dict[str, Any]) -> dict[str, Any]: - verifier = template.get("runtime_verifier", {}) - release = template.get("release", {}) - runtime = tcb.get("runtime_artifact", {}) - request = { - "attestation_type": "public_shared_cell_dep_attestation", - "production_output": "proposals/novaseal/v0-mvp-skeleton/proofs/public_shared_cell_dep_attestation.json", - "template_schema": template.get("schema"), - "template_hash": report_hash("public_celldep_template", template), - "required_public_fields": [ - "network", - "attested_at", - "attestor", - "release.package", - "release.version", - "release.manifest_commit", - "runtime_verifier.verifier_id", - "runtime_verifier.ipc_abi", - "runtime_verifier.out_point", - "runtime_verifier.data_hash", - "runtime_verifier.dep_type", - "runtime_verifier.hash_type", - "runtime_verifier.artifact_hash", - "request_handoff.bundle", - "request_handoff.bundle_hash", - "request_handoff.bundle_hash_algorithm", - "request_handoff.group", - ], - "field_constraints": { - "network": "explicit public CKB mainnet/testnet name; placeholders and local/devnet/regtest/simnet/private/fake labels are rejected", - "attested_at": "UTC timestamp in YYYY-MM-DDTHH:MM:SSZ form; future timestamps are rejected", - "attestor": "real independent release signer or deployer identity; placeholder, first-party NovaSeal/CellScript/a19q3, local/devnet/fake/internal, example, and unknown tokens are rejected", - "release.package": "novaseal", - "release.version": "exact NovaSeal release version 0.0.1-v0-mvp", - "release.manifest_commit": "40-character hex source commit matching the reviewed TCB repo_commit", - "runtime_verifier.verifier_id": "btc.bip340.v0", - "runtime_verifier.ipc_abi": "cellscript-btc-bip340-ipc-v0", - "runtime_verifier.out_point": "0x-prefixed 32-byte CKB transaction hash plus numeric output index", - "runtime_verifier.data_hash": "0x-prefixed 32-byte non-placeholder CellDep data hash", - "runtime_verifier.dep_type": "code", - "runtime_verifier.hash_type": "data1", - "runtime_verifier.artifact_hash": "0x-prefixed 32-byte non-placeholder BIP340 runtime verifier artifact hash", - "request_handoff.bundle": "target/novaseal-external-evidence-handoff-bundle.json", - "request_handoff.bundle_hash": "0x-prefixed 32-byte hash of the NovaSeal external evidence handoff bundle", - "request_handoff.bundle_hash_algorithm": "blake2b-256(person=NovaExtHandoff)", - "request_handoff.group": "public_shared_cell_dep_attestation", - }, - "verifier_id": verifier.get("verifier_id"), - "ipc_abi": verifier.get("ipc_abi"), - "expected_artifact_hash": runtime.get("artifact_hash") or verifier.get("artifact_hash"), - "expected_release_package": release.get("package") if isinstance(release, dict) else None, - "expected_release_version": release.get("version") if isinstance(release, dict) else None, - "expected_release_manifest_commit": tcb.get("repo_commit"), - "expected_dep_type": verifier.get("dep_type"), - "expected_hash_type": verifier.get("hash_type"), - "template_artifact_hash": verifier.get("artifact_hash"), - "required_status": "attested", - "network_must_not_equal": "local-devnet", - } - checks = { - "template_schema_current": request["template_schema"] == "novaseal-public-shared-cell-dep-attestation-v0.1", - "template_status_attested": template.get("status") == "attested", - "release_fields_current": isinstance(release, dict) and set(release) == {"package", "version", "manifest_commit"}, - "release_package_current": release.get("package") == "novaseal" if isinstance(release, dict) else False, - "release_version_current": release.get("version") == "0.0.1-v0-mvp" if isinstance(release, dict) else False, - "release_manifest_commit_present": is_present(release.get("manifest_commit")) if isinstance(release, dict) else False, - "expected_release_manifest_commit_present": is_present(request["expected_release_manifest_commit"]), - "verifier_id_current": request["verifier_id"] == "btc.bip340.v0", - "ipc_abi_current": request["ipc_abi"] == "cellscript-btc-bip340-ipc-v0", - "dep_type_current": request["expected_dep_type"] == "code", - "hash_type_current": request["expected_hash_type"] == "data1", - "artifact_hash_matches_tcb": request["template_artifact_hash"] == request["expected_artifact_hash"], - "required_fields_complete": len(request["required_public_fields"]) == 17, - } - return { - "name": "public_shared_cell_dep_attestation", - "status": "passed" if all(checks.values()) else "failed", - "checks": checks, - "request": request, - } - - -def external_tcb_case(template: dict[str, Any], tcb: dict[str, Any]) -> dict[str, Any]: - runtime = tcb.get("runtime_artifact", {}) - source = tcb.get("source_inventory", {}) - request = { - "attestation_type": "external_bip340_tcb_review_attestation", - "production_output": "proposals/novaseal/v0-mvp-skeleton/proofs/bip340_external_tcb_review_attestation.json", - "template_schema": template.get("schema"), - "template_hash": report_hash("external_tcb_template", template), - "required_public_fields": [ - "reviewer", - "review_date", - "review_scope", - "verifier_id", - "ipc_abi", - "artifact_hash", - "artifact_hash_algorithm", - "source_tree_sha256", - "report_uri", - "request_handoff.bundle", - "request_handoff.bundle_hash", - "request_handoff.bundle_hash_algorithm", - "request_handoff.group", - ], - "field_constraints": { - "reviewer": "real external reviewer identity; placeholder, first-party NovaSeal/CellScript/a19q3, local/devnet/fake/internal, example, and unknown tokens are rejected", - "review_date": "UTC date in YYYY-MM-DD form; future dates are rejected", - "review_scope": "exact BIP340 verifier, RISC-V shell, IPC envelope, and artifact/CellDep pinning scope", - "verifier_id": "btc.bip340.v0", - "ipc_abi": "cellscript-btc-bip340-ipc-v0", - "artifact_hash": "0x-prefixed 32-byte non-placeholder BIP340 runtime verifier artifact hash", - "artifact_hash_algorithm": "sha256", - "source_tree_sha256": "0x-prefixed 32-byte non-placeholder SHA-256 source tree hash", - "report_uri": "HTTPS URI for the public review report or source-controlled review commit; example, loopback, private, and reserved hosts are rejected", - "request_handoff.bundle": "target/novaseal-external-evidence-handoff-bundle.json", - "request_handoff.bundle_hash": "0x-prefixed 32-byte hash of the NovaSeal external evidence handoff bundle", - "request_handoff.bundle_hash_algorithm": "blake2b-256(person=NovaExtHandoff)", - "request_handoff.group": "external_bip340_tcb_review_attestation", - }, - "verifier_id": template.get("verifier_id"), - "ipc_abi": template.get("ipc_abi"), - "expected_artifact_hash": runtime.get("artifact_hash"), - "template_artifact_hash": template.get("artifact_hash"), - "expected_artifact_hash_algorithm": runtime.get("artifact_hash_algorithm"), - "template_artifact_hash_algorithm": template.get("artifact_hash_algorithm"), - "expected_source_tree_sha256": source.get("source_tree_sha256"), - "template_source_tree_sha256": template.get("source_tree_sha256"), - "expected_review_scope": template.get("review_scope"), - "required_status": "accepted", - } - checks = { - "template_schema_current": request["template_schema"] == "novaseal-bip340-external-tcb-review-attestation-v0.1", - "template_status_accepted": template.get("status") == "accepted", - "verifier_id_current": request["verifier_id"] == "btc.bip340.v0", - "ipc_abi_current": request["ipc_abi"] == "cellscript-btc-bip340-ipc-v0", - "artifact_hash_matches_tcb": is_present(request["expected_artifact_hash"]) - and request["template_artifact_hash"] == request["expected_artifact_hash"], - "artifact_hash_algorithm_current": template.get("artifact_hash_algorithm") == "sha256", - "artifact_hash_algorithm_matches_tcb": is_present(request["expected_artifact_hash_algorithm"]) - and request["template_artifact_hash_algorithm"] == request["expected_artifact_hash_algorithm"], - "source_tree_hash_matches_tcb": is_present(request["expected_source_tree_sha256"]) - and request["template_source_tree_sha256"] == request["expected_source_tree_sha256"], - "review_scope_exact": template.get("review_scope") - == [ - "BIP340 verifier core", - "RISC-V runtime verifier shell", - "CellScript BIP340 IPC envelope", - "artifact hash and CellDep pinning requirements", - ], - "required_fields_complete": len(request["required_public_fields"]) == 13, - } - return { - "name": "external_bip340_tcb_review_attestation", - "status": "passed" if all(checks.values()) else "failed", - "checks": checks, - "request": request, - } - - -def build_report(public_template: dict[str, Any], external_template: dict[str, Any], tcb: dict[str, Any]) -> dict[str, Any]: - cases = [public_celldep_case(public_template, tcb), external_tcb_case(external_template, tcb)] - status = "passed" if all(case["status"] == "passed" for case in cases) else "failed" - return { - "schema": "novaseal-external-attestation-adapter-v0.1", - "status": status, - "adapter_status": "request_ready_external_attestations_required", - "source_tcb_review": str(DEFAULT_TCB_REVIEW.relative_to(ROOT)), - "source_tcb_review_hash": report_hash("tcb_review", tcb), - "source_public_cell_dep_template": str(DEFAULT_PUBLIC_TEMPLATE.relative_to(ROOT)), - "source_public_cell_dep_template_hash": report_hash("public_celldep_template", public_template), - "source_external_tcb_template": str(DEFAULT_EXTERNAL_TEMPLATE.relative_to(ROOT)), - "source_external_tcb_template_hash": report_hash("external_tcb_template", external_template), - "production_boundary": "This adapter proves the attestation request package is complete; it does not prove public CellDep deployment or independent external TCB review.", - "summary": { - "total": len(cases), - "matched": len([case for case in cases if case["status"] == "passed"]), - "required_attestations": [case["name"] for case in cases], - }, - "cases": cases, - } - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--tcb-review", type=Path, default=DEFAULT_TCB_REVIEW) - parser.add_argument("--public-template", type=Path, default=DEFAULT_PUBLIC_TEMPLATE) - parser.add_argument("--external-template", type=Path, default=DEFAULT_EXTERNAL_TEMPLATE) - parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) - parser.add_argument("--pretty", action="store_true") - args = parser.parse_args() - - tcb = json.loads(args.tcb_review.read_text(encoding="utf-8")) - public_template = json.loads(args.public_template.read_text(encoding="utf-8")) - external_template = json.loads(args.external_template.read_text(encoding="utf-8")) - report = build_report(public_template, external_template, tcb) - args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") - if args.pretty: - print( - f"wrote {args.output} status={report['status']} " - f"attestations={report['summary']['matched']}/{report['summary']['total']}" - ) - return 0 if report["status"] == "passed" else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/novaseal_external_evidence_handoff_bundle.py b/scripts/novaseal_external_evidence_handoff_bundle.py deleted file mode 100644 index 9c493a9e..00000000 --- a/scripts/novaseal_external_evidence_handoff_bundle.py +++ /dev/null @@ -1,594 +0,0 @@ -#!/usr/bin/env python3 -"""Generate the NovaSeal external evidence handoff bundle. - -This bundle is the machine-readable handoff contract for external production -evidence providers. It aggregates the BTC SPV evidence adapter and external -attestation adapter into one checked request package. It is deliberately not -production evidence: the public BTC SPV evidence, public/shared CellDep -attestation, and external BIP340 TCB review must still be supplied separately. -""" - -from __future__ import annotations - -import argparse -import hashlib -import json -import sys -from pathlib import Path -from typing import Any - - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_BTC_SPV_ADAPTER = ROOT / "target/novaseal-btc-spv-evidence-adapter.json" -DEFAULT_EXTERNAL_ATTESTATION_ADAPTER = ROOT / "target/novaseal-external-attestation-adapter.json" -DEFAULT_OUTPUT = ROOT / "target/novaseal-external-evidence-handoff-bundle.json" - -REPORT_PERSON = b"NovaExtHandoff" -HANDOFF_HASH_ALGORITHM = "blake2b-256(person=NovaExtHandoff)" -HANDOFF_SELF_HASH_FIELDS = ("bundle_hash", "bundle_hash_algorithm") - -PUBLIC_BTC_SPV_EVIDENCE = "proposals/novaseal/v0-mvp-skeleton/proofs/public_btc_spv_evidence.json" -PUBLIC_CELLDEP_ATTESTATION = "proposals/novaseal/v0-mvp-skeleton/proofs/public_shared_cell_dep_attestation.json" -EXTERNAL_TCB_ATTESTATION = "proposals/novaseal/v0-mvp-skeleton/proofs/bip340_external_tcb_review_attestation.json" -RWA_LEGAL_REGISTRY_REVIEW_EVIDENCE = ( - "proposals/novaseal/rwa-receipt-profile-v0/proofs/legal_registry_review_evidence.json" -) - -REQUIRED_BTC_SPV_PROFILES = [ - "btc-transaction-commitment-profile-v0", - "btc-utxo-seal-profile-v0", - "dual-seal-profile-v0", -] -PRODUCTION_BTC_ANCHOR_SOURCES = { - "btc-transaction-commitment-profile-v0": "external_public_btc_transaction", - "btc-utxo-seal-profile-v0": "external_public_btc_spend", - "dual-seal-profile-v0": "external_public_btc_spend", -} - -BTC_SPV_COMMON_BINDING_REQUEST_FIELDS = { - "anchor_source": "expected_anchor_source", - "btc_txid": "expected_btc_txid", - "btc_wtxid": "expected_btc_wtxid", -} -BTC_SPV_PROFILE_BINDING_REQUEST_FIELDS = { - "btc-transaction-commitment-profile-v0": { - "btc_output_index": "expected_btc_output_index", - "btc_amount_sats": "expected_btc_amount_sats", - }, - "btc-utxo-seal-profile-v0": { - "spend_input_index": "expected_spend_input_index", - "sealed_btc_txid": "expected_sealed_btc_txid", - "sealed_btc_vout_index": "expected_sealed_btc_vout_index", - "sealed_btc_amount_sats": "expected_sealed_btc_amount_sats", - "script_pubkey_hash": "expected_script_pubkey_hash", - "sealed_utxo_commitment_hash": "expected_sealed_utxo_commitment_hash", - }, - "dual-seal-profile-v0": { - "spend_input_index": "expected_spend_input_index", - "sealed_btc_txid": "expected_sealed_btc_txid", - "sealed_btc_vout_index": "expected_sealed_btc_vout_index", - "sealed_btc_amount_sats": "expected_sealed_btc_amount_sats", - "script_pubkey_hash": "expected_script_pubkey_hash", - "sealed_utxo_commitment_hash": "expected_sealed_utxo_commitment_hash", - }, -} - -REQUIRED_PUBLIC_CELLDEP_FIELDS = [ - "network", - "attested_at", - "attestor", - "release.package", - "release.version", - "release.manifest_commit", - "runtime_verifier.verifier_id", - "runtime_verifier.ipc_abi", - "runtime_verifier.out_point", - "runtime_verifier.data_hash", - "runtime_verifier.dep_type", - "runtime_verifier.hash_type", - "runtime_verifier.artifact_hash", - "request_handoff.bundle", - "request_handoff.bundle_hash", - "request_handoff.bundle_hash_algorithm", - "request_handoff.group", -] - -REQUIRED_EXTERNAL_TCB_FIELDS = [ - "reviewer", - "review_date", - "review_scope", - "verifier_id", - "ipc_abi", - "artifact_hash", - "artifact_hash_algorithm", - "source_tree_sha256", - "report_uri", - "request_handoff.bundle", - "request_handoff.bundle_hash", - "request_handoff.bundle_hash_algorithm", - "request_handoff.group", -] - -REQUIRED_RWA_LEGAL_REVIEW_FIELDS = [ - "profile", - "reviewer", - "review_date", - "review_scope", - "registry.authority", - "registry.jurisdiction", - "registry.registry_report_hash", - "profile_source_tree_sha256", - "report_uri", - "request_handoff.bundle", - "request_handoff.bundle_hash", - "request_handoff.bundle_hash_algorithm", - "request_handoff.group", -] - -RWA_LEGAL_REVIEW_SOURCE_HASH_PATHS = [ - "proposals/novaseal/rwa-receipt-profile-v0/Cell.toml", - "proposals/novaseal/rwa-receipt-profile-v0/src/nova_rwa_receipt_type.cell", - "proposals/novaseal/rwa-receipt-profile-v0/src/nova_rwa_receipt_lifecycle_type.cell", - "proposals/novaseal/rwa-receipt-profile-v0/schemas", - "proposals/novaseal/rwa-receipt-profile-v0/fixtures", - "proposals/novaseal/rwa-receipt-profile-v0/proofs/invariant_matrix.json", -] - -RWA_LEGAL_REVIEW_FIELD_CONSTRAINTS = { - "profile": "rwa-receipt-profile-v0", - "reviewer": "real external legal or registry reviewer identity; placeholder, first-party NovaSeal/CellScript/a19q3, local/devnet/fake/internal, example, and unknown tokens are rejected", - "review_date": "UTC date in YYYY-MM-DD form; future dates are rejected", - "review_scope": "exact RWA receipt legal-title, custody, registry-state, oracle-fact, and enforceability review scope", - "registry.authority": "real registry or custodian authority identity; placeholder, first-party NovaSeal/CellScript/a19q3, local/devnet/fake/internal, example, and unknown tokens are rejected", - "registry.jurisdiction": "explicit real-world jurisdiction; placeholder, local/devnet/fake/internal, example, and unknown tokens are rejected", - "registry.registry_report_hash": "0x-prefixed 32-byte non-placeholder hash of the external registry/legal review report", - "profile_source_tree_sha256": "0x-prefixed 32-byte non-placeholder SHA-256 hash of the RWA profile source tree", - "report_uri": "HTTPS URI for the public legal/registry review report or source-controlled review commit; example, loopback, private, and reserved hosts are rejected", - "request_handoff.bundle": "target/novaseal-external-evidence-handoff-bundle.json", - "request_handoff.bundle_hash": "0x-prefixed 32-byte hash of the NovaSeal external evidence handoff bundle", - "request_handoff.bundle_hash_algorithm": "blake2b-256(person=NovaExtHandoff)", - "request_handoff.group": "rwa_legal_registry_review_evidence", -} - - -def hex0x(data: bytes) -> str: - return "0x" + data.hex() - - -def canonical_json(value: Any) -> bytes: - return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8") - - -def report_hash(label: str, value: Any) -> str: - h = hashlib.blake2b(digest_size=32, person=REPORT_PERSON) - h.update(label.encode("utf-8")) - h.update(b"\x00") - h.update(canonical_json(value)) - return hex0x(h.digest()) - - -def is_hex32(value: Any) -> bool: - return ( - isinstance(value, str) - and len(value) == 66 - and value.startswith("0x") - and all(char in "0123456789abcdefABCDEF" for char in value[2:]) - ) - - -def is_non_placeholder_hex32(value: Any) -> bool: - return is_hex32(value) and not value[2:].lower() == "00" * 32 - - -def is_non_negative_int(value: Any) -> bool: - return type(value) is int and value >= 0 - - -def is_positive_int(value: Any) -> bool: - return type(value) is int and value > 0 - - -def handoff_reference_hash(value: dict[str, Any]) -> str: - payload = {key: item for key, item in value.items() if key not in HANDOFF_SELF_HASH_FIELDS} - return report_hash("external_evidence_handoff_bundle", payload) - - -def source_tree_hash(paths: list[str]) -> str: - files: set[Path] = set() - allowed_suffixes = {".cell", ".schema", ".toml", ".py", ".json", ".rs"} - for raw in paths: - path = ROOT / raw - if path.is_symlink(): - raise ValueError(f"source tree path must not be a symlink: {path.relative_to(ROOT)}") - if path.is_file(): - files.add(path) - elif path.is_dir(): - for child in path.rglob("*"): - rel_parts = child.relative_to(path).parts - if any(part in {"target", "build", ".git", "__pycache__"} for part in rel_parts): - continue - if child.is_symlink(): - raise ValueError(f"source tree path must not be a symlink: {child.relative_to(ROOT)}") - if child.is_file() and (child.name == "Cargo.lock" or child.suffix in allowed_suffixes): - files.add(child) - h = hashlib.sha256() - for path in sorted(files): - rel_path = str(path.relative_to(ROOT)) - h.update(rel_path.encode("utf-8")) - h.update(b"\x00") - h.update(hashlib.sha256(path.read_bytes()).digest()) - return hex0x(h.digest()) - - -def required_field_set(case: dict[str, Any]) -> set[str]: - fields = case.get("request", {}).get("required_public_fields", []) - return {field for field in fields if isinstance(field, str)} - - -def expected_btc_binding_fields(profile: str) -> set[str]: - return { - "ckb_live_tx_hash", - "live_report_hash", - "service_builder_case_hash", - "service_builder_tx_skeleton_hash", - "service_builder_receipt_binding_hash", - "ckb_btc_commitment_hash", - *BTC_SPV_COMMON_BINDING_REQUEST_FIELDS.keys(), - *BTC_SPV_PROFILE_BINDING_REQUEST_FIELDS.get(profile, {}).keys(), - } - - -def btc_binding_value_valid(profile: str, field: str, value: Any) -> bool: - if field in { - "ckb_live_tx_hash", - "live_report_hash", - "service_builder_case_hash", - "service_builder_tx_skeleton_hash", - "service_builder_receipt_binding_hash", - "ckb_btc_commitment_hash", - "btc_txid", - "btc_wtxid", - "sealed_btc_txid", - "script_pubkey_hash", - "sealed_utxo_commitment_hash", - }: - return is_non_placeholder_hex32(value) - if field == "anchor_source": - return isinstance(value, str) and value == PRODUCTION_BTC_ANCHOR_SOURCES.get(profile) - if field in { - "spend_input_index", - "sealed_btc_vout_index", - "btc_output_index", - }: - return is_non_negative_int(value) - if field in { - "btc_amount_sats", - "sealed_btc_amount_sats", - }: - return is_positive_int(value) - return False - - -def btc_spv_handoff_case(adapter: dict[str, Any]) -> dict[str, Any]: - cases = adapter.get("cases", []) - profiles = {case.get("profile") for case in cases} - expected_scenarios = { - case.get("profile"): case.get("request", {}).get("scenario") - for case in cases - if isinstance(case.get("profile"), str) and isinstance(case.get("request", {}).get("scenario"), str) - } - expected_case_bindings = {} - for case in cases: - profile = case.get("profile") - if not isinstance(profile, str): - continue - request = case.get("request", {}) - binding = { - "ckb_live_tx_hash": case.get("request", {}).get("ckb_live_tx_hash"), - "live_report_hash": case.get("request", {}).get("live_report_hash"), - "service_builder_case_hash": case.get("request", {}).get("service_builder_case_hash"), - "service_builder_tx_skeleton_hash": case.get("request", {}).get("service_builder_tx_skeleton_hash"), - "service_builder_receipt_binding_hash": case.get("request", {}).get("service_builder_receipt_binding_hash"), - "ckb_btc_commitment_hash": request.get("ckb_btc_commitment_hash"), - } - for output_field, request_field in { - **BTC_SPV_COMMON_BINDING_REQUEST_FIELDS, - **BTC_SPV_PROFILE_BINDING_REQUEST_FIELDS.get(profile, {}), - }.items(): - if request.get(request_field) is not None: - binding[output_field] = request[request_field] - expected_case_bindings[profile] = binding - checks = { - "source_adapter_passed": adapter.get("status") == "passed", - "source_adapter_status_request_ready": adapter.get("adapter_status") == "request_ready_external_evidence_required", - "production_output_matches": adapter.get("production_output") == PUBLIC_BTC_SPV_EVIDENCE, - "summary_counts_match": adapter.get("summary", {}).get("total") == len(REQUIRED_BTC_SPV_PROFILES) - and adapter.get("summary", {}).get("matched") == adapter.get("summary", {}).get("total"), - "required_profiles_complete": profiles == set(REQUIRED_BTC_SPV_PROFILES), - "expected_scenarios_complete": set(expected_scenarios) == set(REQUIRED_BTC_SPV_PROFILES) - and all(expected_scenarios.values()), - "expected_case_bindings_complete": set(expected_case_bindings) == set(REQUIRED_BTC_SPV_PROFILES) - and all( - set(binding.keys()) == expected_btc_binding_fields(profile) - and all(btc_binding_value_valid(profile, field, value) for field, value in binding.items()) - for profile, binding in expected_case_bindings.items() - ), - "source_cases_passed": all(case.get("status") == "passed" for case in cases), - } - return { - "group": "public_btc_spv_evidence", - "status": "passed" if all(checks.values()) else "failed", - "checks": checks, - "source_adapter": str(DEFAULT_BTC_SPV_ADAPTER.relative_to(ROOT)), - "source_adapter_hash": report_hash("btc_spv_adapter", adapter), - "production_output": PUBLIC_BTC_SPV_EVIDENCE, - "required_profiles": REQUIRED_BTC_SPV_PROFILES, - "expected_scenarios": expected_scenarios, - "expected_case_bindings": expected_case_bindings, - "required_external_fields": [ - "network", - "generated_at", - "evidence_provider", - "required_profiles", - "profile", - "scenario", - "ckb_live_tx_hash", - "live_report_hash", - "service_builder_case_hash", - "service_builder_tx_skeleton_hash", - "service_builder_receipt_binding_hash", - "ckb_btc_commitment_hash", - "btc_txid", - "btc_wtxid", - "btc_tx_hex", - "btc_block_hash", - "btc_block_header", - "btc_merkle_proof.tx_index", - "btc_merkle_proof.merkle_branch", - "btc_merkle_proof.merkle_root", - "btc_merkle_proof.block_height", - "btc_merkle_proof.observed_tip_height", - "btc_transaction_binding.kind", - "btc_transaction_binding.btc_output_index", - "btc_transaction_binding.btc_amount_sats", - "btc_transaction_binding.spend_input_index", - "btc_transaction_binding.sealed_btc_txid", - "btc_transaction_binding.sealed_btc_vout_index", - "btc_transaction_binding.sealed_btc_amount_sats", - "btc_transaction_binding.script_pubkey_hash", - "btc_transaction_binding.sealed_btc_tx_hex", - "btc_transaction_binding.sealed_utxo_commitment_hash", - "spv_proof_hash", - "minimum_confirmations", - "confirmations", - "spv_client_cell_dep.out_point", - "spv_client_cell_dep.data_hash", - "spv_client_cell_dep.dep_type", - "spv_client_cell_dep.hash_type", - "source_service.name", - "source_service.commit", - "source_service.report_hash", - "request_handoff.bundle", - "request_handoff.bundle_hash", - "request_handoff.bundle_hash_algorithm", - "request_handoff.group", - ], - "field_constraints": { - "network": "explicit public mainnet/testnet name; placeholders and local/devnet/regtest/simnet/private/fake labels are rejected", - "generated_at": "UTC timestamp in YYYY-MM-DDTHH:MM:SSZ form; future timestamps are rejected", - "evidence_provider": "real external provider identity; placeholder, first-party NovaSeal/CellScript/a19q3, local/devnet/fake/internal, example, and unknown tokens are rejected", - "ckb_live_tx_hash": "0x-prefixed 32-byte CKB live transaction hash matching the current NovaSeal service-builder case", - "live_report_hash": "0x-prefixed 32-byte hash of the current NovaSeal live devnet report for this profile", - "service_builder_case_hash": "0x-prefixed 32-byte hash of the current NovaSeal service-builder case for this profile", - "service_builder_tx_skeleton_hash": "0x-prefixed 32-byte service-builder transaction skeleton hash for this profile", - "service_builder_receipt_binding_hash": "0x-prefixed 32-byte service-builder receipt binding hash for this profile", - "ckb_btc_commitment_hash": "0x-prefixed 32-byte CKB-side BTC commitment hash from the current live profile report", - "btc_txid": "0x-prefixed 32-byte non-placeholder Bitcoin transaction id", - "btc_wtxid": "0x-prefixed 32-byte Bitcoin witness transaction id derived from btc_tx_hex", - "btc_tx_hex": "0x-prefixed raw Bitcoin transaction bytes whose txid/wtxid match the public evidence case", - "btc_block_hash": "0x-prefixed 32-byte non-placeholder Bitcoin block hash anchoring the SPV proof", - "btc_block_header": "0x-prefixed 80-byte Bitcoin block header whose double-SHA256 hash matches btc_block_hash", - "btc_merkle_proof.tx_index": "zero-based transaction index used to orient the Merkle branch", - "btc_merkle_proof.merkle_branch": ( - "array of 0x-prefixed 32-byte Bitcoin sibling hashes in display order; " - "empty only for tx_index 0 in a single-transaction block" - ), - "btc_merkle_proof.merkle_root": "0x-prefixed 32-byte Bitcoin Merkle root matching the block header", - "btc_merkle_proof.block_height": "public Bitcoin block height containing btc_txid", - "btc_merkle_proof.observed_tip_height": "public Bitcoin tip height used to compute confirmations", - "btc_transaction_binding.kind": "profile-specific binding kind: btc_transaction_output, btc_utxo_spend, or dual_seal_btc_closure", - "btc_transaction_binding.btc_output_index": "BTC transaction commitment output index; required for btc-transaction-commitment-profile-v0", - "btc_transaction_binding.btc_amount_sats": "BTC transaction commitment output amount in sats; required for btc-transaction-commitment-profile-v0", - "btc_transaction_binding.spend_input_index": "Bitcoin spend input index; required for UTXO and dual-seal closure profiles", - "btc_transaction_binding.sealed_btc_txid": "sealed Bitcoin transaction id whose output is spent; required for btc-utxo-seal-profile-v0 and dual-seal-profile-v0", - "btc_transaction_binding.sealed_btc_vout_index": "sealed Bitcoin output index; required for btc-utxo-seal-profile-v0 and dual-seal-profile-v0", - "btc_transaction_binding.sealed_btc_amount_sats": "sealed Bitcoin output amount in sats; required for btc-utxo-seal-profile-v0 and dual-seal-profile-v0", - "btc_transaction_binding.script_pubkey_hash": "0x-prefixed CKB Blake2b-256 hash of the sealed output scriptPubKey bytes; required for btc-utxo-seal-profile-v0 and dual-seal-profile-v0", - "btc_transaction_binding.sealed_btc_tx_hex": "0x-prefixed raw sealed Bitcoin transaction bytes; required for btc-utxo-seal-profile-v0 and dual-seal-profile-v0", - "btc_transaction_binding.sealed_utxo_commitment_hash": "0x-prefixed 32-byte CKB-side sealed UTXO commitment hash; required for btc-utxo-seal-profile-v0 and dual-seal-profile-v0", - "spv_proof_hash": "0x-prefixed SHA-256 hash of the canonical BTC SPV proof material carried in this case", - "minimum_confirmations": "integer confirmation floor; at least 6", - "confirmations": "integer observed confirmations meeting minimum_confirmations", - "spv_client_cell_dep.out_point": "0x-prefixed 32-byte CKB transaction hash plus numeric output index", - "spv_client_cell_dep.data_hash": "0x-prefixed 32-byte non-placeholder SPV client data hash", - "spv_client_cell_dep.dep_type": "code", - "spv_client_cell_dep.hash_type": "data, data1, or type CKB script hash type", - "source_service.name": "real external SPV service identity; placeholder, first-party NovaSeal/CellScript/a19q3, local/devnet/fake/internal, example, and unknown tokens are rejected", - "source_service.commit": "40-character hex service source commit", - "source_service.report_hash": "0x-prefixed 32-byte non-placeholder SPV service report hash", - "request_handoff.bundle": "target/novaseal-external-evidence-handoff-bundle.json", - "request_handoff.bundle_hash": "0x-prefixed 32-byte hash of the NovaSeal external evidence handoff bundle", - "request_handoff.bundle_hash_algorithm": "blake2b-256(person=NovaExtHandoff)", - "request_handoff.group": "public_btc_spv_evidence", - }, - } - - -def attestation_case( - adapter: dict[str, Any], - *, - case_name: str, - group: str, - production_output: str, - required_fields: list[str], -) -> dict[str, Any]: - cases = adapter.get("cases", []) - source_case = next((case for case in cases if case.get("name") == case_name), {}) - request = source_case.get("request", {}) - fields = required_field_set(source_case) - checks = { - "source_adapter_passed": adapter.get("status") == "passed", - "source_adapter_status_request_ready": adapter.get("adapter_status") == "request_ready_external_attestations_required", - "source_case_passed": source_case.get("status") == "passed", - "production_output_matches": request.get("production_output") == production_output, - "required_fields_complete": set(required_fields).issubset(fields), - } - expected_values = {} - if request.get("expected_release_package"): - expected_values["release.package"] = request["expected_release_package"] - if request.get("expected_release_version"): - expected_values["release.version"] = request["expected_release_version"] - if request.get("expected_release_manifest_commit"): - expected_values["release.manifest_commit"] = request["expected_release_manifest_commit"] - if request.get("expected_dep_type"): - expected_values["runtime_verifier.dep_type"] = request["expected_dep_type"] - if request.get("expected_hash_type"): - expected_values["runtime_verifier.hash_type"] = request["expected_hash_type"] - if case_name == "public_shared_cell_dep_attestation" and request.get("ipc_abi"): - expected_values["runtime_verifier.ipc_abi"] = request["ipc_abi"] - if case_name == "public_shared_cell_dep_attestation" and request.get("verifier_id"): - expected_values["runtime_verifier.verifier_id"] = request["verifier_id"] - if case_name == "external_bip340_tcb_review_attestation" and request.get("ipc_abi"): - expected_values["ipc_abi"] = request["ipc_abi"] - if case_name == "external_bip340_tcb_review_attestation" and request.get("verifier_id"): - expected_values["verifier_id"] = request["verifier_id"] - if request.get("expected_artifact_hash"): - expected_values["artifact_hash"] = request["expected_artifact_hash"] - if request.get("expected_artifact_hash_algorithm"): - expected_values["artifact_hash_algorithm"] = request["expected_artifact_hash_algorithm"] - if request.get("expected_review_scope"): - expected_values["review_scope"] = request["expected_review_scope"] - if request.get("expected_source_tree_sha256"): - expected_values["source_tree_sha256"] = request["expected_source_tree_sha256"] - - result = { - "group": group, - "status": "passed" if all(checks.values()) else "failed", - "checks": checks, - "source_adapter": str(DEFAULT_EXTERNAL_ATTESTATION_ADAPTER.relative_to(ROOT)), - "source_adapter_hash": report_hash("external_attestation_adapter", adapter), - "source_case": case_name, - "production_output": production_output, - "required_external_fields": required_fields, - "field_constraints": source_case.get("request", {}).get("field_constraints", {}), - } - if expected_values: - result["expected_values"] = expected_values - return result - - -def rwa_legal_registry_review_case(external_attestation_adapter: dict[str, Any]) -> dict[str, Any]: - source_hash = source_tree_hash(RWA_LEGAL_REVIEW_SOURCE_HASH_PATHS) - checks = { - "source_external_attestation_adapter_passed": external_attestation_adapter.get("status") == "passed", - "source_external_attestation_adapter_status_request_ready": external_attestation_adapter.get("adapter_status") - == "request_ready_external_attestations_required", - "production_output_matches": RWA_LEGAL_REGISTRY_REVIEW_EVIDENCE.endswith( - "legal_registry_review_evidence.json" - ), - "profile_source_tree_hash_current": len(source_hash) == 66 and source_hash.startswith("0x"), - } - return { - "group": "rwa_legal_registry_review_evidence", - "status": "passed" if all(checks.values()) else "failed", - "checks": checks, - "source_adapter": str(DEFAULT_EXTERNAL_ATTESTATION_ADAPTER.relative_to(ROOT)), - "source_adapter_hash": report_hash("external_attestation_adapter", external_attestation_adapter), - "production_output": RWA_LEGAL_REGISTRY_REVIEW_EVIDENCE, - "required_external_fields": REQUIRED_RWA_LEGAL_REVIEW_FIELDS, - "field_constraints": RWA_LEGAL_REVIEW_FIELD_CONSTRAINTS, - "expected_values": { - "profile": "rwa-receipt-profile-v0", - "profile_source_tree_sha256": source_hash, - "review_scope": [ - "RWA receipt legal title boundary", - "RWA receipt custody and registry-state provenance", - "RWA receipt oracle-fact exclusion boundary", - "RWA receipt enforceability and jurisdiction boundary", - ], - }, - } - - -def build_report(btc_spv_adapter: dict[str, Any], external_attestation_adapter: dict[str, Any]) -> dict[str, Any]: - cases = [ - btc_spv_handoff_case(btc_spv_adapter), - attestation_case( - external_attestation_adapter, - case_name="public_shared_cell_dep_attestation", - group="public_shared_cell_dep_attestation", - production_output=PUBLIC_CELLDEP_ATTESTATION, - required_fields=REQUIRED_PUBLIC_CELLDEP_FIELDS, - ), - attestation_case( - external_attestation_adapter, - case_name="external_bip340_tcb_review_attestation", - group="external_bip340_tcb_review_attestation", - production_output=EXTERNAL_TCB_ATTESTATION, - required_fields=REQUIRED_EXTERNAL_TCB_FIELDS, - ), - rwa_legal_registry_review_case(external_attestation_adapter), - ] - production_outputs = [case["production_output"] for case in cases] - status = "passed" if all(case["status"] == "passed" for case in cases) else "failed" - report = { - "schema": "novaseal-external-evidence-handoff-bundle-v0.1", - "status": status, - "handoff_status": "request_bundle_ready_external_evidence_required", - "source_btc_spv_adapter": str(DEFAULT_BTC_SPV_ADAPTER.relative_to(ROOT)), - "source_btc_spv_adapter_hash": report_hash("btc_spv_adapter", btc_spv_adapter), - "source_external_attestation_adapter": str(DEFAULT_EXTERNAL_ATTESTATION_ADAPTER.relative_to(ROOT)), - "source_external_attestation_adapter_hash": report_hash( - "external_attestation_adapter", external_attestation_adapter - ), - "production_outputs": production_outputs, - "production_boundary": "This handoff proves external request completeness; it does not satisfy external production evidence.", - "summary": { - "total": len(cases), - "matched": len([case for case in cases if case["status"] == "passed"]), - "groups": [case["group"] for case in cases], - }, - "cases": cases, - } - report["bundle_hash_algorithm"] = HANDOFF_HASH_ALGORITHM - report["bundle_hash"] = handoff_reference_hash(report) - return report - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--btc-spv-adapter", type=Path, default=DEFAULT_BTC_SPV_ADAPTER) - parser.add_argument("--external-attestation-adapter", type=Path, default=DEFAULT_EXTERNAL_ATTESTATION_ADAPTER) - parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) - parser.add_argument("--pretty", action="store_true") - args = parser.parse_args() - - btc_spv_adapter = json.loads(args.btc_spv_adapter.read_text(encoding="utf-8")) - external_attestation_adapter = json.loads(args.external_attestation_adapter.read_text(encoding="utf-8")) - try: - report = build_report(btc_spv_adapter, external_attestation_adapter) - except ValueError as error: - print(f"error: {error}", file=sys.stderr) - return 1 - args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") - if args.pretty: - print( - f"wrote {args.output} status={report['status']} " - f"groups={report['summary']['matched']}/{report['summary']['total']}" - ) - return 0 if report["status"] == "passed" else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/novaseal_fiber_node_experiments.py b/scripts/novaseal_fiber_node_experiments.py deleted file mode 100644 index 6c85b8b0..00000000 --- a/scripts/novaseal_fiber_node_experiments.py +++ /dev/null @@ -1,688 +0,0 @@ -#!/usr/bin/env python3 -"""Build NovaSeal evidence from the cloned Fiber Network Node repository. - -The report is deliberately stricter than a source inventory. It records the -exact Fiber clone, checks that the expected devnet/e2e workflow suites exist, -maps each suite back to NovaSeal profiles, and optionally runs selected Bruno -e2e suites against Fiber's own devnet runner. - -Without --run-suite or --run-all the report is a discovery contract, not live -execution evidence. -""" - -from __future__ import annotations - -import argparse -import json -import os -import pathlib -import re -import shutil -import signal -import subprocess -import time -from dataclasses import dataclass -from typing import Any - - -SCHEMA = "novaseal-fiber-node-execution-v0.4" -SUPPORTED_PREVIOUS_SCHEMAS = { - "novaseal-fiber-node-execution-v0.1", - "novaseal-fiber-node-execution-v0.2", - "novaseal-fiber-node-execution-v0.3", - SCHEMA, -} - - -@dataclass(frozen=True) -class FiberWorkflow: - suite: str - category: str - description: str - mapped_profiles: tuple[str, ...] - expected_terms: tuple[str, ...] - requires_lnd: bool = False - - -REQUIRED_WORKFLOWS: tuple[FiberWorkflow, ...] = ( - FiberWorkflow( - suite="open-use-close-a-channel", - category="channel-lifecycle", - description="single-channel open, TLC add/remove, cooperative shutdown, and closed-state checks", - mapped_profiles=("fiber-candidate-profile-v0",), - expected_terms=("open-channel", "add-tlc", "remove-tlc", "shutdown", "list-channel"), - ), - FiberWorkflow( - suite="3-nodes-transfer", - category="multi-hop-transfer", - description="three-node channel graph with routed TLC transfer and shutdown", - mapped_profiles=("fiber-candidate-profile-v0",), - expected_terms=("connect", "open-channel", "add-tlc", "remove-tlc", "shutdown"), - ), - FiberWorkflow( - suite="router-pay", - category="multi-hop-payment", - description="router payment workflow with invoice, keysend, graph, duplicate, and failure paths", - mapped_profiles=("fiber-candidate-profile-v0",), - expected_terms=("send-payment", "gen-invoice", "get-payment-status", "list-graph", "will-fail"), - ), - FiberWorkflow( - suite="invoice-ops", - category="invoice", - description="invoice generation, duplicate rejection, decode, lookup, and cancellation", - mapped_profiles=("fiber-candidate-profile-v0",), - expected_terms=("gen-invoice", "duplicate", "decode", "get-invoice", "cancel"), - ), - FiberWorkflow( - suite="shutdown-force", - category="force-close", - description="force shutdown after peer disconnect and closed-channel assertions", - mapped_profiles=("fiber-candidate-profile-v0",), - expected_terms=("shutdown-force", "disconnect", "closed-channel", "trigger-check"), - ), - FiberWorkflow( - suite="reestablish", - category="reconnect", - description="channel reestablishment after disconnect before TLC removal and shutdown", - mapped_profiles=("fiber-candidate-profile-v0",), - expected_terms=("disconnect", "reconnect", "remove-tlc", "shutdown"), - ), - FiberWorkflow( - suite="external-funding-open", - category="external-funding", - description="external funding script, signing, submission, channel ready, shutdown, and balance checks", - mapped_profiles=("fiber-candidate-profile-v0", "btc-transaction-commitment-profile-v0"), - expected_terms=("funding-script", "external-funding", "sign", "submit", "balance-after"), - ), - FiberWorkflow( - suite="funding-tx-verification", - category="funding-verification", - description="funding transaction verification with a shell builder and auto-accepted channel check", - mapped_profiles=("fiber-candidate-profile-v0", "btc-transaction-commitment-profile-v0"), - expected_terms=("funding-tx", "verification", "open-channel", "auto-accepted"), - ), - FiberWorkflow( - suite="udt", - category="udt-channel", - description="UDT channel open, invoice/TLC flow, invalid open, manual accept, and shutdown", - mapped_profiles=("fiber-candidate-profile-v0", "fungible-xudt-profile-v0"), - expected_terms=("udt", "open-channel", "add-tlc", "remove-tlc", "invalid", "shutdown"), - ), - FiberWorkflow( - suite="udt-router-pay", - category="udt-routing", - description="multi-hop routed UDT payment including invoice and keysend paths", - mapped_profiles=("fiber-candidate-profile-v0", "fungible-xudt-profile-v0"), - expected_terms=("udt", "router", "send-payment", "gen-invoice", "keysend"), - ), - FiberWorkflow( - suite="watchtower/force-close-after-open-channel", - category="watchtower", - description="watchtower force-close settlement after opening a channel", - mapped_profiles=("fiber-candidate-profile-v0",), - expected_terms=("force-close", "commitment-tx", "settlement", "check-balance"), - ), - FiberWorkflow( - suite="watchtower/force-close-with-pending-tlcs", - category="watchtower", - description="force-close with pending TLCs, settlement transaction generation, and balance checks", - mapped_profiles=("fiber-candidate-profile-v0",), - expected_terms=("pending-tlcs", "force-close", "settlement", "commitment-tx", "check-balance"), - ), - FiberWorkflow( - suite="watchtower/force-close-with-pending-tlcs-and-udt", - category="watchtower-udt", - description="force-close with pending UDT TLCs and CKB/UDT balance checks", - mapped_profiles=("fiber-candidate-profile-v0", "fungible-xudt-profile-v0"), - expected_terms=("pending-tlcs", "udt", "force-close", "settlement", "check-balance"), - ), - FiberWorkflow( - suite="watchtower/force-close-preimage-multiple", - category="watchtower-preimage", - description="multiple preimage settlement path after force-close", - mapped_profiles=("fiber-candidate-profile-v0",), - expected_terms=("preimage", "force-close", "settlement", "check-balance"), - ), - FiberWorkflow( - suite="cross-chain-hub", - category="cross-chain", - description="Fiber plus Lightning/BTC hub send and receive order workflow", - mapped_profiles=( - "fiber-candidate-profile-v0", - "btc-transaction-commitment-profile-v0", - "btc-utxo-seal-profile-v0", - ), - expected_terms=("btc", "lnd", "send-payment", "order", "wrapped-btc", "shutdown"), - requires_lnd=True, - ), - FiberWorkflow( - suite="cross-chain-hub-separate", - category="cross-chain", - description="Fiber plus Lightning/BTC hub workflow with CCH running as a separate service", - mapped_profiles=( - "fiber-candidate-profile-v0", - "btc-transaction-commitment-profile-v0", - "btc-utxo-seal-profile-v0", - ), - expected_terms=("btc", "lnd", "send-payment", "order", "wrapped-btc", "shutdown"), - requires_lnd=True, - ), -) - - -def parse_args() -> argparse.Namespace: - repo_root = pathlib.Path(__file__).resolve().parents[1] - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--repo-root", type=pathlib.Path, default=repo_root) - parser.add_argument("--fiber-repo", type=pathlib.Path, default=repo_root.parent / "fiber") - parser.add_argument("--output", type=pathlib.Path, default=repo_root / "target/novaseal-fiber-node-experiments.json") - parser.add_argument("--pretty", action="store_true") - parser.add_argument("--run-suite", action="append", choices=[workflow.suite for workflow in REQUIRED_WORKFLOWS]) - parser.add_argument("--run-all", action="store_true") - parser.add_argument("--assume-nodes-running", action="store_true") - parser.add_argument("--timeout-seconds", type=int, default=1800) - return parser.parse_args() - - -def run_cmd( - args: list[str], - cwd: pathlib.Path, - *, - timeout: int | None = None, - env: dict[str, str] | None = None, -) -> subprocess.CompletedProcess[str]: - return subprocess.run(args, cwd=cwd, text=True, capture_output=True, timeout=timeout, env=env) - - -def git_value(fiber_repo: pathlib.Path, args: list[str]) -> str | None: - completed = run_cmd(["git", *args], fiber_repo) - if completed.returncode != 0: - return None - return completed.stdout.strip() - - -def fiber_repo_provenance(fiber_repo: pathlib.Path) -> dict[str, Any]: - return { - "path": fiber_repo.as_posix(), - "origin": git_value(fiber_repo, ["remote", "get-url", "origin"]), - "branch": git_value(fiber_repo, ["branch", "--show-current"]), - "commit": git_value(fiber_repo, ["rev-parse", "HEAD"]), - "dirty": bool(git_value(fiber_repo, ["status", "--short"])), - } - - -def same_fiber_repo_provenance(left: dict[str, Any] | None, right: dict[str, Any]) -> bool: - if not isinstance(left, dict): - return False - return all(left.get(key) == right.get(key) for key in ("path", "origin", "branch", "commit", "dirty")) - - -def rel(path: pathlib.Path, root: pathlib.Path) -> str: - try: - return path.relative_to(root).as_posix() - except ValueError: - return path.as_posix() - - -def suite_dir(fiber_repo: pathlib.Path, suite: str) -> pathlib.Path: - return fiber_repo / "tests" / "bruno" / "e2e" / suite - - -def suite_files(fiber_repo: pathlib.Path, suite: str) -> list[pathlib.Path]: - directory = suite_dir(fiber_repo, suite) - if not directory.is_dir(): - return [] - return sorted(directory.glob("*.bru")) - - -def terms_present(files: list[pathlib.Path], expected_terms: tuple[str, ...]) -> dict[str, bool]: - names = " ".join(str(path).lower() for path in files) - return {term: term.lower() in names for term in expected_terms} - - -def extract_rpc_methods(files: list[pathlib.Path]) -> list[str]: - methods: set[str] = set() - for path in files: - try: - for line in path.read_text(encoding="utf-8").splitlines(): - marker = '"method"' - if marker not in line: - continue - after = line.split(":", 1)[-1].strip().strip(",").strip() - if after.startswith('"') and after.endswith('"'): - methods.add(after.strip('"')) - except UnicodeDecodeError: - continue - return sorted(methods) - - -def workflow_report(fiber_repo: pathlib.Path, workflow: FiberWorkflow, execution: dict[str, Any] | None) -> dict[str, Any]: - files = suite_files(fiber_repo, workflow.suite) - terms = terms_present(files, workflow.expected_terms) - present = bool(files) and all(terms.values()) - status = "present" if present else "missing" - if execution is not None: - status = execution["status"] - return { - "suite": workflow.suite, - "category": workflow.category, - "description": workflow.description, - "mapped_profiles": list(workflow.mapped_profiles), - "requires_lnd": workflow.requires_lnd, - "status": status, - "present": present, - "step_count": len(files), - "expected_terms": terms, - "rpc_methods": extract_rpc_methods(files), - "evidence_files": [rel(path, fiber_repo) for path in files], - "execution": execution, - } - - -def write_text(path: pathlib.Path, value: str) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(value, encoding="utf-8") - - -def previous_executions(output: pathlib.Path, current_fiber_repo: dict[str, Any]) -> dict[str, dict[str, Any]]: - if not output.is_file(): - return {} - try: - report = json.loads(output.read_text(encoding="utf-8")) - except (json.JSONDecodeError, OSError): - return {} - if report.get("schema") not in SUPPORTED_PREVIOUS_SCHEMAS or not same_fiber_repo_provenance( - report.get("fiber_repo"), current_fiber_repo - ): - return {} - executions: dict[str, dict[str, Any]] = {} - for workflow in report.get("workflows", []): - if not isinstance(workflow, dict): - continue - suite = workflow.get("suite") - execution = workflow.get("execution") - if ( - isinstance(suite, str) - and isinstance(execution, dict) - and same_fiber_repo_provenance(execution.get("fiber_repo"), current_fiber_repo) - ): - executions[suite] = execution - return executions - - -def cleanup_fiber_processes(fiber_repo: pathlib.Path, *, include_all_fiber_devnet: bool = False) -> None: - patterns = [ - re.compile(r"\.\./\.\./target/[^ ]*/fnn -d (?:[123]|cch)(?:\s|$)"), - re.compile(rf"ckb run -C {re.escape(str(fiber_repo / 'tests' / 'deploy' / 'node-data'))}"), - re.compile(rf"bitcoind -conf={re.escape(str(fiber_repo / 'tests' / 'deploy' / 'lnd-init' / 'bitcoind' / 'bitcoin.conf'))}"), - re.compile(rf"lnd --lnddir={re.escape(str(fiber_repo / 'tests' / 'deploy' / 'lnd-init' / 'lnd-bob'))}"), - re.compile(rf"lnd --lnddir={re.escape(str(fiber_repo / 'tests' / 'deploy' / 'lnd-init' / 'lnd-ingrid'))}"), - ] - if include_all_fiber_devnet: - patterns.extend( - [ - re.compile(r"bash \./tests/nodes/start\.sh e2e/"), - re.compile(r"ckb run -C .*/tests/deploy/node-data(?:\s|$)"), - re.compile(r"bitcoind -conf=.*/tests/deploy/lnd-init/bitcoind/bitcoin\.conf(?:\s|$)"), - re.compile(r"lnd --lnddir=.*/tests/deploy/lnd-init/lnd-(?:bob|ingrid)(?:\s|$)"), - ] - ) - completed = subprocess.run(["ps", "-axo", "pid=,command="], text=True, capture_output=True, check=False) - matched_pids: list[int] = [] - for line in completed.stdout.splitlines(): - fields = line.strip().split(maxsplit=1) - if len(fields) != 2: - continue - pid_text, command = fields - if not any(pattern.search(command) for pattern in patterns): - continue - try: - pid = int(pid_text) - except ValueError: - continue - if pid == os.getpid(): - continue - try: - os.kill(pid, signal.SIGTERM) - matched_pids.append(pid) - except ProcessLookupError: - continue - time.sleep(2) - for pid in matched_pids: - try: - os.kill(pid, 0) - except ProcessLookupError: - continue - os.kill(pid, signal.SIGKILL) - - -def wait_for_fiber_nodes( - fiber_repo: pathlib.Path, - node_process: subprocess.Popen[str], - log_dir: pathlib.Path, - timeout: int, - env: dict[str, str], -) -> dict[str, Any] | None: - started_at = time.time() - wait_process = subprocess.Popen( - ["./tests/nodes/wait.sh"], - cwd=fiber_repo, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=env, - ) - while True: - wait_returncode = wait_process.poll() - if wait_returncode is not None: - wait_stdout, wait_stderr = wait_process.communicate() - write_text(log_dir / "wait.stdout", wait_stdout) - write_text(log_dir / "wait.stderr", wait_stderr) - if wait_returncode != 0: - return { - "failure": "fiber node wait failed", - "wait_returncode": wait_returncode, - } - if node_process.poll() is not None: - return { - "failure": "fiber node launcher exited after readiness check", - "node_returncode": node_process.returncode, - "wait_returncode": wait_returncode, - } - return None - - if node_process.poll() is not None: - wait_process.terminate() - try: - wait_stdout, wait_stderr = wait_process.communicate(timeout=10) - except subprocess.TimeoutExpired: - wait_process.kill() - wait_stdout, wait_stderr = wait_process.communicate(timeout=10) - write_text(log_dir / "wait.stdout", wait_stdout) - write_text(log_dir / "wait.stderr", wait_stderr) - return { - "failure": "fiber node launcher exited before readiness check completed", - "node_returncode": node_process.returncode, - "wait_returncode": wait_process.returncode, - } - - if time.time() - started_at > timeout: - wait_process.terminate() - try: - wait_stdout, wait_stderr = wait_process.communicate(timeout=10) - except subprocess.TimeoutExpired: - wait_process.kill() - wait_stdout, wait_stderr = wait_process.communicate(timeout=10) - write_text(log_dir / "wait.stdout", wait_stdout) - write_text(log_dir / "wait.stderr", wait_stderr) - return { - "failure": "fiber node wait timed out", - "wait_timeout_seconds": timeout, - } - - time.sleep(1) - - -def fiber_run_env(base_env: dict[str, str], log_dir: pathlib.Path) -> dict[str, str]: - env = dict(base_env) - real_ckb_cli = shutil.which("ckb-cli", path=env.get("PATH")) - if real_ckb_cli is None: - return env - tool_bin = log_dir / "tool-bin" - tool_bin.mkdir(parents=True, exist_ok=True) - wrapper = tool_bin / "ckb-cli" - wrapper.write_text( - "#!/usr/bin/env bash\n" - "set -euo pipefail\n" - "if [[ \"$*\" == *\"account import\"* ]]; then\n" - " echo 'novaseal test wrapper: skipped interactive ckb-cli account import' >&2\n" - " exit 0\n" - "fi\n" - "exec \"${REAL_CKB_CLI}\" \"$@\"\n", - encoding="utf-8", - ) - wrapper.chmod(0o755) - env["REAL_CKB_CLI"] = real_ckb_cli - env["PATH"] = f"{tool_bin}{os.pathsep}{env.get('PATH', '')}" - return env - - -def bruno_workspace_for_suite( - fiber_repo: pathlib.Path, - suite: str, - log_dir: pathlib.Path, -) -> tuple[pathlib.Path, list[str]]: - """Return a Bruno workspace, applying explicit suite compatibility patches when needed.""" - source = fiber_repo / "tests" / "bruno" - patches: list[str] = [] - patched_suites = { - "watchtower/force-close-with-pending-tlcs-and-udt", - "cross-chain-hub", - "cross-chain-hub-separate", - } - if suite not in patched_suites: - return source, patches - - workspace = log_dir / "bruno-worktree" - if workspace.exists(): - shutil.rmtree(workspace) - shutil.copytree(source, workspace, ignore=shutil.ignore_patterns("node_modules")) - - replacements: dict[str, str] = {} - if suite == "watchtower/force-close-with-pending-tlcs-and-udt": - replacements.update( - { - 'bru.setVar("NODE1_BALANCE", capacity);': 'bru.setVar("NODE1_BALANCE", capacity.toString());', - 'bru.setVar("NODE2_BALANCE", capacity);': 'bru.setVar("NODE2_BALANCE", capacity.toString());', - 'bru.setVar("NODE1_NEW_BALANCE", capacity);': 'bru.setVar("NODE1_NEW_BALANCE", capacity.toString());', - 'bru.setVar("NODE2_NEW_BALANCE", capacity);': 'bru.setVar("NODE2_NEW_BALANCE", capacity.toString());', - } - ) - if suite in {"cross-chain-hub", "cross-chain-hub-separate"}: - replacements.update( - { - 'bru.setVar("FIBER_PAY_REQ", res.body.result.invoice_address);\n bru.setVar("PAYMENT_HASH", res.body.result.invoice.data.payment_hash);': ( - 'bru.setVar("FIBER_PAY_REQ", res.body.result.invoice_address);\n' - ' bru.setVar("PAYMENT_HASH", res.body.result.invoice.data.payment_hash);\n' - ' console.log("receive_fiber_pay_req", res.body.result.invoice_address);\n' - ' console.log("receive_payment_hash", res.body.result.invoice.data.payment_hash);' - ), - 'bru.setVar("BTC_PAY_REQ", res.body.result.incoming_invoice.Lightning);\n console.log(res.body.result.incoming_invoice.Lightning);': ( - 'console.log("receive_btc_body", JSON.stringify(res.body));\n' - ' if (res.body.result) {\n' - ' bru.setVar("BTC_PAY_REQ", res.body.result.incoming_invoice.Lightning);\n' - ' console.log(res.body.result.incoming_invoice.Lightning);\n' - ' }' - ), - 'if (resp.data !== undefined) {\n resp.data.destroy();\n }': ( - 'if (resp.data !== undefined && typeof resp.data.destroy === "function") {\n' - ' resp.data.destroy();\n' - ' }' - ), - } - ) - suite_path = workspace / "e2e" / suite - for path in sorted(suite_path.glob("*.bru")): - text = path.read_text(encoding="utf-8") - updated = text - for old, new in replacements.items(): - updated = updated.replace(old, new) - if updated != text: - path.write_text(updated, encoding="utf-8") - patches.append(rel(path, workspace)) - return workspace, patches - - -def run_workflow(args: argparse.Namespace, workflow: FiberWorkflow) -> dict[str, Any]: - fiber_repo = args.fiber_repo.resolve() - fiber_repo_info = fiber_repo_provenance(fiber_repo) - suite_arg = f"e2e/{workflow.suite}" - log_dir = args.output.resolve().parent / "novaseal-fiber-node-experiments" / workflow.suite.replace("/", "__") - log_dir.mkdir(parents=True, exist_ok=True) - env = fiber_run_env(os.environ, log_dir) - clean_external_devnet_state = bool(env.get("REMOVE_OLD_STATE") or env.get("NOVASEAL_CLEAN_FIBER_DEVNET_PROCESSES")) - started_node = False - node_process: subprocess.Popen[str] | None = None - node_log_handle = None - started_at = time.time() - try: - if not args.assume_nodes_running: - cleanup_fiber_processes(fiber_repo, include_all_fiber_devnet=clean_external_devnet_state) - node_log = log_dir / "start-node.log" - node_log_handle = node_log.open("w", encoding="utf-8") - node_process = subprocess.Popen( - ["./tests/nodes/start.sh", suite_arg], - cwd=fiber_repo, - text=True, - stdout=node_log_handle, - stderr=subprocess.STDOUT, - start_new_session=True, - env=env, - ) - started_node = True - readiness_failure = wait_for_fiber_nodes(fiber_repo, node_process, log_dir, args.timeout_seconds, env) - if readiness_failure is not None: - return { - "status": "failed", - "started_node": started_node, - "command": ["./tests/nodes/start.sh", suite_arg], - "duration_seconds": round(time.time() - started_at, 3), - "fiber_repo": fiber_repo_info, - **readiness_failure, - } - bruno_cwd, bruno_compatibility_patches = bruno_workspace_for_suite(fiber_repo, workflow.suite, log_dir) - command = ["npm", "exec", "--", "@usebruno/cli", "run", suite_arg, "-r", "--env", "test"] - completed = run_cmd(command, bruno_cwd, timeout=args.timeout_seconds, env=env) - write_text(log_dir / "bruno.stdout", completed.stdout) - write_text(log_dir / "bruno.stderr", completed.stderr) - execution = { - "status": "passed" if completed.returncode == 0 else "failed", - "started_node": started_node, - "command": command, - "returncode": completed.returncode, - "noninteractive_ckb_cli_account_import_wrapper": (log_dir / "tool-bin" / "ckb-cli").is_file(), - "stdout_log": rel(log_dir / "bruno.stdout", args.repo_root.resolve()), - "stderr_log": rel(log_dir / "bruno.stderr", args.repo_root.resolve()), - "duration_seconds": round(time.time() - started_at, 3), - "fiber_repo": fiber_repo_info, - } - if bruno_compatibility_patches: - execution["bruno_cwd"] = rel(bruno_cwd, args.repo_root.resolve()) - execution["bruno_compatibility_patches"] = bruno_compatibility_patches - return execution - finally: - if node_process is not None and node_process.poll() is None: - if hasattr(os, "killpg"): - os.killpg(os.getpgid(node_process.pid), signal.SIGTERM) - else: - node_process.terminate() - try: - node_process.wait(timeout=20) - except subprocess.TimeoutExpired: - node_process.kill() - node_process.wait(timeout=20) - if started_node: - cleanup_fiber_processes(fiber_repo, include_all_fiber_devnet=clean_external_devnet_state) - if node_log_handle is not None: - node_log_handle.close() - - -def build_report(args: argparse.Namespace) -> dict[str, Any]: - repo_root = args.repo_root.resolve() - fiber_repo = args.fiber_repo.resolve() - fiber_repo_info = fiber_repo_provenance(fiber_repo) - run_suites = {workflow.suite for workflow in REQUIRED_WORKFLOWS} if args.run_all else set(args.run_suite or []) - - executions = previous_executions(args.output.resolve(), fiber_repo_info) - for workflow in REQUIRED_WORKFLOWS: - if workflow.suite in run_suites: - executions[workflow.suite] = run_workflow(args, workflow) - - workflows = [workflow_report(fiber_repo, workflow, executions.get(workflow.suite)) for workflow in REQUIRED_WORKFLOWS] - present_count = sum(1 for row in workflows if row["present"]) - executed_count = sum(1 for row in workflows if row["execution"] is not None) - passed_execution_count = sum(1 for row in workflows if row["execution"] is not None and row["execution"]["status"] == "passed") - all_present = present_count == len(REQUIRED_WORKFLOWS) - all_executed = executed_count == len(REQUIRED_WORKFLOWS) - all_executed_passed = all_executed and passed_execution_count == len(REQUIRED_WORKFLOWS) - partial_execution_passed = 0 < executed_count < len(REQUIRED_WORKFLOWS) and executed_count == passed_execution_count - runnable_contract_present = all( - (fiber_repo / path).is_file() - for path in ( - "tests/nodes/start.sh", - "tests/nodes/wait.sh", - "package.json", - "tests/bruno/bruno.json", - "docs/dev/README.md", - "Cargo.lock", - ) - ) - if not fiber_repo.is_dir(): - status = "missing_fiber_clone" - elif all_executed_passed: - status = "passed" - elif executed_count > 0 and passed_execution_count != executed_count: - status = "failed" - elif partial_execution_passed: - status = "partial_execution_passed" - elif all_present and runnable_contract_present: - status = "discovery_ready_live_not_run" - else: - status = "incomplete" - - mapped_profiles = sorted({profile for workflow in REQUIRED_WORKFLOWS for profile in workflow.mapped_profiles}) - return { - "schema": SCHEMA, - "status": status, - "generated_at_unix": int(time.time()), - "classification": "fiber_node_execution_v0", - "fiber_repo": fiber_repo_info, - "devnet_contract": { - "runnable_devnet_contract_present": runnable_contract_present, - "start_command": "./tests/nodes/start.sh e2e/", - "wait_command": "./tests/nodes/wait.sh", - "bruno_command": "cd tests/bruno && npm exec -- @usebruno/cli run e2e/ -r --env test", - "source_docs": "docs/dev/README.md", - }, - "workflow_coverage": { - "required_count": len(REQUIRED_WORKFLOWS), - "present_count": present_count, - "executed_count": executed_count, - "passed_execution_count": passed_execution_count, - "all_required_workflows_present": all_present, - "all_required_workflows_executed": all_executed, - "all_required_workflows_executed_passed": all_executed_passed, - "partial_execution_passed": partial_execution_passed, - }, - "profiles_covered": mapped_profiles, - "workflows": workflows, - "acceptance_boundary": { - "discovery_ready_live_not_run": "the Fiber clone exposes the expected devnet/e2e workflow surface, but no live Fiber node execution is claimed", - "passed": "all required Fiber workflow suites were executed through Fiber's devnet node runner and Bruno e2e harness", - "partial_execution_passed": "at least one selected Fiber workflow suite was executed and passed, but complete Fiber coverage is not claimed", - "novaseal_mapping": "NovaSeal consumes this as external Fiber-node evidence; it does not replace NovaSeal's own CKB stateful profile reports", - }, - "generated_by": { - "script": "scripts/novaseal_fiber_node_experiments.py", - "implementation": "cellscript::scripts::novaseal_fiber_node_experiments", - }, - "tooling": { - "npm": shutil.which("npm"), - "cargo": shutil.which("cargo"), - "ckb": shutil.which("ckb"), - "ckb_cli": shutil.which("ckb-cli"), - }, - } - - -def main() -> int: - args = parse_args() - report = build_report(args) - args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_text(json.dumps(report, indent=2 if args.pretty else None, sort_keys=True) + "\n", encoding="utf-8") - print(args.output) - return 0 if report["status"] not in {"missing_fiber_clone", "incomplete", "failed"} else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/novaseal_planned_profiles_devnet_stateful_live.py b/scripts/novaseal_planned_profiles_devnet_stateful_live.py deleted file mode 100755 index c933e9f2..00000000 --- a/scripts/novaseal_planned_profiles_devnet_stateful_live.py +++ /dev/null @@ -1,4709 +0,0 @@ -#!/usr/bin/env python3 -"""Run or describe NovaSeal V1 planned-profile live devnet reports. - -The certification gate only accepts reports produced from real CKB devnet -transactions with fresh source/artifact provenance. Profiles without an -implemented live runner still emit `status=not_run` contract reports. -""" - -from __future__ import annotations - -import argparse -import json -import pathlib -import subprocess -import time -from dataclasses import dataclass -from typing import Any - -from novaseal_devnet_stateful_live import ( - RECEIPT_CAPACITY, - SHANNONS, - STATE_CAPACITY, - TEST_AUX_RAND, - TEST_SECRET_KEY, - ZERO_HASH, - CkbDevnet, - LiveAcceptanceError, - always_success_dep, - always_success_lock, - cell_data_hash, - ckb_hash, - deploy_code_cell, - hex0x, - resolve_ckb_bin, - schnorr_sign, - stateful_provenance, - transaction, - u8, - u16, - u32, - u64, - xonly_pubkey, -) - - -def data_packed_hash(_type_name: str, packed: bytes) -> bytes: - return cell_data_hash(packed) - - -FUNGIBLE_XUDT_VERSION = 0 -OP_ISSUE = 0 -OP_TRANSFER = 1 -OP_SETTLE = 2 -STATUS_ACTIVE = 1 -STATUS_SETTLED = 2 -RWA_RECEIPT_VERSION = 0 -OP_MATERIALIZE = 0 -OP_CLAIM = 1 -OP_RWA_SETTLE = 2 -STATUS_MATERIALIZED = 1 -STATUS_CLAIMED = 2 -STATUS_RWA_SETTLED = 3 -BTC_TX_COMMITMENT_VERSION = 0 -OP_BTC_COMMIT_TRANSACTION = 0 -OP_BTC_INITIALIZE_ACTIVE_STATE = 255 -BTC_STATUS_COMMITTED = 2 -BTC_UTXO_SEAL_VERSION = 0 -OP_BTC_UTXO_CLOSE = 0 -OP_BTC_UTXO_INITIALIZE_ACTIVE_SEAL = 255 -BTC_STATUS_CLOSED = 2 -DUAL_SEAL_VERSION = 0 -OP_DUAL_SEAL_FINALIZE = 0 -OP_DUAL_SEAL_INITIALIZE_ACTIVE = 255 -DUAL_STATUS_FINALIZED = 2 -FIBER_CANDIDATE_VERSION = 0 -OP_FIBER_SETTLE = 0 -OP_FIBER_INITIALIZE_ACTIVE_CANDIDATE = 255 -FIBER_STATUS_SETTLED = 2 -HOLDER_SECRET_KEY = bytes.fromhex("22" * 32) -HOLDER_AUX_RAND = bytes([0x42]) * 32 -RECEIVER_SECRET_KEY = bytes.fromhex("33" * 32) -RECEIVER_AUX_RAND = bytes([0x66]) * 32 -BTC_ANCHOR_SOURCE_LOCAL = "local_deterministic_fixture" -BIP340_CHILD_REJECTED_ERROR_CODE = 56 - - -@dataclass(frozen=True) -class ReportContract: - profile: str - output: str - source: str - source_actions: tuple[str, ...] - lifecycle_action: str | None - tx_hashes: tuple[tuple[str, str], ...] - live_checks: tuple[tuple[str, str], ...] - negative_cases: tuple[tuple[str, str], ...] - - -REPORT_CONTRACTS = { - "fungible-xudt": ReportContract( - profile="fungible-xudt", - output="target/novaseal-fungible-xudt-devnet-stateful-live.json", - source="proposals/novaseal/fungible-xudt-profile-v0/src/nova_fungible_xudt_lifecycle_type.cell", - source_actions=("issue_xudt", "transfer_xudt", "settle_xudt", "nova_fungible_xudt_lifecycle"), - lifecycle_action="nova_fungible_xudt_lifecycle", - tx_hashes=( - ("issue", "/issue/commit/tx_hash"), - ("transfer", "/transfer/commit/tx_hash"), - ("settle", "/settle/commit/tx_hash"), - ), - live_checks=( - ("issue_balance_live", "/issue/balance_live"), - ("issue_receipt_live", "/issue/receipt_live"), - ("transfer_old_balance_not_live", "/transfer/old_balance_not_live"), - ("transfer_sender_balance_live", "/transfer/sender_balance_live"), - ("transfer_receiver_balance_live", "/transfer/receiver_balance_live"), - ("transfer_receipt_live", "/transfer/receipt_live"), - ("transfer_amount_conserved", "/transfer/amount_conserved"), - ("settle_old_balance_not_live", "/settle/old_balance_not_live"), - ("settlement_receipt_live", "/settle/settlement_receipt_live"), - ("post_negative_state_still_live", "/negative_cases/post_negative_state_still_live"), - ), - negative_cases=( - ("wrong_holder_signature_rejected", "wrong_holder_signature_dry_run"), - ("transfer_amount_mismatch_rejected", "transfer_amount_mismatch_dry_run"), - ("settle_wrong_holder_signature_rejected", "settle_wrong_holder_signature_dry_run"), - ), - ), - "rwa-receipt": ReportContract( - profile="rwa-receipt", - output="target/novaseal-rwa-receipt-devnet-stateful-live.json", - source="proposals/novaseal/rwa-receipt-profile-v0/src/nova_rwa_receipt_lifecycle_type.cell", - source_actions=("materialize_rwa_receipt", "claim_rwa_receipt", "settle_rwa_receipt", "nova_rwa_receipt_lifecycle"), - lifecycle_action="nova_rwa_receipt_lifecycle", - tx_hashes=( - ("materialize", "/materialize/commit/tx_hash"), - ("claim", "/claim/commit/tx_hash"), - ("settle", "/settle/commit/tx_hash"), - ), - live_checks=( - ("materialized_receipt_live", "/materialize/receipt_live"), - ("materialized_audit_event_live", "/materialize/audit_event_live"), - ("claim_old_receipt_not_live", "/claim/old_receipt_not_live"), - ("claimed_receipt_live", "/claim/claimed_receipt_live"), - ("claim_event_live", "/claim/claim_event_live"), - ("settle_old_claim_not_live", "/settle/old_claim_not_live"), - ("settlement_receipt_live", "/settle/settlement_receipt_live"), - ("settlement_event_live", "/settle/settlement_event_live"), - ("amount_conserved", "/settle/amount_conserved"), - ("post_negative_state_still_live", "/negative_cases/post_negative_state_still_live"), - ), - negative_cases=( - ("wrong_holder_claim_rejected", "wrong_holder_claim_dry_run"), - ("wrong_issuer_settlement_rejected", "wrong_issuer_settlement_dry_run"), - ("amount_mutation_rejected", "amount_mutation_dry_run"), - ), - ), - "btc-transaction-commitment": ReportContract( - profile="btc-transaction-commitment", - output="target/novaseal-btc-transaction-commitment-devnet-stateful-live.json", - source="proposals/novaseal/btc-transaction-commitment-profile-v0/src/nova_btc_transaction_commitment_type.cell", - source_actions=("commit_btc_transaction_transition", "nova_btc_transaction_commitment_lifecycle"), - lifecycle_action="nova_btc_transaction_commitment_lifecycle", - tx_hashes=(("commit_transaction", "/commit_transaction/commit/tx_hash"),), - live_checks=( - ("old_state_not_live", "/commit_transaction/old_state_not_live"), - ("new_state_live", "/commit_transaction/new_state_live"), - ("receipt_live", "/commit_transaction/receipt_live"), - ("btc_tx_tuple_bound", "/commit_transaction/btc_tx_tuple_bound"), - ("transition_commitment_bound", "/commit_transaction/transition_commitment_bound"), - ("public_btc_verification_executed", "/commit_transaction/public_btc_verification_executed"), - ("post_negative_state_still_live", "/negative_cases/post_negative_state_still_live"), - ), - negative_cases=( - ("wrong_committer_signature_rejected", "wrong_committer_signature_dry_run"), - ("zero_btc_txid_rejected", "zero_btc_txid_dry_run"), - ("transition_hash_mismatch_rejected", "transition_hash_mismatch_dry_run"), - ), - ), - "btc-utxo-seal": ReportContract( - profile="btc-utxo-seal", - output="target/novaseal-btc-utxo-seal-devnet-stateful-live.json", - source="proposals/novaseal/btc-utxo-seal-profile-v0/src/nova_btc_utxo_seal_type.cell", - source_actions=("close_btc_utxo_seal", "nova_btc_utxo_seal_lifecycle"), - lifecycle_action="nova_btc_utxo_seal_lifecycle", - tx_hashes=(("close_utxo_seal", "/close_utxo_seal/commit/tx_hash"),), - live_checks=( - ("old_state_not_live", "/close_utxo_seal/old_state_not_live"), - ("new_state_live", "/close_utxo_seal/new_state_live"), - ("receipt_live", "/close_utxo_seal/receipt_live"), - ("sealed_utxo_tuple_bound", "/close_utxo_seal/sealed_utxo_tuple_bound"), - ("spend_tuple_bound", "/close_utxo_seal/spend_tuple_bound"), - ("public_btc_spend_verification_executed", "/close_utxo_seal/public_btc_spend_verification_executed"), - ("post_negative_state_still_live", "/negative_cases/post_negative_state_still_live"), - ), - negative_cases=( - ("wrong_owner_signature_rejected", "wrong_owner_signature_dry_run"), - ("utxo_commitment_mismatch_rejected", "utxo_commitment_mismatch_dry_run"), - ("zero_spend_txid_rejected", "zero_spend_txid_dry_run"), - ), - ), - "dual-seal": ReportContract( - profile="dual-seal", - output="target/novaseal-dual-seal-devnet-stateful-live.json", - source="proposals/novaseal/dual-seal-profile-v0/src/nova_dual_seal_type.cell", - source_actions=("finalize_dual_seal", "nova_dual_seal_lifecycle"), - lifecycle_action="nova_dual_seal_lifecycle", - tx_hashes=(("finalize_dual_seal", "/finalize_dual_seal/commit/tx_hash"),), - live_checks=( - ("old_state_not_live", "/finalize_dual_seal/old_state_not_live"), - ("receipt_live", "/finalize_dual_seal/receipt_live"), - ("btc_closure_bound", "/finalize_dual_seal/btc_closure_bound"), - ("ckb_maturity_executed", "/finalize_dual_seal/ckb_maturity_executed"), - ("dual_authority_executed", "/finalize_dual_seal/dual_authority_executed"), - ("post_negative_state_still_live", "/negative_cases/post_negative_state_still_live"), - ), - negative_cases=( - ("wrong_btc_owner_signature_rejected", "wrong_btc_owner_signature_dry_run"), - ("wrong_ckb_authority_signature_rejected", "wrong_ckb_authority_signature_dry_run"), - ("btc_closure_commitment_missing_rejected", "btc_closure_commitment_missing_dry_run"), - ), - ), - "fiber-candidate": ReportContract( - profile="fiber-candidate", - output="target/novaseal-fiber-candidate-devnet-stateful-live.json", - source="proposals/novaseal/fiber-candidate-profile-v0/src/nova_fiber_candidate_type.cell", - source_actions=("settle_fiber_candidate", "nova_fiber_candidate_lifecycle"), - lifecycle_action="nova_fiber_candidate_lifecycle", - tx_hashes=(("settle_fiber_candidate", "/settle_fiber_candidate/commit/tx_hash"),), - live_checks=( - ("old_candidate_not_live", "/settle_fiber_candidate/old_candidate_not_live"), - ("new_candidate_live", "/settle_fiber_candidate/new_candidate_live"), - ("receipt_live", "/settle_fiber_candidate/receipt_live"), - ("balance_commitment_progressed", "/settle_fiber_candidate/balance_commitment_progressed"), - ("fiber_execution_executed", "/settle_fiber_candidate/fiber_execution_executed"), - ("post_negative_state_still_live", "/negative_cases/post_negative_state_still_live"), - ), - negative_cases=( - ("wrong_operator_signature_rejected", "wrong_operator_signature_dry_run"), - ("balance_commitment_replay_rejected", "balance_commitment_replay_dry_run"), - ), - ), -} - - -def parse_args() -> argparse.Namespace: - repo_root = pathlib.Path(__file__).resolve().parents[1] - default_ckb_repo = repo_root.parent / "ckb" - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--repo-root", type=pathlib.Path, default=repo_root) - parser.add_argument("--ckb-repo", type=pathlib.Path, default=default_ckb_repo) - parser.add_argument("--ckb-bin", type=pathlib.Path) - parser.add_argument("--profile", choices=sorted(REPORT_CONTRACTS), required=True) - parser.add_argument("--output", type=pathlib.Path) - parser.add_argument("--run-dir", type=pathlib.Path) - parser.add_argument("--pretty", action="store_true") - parser.add_argument("--keep-node", action="store_true") - parser.add_argument("--list-contract", action="store_true") - parser.add_argument("--prepare-artifacts", action="store_true") - parser.add_argument("--live", action="store_true") - return parser.parse_args() - - -def named_pointer_rows(rows: tuple[tuple[str, str], ...], pointer_name: str) -> list[dict[str, str]]: - return [{"name": name, pointer_name: pointer} for name, pointer in rows] - - -def not_run_report(contract: ReportContract) -> dict[str, Any]: - return { - "schema": "novaseal-planned-profile-devnet-stateful-live-v0.1", - "profile": contract.profile, - "status": "not_run", - "live_devnet_rpc_executed": False, - "stateful_lifecycle_executed": False, - "artifact_contract": { - "source": contract.source, - "source_actions": list(contract.source_actions), - "lifecycle_action": contract.lifecycle_action, - "stable_lifecycle_artifact_required": True, - "dispatcher_required": contract.lifecycle_action is None, - "dispatcher_gap": ( - "multi-step workflow requires one stable lifecycle/dispatcher action before live CKB state can move across steps" - if contract.lifecycle_action is None - else None - ), - }, - "expected_tx_hashes": named_pointer_rows(contract.tx_hashes, "pointer"), - "required_live_checks": named_pointer_rows(contract.live_checks, "pointer"), - "required_negative_cases": named_pointer_rows(contract.negative_cases, "key"), - "provenance": { - "repo_commit": None, - "source_tree": None, - "artifacts": None, - }, - "negative_cases": { - key: { - "status": "not_run", - "matched_expected": False, - } - for _, key in contract.negative_cases - }, - "next_engineering_step": ( - "Replace this contract report with profile-specific live CKB devnet " - "transaction evidence, including fresh source/artifact provenance." - ), - } - - -def write_json(path: pathlib.Path, value: dict[str, Any], pretty: bool) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(value, indent=2 if pretty else None, sort_keys=True) + "\n", encoding="utf-8") - - -def prepare_lifecycle_artifact(repo_root: pathlib.Path, contract: ReportContract, pretty: bool) -> dict[str, Any]: - if contract.lifecycle_action is None: - return { - "schema": "novaseal-planned-profile-artifact-prep-v0.1", - "profile": contract.profile, - "status": "blocked_missing_dispatcher", - "source": contract.source, - "source_actions": list(contract.source_actions), - "required": "add a profile lifecycle/dispatcher action, then compile that single entry action for live devnet use", - } - - output = repo_root / "target/novaseal-planned-profile-artifacts" / contract.profile / f"{contract.lifecycle_action}.elf" - output.parent.mkdir(parents=True, exist_ok=True) - cmd = [ - "cargo", - "run", - "--quiet", - "--bin", - "cellc", - "--", - contract.source, - "--target-profile", - "ckb", - "--target", - "riscv64-elf", - "--entry-action", - contract.lifecycle_action, - "-o", - str(output), - ] - completed = subprocess.run(cmd, cwd=repo_root, text=True, capture_output=True) - report: dict[str, Any] = { - "schema": "novaseal-planned-profile-artifact-prep-v0.1", - "profile": contract.profile, - "source": contract.source, - "lifecycle_action": contract.lifecycle_action, - "artifact": output.as_posix(), - "status": "passed" if completed.returncode == 0 else "failed", - "command": cmd, - } - if completed.returncode != 0: - report["stderr"] = completed.stderr - report["stdout"] = completed.stdout - return report - report["size_bytes"] = output.stat().st_size - return report - - -def signature_payload(secret_key: bytes, message_hash: bytes, aux_rand: bytes) -> bytes: - pubkey, signature = schnorr_sign(message_hash, secret_key, aux_rand) - return pubkey + signature - - -def lifecycle_type(lifecycle_data_hash: str) -> dict[str, str]: - return {"code_hash": lifecycle_data_hash, "hash_type": "data2", "args": "0x"} - - -def pack_canonical_envelope(envelope: dict[str, Any]) -> bytes: - return ( - envelope["profile_id"] - + envelope["policy_hash"] - + u8(envelope["action"]) - + u8(envelope["terminal_path"]) - + envelope["subject_id"] - + envelope["old_state_commitment"] - + envelope["new_state_commitment"] - + u64(envelope["old_nonce"]) - + u64(envelope["new_nonce"]) - + u64(envelope["expiry"]) - + envelope["authority_hash"] - + envelope["profile_body_hash"] - + envelope["payout_commitment_hash"] - ) - - -def canonical_envelope_hash( - *, - action: int, - asset_id: bytes, - xudt_type_hash: bytes, - old_state_commitment: bytes, - new_state_commitment: bytes, - old_nonce: int, - new_nonce: int, - expiry: int, - authority_hash: bytes, - profile_body_hash: bytes, - payout_commitment_hash: bytes, -) -> bytes: - return data_packed_hash( - "NovaSealCanonicalEnvelopeV0", - pack_canonical_envelope( - { - "profile_id": asset_id, - "policy_hash": xudt_type_hash, - "action": action, - "terminal_path": action, - "subject_id": asset_id, - "old_state_commitment": old_state_commitment, - "new_state_commitment": new_state_commitment, - "old_nonce": old_nonce, - "new_nonce": new_nonce, - "expiry": expiry, - "authority_hash": authority_hash, - "profile_body_hash": profile_body_hash, - "payout_commitment_hash": payout_commitment_hash, - } - ), - ) - - -def pack_xudt_intent_core(core: dict[str, Any]) -> bytes: - return ( - u8(core["action"]) - + core["asset_id"] - + core["xudt_type_hash"] - + core["issuer_authority_hash"] - + core["old_holder_authority_hash"] - + core["new_holder_authority_hash"] - + u8(core["old_status"]) - + u8(core["new_status"]) - + u64(core["old_amount"]) - + u64(core["transfer_amount"]) - + u64(core["new_amount"]) - + u64(core["old_nonce"]) - + u64(core["new_nonce"]) - + u64(core["expiry"]) - + core["payout_commitment_hash"] - ) - - -def pack_xudt_signed_intent(core_data: bytes, canonical_hash: bytes, expected_receipt_hash: bytes) -> bytes: - return core_data + canonical_hash + expected_receipt_hash - - -def pack_xudt_state_commitment(cell: dict[str, Any]) -> bytes: - return ( - u16(cell["version"]) - + cell["asset_id"] - + cell["xudt_type_hash"] - + cell["issuer_authority_hash"] - + cell["holder_authority_hash"] - + u64(cell["amount"]) - + u8(cell["status"]) - + u64(cell["nonce"]) - + u64(cell["expiry"]) - ) - - -def pack_xudt_receipt_commitment(commitment: dict[str, Any]) -> bytes: - return ( - u8(commitment["action"]) - + commitment["asset_id"] - + commitment["xudt_type_hash"] - + commitment["old_holder_authority_hash"] - + commitment["new_holder_authority_hash"] - + u8(commitment["old_status"]) - + u8(commitment["new_status"]) - + u64(commitment["old_amount"]) - + u64(commitment["transfer_amount"]) - + u64(commitment["new_amount"]) - + u64(commitment["old_nonce"]) - + u64(commitment["new_nonce"]) - + commitment["intent_core_hash"] - + commitment["payout_commitment_hash"] - ) - - -def pack_xudt_cell(cell: dict[str, Any]) -> bytes: - return ( - u16(cell["version"]) - + cell["asset_id"] - + cell["xudt_type_hash"] - + cell["issuer_authority_hash"] - + cell["holder_authority_hash"] - + u64(cell["amount"]) - + u8(cell["status"]) - + cell["latest_receipt_hash"] - + u64(cell["nonce"]) - + u64(cell["expiry"]) - ) - - -def pack_xudt_receipt(receipt: dict[str, Any]) -> bytes: - return ( - u8(receipt["action"]) - + receipt["asset_id"] - + receipt["xudt_type_hash"] - + receipt["old_holder_authority_hash"] - + receipt["new_holder_authority_hash"] - + u8(receipt["old_status"]) - + u8(receipt["new_status"]) - + u64(receipt["old_amount"]) - + u64(receipt["transfer_amount"]) - + u64(receipt["new_amount"]) - + u64(receipt["old_nonce"]) - + u64(receipt["new_nonce"]) - + receipt["intent_core_hash"] - + receipt["signed_intent_hash"] - + receipt["payout_commitment_hash"] - + receipt["latest_receipt_hash"] - + receipt["signer_authority_hash"] - + u64(receipt["expiry"]) - ) - - -def zero_xudt_cell() -> dict[str, Any]: - return { - "version": 0, - "asset_id": ZERO_HASH, - "xudt_type_hash": ZERO_HASH, - "issuer_authority_hash": ZERO_HASH, - "holder_authority_hash": ZERO_HASH, - "amount": 0, - "status": 0, - "latest_receipt_hash": ZERO_HASH, - "nonce": 0, - "expiry": 0, - } - - -def xudt_entry_witness(op: int, old_cell_data: bytes, new_cell_data: bytes, signed_intent: bytes, sig_payload: bytes) -> str: - payload = ( - b"CSARGv1\0" - + u8(op) - + u32(len(old_cell_data)) - + old_cell_data - + u32(len(new_cell_data)) - + new_cell_data - + u32(len(signed_intent)) - + signed_intent - + u32(len(sig_payload)) - + sig_payload - ) - return hex0x(payload) - - -def xudt_base_state(label: str) -> dict[str, Any]: - return { - "asset_id": ckb_hash(f"NovaSeal fungible xUDT asset {label}".encode("ascii")), - "xudt_type_hash": ckb_hash(f"NovaSeal fungible xUDT type {label}".encode("ascii")), - "issuer_authority_hash": xonly_pubkey(TEST_SECRET_KEY), - "holder_authority_hash": xonly_pubkey(HOLDER_SECRET_KEY), - "amount": 1_000, - "expiry": (1 << 63) - 1, - } - - -def build_xudt_material( - *, - op: int, - base: dict[str, Any], - old_cell: dict[str, Any] | None, - new_holder_authority_hash: bytes | None = None, - mutate_signature: bool = False, - transfer_amount_override: int | None = None, -) -> dict[str, Any]: - payout_commitment_hash = ZERO_HASH - if op == OP_ISSUE: - old_holder = ZERO_HASH - new_holder = base["holder_authority_hash"] - old_status = 0 - new_status = STATUS_ACTIVE - old_amount = 0 - transfer_amount = base["amount"] - new_amount = base["amount"] - old_nonce = 0 - new_nonce = 0 - expiry = base["expiry"] - authority_hash = base["issuer_authority_hash"] - signer_secret = TEST_SECRET_KEY - signer_aux = TEST_AUX_RAND - old_state_commitment = ZERO_HASH - new_cell = { - "version": FUNGIBLE_XUDT_VERSION, - "asset_id": base["asset_id"], - "xudt_type_hash": base["xudt_type_hash"], - "issuer_authority_hash": base["issuer_authority_hash"], - "holder_authority_hash": new_holder, - "amount": new_amount, - "status": STATUS_ACTIVE, - "latest_receipt_hash": ZERO_HASH, - "nonce": 0, - "expiry": expiry, - } - new_state_commitment = data_packed_hash("NovaFungibleXudtStateCommitmentV0", pack_xudt_state_commitment(new_cell)) - else: - if old_cell is None: - raise LiveAcceptanceError("xUDT non-issue material requires an old cell") - new_nonce = old_cell["nonce"] + 1 - expiry = old_cell["expiry"] - old_state_commitment = data_packed_hash("NovaFungibleXudtStateCommitmentV0", pack_xudt_state_commitment(old_cell)) - if op == OP_TRANSFER: - old_holder = old_cell["holder_authority_hash"] - new_holder = new_holder_authority_hash or xonly_pubkey(RECEIVER_SECRET_KEY) - old_status = STATUS_ACTIVE - new_status = STATUS_ACTIVE - old_amount = old_cell["amount"] - transfer_amount = transfer_amount_override if transfer_amount_override is not None else old_cell["amount"] - new_amount = old_cell["amount"] - old_nonce = old_cell["nonce"] - authority_hash = old_cell["holder_authority_hash"] - signer_secret = HOLDER_SECRET_KEY - signer_aux = HOLDER_AUX_RAND - new_cell = dict(old_cell) - new_cell.update( - { - "holder_authority_hash": new_holder, - "latest_receipt_hash": ZERO_HASH, - "nonce": new_nonce, - } - ) - new_state_commitment = data_packed_hash("NovaFungibleXudtStateCommitmentV0", pack_xudt_state_commitment(new_cell)) - elif op == OP_SETTLE: - old_holder = old_cell["holder_authority_hash"] - new_holder = old_cell["holder_authority_hash"] - old_status = STATUS_ACTIVE - new_status = STATUS_SETTLED - old_amount = old_cell["amount"] - transfer_amount = old_cell["amount"] - new_amount = 0 - old_nonce = old_cell["nonce"] - authority_hash = old_cell["holder_authority_hash"] - signer_secret = RECEIVER_SECRET_KEY - signer_aux = RECEIVER_AUX_RAND - new_cell = zero_xudt_cell() - new_state_commitment = ZERO_HASH - else: - raise LiveAcceptanceError(f"unknown xUDT op {op}") - - core = { - "action": op, - "asset_id": base["asset_id"], - "xudt_type_hash": base["xudt_type_hash"], - "issuer_authority_hash": base["issuer_authority_hash"], - "old_holder_authority_hash": old_holder, - "new_holder_authority_hash": new_holder, - "old_status": old_status, - "new_status": new_status, - "old_amount": old_amount, - "transfer_amount": transfer_amount, - "new_amount": new_amount, - "old_nonce": old_nonce, - "new_nonce": new_nonce, - "expiry": expiry, - "payout_commitment_hash": payout_commitment_hash, - } - core_data = pack_xudt_intent_core(core) - intent_core_hash = data_packed_hash("NovaFungibleXudtIntentCoreV0", core_data) - receipt_commitment = { - "action": op, - "asset_id": base["asset_id"], - "xudt_type_hash": base["xudt_type_hash"], - "old_holder_authority_hash": old_holder, - "new_holder_authority_hash": new_holder, - "old_status": old_status, - "new_status": new_status, - "old_amount": old_amount, - "transfer_amount": transfer_amount, - "new_amount": new_amount, - "old_nonce": old_nonce, - "new_nonce": new_nonce, - "intent_core_hash": intent_core_hash, - "payout_commitment_hash": payout_commitment_hash, - } - materialized_receipt_hash = data_packed_hash( - "NovaFungibleXudtReceiptCommitmentV0", - pack_xudt_receipt_commitment(receipt_commitment), - ) - canonical_hash = canonical_envelope_hash( - action=op, - asset_id=base["asset_id"], - xudt_type_hash=base["xudt_type_hash"], - old_state_commitment=old_state_commitment, - new_state_commitment=new_state_commitment, - old_nonce=old_nonce, - new_nonce=new_nonce, - expiry=expiry, - authority_hash=authority_hash, - profile_body_hash=intent_core_hash, - payout_commitment_hash=payout_commitment_hash, - ) - signed_intent = pack_xudt_signed_intent(core_data, canonical_hash, materialized_receipt_hash) - signed_intent_hash = data_packed_hash("NovaFungibleXudtSignedIntentV0", signed_intent) - sig_payload = bytearray(signature_payload(signer_secret, signed_intent_hash, signer_aux)) - if mutate_signature: - sig_payload[-1] ^= 1 - receipt = { - "action": op, - "asset_id": base["asset_id"], - "xudt_type_hash": base["xudt_type_hash"], - "old_holder_authority_hash": old_holder, - "new_holder_authority_hash": new_holder, - "old_status": old_status, - "new_status": new_status, - "old_amount": old_amount, - "transfer_amount": transfer_amount, - "new_amount": new_amount, - "old_nonce": old_nonce, - "new_nonce": new_nonce, - "intent_core_hash": intent_core_hash, - "signed_intent_hash": signed_intent_hash, - "payout_commitment_hash": payout_commitment_hash, - "latest_receipt_hash": materialized_receipt_hash, - "signer_authority_hash": authority_hash, - "expiry": expiry, - } - material_new_cell = dict(new_cell) - if op in (OP_ISSUE, OP_TRANSFER): - material_new_cell["latest_receipt_hash"] = materialized_receipt_hash - new_cell_data = pack_xudt_cell(material_new_cell) - return { - "old_cell": old_cell or zero_xudt_cell(), - "old_cell_data": pack_xudt_cell(old_cell or zero_xudt_cell()), - "new_cell": material_new_cell, - "new_cell_data": new_cell_data, - "receipt_data": pack_xudt_receipt(receipt), - "signed_intent": signed_intent, - "signed_intent_hash": signed_intent_hash, - "latest_receipt_hash": materialized_receipt_hash, - "signature_payload": bytes(sig_payload), - "receipt_commitment": receipt_commitment, - } - - -def build_xudt_issue_tx( - funding: dict[str, Any], - lifecycle_data_hash: str, - cell_deps: list[dict[str, Any]], - header_hash: str, - material: dict[str, Any], -) -> dict[str, Any]: - change_capacity = funding["total_capacity"] - STATE_CAPACITY - RECEIPT_CAPACITY - if change_capacity <= 0: - raise LiveAcceptanceError("xUDT issue funding capacity is too small") - witness = xudt_entry_witness( - OP_ISSUE, - material["old_cell_data"], - material["new_cell_data"], - material["signed_intent"], - material["signature_payload"], - ) - return transaction( - funding, - [ - {"capacity": hex(STATE_CAPACITY), "lock": always_success_lock(), "type": lifecycle_type(lifecycle_data_hash)}, - {"capacity": hex(RECEIPT_CAPACITY), "lock": always_success_lock(), "type": None}, - {"capacity": hex(change_capacity), "lock": always_success_lock(), "type": None}, - ], - [hex0x(material["new_cell_data"]), hex0x(material["receipt_data"]), "0x"], - cell_deps, - [witness] + ["0x" for _ in funding["cells"][1:]], - [header_hash], - ) - - -def build_xudt_transfer_tx( - *, - old_ref: dict[str, Any], - funding: dict[str, Any], - lifecycle_data_hash: str, - cell_deps: list[dict[str, Any]], - header_hash: str, - material: dict[str, Any], -) -> dict[str, Any]: - change_capacity = funding["total_capacity"] - RECEIPT_CAPACITY - if change_capacity <= 0: - raise LiveAcceptanceError("xUDT transfer funding capacity is too small") - witness = xudt_entry_witness( - OP_TRANSFER, - material["old_cell_data"], - material["new_cell_data"], - material["signed_intent"], - material["signature_payload"], - ) - return transaction( - [old_ref] + funding["cells"], - [ - {"capacity": hex(old_ref["capacity"]), "lock": always_success_lock(), "type": lifecycle_type(lifecycle_data_hash)}, - {"capacity": hex(RECEIPT_CAPACITY), "lock": always_success_lock(), "type": None}, - {"capacity": hex(change_capacity), "lock": always_success_lock(), "type": None}, - ], - [hex0x(material["new_cell_data"]), hex0x(material["receipt_data"]), "0x"], - cell_deps, - [witness] + ["0x" for _ in funding["cells"]], - [header_hash], - ) - - -def build_xudt_settle_tx( - *, - old_ref: dict[str, Any], - funding: dict[str, Any], - lifecycle_data_hash: str, - cell_deps: list[dict[str, Any]], - header_hash: str, - material: dict[str, Any], -) -> dict[str, Any]: - change_capacity = old_ref["capacity"] + funding["total_capacity"] - RECEIPT_CAPACITY - if change_capacity <= 0: - raise LiveAcceptanceError("xUDT settle funding capacity is too small") - witness = xudt_entry_witness( - OP_SETTLE, - material["old_cell_data"], - material["new_cell_data"], - material["signed_intent"], - material["signature_payload"], - ) - return transaction( - [old_ref] + funding["cells"], - [ - {"capacity": hex(RECEIPT_CAPACITY), "lock": always_success_lock(), "type": None}, - {"capacity": hex(change_capacity), "lock": always_success_lock(), "type": None}, - ], - [hex0x(material["receipt_data"]), "0x"], - cell_deps, - [witness] + ["0x" for _ in funding["cells"]], - [header_hash], - ) - - -def pack_rwa_intent_core(core: dict[str, Any]) -> bytes: - return ( - u8(core["action"]) - + core["receipt_id"] - + core["registry_hash"] - + core["asset_commitment_hash"] - + core["document_hash"] - + core["issuer_authority_hash"] - + core["holder_authority_hash"] - + u8(core["old_status"]) - + u8(core["new_status"]) - + u64(core["old_amount"]) - + u64(core["settlement_amount"]) - + u64(core["old_nonce"]) - + u64(core["new_nonce"]) - + u64(core["expiry"]) - + core["payout_commitment_hash"] - ) - - -def pack_rwa_signed_intent( - core_data: bytes, - canonical_hash: bytes, - expected_receipt_hash: bytes, - expected_cell_data_hash: bytes, - expected_event_data_hash: bytes, -) -> bytes: - return core_data + canonical_hash + expected_receipt_hash + expected_cell_data_hash + expected_event_data_hash - - -def pack_rwa_state_commitment(cell: dict[str, Any]) -> bytes: - return ( - u16(cell["version"]) - + cell["receipt_id"] - + cell["registry_hash"] - + cell["asset_commitment_hash"] - + cell["document_hash"] - + cell["issuer_authority_hash"] - + cell["holder_authority_hash"] - + u64(cell["amount"]) - + u8(cell["status"]) - + u64(cell["nonce"]) - + u64(cell["expiry"]) - ) - - -def pack_rwa_event_commitment(event: dict[str, Any]) -> bytes: - return ( - u8(event["action"]) - + event["receipt_id"] - + event["registry_hash"] - + event["asset_commitment_hash"] - + event["document_hash"] - + event["issuer_authority_hash"] - + event["holder_authority_hash"] - + u8(event["old_status"]) - + u8(event["new_status"]) - + u64(event["old_amount"]) - + u64(event["settlement_amount"]) - + u64(event["old_nonce"]) - + u64(event["new_nonce"]) - + event["intent_core_hash"] - + event["payout_commitment_hash"] - ) - - -def pack_rwa_cell(cell: dict[str, Any]) -> bytes: - return ( - u16(cell["version"]) - + cell["receipt_id"] - + cell["registry_hash"] - + cell["asset_commitment_hash"] - + cell["document_hash"] - + cell["issuer_authority_hash"] - + cell["holder_authority_hash"] - + u64(cell["amount"]) - + u8(cell["status"]) - + cell["latest_receipt_hash"] - + u64(cell["nonce"]) - + u64(cell["expiry"]) - ) - - -def pack_rwa_event(event: dict[str, Any]) -> bytes: - return ( - u8(event["action"]) - + event["receipt_id"] - + event["registry_hash"] - + event["asset_commitment_hash"] - + event["document_hash"] - + event["issuer_authority_hash"] - + event["holder_authority_hash"] - + u8(event["old_status"]) - + u8(event["new_status"]) - + u64(event["old_amount"]) - + u64(event["settlement_amount"]) - + u64(event["old_nonce"]) - + u64(event["new_nonce"]) - + event["intent_core_hash"] - + event["payout_commitment_hash"] - + event["latest_receipt_hash"] - + event["signer_authority_hash"] - + u64(event["expiry"]) - ) - - -def zero_rwa_cell() -> dict[str, Any]: - return { - "version": 0, - "receipt_id": ZERO_HASH, - "registry_hash": ZERO_HASH, - "asset_commitment_hash": ZERO_HASH, - "document_hash": ZERO_HASH, - "issuer_authority_hash": ZERO_HASH, - "holder_authority_hash": ZERO_HASH, - "amount": 0, - "status": 0, - "latest_receipt_hash": ZERO_HASH, - "nonce": 0, - "expiry": 0, - } - - -def rwa_entry_witness( - op: int, - old_cell_data: bytes, - signed_intent: bytes, - signer_sig: bytes, - cosigner_sig: bytes, -) -> str: - payload = ( - b"CSARGv1\0" - + u8(op) - + u32(len(old_cell_data)) - + old_cell_data - + u32(len(signed_intent)) - + signed_intent - + u32(len(signer_sig)) - + signer_sig - + u32(len(cosigner_sig)) - + cosigner_sig - ) - return hex0x(payload) - - -def rwa_base_state(label: str) -> dict[str, Any]: - return { - "receipt_id": ckb_hash(f"NovaSeal RWA receipt {label}".encode("ascii")), - "registry_hash": ckb_hash(f"NovaSeal RWA registry {label}".encode("ascii")), - "asset_commitment_hash": ckb_hash(f"NovaSeal RWA asset {label}".encode("ascii")), - "document_hash": ckb_hash(f"NovaSeal RWA document {label}".encode("ascii")), - "issuer_authority_hash": xonly_pubkey(TEST_SECRET_KEY), - "holder_authority_hash": xonly_pubkey(HOLDER_SECRET_KEY), - "amount": 10_000, - "expiry": (1 << 63) - 1, - } - - -def rwa_canonical_hash( - *, - op: int, - base: dict[str, Any], - old_state_commitment: bytes, - new_state_commitment: bytes, - old_nonce: int, - new_nonce: int, - expiry: int, - authority_hash: bytes, - profile_body_hash: bytes, - payout_commitment_hash: bytes, -) -> bytes: - return canonical_envelope_hash( - action=op, - asset_id=base["receipt_id"], - xudt_type_hash=base["registry_hash"], - old_state_commitment=old_state_commitment, - new_state_commitment=new_state_commitment, - old_nonce=old_nonce, - new_nonce=new_nonce, - expiry=expiry, - authority_hash=authority_hash, - profile_body_hash=profile_body_hash, - payout_commitment_hash=payout_commitment_hash, - ) - - -def build_rwa_material( - *, - op: int, - base: dict[str, Any], - old_cell: dict[str, Any] | None, - mutate_issuer_signature: bool = False, - mutate_holder_signature: bool = False, - settlement_amount_override: int | None = None, -) -> dict[str, Any]: - payout_commitment_hash = ZERO_HASH - if op == OP_MATERIALIZE: - old_status = 0 - new_status = STATUS_MATERIALIZED - old_amount = 0 - settlement_amount = base["amount"] - old_nonce = 0 - new_nonce = 0 - expiry = base["expiry"] - authority_hash = base["issuer_authority_hash"] - signer_authority_hash = base["issuer_authority_hash"] - old_state_commitment = ZERO_HASH - new_cell = { - "version": RWA_RECEIPT_VERSION, - "receipt_id": base["receipt_id"], - "registry_hash": base["registry_hash"], - "asset_commitment_hash": base["asset_commitment_hash"], - "document_hash": base["document_hash"], - "issuer_authority_hash": base["issuer_authority_hash"], - "holder_authority_hash": base["holder_authority_hash"], - "amount": base["amount"], - "status": STATUS_MATERIALIZED, - "latest_receipt_hash": ZERO_HASH, - "nonce": 0, - "expiry": expiry, - } - new_state_commitment = data_packed_hash("NovaRwaReceiptStateCommitmentV0", pack_rwa_state_commitment(new_cell)) - else: - if old_cell is None: - raise LiveAcceptanceError("RWA non-materialize material requires an old cell") - old_state_commitment = data_packed_hash("NovaRwaReceiptStateCommitmentV0", pack_rwa_state_commitment(old_cell)) - old_nonce = old_cell["nonce"] - new_nonce = old_nonce + 1 - expiry = old_cell["expiry"] - old_amount = old_cell["amount"] - settlement_amount = settlement_amount_override if settlement_amount_override is not None else old_cell["amount"] - if op == OP_CLAIM: - old_status = STATUS_MATERIALIZED - new_status = STATUS_CLAIMED - authority_hash = old_cell["holder_authority_hash"] - signer_authority_hash = old_cell["holder_authority_hash"] - new_cell = dict(old_cell) - new_cell.update({"status": STATUS_CLAIMED, "latest_receipt_hash": ZERO_HASH, "nonce": new_nonce}) - new_state_commitment = data_packed_hash("NovaRwaReceiptStateCommitmentV0", pack_rwa_state_commitment(new_cell)) - elif op == OP_RWA_SETTLE: - old_status = STATUS_CLAIMED - new_status = STATUS_RWA_SETTLED - authority_hash = old_cell["issuer_authority_hash"] - signer_authority_hash = old_cell["issuer_authority_hash"] - new_cell = zero_rwa_cell() - new_state_commitment = ZERO_HASH - else: - raise LiveAcceptanceError(f"unknown RWA op {op}") - - core = { - "action": op, - "receipt_id": base["receipt_id"], - "registry_hash": base["registry_hash"], - "asset_commitment_hash": base["asset_commitment_hash"], - "document_hash": base["document_hash"], - "issuer_authority_hash": base["issuer_authority_hash"], - "holder_authority_hash": base["holder_authority_hash"], - "old_status": old_status, - "new_status": new_status, - "old_amount": old_amount, - "settlement_amount": settlement_amount, - "old_nonce": old_nonce, - "new_nonce": new_nonce, - "expiry": expiry, - "payout_commitment_hash": payout_commitment_hash, - } - core_data = pack_rwa_intent_core(core) - intent_core_hash = data_packed_hash("NovaRwaReceiptIntentCoreV0", core_data) - event_commitment = { - "action": op, - "receipt_id": base["receipt_id"], - "registry_hash": base["registry_hash"], - "asset_commitment_hash": base["asset_commitment_hash"], - "document_hash": base["document_hash"], - "issuer_authority_hash": base["issuer_authority_hash"], - "holder_authority_hash": base["holder_authority_hash"], - "old_status": old_status, - "new_status": new_status, - "old_amount": old_amount, - "settlement_amount": settlement_amount, - "old_nonce": old_nonce, - "new_nonce": new_nonce, - "intent_core_hash": intent_core_hash, - "payout_commitment_hash": payout_commitment_hash, - } - materialized_receipt_hash = data_packed_hash("NovaRwaReceiptEventCommitmentV0", pack_rwa_event_commitment(event_commitment)) - canonical_hash = rwa_canonical_hash( - op=op, - base=base, - old_state_commitment=old_state_commitment, - new_state_commitment=new_state_commitment, - old_nonce=old_nonce, - new_nonce=new_nonce, - expiry=expiry, - authority_hash=authority_hash, - profile_body_hash=intent_core_hash, - payout_commitment_hash=payout_commitment_hash, - ) - material_new_cell = dict(new_cell) - if op in (OP_MATERIALIZE, OP_CLAIM): - material_new_cell["latest_receipt_hash"] = materialized_receipt_hash - new_cell_data = pack_rwa_cell(material_new_cell) - expected_cell_data_hash = cell_data_hash(new_cell_data) if op in (OP_MATERIALIZE, OP_CLAIM) else ZERO_HASH - event = dict(event_commitment) - event.update( - { - "latest_receipt_hash": materialized_receipt_hash, - "signer_authority_hash": signer_authority_hash, - "expiry": expiry, - } - ) - event_data = pack_rwa_event(event) - expected_event_data_hash = cell_data_hash(event_data) - signed_intent = pack_rwa_signed_intent( - core_data, - canonical_hash, - materialized_receipt_hash, - expected_cell_data_hash, - expected_event_data_hash, - ) - signed_intent_hash = data_packed_hash("NovaRwaReceiptSignedIntentV0", signed_intent) - issuer_sig = bytearray(signature_payload(TEST_SECRET_KEY, signed_intent_hash, TEST_AUX_RAND)) - holder_sig = bytearray(signature_payload(HOLDER_SECRET_KEY, signed_intent_hash, HOLDER_AUX_RAND)) - if mutate_issuer_signature: - issuer_sig[-1] ^= 1 - if mutate_holder_signature: - holder_sig[-1] ^= 1 - signer_sig = bytes(holder_sig) if op == OP_CLAIM else bytes(issuer_sig) - cosigner_sig = bytes(holder_sig) if op == OP_RWA_SETTLE else bytes(issuer_sig) - return { - "old_cell": old_cell or zero_rwa_cell(), - "old_cell_data": pack_rwa_cell(old_cell or zero_rwa_cell()), - "new_cell": material_new_cell, - "new_cell_data": new_cell_data, - "event_data": event_data, - "signed_intent": signed_intent, - "signed_intent_hash": signed_intent_hash, - "latest_receipt_hash": materialized_receipt_hash, - "issuer_sig": bytes(issuer_sig), - "holder_sig": bytes(holder_sig), - "signer_sig": signer_sig, - "cosigner_sig": cosigner_sig, - } - - -def build_rwa_state_event_tx( - *, - op: int, - old_ref: dict[str, Any] | None, - funding: dict[str, Any], - lifecycle_data_hash: str, - cell_deps: list[dict[str, Any]], - header_hash: str, - material: dict[str, Any], -) -> dict[str, Any]: - if op == OP_MATERIALIZE: - change_capacity = funding["total_capacity"] - STATE_CAPACITY - RECEIPT_CAPACITY - inputs = funding - witnesses = [ - rwa_entry_witness( - op, - material["old_cell_data"], - material["signed_intent"], - material["signer_sig"], - material["cosigner_sig"], - ) - ] + ["0x" for _ in funding["cells"][1:]] - elif old_ref is not None: - change_capacity = funding["total_capacity"] - RECEIPT_CAPACITY - inputs = [old_ref] + funding["cells"] - witnesses = [ - rwa_entry_witness( - op, - material["old_cell_data"], - material["signed_intent"], - material["signer_sig"], - material["cosigner_sig"], - ) - ] + ["0x" for _ in funding["cells"]] - else: - raise LiveAcceptanceError("RWA state/event tx requires an old ref") - if change_capacity <= 0: - raise LiveAcceptanceError("RWA state/event funding capacity is too small") - return transaction( - inputs, - [ - {"capacity": hex(STATE_CAPACITY if old_ref is None else old_ref["capacity"]), "lock": always_success_lock(), "type": lifecycle_type(lifecycle_data_hash)}, - {"capacity": hex(RECEIPT_CAPACITY), "lock": always_success_lock(), "type": None}, - {"capacity": hex(change_capacity), "lock": always_success_lock(), "type": None}, - ], - [hex0x(material["new_cell_data"]), hex0x(material["event_data"]), "0x"], - cell_deps, - witnesses, - [header_hash], - ) - - -def build_rwa_settle_tx( - *, - old_ref: dict[str, Any], - funding: dict[str, Any], - lifecycle_data_hash: str, - cell_deps: list[dict[str, Any]], - header_hash: str, - material: dict[str, Any], -) -> dict[str, Any]: - change_capacity = old_ref["capacity"] + funding["total_capacity"] - RECEIPT_CAPACITY - if change_capacity <= 0: - raise LiveAcceptanceError("RWA settle funding capacity is too small") - witness = rwa_entry_witness( - OP_RWA_SETTLE, - material["old_cell_data"], - material["signed_intent"], - material["signer_sig"], - material["cosigner_sig"], - ) - return transaction( - [old_ref] + funding["cells"], - [ - {"capacity": hex(RECEIPT_CAPACITY), "lock": always_success_lock(), "type": None}, - {"capacity": hex(change_capacity), "lock": always_success_lock(), "type": None}, - ], - [hex0x(material["event_data"]), "0x"], - cell_deps, - [witness] + ["0x" for _ in funding["cells"]], - [header_hash], - ) - - -def pack_btc_tx_public_commitment(commitment: dict[str, Any]) -> bytes: - return ( - commitment["btc_txid"] - + commitment["btc_wtxid"] - + u32(commitment["btc_output_index"]) - + u64(commitment["btc_amount_sats"]) - + commitment["transition_commitment_hash"] - ) - - -def pack_btc_tx_intent_core(core: dict[str, Any]) -> bytes: - return ( - u8(core["action"]) - + core["seal_id"] - + core["policy_hash"] - + core["committer_authority_hash"] - + core["btc_txid"] - + core["btc_wtxid"] - + u32(core["btc_output_index"]) - + u64(core["btc_amount_sats"]) - + core["old_state_hash"] - + core["new_state_hash"] - + core["transition_commitment_hash"] - + u8(core["old_status"]) - + u8(core["new_status"]) - + u64(core["old_nonce"]) - + u64(core["new_nonce"]) - + u64(core["expiry"]) - + core["payout_commitment_hash"] - ) - - -def pack_btc_tx_signed_intent(core_data: bytes, canonical_hash: bytes, expected_receipt_hash: bytes) -> bytes: - return core_data + canonical_hash + expected_receipt_hash - - -def pack_btc_tx_state_commitment(cell: dict[str, Any]) -> bytes: - return ( - u16(cell["version"]) - + cell["seal_id"] - + cell["policy_hash"] - + cell["committer_authority_hash"] - + cell["btc_tx_commitment_hash"] - + cell["state_hash"] - + u8(cell["status"]) - + u64(cell["nonce"]) - + u64(cell["expiry"]) - ) - - -def pack_btc_tx_receipt_commitment(commitment: dict[str, Any]) -> bytes: - return ( - u8(commitment["action"]) - + commitment["seal_id"] - + commitment["policy_hash"] - + commitment["committer_authority_hash"] - + commitment["btc_tx_commitment_hash"] - + commitment["old_state_hash"] - + commitment["new_state_hash"] - + u8(commitment["old_status"]) - + u8(commitment["new_status"]) - + u64(commitment["old_nonce"]) - + u64(commitment["new_nonce"]) - + commitment["intent_core_hash"] - + commitment["payout_commitment_hash"] - ) - - -def pack_btc_tx_cell(cell: dict[str, Any]) -> bytes: - return ( - u16(cell["version"]) - + cell["seal_id"] - + cell["policy_hash"] - + cell["committer_authority_hash"] - + cell["btc_tx_commitment_hash"] - + cell["state_hash"] - + u8(cell["status"]) - + cell["latest_receipt_hash"] - + u64(cell["nonce"]) - + u64(cell["expiry"]) - ) - - -def pack_btc_tx_receipt(receipt: dict[str, Any]) -> bytes: - return ( - u8(receipt["action"]) - + receipt["seal_id"] - + receipt["policy_hash"] - + receipt["committer_authority_hash"] - + receipt["btc_tx_commitment_hash"] - + receipt["old_state_hash"] - + receipt["new_state_hash"] - + u8(receipt["old_status"]) - + u8(receipt["new_status"]) - + u64(receipt["old_nonce"]) - + u64(receipt["new_nonce"]) - + receipt["intent_core_hash"] - + receipt["signed_intent_hash"] - + receipt["payout_commitment_hash"] - + receipt["latest_receipt_hash"] - + receipt["signer_authority_hash"] - + u64(receipt["expiry"]) - ) - - -def zero_btc_tx_cell() -> dict[str, Any]: - return { - "version": 0, - "seal_id": ZERO_HASH, - "policy_hash": ZERO_HASH, - "committer_authority_hash": ZERO_HASH, - "btc_tx_commitment_hash": ZERO_HASH, - "state_hash": ZERO_HASH, - "status": 0, - "latest_receipt_hash": ZERO_HASH, - "nonce": 0, - "expiry": 0, - } - - -def btc_tx_entry_witness(op: int, old_cell_data: bytes, signed_intent: bytes, sig_payload: bytes) -> str: - payload = ( - b"CSARGv1\0" - + u8(op) - + u32(len(old_cell_data)) - + old_cell_data - + u32(len(signed_intent)) - + signed_intent - + u32(len(sig_payload)) - + sig_payload - ) - return hex0x(payload) - - -def btc_tx_base_state(label: str) -> dict[str, Any]: - return { - "seal_id": ckb_hash(f"NovaSeal BTC transaction seal {label}".encode("ascii")), - "policy_hash": ckb_hash(f"NovaSeal BTC transaction policy {label}".encode("ascii")), - "committer_authority_hash": xonly_pubkey(TEST_SECRET_KEY), - "initial_state_hash": ckb_hash(f"NovaSeal BTC transaction active state {label}".encode("ascii")), - "committed_state_hash": ckb_hash(f"NovaSeal BTC transaction committed state {label}".encode("ascii")), - "btc_txid": ckb_hash(f"NovaSeal BTC txid {label}".encode("ascii")), - "btc_wtxid": ckb_hash(f"NovaSeal BTC wtxid {label}".encode("ascii")), - "btc_output_index": 2, - "btc_amount_sats": 125_000, - "expiry": (1 << 63) - 1, - } - - -def btc_tx_canonical_hash( - *, - op: int, - base: dict[str, Any], - old_state_commitment: bytes, - new_state_commitment: bytes, - old_nonce: int, - new_nonce: int, - expiry: int, - authority_hash: bytes, - profile_body_hash: bytes, - payout_commitment_hash: bytes, -) -> bytes: - return canonical_envelope_hash( - action=op, - asset_id=base["seal_id"], - xudt_type_hash=base["policy_hash"], - old_state_commitment=old_state_commitment, - new_state_commitment=new_state_commitment, - old_nonce=old_nonce, - new_nonce=new_nonce, - expiry=expiry, - authority_hash=authority_hash, - profile_body_hash=profile_body_hash, - payout_commitment_hash=payout_commitment_hash, - ) - - -def build_btc_tx_material( - *, - op: int, - base: dict[str, Any], - old_cell: dict[str, Any] | None, - mutate_signature: bool = False, - zero_btc_txid: bool = False, - transition_hash_mismatch: bool = False, -) -> dict[str, Any]: - payout_commitment_hash = ZERO_HASH - if op == OP_BTC_INITIALIZE_ACTIVE_STATE: - old_status = 0 - new_status = STATUS_ACTIVE - old_nonce = 0 - new_nonce = 0 - old_state_hash = ZERO_HASH - new_state_hash = base["initial_state_hash"] - btc_txid = ZERO_HASH - btc_wtxid = ZERO_HASH - btc_output_index = 0 - btc_amount_sats = 0 - transition_commitment_hash = ZERO_HASH - btc_tx_commitment_hash = ZERO_HASH - old_state_commitment = ZERO_HASH - expected_receipt_hash = ZERO_HASH - new_cell = { - "version": BTC_TX_COMMITMENT_VERSION, - "seal_id": base["seal_id"], - "policy_hash": base["policy_hash"], - "committer_authority_hash": base["committer_authority_hash"], - "btc_tx_commitment_hash": ZERO_HASH, - "state_hash": new_state_hash, - "status": STATUS_ACTIVE, - "latest_receipt_hash": ZERO_HASH, - "nonce": 0, - "expiry": base["expiry"], - } - new_state_commitment = data_packed_hash("NovaBtcTransactionCommitmentStateV0", pack_btc_tx_state_commitment(new_cell)) - receipt_data = b"" - elif op == OP_BTC_COMMIT_TRANSACTION: - if old_cell is None: - raise LiveAcceptanceError("BTC transaction commit material requires an old cell") - old_status = STATUS_ACTIVE - new_status = BTC_STATUS_COMMITTED - old_nonce = old_cell["nonce"] - new_nonce = old_nonce + 1 - old_state_hash = old_cell["state_hash"] - new_state_hash = base["committed_state_hash"] - btc_txid = ZERO_HASH if zero_btc_txid else base["btc_txid"] - btc_wtxid = base["btc_wtxid"] - btc_output_index = base["btc_output_index"] - btc_amount_sats = base["btc_amount_sats"] - transition_commitment_hash = ( - ckb_hash(b"NovaSeal BTC transaction mismatched transition") if transition_hash_mismatch else ckb_hash(new_state_hash) - ) - btc_tx_commitment_hash = data_packed_hash( - "BtcTransactionPublicCommitmentV0", - pack_btc_tx_public_commitment( - { - "btc_txid": btc_txid, - "btc_wtxid": btc_wtxid, - "btc_output_index": btc_output_index, - "btc_amount_sats": btc_amount_sats, - "transition_commitment_hash": transition_commitment_hash, - } - ), - ) - old_state_commitment = data_packed_hash("NovaBtcTransactionCommitmentStateV0", pack_btc_tx_state_commitment(old_cell)) - new_cell = { - "version": BTC_TX_COMMITMENT_VERSION, - "seal_id": old_cell["seal_id"], - "policy_hash": old_cell["policy_hash"], - "committer_authority_hash": old_cell["committer_authority_hash"], - "btc_tx_commitment_hash": btc_tx_commitment_hash, - "state_hash": new_state_hash, - "status": BTC_STATUS_COMMITTED, - "latest_receipt_hash": ZERO_HASH, - "nonce": new_nonce, - "expiry": old_cell["expiry"], - } - new_state_commitment = data_packed_hash("NovaBtcTransactionCommitmentStateV0", pack_btc_tx_state_commitment(new_cell)) - receipt_commitment = { - "action": OP_BTC_COMMIT_TRANSACTION, - "seal_id": old_cell["seal_id"], - "policy_hash": old_cell["policy_hash"], - "committer_authority_hash": old_cell["committer_authority_hash"], - "btc_tx_commitment_hash": btc_tx_commitment_hash, - "old_state_hash": old_cell["state_hash"], - "new_state_hash": new_state_hash, - "old_status": STATUS_ACTIVE, - "new_status": BTC_STATUS_COMMITTED, - "old_nonce": old_nonce, - "new_nonce": new_nonce, - "intent_core_hash": ZERO_HASH, - "payout_commitment_hash": payout_commitment_hash, - } - # Filled after the intent core hash is known. - expected_receipt_hash = ZERO_HASH - receipt_data = b"" - else: - raise LiveAcceptanceError(f"unknown BTC transaction op {op}") - - core = { - "action": op, - "seal_id": base["seal_id"], - "policy_hash": base["policy_hash"], - "committer_authority_hash": base["committer_authority_hash"], - "btc_txid": btc_txid, - "btc_wtxid": btc_wtxid, - "btc_output_index": btc_output_index, - "btc_amount_sats": btc_amount_sats, - "old_state_hash": old_state_hash, - "new_state_hash": new_state_hash, - "transition_commitment_hash": transition_commitment_hash, - "old_status": old_status, - "new_status": new_status, - "old_nonce": old_nonce, - "new_nonce": new_nonce, - "expiry": base["expiry"], - "payout_commitment_hash": payout_commitment_hash, - } - core_data = pack_btc_tx_intent_core(core) - intent_core_hash = data_packed_hash("NovaBtcTransactionCommitmentIntentCoreV0", core_data) - if op == OP_BTC_COMMIT_TRANSACTION: - receipt_commitment["intent_core_hash"] = intent_core_hash - expected_receipt_hash = data_packed_hash( - "NovaBtcTransactionCommitmentReceiptCommitmentV0", - pack_btc_tx_receipt_commitment(receipt_commitment), - ) - new_cell["latest_receipt_hash"] = expected_receipt_hash - canonical_hash = btc_tx_canonical_hash( - op=op, - base=base, - old_state_commitment=old_state_commitment, - new_state_commitment=new_state_commitment, - old_nonce=old_nonce, - new_nonce=new_nonce, - expiry=base["expiry"], - authority_hash=base["committer_authority_hash"], - profile_body_hash=intent_core_hash, - payout_commitment_hash=payout_commitment_hash, - ) - signed_intent = pack_btc_tx_signed_intent(core_data, canonical_hash, expected_receipt_hash) - signed_intent_hash = data_packed_hash("NovaBtcTransactionCommitmentSignedIntentV0", signed_intent) - sig_payload = bytearray(signature_payload(TEST_SECRET_KEY, signed_intent_hash, TEST_AUX_RAND)) - if mutate_signature: - sig_payload[-1] ^= 1 - new_cell_data = pack_btc_tx_cell(new_cell) - receipt = None - if op == OP_BTC_COMMIT_TRANSACTION: - receipt = { - "action": OP_BTC_COMMIT_TRANSACTION, - "seal_id": old_cell["seal_id"], - "policy_hash": old_cell["policy_hash"], - "committer_authority_hash": old_cell["committer_authority_hash"], - "btc_tx_commitment_hash": btc_tx_commitment_hash, - "old_state_hash": old_cell["state_hash"], - "new_state_hash": new_state_hash, - "old_status": STATUS_ACTIVE, - "new_status": BTC_STATUS_COMMITTED, - "old_nonce": old_nonce, - "new_nonce": new_nonce, - "intent_core_hash": intent_core_hash, - "signed_intent_hash": signed_intent_hash, - "payout_commitment_hash": payout_commitment_hash, - "latest_receipt_hash": expected_receipt_hash, - "signer_authority_hash": old_cell["committer_authority_hash"], - "expiry": old_cell["expiry"], - } - receipt_data = pack_btc_tx_receipt(receipt) - return { - "old_cell": old_cell or zero_btc_tx_cell(), - "old_cell_data": pack_btc_tx_cell(old_cell or zero_btc_tx_cell()), - "new_cell": new_cell, - "new_cell_data": new_cell_data, - "receipt": receipt, - "receipt_data": receipt_data, - "signed_intent": signed_intent, - "signed_intent_hash": signed_intent_hash, - "signature_payload": bytes(sig_payload), - "btc_txid": btc_txid, - "btc_wtxid": btc_wtxid, - "btc_output_index": btc_output_index, - "btc_amount_sats": btc_amount_sats, - "btc_tx_commitment_hash": btc_tx_commitment_hash, - "transition_commitment_hash": transition_commitment_hash, - "latest_receipt_hash": expected_receipt_hash, - } - - -def build_btc_tx_initialize_tx( - funding: dict[str, Any], - lifecycle_data_hash: str, - cell_deps: list[dict[str, Any]], - header_hash: str, - material: dict[str, Any], -) -> dict[str, Any]: - change_capacity = funding["total_capacity"] - STATE_CAPACITY - if change_capacity <= 0: - raise LiveAcceptanceError("BTC transaction initialize funding capacity is too small") - witness = btc_tx_entry_witness( - OP_BTC_INITIALIZE_ACTIVE_STATE, - material["old_cell_data"], - material["signed_intent"], - material["signature_payload"], - ) - return transaction( - funding, - [ - {"capacity": hex(STATE_CAPACITY), "lock": always_success_lock(), "type": lifecycle_type(lifecycle_data_hash)}, - {"capacity": hex(change_capacity), "lock": always_success_lock(), "type": None}, - ], - [hex0x(material["new_cell_data"]), "0x"], - cell_deps, - [witness] + ["0x" for _ in funding["cells"][1:]], - [header_hash], - ) - - -def build_btc_tx_commit_tx( - *, - old_ref: dict[str, Any], - funding: dict[str, Any], - lifecycle_data_hash: str, - cell_deps: list[dict[str, Any]], - header_hash: str, - material: dict[str, Any], -) -> dict[str, Any]: - change_capacity = funding["total_capacity"] - RECEIPT_CAPACITY - if change_capacity <= 0: - raise LiveAcceptanceError("BTC transaction commit funding capacity is too small") - witness = btc_tx_entry_witness( - OP_BTC_COMMIT_TRANSACTION, - material["old_cell_data"], - material["signed_intent"], - material["signature_payload"], - ) - return transaction( - [old_ref] + funding["cells"], - [ - {"capacity": hex(old_ref["capacity"]), "lock": always_success_lock(), "type": lifecycle_type(lifecycle_data_hash)}, - {"capacity": hex(RECEIPT_CAPACITY), "lock": always_success_lock(), "type": None}, - {"capacity": hex(change_capacity), "lock": always_success_lock(), "type": None}, - ], - [hex0x(material["new_cell_data"]), hex0x(material["receipt_data"]), "0x"], - cell_deps, - [witness] + ["0x" for _ in funding["cells"]], - [header_hash], - ) - - -def pack_btc_utxo_commitment(commitment: dict[str, Any]) -> bytes: - return ( - commitment["btc_txid"] - + u32(commitment["btc_vout_index"]) - + u64(commitment["btc_amount_sats"]) - + commitment["script_pubkey_hash"] - ) - - -def pack_btc_utxo_closure_commitment(commitment: dict[str, Any]) -> bytes: - return ( - commitment["sealed_utxo_commitment_hash"] - + commitment["spend_txid"] - + commitment["spend_wtxid"] - + u32(commitment["spend_input_index"]) - + commitment["transition_commitment_hash"] - + commitment["payout_commitment_hash"] - ) - - -def pack_btc_utxo_intent_core(core: dict[str, Any]) -> bytes: - return ( - u8(core["action"]) - + core["seal_id"] - + core["policy_hash"] - + core["owner_authority_hash"] - + core["btc_txid"] - + u32(core["btc_vout_index"]) - + u64(core["btc_amount_sats"]) - + core["script_pubkey_hash"] - + core["spend_txid"] - + core["spend_wtxid"] - + u32(core["spend_input_index"]) - + core["old_state_hash"] - + core["new_state_hash"] - + core["transition_commitment_hash"] - + u8(core["old_status"]) - + u8(core["new_status"]) - + u64(core["old_nonce"]) - + u64(core["new_nonce"]) - + u64(core["expiry"]) - + core["payout_commitment_hash"] - ) - - -def pack_btc_utxo_signed_intent(core_data: bytes, canonical_hash: bytes, expected_receipt_hash: bytes) -> bytes: - return core_data + canonical_hash + expected_receipt_hash - - -def pack_btc_utxo_signing_digest(intent_core_hash: bytes, canonical_hash: bytes, expected_receipt_hash: bytes) -> bytes: - return intent_core_hash + canonical_hash + expected_receipt_hash - - -def pack_btc_utxo_state_commitment(cell: dict[str, Any]) -> bytes: - return ( - u16(cell["version"]) - + cell["seal_id"] - + cell["policy_hash"] - + cell["owner_authority_hash"] - + cell["sealed_utxo_commitment_hash"] - + cell["state_hash"] - + u8(cell["status"]) - + u64(cell["nonce"]) - + u64(cell["expiry"]) - ) - - -def pack_btc_utxo_receipt_commitment(commitment: dict[str, Any]) -> bytes: - return ( - u8(commitment["action"]) - + commitment["seal_id"] - + commitment["policy_hash"] - + commitment["owner_authority_hash"] - + commitment["sealed_utxo_commitment_hash"] - + commitment["closure_commitment_hash"] - + commitment["old_state_hash"] - + commitment["new_state_hash"] - + u8(commitment["old_status"]) - + u8(commitment["new_status"]) - + u64(commitment["old_nonce"]) - + u64(commitment["new_nonce"]) - + commitment["intent_core_hash"] - + commitment["payout_commitment_hash"] - ) - - -def pack_btc_utxo_cell(cell: dict[str, Any]) -> bytes: - return ( - u16(cell["version"]) - + cell["seal_id"] - + cell["policy_hash"] - + cell["owner_authority_hash"] - + cell["sealed_utxo_commitment_hash"] - + cell["state_hash"] - + u8(cell["status"]) - + cell["latest_receipt_hash"] - + u64(cell["nonce"]) - + u64(cell["expiry"]) - ) - - -def pack_btc_utxo_receipt(receipt: dict[str, Any]) -> bytes: - return ( - u8(receipt["action"]) - + receipt["seal_id"] - + receipt["policy_hash"] - + receipt["owner_authority_hash"] - + receipt["sealed_utxo_commitment_hash"] - + receipt["closure_commitment_hash"] - + receipt["old_state_hash"] - + receipt["new_state_hash"] - + u8(receipt["old_status"]) - + u8(receipt["new_status"]) - + u64(receipt["old_nonce"]) - + u64(receipt["new_nonce"]) - + receipt["intent_core_hash"] - + receipt["signed_intent_hash"] - + receipt["payout_commitment_hash"] - + receipt["latest_receipt_hash"] - + receipt["signer_authority_hash"] - + u64(receipt["expiry"]) - ) - - -def zero_btc_utxo_cell() -> dict[str, Any]: - return { - "version": 0, - "seal_id": ZERO_HASH, - "policy_hash": ZERO_HASH, - "owner_authority_hash": ZERO_HASH, - "sealed_utxo_commitment_hash": ZERO_HASH, - "state_hash": ZERO_HASH, - "status": 0, - "latest_receipt_hash": ZERO_HASH, - "nonce": 0, - "expiry": 0, - } - - -def btc_utxo_entry_witness(op: int, old_cell_data: bytes, signed_intent: bytes, sig_payload: bytes) -> str: - payload = ( - b"CSARGv1\0" - + u8(op) - + u32(len(old_cell_data)) - + old_cell_data - + u32(len(signed_intent)) - + signed_intent - + u32(len(sig_payload)) - + sig_payload - ) - return hex0x(payload) - - -def btc_utxo_base_state(label: str) -> dict[str, Any]: - return { - "seal_id": ckb_hash(f"NovaSeal BTC UTXO seal {label}".encode("ascii")), - "policy_hash": ckb_hash(f"NovaSeal BTC UTXO policy {label}".encode("ascii")), - "owner_authority_hash": xonly_pubkey(TEST_SECRET_KEY), - "initial_state_hash": ckb_hash(f"NovaSeal BTC UTXO active state {label}".encode("ascii")), - "closed_state_hash": ckb_hash(f"NovaSeal BTC UTXO closed state {label}".encode("ascii")), - "btc_txid": ckb_hash(f"NovaSeal BTC UTXO txid {label}".encode("ascii")), - "btc_vout_index": 1, - "btc_amount_sats": 250_000, - "script_pubkey_hash": ckb_hash(f"NovaSeal BTC UTXO script pubkey {label}".encode("ascii")), - "spend_txid": ckb_hash(f"NovaSeal BTC UTXO spend txid {label}".encode("ascii")), - "spend_wtxid": ckb_hash(f"NovaSeal BTC UTXO spend wtxid {label}".encode("ascii")), - "spend_input_index": 0, - "expiry": (1 << 63) - 1, - } - - -def btc_utxo_canonical_hash( - *, - op: int, - base: dict[str, Any], - old_state_commitment: bytes, - new_state_commitment: bytes, - old_nonce: int, - new_nonce: int, - expiry: int, - authority_hash: bytes, - profile_body_hash: bytes, - payout_commitment_hash: bytes, -) -> bytes: - return canonical_envelope_hash( - action=op, - asset_id=base["seal_id"], - xudt_type_hash=base["policy_hash"], - old_state_commitment=old_state_commitment, - new_state_commitment=new_state_commitment, - old_nonce=old_nonce, - new_nonce=new_nonce, - expiry=expiry, - authority_hash=authority_hash, - profile_body_hash=profile_body_hash, - payout_commitment_hash=payout_commitment_hash, - ) - - -def build_btc_utxo_material( - *, - op: int, - base: dict[str, Any], - old_cell: dict[str, Any] | None, - mutate_signature: bool = False, - utxo_commitment_mismatch: bool = False, - zero_spend_txid: bool = False, -) -> dict[str, Any]: - payout_commitment_hash = ZERO_HASH - btc_txid = ckb_hash(b"NovaSeal mismatched UTXO txid") if utxo_commitment_mismatch else base["btc_txid"] - sealed_utxo_commitment_hash = data_packed_hash( - "BtcUtxoCommitmentV0", - pack_btc_utxo_commitment( - { - "btc_txid": btc_txid, - "btc_vout_index": base["btc_vout_index"], - "btc_amount_sats": base["btc_amount_sats"], - "script_pubkey_hash": base["script_pubkey_hash"], - } - ), - ) - if op == OP_BTC_UTXO_INITIALIZE_ACTIVE_SEAL: - old_status = 0 - new_status = STATUS_ACTIVE - old_nonce = 0 - new_nonce = 0 - old_state_hash = ZERO_HASH - new_state_hash = base["initial_state_hash"] - spend_txid = ZERO_HASH - spend_wtxid = ZERO_HASH - spend_input_index = 0 - transition_commitment_hash = ZERO_HASH - closure_commitment_hash = ZERO_HASH - old_state_commitment = ZERO_HASH - expected_receipt_hash = ZERO_HASH - new_cell = { - "version": BTC_UTXO_SEAL_VERSION, - "seal_id": base["seal_id"], - "policy_hash": base["policy_hash"], - "owner_authority_hash": base["owner_authority_hash"], - "sealed_utxo_commitment_hash": sealed_utxo_commitment_hash, - "state_hash": new_state_hash, - "status": STATUS_ACTIVE, - "latest_receipt_hash": ZERO_HASH, - "nonce": 0, - "expiry": base["expiry"], - } - new_state_commitment = data_packed_hash("NovaBtcUtxoSealStateV0", pack_btc_utxo_state_commitment(new_cell)) - receipt_data = b"" - elif op == OP_BTC_UTXO_CLOSE: - if old_cell is None: - raise LiveAcceptanceError("BTC UTXO close material requires an old cell") - old_status = STATUS_ACTIVE - new_status = BTC_STATUS_CLOSED - old_nonce = old_cell["nonce"] - new_nonce = old_nonce + 1 - old_state_hash = old_cell["state_hash"] - new_state_hash = base["closed_state_hash"] - spend_txid = ZERO_HASH if zero_spend_txid else base["spend_txid"] - spend_wtxid = base["spend_wtxid"] - spend_input_index = base["spend_input_index"] - transition_commitment_hash = ckb_hash(new_state_hash) - closure_commitment_hash = data_packed_hash( - "BtcUtxoClosureCommitmentV0", - pack_btc_utxo_closure_commitment( - { - "sealed_utxo_commitment_hash": sealed_utxo_commitment_hash, - "spend_txid": spend_txid, - "spend_wtxid": spend_wtxid, - "spend_input_index": spend_input_index, - "transition_commitment_hash": transition_commitment_hash, - "payout_commitment_hash": payout_commitment_hash, - } - ), - ) - old_state_commitment = data_packed_hash("NovaBtcUtxoSealStateV0", pack_btc_utxo_state_commitment(old_cell)) - new_cell = { - "version": BTC_UTXO_SEAL_VERSION, - "seal_id": old_cell["seal_id"], - "policy_hash": old_cell["policy_hash"], - "owner_authority_hash": old_cell["owner_authority_hash"], - "sealed_utxo_commitment_hash": sealed_utxo_commitment_hash, - "state_hash": new_state_hash, - "status": BTC_STATUS_CLOSED, - "latest_receipt_hash": ZERO_HASH, - "nonce": new_nonce, - "expiry": old_cell["expiry"], - } - receipt_commitment = { - "action": OP_BTC_UTXO_CLOSE, - "seal_id": old_cell["seal_id"], - "policy_hash": old_cell["policy_hash"], - "owner_authority_hash": old_cell["owner_authority_hash"], - "sealed_utxo_commitment_hash": sealed_utxo_commitment_hash, - "closure_commitment_hash": closure_commitment_hash, - "old_state_hash": old_cell["state_hash"], - "new_state_hash": new_state_hash, - "old_status": STATUS_ACTIVE, - "new_status": BTC_STATUS_CLOSED, - "old_nonce": old_nonce, - "new_nonce": new_nonce, - "intent_core_hash": ZERO_HASH, - "payout_commitment_hash": payout_commitment_hash, - } - expected_receipt_hash = ZERO_HASH - new_state_commitment = closure_commitment_hash - receipt_data = b"" - else: - raise LiveAcceptanceError(f"unknown BTC UTXO op {op}") - - core = { - "action": op, - "seal_id": base["seal_id"], - "policy_hash": base["policy_hash"], - "owner_authority_hash": base["owner_authority_hash"], - "btc_txid": btc_txid, - "btc_vout_index": base["btc_vout_index"], - "btc_amount_sats": base["btc_amount_sats"], - "script_pubkey_hash": base["script_pubkey_hash"], - "spend_txid": spend_txid, - "spend_wtxid": spend_wtxid, - "spend_input_index": spend_input_index, - "old_state_hash": old_state_hash, - "new_state_hash": new_state_hash, - "transition_commitment_hash": transition_commitment_hash, - "old_status": old_status, - "new_status": new_status, - "old_nonce": old_nonce, - "new_nonce": new_nonce, - "expiry": base["expiry"], - "payout_commitment_hash": payout_commitment_hash, - } - core_data = pack_btc_utxo_intent_core(core) - intent_core_hash = data_packed_hash("NovaBtcUtxoSealIntentCoreV0", core_data) - if op == OP_BTC_UTXO_CLOSE: - receipt_commitment["intent_core_hash"] = intent_core_hash - expected_receipt_hash = data_packed_hash( - "NovaBtcUtxoSealReceiptCommitmentV0", - pack_btc_utxo_receipt_commitment(receipt_commitment), - ) - new_cell["latest_receipt_hash"] = expected_receipt_hash - canonical_hash = btc_utxo_canonical_hash( - op=op, - base=base, - old_state_commitment=old_state_commitment, - new_state_commitment=new_state_commitment, - old_nonce=old_nonce, - new_nonce=new_nonce, - expiry=base["expiry"], - authority_hash=base["owner_authority_hash"], - profile_body_hash=intent_core_hash, - payout_commitment_hash=payout_commitment_hash, - ) - signed_intent = pack_btc_utxo_signed_intent(core_data, canonical_hash, expected_receipt_hash) - signed_intent_hash = data_packed_hash( - "NovaBtcUtxoSealSigningDigestV0", - pack_btc_utxo_signing_digest(intent_core_hash, canonical_hash, expected_receipt_hash), - ) - sig_payload = bytearray(signature_payload(TEST_SECRET_KEY, signed_intent_hash, TEST_AUX_RAND)) - if mutate_signature: - sig_payload[-1] ^= 1 - new_cell_data = pack_btc_utxo_cell(new_cell) - receipt = None - if op == OP_BTC_UTXO_CLOSE: - receipt = { - "action": OP_BTC_UTXO_CLOSE, - "seal_id": old_cell["seal_id"], - "policy_hash": old_cell["policy_hash"], - "owner_authority_hash": old_cell["owner_authority_hash"], - "sealed_utxo_commitment_hash": sealed_utxo_commitment_hash, - "closure_commitment_hash": closure_commitment_hash, - "old_state_hash": old_cell["state_hash"], - "new_state_hash": new_state_hash, - "old_status": STATUS_ACTIVE, - "new_status": BTC_STATUS_CLOSED, - "old_nonce": old_nonce, - "new_nonce": new_nonce, - "intent_core_hash": intent_core_hash, - "signed_intent_hash": signed_intent_hash, - "payout_commitment_hash": payout_commitment_hash, - "latest_receipt_hash": expected_receipt_hash, - "signer_authority_hash": old_cell["owner_authority_hash"], - "expiry": old_cell["expiry"], - } - receipt_data = pack_btc_utxo_receipt(receipt) - return { - "old_cell": old_cell or zero_btc_utxo_cell(), - "old_cell_data": pack_btc_utxo_cell(old_cell or zero_btc_utxo_cell()), - "new_cell": new_cell, - "new_cell_data": new_cell_data, - "receipt": receipt, - "receipt_data": receipt_data, - "signed_intent": signed_intent, - "signed_intent_hash": signed_intent_hash, - "signature_payload": bytes(sig_payload), - "btc_txid": btc_txid, - "btc_vout_index": base["btc_vout_index"], - "btc_amount_sats": base["btc_amount_sats"], - "script_pubkey_hash": base["script_pubkey_hash"], - "spend_txid": spend_txid, - "spend_wtxid": spend_wtxid, - "spend_input_index": spend_input_index, - "sealed_utxo_commitment_hash": sealed_utxo_commitment_hash, - "closure_commitment_hash": closure_commitment_hash, - "transition_commitment_hash": transition_commitment_hash, - "latest_receipt_hash": expected_receipt_hash, - } - - -def build_btc_utxo_initialize_tx( - funding: dict[str, Any], - lifecycle_data_hash: str, - cell_deps: list[dict[str, Any]], - header_hash: str, - material: dict[str, Any], -) -> dict[str, Any]: - change_capacity = funding["total_capacity"] - STATE_CAPACITY - if change_capacity <= 0: - raise LiveAcceptanceError("BTC UTXO initialize funding capacity is too small") - witness = btc_utxo_entry_witness( - OP_BTC_UTXO_INITIALIZE_ACTIVE_SEAL, - material["old_cell_data"], - material["signed_intent"], - material["signature_payload"], - ) - return transaction( - funding, - [ - {"capacity": hex(STATE_CAPACITY), "lock": always_success_lock(), "type": lifecycle_type(lifecycle_data_hash)}, - {"capacity": hex(change_capacity), "lock": always_success_lock(), "type": None}, - ], - [hex0x(material["new_cell_data"]), "0x"], - cell_deps, - [witness] + ["0x" for _ in funding["cells"][1:]], - [header_hash], - ) - - -def build_btc_utxo_close_tx( - *, - old_ref: dict[str, Any], - funding: dict[str, Any], - lifecycle_data_hash: str, - cell_deps: list[dict[str, Any]], - header_hash: str, - material: dict[str, Any], -) -> dict[str, Any]: - change_capacity = funding["total_capacity"] - RECEIPT_CAPACITY - if change_capacity <= 0: - raise LiveAcceptanceError("BTC UTXO close funding capacity is too small") - witness = btc_utxo_entry_witness( - OP_BTC_UTXO_CLOSE, - material["old_cell_data"], - material["signed_intent"], - material["signature_payload"], - ) - return transaction( - [old_ref] + funding["cells"], - [ - {"capacity": hex(old_ref["capacity"]), "lock": always_success_lock(), "type": lifecycle_type(lifecycle_data_hash)}, - {"capacity": hex(RECEIPT_CAPACITY), "lock": always_success_lock(), "type": None}, - {"capacity": hex(change_capacity), "lock": always_success_lock(), "type": None}, - ], - [hex0x(material["new_cell_data"]), hex0x(material["receipt_data"]), "0x"], - cell_deps, - [witness] + ["0x" for _ in funding["cells"]], - [header_hash], - ) - - -def pack_dual_seal_finality_commitment(commitment: dict[str, Any]) -> bytes: - return ( - commitment["sealed_utxo_commitment_hash"] - + commitment["btc_closure_commitment_hash"] - + commitment["old_ckb_state_hash"] - + commitment["new_ckb_state_hash"] - + u64(commitment["maturity_timepoint"]) - + commitment["payout_commitment_hash"] - ) - - -def pack_dual_seal_intent_core(core: dict[str, Any]) -> bytes: - return ( - u8(core["action"]) - + core["dual_seal_id"] - + core["policy_hash"] - + core["btc_owner_authority_hash"] - + core["ckb_authority_hash"] - + core["sealed_utxo_commitment_hash"] - + core["btc_closure_commitment_hash"] - + core["old_ckb_state_hash"] - + core["new_ckb_state_hash"] - + u64(core["maturity_timepoint"]) - + u8(core["old_status"]) - + u8(core["new_status"]) - + u64(core["old_nonce"]) - + u64(core["new_nonce"]) - + u64(core["expiry"]) - + core["payout_commitment_hash"] - ) - - -def pack_dual_seal_signed_intent(core_data: bytes, canonical_hash: bytes, expected_receipt_hash: bytes) -> bytes: - return core_data + canonical_hash + expected_receipt_hash - - -def pack_dual_seal_state_commitment(cell: dict[str, Any]) -> bytes: - return ( - u16(cell["version"]) - + cell["dual_seal_id"] - + cell["policy_hash"] - + cell["btc_owner_authority_hash"] - + cell["ckb_authority_hash"] - + cell["sealed_utxo_commitment_hash"] - + cell["ckb_state_hash"] - + u8(cell["status"]) - + u64(cell["nonce"]) - + u64(cell["maturity_timepoint"]) - + u64(cell["expiry"]) - ) - - -def pack_dual_seal_receipt_commitment(commitment: dict[str, Any]) -> bytes: - return ( - u8(commitment["action"]) - + commitment["dual_seal_id"] - + commitment["policy_hash"] - + commitment["btc_owner_authority_hash"] - + commitment["ckb_authority_hash"] - + commitment["sealed_utxo_commitment_hash"] - + commitment["btc_closure_commitment_hash"] - + commitment["old_ckb_state_hash"] - + commitment["new_ckb_state_hash"] - + u8(commitment["old_status"]) - + u8(commitment["new_status"]) - + u64(commitment["old_nonce"]) - + u64(commitment["new_nonce"]) - + commitment["intent_core_hash"] - + commitment["payout_commitment_hash"] - ) - - -def pack_dual_seal_cell(cell: dict[str, Any]) -> bytes: - return ( - u16(cell["version"]) - + cell["dual_seal_id"] - + cell["policy_hash"] - + cell["btc_owner_authority_hash"] - + cell["ckb_authority_hash"] - + cell["sealed_utxo_commitment_hash"] - + cell["ckb_state_hash"] - + u8(cell["status"]) - + cell["latest_receipt_hash"] - + u64(cell["nonce"]) - + u64(cell["maturity_timepoint"]) - + u64(cell["expiry"]) - ) - - -def pack_dual_seal_receipt(receipt: dict[str, Any]) -> bytes: - return ( - u8(receipt["action"]) - + receipt["dual_seal_id"] - + receipt["policy_hash"] - + receipt["btc_owner_authority_hash"] - + receipt["ckb_authority_hash"] - + receipt["sealed_utxo_commitment_hash"] - + receipt["btc_closure_commitment_hash"] - + receipt["old_ckb_state_hash"] - + receipt["new_ckb_state_hash"] - + u8(receipt["old_status"]) - + u8(receipt["new_status"]) - + u64(receipt["old_nonce"]) - + u64(receipt["new_nonce"]) - + receipt["intent_core_hash"] - + receipt["signed_intent_hash"] - + receipt["payout_commitment_hash"] - + receipt["latest_receipt_hash"] - + receipt["signer_authority_hash"] - + u64(receipt["maturity_timepoint"]) - + u64(receipt["expiry"]) - ) - - -def zero_dual_seal_cell() -> dict[str, Any]: - return { - "version": 0, - "dual_seal_id": ZERO_HASH, - "policy_hash": ZERO_HASH, - "btc_owner_authority_hash": ZERO_HASH, - "ckb_authority_hash": ZERO_HASH, - "sealed_utxo_commitment_hash": ZERO_HASH, - "ckb_state_hash": ZERO_HASH, - "status": 0, - "latest_receipt_hash": ZERO_HASH, - "nonce": 0, - "maturity_timepoint": 0, - "expiry": 0, - } - - -def dual_seal_entry_witness( - op: int, - old_cell_data: bytes, - signed_intent: bytes, - btc_owner_sig_payload: bytes, - ckb_sig_payload: bytes, -) -> str: - payload = ( - b"CSARGv1\0" - + u8(op) - + u32(len(old_cell_data)) - + old_cell_data - + u32(len(signed_intent)) - + signed_intent - + u32(len(btc_owner_sig_payload)) - + btc_owner_sig_payload - + u32(len(ckb_sig_payload)) - + ckb_sig_payload - ) - return hex0x(payload) - - -def dual_seal_base_state(label: str) -> dict[str, Any]: - sealed_btc_txid = ckb_hash(f"NovaSeal dual sealed BTC txid {label}".encode("ascii")) - sealed_btc_vout_index = 1 - sealed_btc_amount_sats = 350_000 - script_pubkey_hash = ckb_hash(f"NovaSeal dual sealed BTC script pubkey {label}".encode("ascii")) - sealed_utxo_commitment_hash = data_packed_hash( - "BtcUtxoCommitmentV0", - pack_btc_utxo_commitment( - { - "btc_txid": sealed_btc_txid, - "btc_vout_index": sealed_btc_vout_index, - "btc_amount_sats": sealed_btc_amount_sats, - "script_pubkey_hash": script_pubkey_hash, - } - ), - ) - return { - "dual_seal_id": ckb_hash(f"NovaSeal dual seal {label}".encode("ascii")), - "policy_hash": ckb_hash(f"NovaSeal dual policy {label}".encode("ascii")), - "btc_owner_authority_hash": xonly_pubkey(TEST_SECRET_KEY), - "ckb_authority_hash": xonly_pubkey(HOLDER_SECRET_KEY), - "sealed_btc_txid": sealed_btc_txid, - "sealed_btc_vout_index": sealed_btc_vout_index, - "sealed_btc_amount_sats": sealed_btc_amount_sats, - "script_pubkey_hash": script_pubkey_hash, - "sealed_utxo_commitment_hash": sealed_utxo_commitment_hash, - "initial_ckb_state_hash": ckb_hash(f"NovaSeal dual active CKB state {label}".encode("ascii")), - "final_ckb_state_hash": ckb_hash(f"NovaSeal dual finalized CKB state {label}".encode("ascii")), - "btc_closure_commitment_hash": ckb_hash(f"NovaSeal dual BTC closure {label}".encode("ascii")), - "btc_txid": ckb_hash(f"NovaSeal dual BTC closure txid {label}".encode("ascii")), - "btc_wtxid": ckb_hash(f"NovaSeal dual BTC closure wtxid {label}".encode("ascii")), - "spend_input_index": 0, - "maturity_timepoint": 0, - "expiry": (1 << 63) - 1, - } - - -def dual_seal_canonical_hash( - *, - op: int, - base: dict[str, Any], - old_state_commitment: bytes, - new_state_commitment: bytes, - old_nonce: int, - new_nonce: int, - expiry: int, - authority_hash: bytes, - profile_body_hash: bytes, - payout_commitment_hash: bytes, -) -> bytes: - return canonical_envelope_hash( - action=op, - asset_id=base["dual_seal_id"], - xudt_type_hash=base["policy_hash"], - old_state_commitment=old_state_commitment, - new_state_commitment=new_state_commitment, - old_nonce=old_nonce, - new_nonce=new_nonce, - expiry=expiry, - authority_hash=authority_hash, - profile_body_hash=profile_body_hash, - payout_commitment_hash=payout_commitment_hash, - ) - - -def build_dual_seal_material( - *, - op: int, - base: dict[str, Any], - old_cell: dict[str, Any] | None, - mutate_btc_owner_signature: bool = False, - mutate_ckb_authority_signature: bool = False, - zero_btc_closure: bool = False, -) -> dict[str, Any]: - payout_commitment_hash = ZERO_HASH - if op == OP_DUAL_SEAL_INITIALIZE_ACTIVE: - old_status = 0 - new_status = STATUS_ACTIVE - old_nonce = 0 - new_nonce = 0 - old_ckb_state_hash = ZERO_HASH - new_ckb_state_hash = base["initial_ckb_state_hash"] - btc_closure_commitment_hash = ZERO_HASH - old_state_commitment = ZERO_HASH - expected_receipt_hash = ZERO_HASH - new_cell = { - "version": DUAL_SEAL_VERSION, - "dual_seal_id": base["dual_seal_id"], - "policy_hash": base["policy_hash"], - "btc_owner_authority_hash": base["btc_owner_authority_hash"], - "ckb_authority_hash": base["ckb_authority_hash"], - "sealed_utxo_commitment_hash": base["sealed_utxo_commitment_hash"], - "ckb_state_hash": new_ckb_state_hash, - "status": STATUS_ACTIVE, - "latest_receipt_hash": ZERO_HASH, - "nonce": 0, - "maturity_timepoint": base["maturity_timepoint"], - "expiry": base["expiry"], - } - new_state_commitment = data_packed_hash("NovaDualSealStateV0", pack_dual_seal_state_commitment(new_cell)) - receipt_data = b"" - elif op == OP_DUAL_SEAL_FINALIZE: - if old_cell is None: - raise LiveAcceptanceError("dual-seal finalization material requires an old cell") - old_status = STATUS_ACTIVE - new_status = DUAL_STATUS_FINALIZED - old_nonce = old_cell["nonce"] - new_nonce = old_nonce + 1 - old_ckb_state_hash = old_cell["ckb_state_hash"] - new_ckb_state_hash = base["final_ckb_state_hash"] - btc_closure_commitment_hash = ZERO_HASH if zero_btc_closure else base["btc_closure_commitment_hash"] - old_state_commitment = data_packed_hash("NovaDualSealStateV0", pack_dual_seal_state_commitment(old_cell)) - finality_commitment_hash = data_packed_hash( - "DualSealFinalityCommitmentV0", - pack_dual_seal_finality_commitment( - { - "sealed_utxo_commitment_hash": old_cell["sealed_utxo_commitment_hash"], - "btc_closure_commitment_hash": btc_closure_commitment_hash, - "old_ckb_state_hash": old_cell["ckb_state_hash"], - "new_ckb_state_hash": new_ckb_state_hash, - "maturity_timepoint": old_cell["maturity_timepoint"], - "payout_commitment_hash": payout_commitment_hash, - } - ), - ) - new_state_commitment = finality_commitment_hash - receipt_commitment = { - "action": OP_DUAL_SEAL_FINALIZE, - "dual_seal_id": old_cell["dual_seal_id"], - "policy_hash": old_cell["policy_hash"], - "btc_owner_authority_hash": old_cell["btc_owner_authority_hash"], - "ckb_authority_hash": old_cell["ckb_authority_hash"], - "sealed_utxo_commitment_hash": old_cell["sealed_utxo_commitment_hash"], - "btc_closure_commitment_hash": btc_closure_commitment_hash, - "old_ckb_state_hash": old_cell["ckb_state_hash"], - "new_ckb_state_hash": new_ckb_state_hash, - "old_status": STATUS_ACTIVE, - "new_status": DUAL_STATUS_FINALIZED, - "old_nonce": old_nonce, - "new_nonce": new_nonce, - "intent_core_hash": ZERO_HASH, - "payout_commitment_hash": payout_commitment_hash, - } - expected_receipt_hash = ZERO_HASH - new_cell = zero_dual_seal_cell() - receipt_data = b"" - else: - raise LiveAcceptanceError(f"unknown dual-seal op {op}") - - core = { - "action": op, - "dual_seal_id": base["dual_seal_id"], - "policy_hash": base["policy_hash"], - "btc_owner_authority_hash": base["btc_owner_authority_hash"], - "ckb_authority_hash": base["ckb_authority_hash"], - "sealed_utxo_commitment_hash": base["sealed_utxo_commitment_hash"], - "btc_closure_commitment_hash": btc_closure_commitment_hash, - "old_ckb_state_hash": old_ckb_state_hash, - "new_ckb_state_hash": new_ckb_state_hash, - "maturity_timepoint": base["maturity_timepoint"], - "old_status": old_status, - "new_status": new_status, - "old_nonce": old_nonce, - "new_nonce": new_nonce, - "expiry": base["expiry"], - "payout_commitment_hash": payout_commitment_hash, - } - core_data = pack_dual_seal_intent_core(core) - intent_core_hash = data_packed_hash("NovaDualSealIntentCoreV0", core_data) - if op == OP_DUAL_SEAL_FINALIZE: - receipt_commitment["intent_core_hash"] = intent_core_hash - expected_receipt_hash = data_packed_hash( - "NovaDualSealReceiptCommitmentV0", - pack_dual_seal_receipt_commitment(receipt_commitment), - ) - canonical_hash = dual_seal_canonical_hash( - op=op, - base=base, - old_state_commitment=old_state_commitment, - new_state_commitment=new_state_commitment, - old_nonce=old_nonce, - new_nonce=new_nonce, - expiry=base["expiry"], - authority_hash=base["ckb_authority_hash"], - profile_body_hash=intent_core_hash, - payout_commitment_hash=payout_commitment_hash, - ) - signed_intent = pack_dual_seal_signed_intent(core_data, canonical_hash, expected_receipt_hash) - signed_intent_hash = data_packed_hash("NovaDualSealSignedIntentV0", signed_intent) - btc_owner_sig_payload = bytearray(signature_payload(TEST_SECRET_KEY, signed_intent_hash, TEST_AUX_RAND)) - ckb_sig_payload = bytearray(signature_payload(HOLDER_SECRET_KEY, signed_intent_hash, HOLDER_AUX_RAND)) - if mutate_btc_owner_signature: - btc_owner_sig_payload[-1] ^= 1 - if mutate_ckb_authority_signature: - ckb_sig_payload[-1] ^= 1 - new_cell_data = pack_dual_seal_cell(new_cell) - receipt = None - if op == OP_DUAL_SEAL_FINALIZE: - receipt = { - "action": OP_DUAL_SEAL_FINALIZE, - "dual_seal_id": old_cell["dual_seal_id"], - "policy_hash": old_cell["policy_hash"], - "btc_owner_authority_hash": old_cell["btc_owner_authority_hash"], - "ckb_authority_hash": old_cell["ckb_authority_hash"], - "sealed_utxo_commitment_hash": old_cell["sealed_utxo_commitment_hash"], - "btc_closure_commitment_hash": btc_closure_commitment_hash, - "old_ckb_state_hash": old_cell["ckb_state_hash"], - "new_ckb_state_hash": new_ckb_state_hash, - "old_status": STATUS_ACTIVE, - "new_status": DUAL_STATUS_FINALIZED, - "old_nonce": old_nonce, - "new_nonce": new_nonce, - "intent_core_hash": intent_core_hash, - "signed_intent_hash": signed_intent_hash, - "payout_commitment_hash": payout_commitment_hash, - "latest_receipt_hash": expected_receipt_hash, - "signer_authority_hash": old_cell["ckb_authority_hash"], - "maturity_timepoint": old_cell["maturity_timepoint"], - "expiry": old_cell["expiry"], - } - receipt_data = pack_dual_seal_receipt(receipt) - return { - "old_cell": old_cell or zero_dual_seal_cell(), - "old_cell_data": pack_dual_seal_cell(old_cell or zero_dual_seal_cell()), - "new_cell": new_cell, - "new_cell_data": new_cell_data, - "receipt": receipt, - "receipt_data": receipt_data, - "signed_intent": signed_intent, - "signed_intent_hash": signed_intent_hash, - "btc_owner_signature_payload": bytes(btc_owner_sig_payload), - "ckb_signature_payload": bytes(ckb_sig_payload), - "finality_commitment_hash": new_state_commitment, - "btc_closure_commitment_hash": btc_closure_commitment_hash, - "sealed_btc_txid": base["sealed_btc_txid"], - "sealed_btc_vout_index": base["sealed_btc_vout_index"], - "sealed_btc_amount_sats": base["sealed_btc_amount_sats"], - "script_pubkey_hash": base["script_pubkey_hash"], - "btc_txid": base["btc_txid"], - "btc_wtxid": base["btc_wtxid"], - "spend_input_index": base["spend_input_index"], - "latest_receipt_hash": expected_receipt_hash, - } - - -def build_dual_seal_initialize_tx( - funding: dict[str, Any], - lifecycle_data_hash: str, - cell_deps: list[dict[str, Any]], - header_hash: str, - material: dict[str, Any], -) -> dict[str, Any]: - change_capacity = funding["total_capacity"] - STATE_CAPACITY - if change_capacity <= 0: - raise LiveAcceptanceError("dual-seal initialize funding capacity is too small") - witness = dual_seal_entry_witness( - OP_DUAL_SEAL_INITIALIZE_ACTIVE, - material["old_cell_data"], - material["signed_intent"], - material["btc_owner_signature_payload"], - material["ckb_signature_payload"], - ) - return transaction( - funding, - [ - {"capacity": hex(STATE_CAPACITY), "lock": always_success_lock(), "type": lifecycle_type(lifecycle_data_hash)}, - {"capacity": hex(change_capacity), "lock": always_success_lock(), "type": None}, - ], - [hex0x(material["new_cell_data"]), "0x"], - cell_deps, - [witness] + ["0x" for _ in funding["cells"][1:]], - [header_hash], - ) - - -def build_dual_seal_finalize_tx( - *, - old_ref: dict[str, Any], - funding: dict[str, Any], - lifecycle_data_hash: str, - cell_deps: list[dict[str, Any]], - header_hash: str, - material: dict[str, Any], -) -> dict[str, Any]: - change_capacity = old_ref["capacity"] + funding["total_capacity"] - RECEIPT_CAPACITY - if change_capacity <= 0: - raise LiveAcceptanceError("dual-seal finalize funding capacity is too small") - witness = dual_seal_entry_witness( - OP_DUAL_SEAL_FINALIZE, - material["old_cell_data"], - material["signed_intent"], - material["btc_owner_signature_payload"], - material["ckb_signature_payload"], - ) - return transaction( - [old_ref] + funding["cells"], - [ - {"capacity": hex(RECEIPT_CAPACITY), "lock": always_success_lock(), "type": None}, - {"capacity": hex(change_capacity), "lock": always_success_lock(), "type": None}, - ], - [hex0x(material["receipt_data"]), "0x"], - cell_deps, - [witness] + ["0x" for _ in funding["cells"]], - [header_hash], - ) - - -def pack_fiber_settlement_commitment(commitment: dict[str, Any]) -> bytes: - return ( - commitment["channel_id"] - + commitment["route_commitment_hash"] - + commitment["payment_hash"] - + commitment["old_balance_commitment_hash"] - + commitment["new_balance_commitment_hash"] - + u64(commitment["settlement_amount"]) - + commitment["payout_commitment_hash"] - ) - - -def pack_fiber_intent_core(core: dict[str, Any]) -> bytes: - return ( - u8(core["action"]) - + core["candidate_id"] - + core["policy_hash"] - + core["operator_authority_hash"] - + core["channel_id"] - + core["route_commitment_hash"] - + core["payment_hash"] - + core["old_balance_commitment_hash"] - + core["new_balance_commitment_hash"] - + u64(core["settlement_amount"]) - + u8(core["old_status"]) - + u8(core["new_status"]) - + u64(core["old_nonce"]) - + u64(core["new_nonce"]) - + u64(core["expiry"]) - + core["payout_commitment_hash"] - ) - - -def pack_fiber_signed_intent(core_data: bytes, canonical_hash: bytes, expected_receipt_hash: bytes) -> bytes: - return core_data + canonical_hash + expected_receipt_hash - - -def pack_fiber_state_commitment(cell: dict[str, Any]) -> bytes: - return ( - u16(cell["version"]) - + cell["candidate_id"] - + cell["policy_hash"] - + cell["operator_authority_hash"] - + cell["channel_id"] - + cell["balance_commitment_hash"] - + u8(cell["status"]) - + u64(cell["nonce"]) - + u64(cell["expiry"]) - ) - - -def pack_fiber_receipt_commitment(commitment: dict[str, Any]) -> bytes: - return ( - u8(commitment["action"]) - + commitment["candidate_id"] - + commitment["policy_hash"] - + commitment["operator_authority_hash"] - + commitment["channel_id"] - + commitment["route_commitment_hash"] - + commitment["payment_hash"] - + commitment["old_balance_commitment_hash"] - + commitment["new_balance_commitment_hash"] - + u64(commitment["settlement_amount"]) - + u8(commitment["old_status"]) - + u8(commitment["new_status"]) - + u64(commitment["old_nonce"]) - + u64(commitment["new_nonce"]) - + commitment["intent_core_hash"] - + commitment["payout_commitment_hash"] - ) - - -def pack_fiber_cell(cell: dict[str, Any]) -> bytes: - return ( - u16(cell["version"]) - + cell["candidate_id"] - + cell["policy_hash"] - + cell["operator_authority_hash"] - + cell["channel_id"] - + cell["balance_commitment_hash"] - + u8(cell["status"]) - + cell["latest_receipt_hash"] - + u64(cell["nonce"]) - + u64(cell["expiry"]) - ) - - -def pack_fiber_receipt(receipt: dict[str, Any]) -> bytes: - return ( - u8(receipt["action"]) - + receipt["candidate_id"] - + receipt["policy_hash"] - + receipt["operator_authority_hash"] - + receipt["channel_id"] - + receipt["route_commitment_hash"] - + receipt["payment_hash"] - + receipt["old_balance_commitment_hash"] - + receipt["new_balance_commitment_hash"] - + u64(receipt["settlement_amount"]) - + u8(receipt["old_status"]) - + u8(receipt["new_status"]) - + u64(receipt["old_nonce"]) - + u64(receipt["new_nonce"]) - + receipt["intent_core_hash"] - + receipt["signed_intent_hash"] - + receipt["payout_commitment_hash"] - + receipt["latest_receipt_hash"] - + receipt["signer_authority_hash"] - + u64(receipt["expiry"]) - ) - - -def zero_fiber_cell() -> dict[str, Any]: - return { - "version": 0, - "candidate_id": ZERO_HASH, - "policy_hash": ZERO_HASH, - "operator_authority_hash": ZERO_HASH, - "channel_id": ZERO_HASH, - "balance_commitment_hash": ZERO_HASH, - "status": 0, - "latest_receipt_hash": ZERO_HASH, - "nonce": 0, - "expiry": 0, - } - - -def fiber_entry_witness(op: int, old_cell_data: bytes, signed_intent: bytes, sig_payload: bytes) -> str: - payload = ( - b"CSARGv1\0" - + u8(op) - + u32(len(old_cell_data)) - + old_cell_data - + u32(len(signed_intent)) - + signed_intent - + u32(len(sig_payload)) - + sig_payload - ) - return hex0x(payload) - - -def fiber_base_state(label: str) -> dict[str, Any]: - return { - "candidate_id": ckb_hash(f"NovaSeal Fiber candidate {label}".encode("ascii")), - "policy_hash": ckb_hash(f"NovaSeal Fiber policy {label}".encode("ascii")), - "operator_authority_hash": xonly_pubkey(TEST_SECRET_KEY), - "channel_id": ckb_hash(f"NovaSeal Fiber channel {label}".encode("ascii")), - "initial_balance_commitment_hash": ckb_hash(f"NovaSeal Fiber initial balance {label}".encode("ascii")), - "settled_balance_commitment_hash": ckb_hash(f"NovaSeal Fiber settled balance {label}".encode("ascii")), - "route_commitment_hash": ckb_hash(f"NovaSeal Fiber route {label}".encode("ascii")), - "payment_hash": ckb_hash(f"NovaSeal Fiber payment {label}".encode("ascii")), - "settlement_amount": 42_000, - "expiry": (1 << 63) - 1, - } - - -def fiber_canonical_hash( - *, - op: int, - base: dict[str, Any], - old_state_commitment: bytes, - new_state_commitment: bytes, - old_nonce: int, - new_nonce: int, - expiry: int, - authority_hash: bytes, - profile_body_hash: bytes, - payout_commitment_hash: bytes, -) -> bytes: - return canonical_envelope_hash( - action=op, - asset_id=base["candidate_id"], - xudt_type_hash=base["policy_hash"], - old_state_commitment=old_state_commitment, - new_state_commitment=new_state_commitment, - old_nonce=old_nonce, - new_nonce=new_nonce, - expiry=expiry, - authority_hash=authority_hash, - profile_body_hash=profile_body_hash, - payout_commitment_hash=payout_commitment_hash, - ) - - -def build_fiber_material( - *, - op: int, - base: dict[str, Any], - old_cell: dict[str, Any] | None, - mutate_signature: bool = False, - balance_replay: bool = False, -) -> dict[str, Any]: - payout_commitment_hash = ZERO_HASH - if op == OP_FIBER_INITIALIZE_ACTIVE_CANDIDATE: - old_balance = ZERO_HASH - new_balance = base["initial_balance_commitment_hash"] - route_commitment_hash = ZERO_HASH - payment_hash = ZERO_HASH - settlement_amount = 0 - old_status = 0 - new_status = STATUS_ACTIVE - old_nonce = 0 - new_nonce = 0 - old_state_commitment = ZERO_HASH - expected_receipt_hash = ZERO_HASH - new_cell = { - "version": FIBER_CANDIDATE_VERSION, - "candidate_id": base["candidate_id"], - "policy_hash": base["policy_hash"], - "operator_authority_hash": base["operator_authority_hash"], - "channel_id": base["channel_id"], - "balance_commitment_hash": new_balance, - "status": STATUS_ACTIVE, - "latest_receipt_hash": ZERO_HASH, - "nonce": 0, - "expiry": base["expiry"], - } - new_state_commitment = data_packed_hash("NovaFiberCandidateStateV0", pack_fiber_state_commitment(new_cell)) - receipt_data = b"" - elif op == OP_FIBER_SETTLE: - if old_cell is None: - raise LiveAcceptanceError("Fiber settle material requires an old cell") - old_balance = old_cell["balance_commitment_hash"] - new_balance = old_cell["balance_commitment_hash"] if balance_replay else base["settled_balance_commitment_hash"] - route_commitment_hash = base["route_commitment_hash"] - payment_hash = base["payment_hash"] - settlement_amount = base["settlement_amount"] - old_status = STATUS_ACTIVE - new_status = FIBER_STATUS_SETTLED - old_nonce = old_cell["nonce"] - new_nonce = old_nonce + 1 - old_state_commitment = data_packed_hash("NovaFiberCandidateStateV0", pack_fiber_state_commitment(old_cell)) - new_cell = { - "version": FIBER_CANDIDATE_VERSION, - "candidate_id": old_cell["candidate_id"], - "policy_hash": old_cell["policy_hash"], - "operator_authority_hash": old_cell["operator_authority_hash"], - "channel_id": old_cell["channel_id"], - "balance_commitment_hash": new_balance, - "status": FIBER_STATUS_SETTLED, - "latest_receipt_hash": ZERO_HASH, - "nonce": new_nonce, - "expiry": old_cell["expiry"], - } - new_state_commitment = data_packed_hash("NovaFiberCandidateStateV0", pack_fiber_state_commitment(new_cell)) - receipt_commitment = { - "action": OP_FIBER_SETTLE, - "candidate_id": old_cell["candidate_id"], - "policy_hash": old_cell["policy_hash"], - "operator_authority_hash": old_cell["operator_authority_hash"], - "channel_id": old_cell["channel_id"], - "route_commitment_hash": route_commitment_hash, - "payment_hash": payment_hash, - "old_balance_commitment_hash": old_balance, - "new_balance_commitment_hash": new_balance, - "settlement_amount": settlement_amount, - "old_status": STATUS_ACTIVE, - "new_status": FIBER_STATUS_SETTLED, - "old_nonce": old_nonce, - "new_nonce": new_nonce, - "intent_core_hash": ZERO_HASH, - "payout_commitment_hash": payout_commitment_hash, - } - expected_receipt_hash = ZERO_HASH - receipt_data = b"" - else: - raise LiveAcceptanceError(f"unknown Fiber op {op}") - - core = { - "action": op, - "candidate_id": base["candidate_id"], - "policy_hash": base["policy_hash"], - "operator_authority_hash": base["operator_authority_hash"], - "channel_id": base["channel_id"], - "route_commitment_hash": route_commitment_hash, - "payment_hash": payment_hash, - "old_balance_commitment_hash": old_balance, - "new_balance_commitment_hash": new_balance, - "settlement_amount": settlement_amount, - "old_status": old_status, - "new_status": new_status, - "old_nonce": old_nonce, - "new_nonce": new_nonce, - "expiry": base["expiry"], - "payout_commitment_hash": payout_commitment_hash, - } - core_data = pack_fiber_intent_core(core) - intent_core_hash = data_packed_hash("NovaFiberCandidateIntentCoreV0", core_data) - if op == OP_FIBER_SETTLE: - receipt_commitment["intent_core_hash"] = intent_core_hash - expected_receipt_hash = data_packed_hash( - "NovaFiberCandidateReceiptCommitmentV0", - pack_fiber_receipt_commitment(receipt_commitment), - ) - new_cell["latest_receipt_hash"] = expected_receipt_hash - canonical_hash = fiber_canonical_hash( - op=op, - base=base, - old_state_commitment=old_state_commitment, - new_state_commitment=new_state_commitment, - old_nonce=old_nonce, - new_nonce=new_nonce, - expiry=base["expiry"], - authority_hash=base["operator_authority_hash"], - profile_body_hash=intent_core_hash, - payout_commitment_hash=payout_commitment_hash, - ) - signed_intent = pack_fiber_signed_intent(core_data, canonical_hash, expected_receipt_hash) - signed_intent_hash = data_packed_hash("NovaFiberCandidateSignedIntentV0", signed_intent) - sig_payload = bytearray(signature_payload(TEST_SECRET_KEY, signed_intent_hash, TEST_AUX_RAND)) - if mutate_signature: - sig_payload[-1] ^= 1 - new_cell_data = pack_fiber_cell(new_cell) - receipt = None - settlement_commitment_hash = ZERO_HASH - if op == OP_FIBER_SETTLE: - settlement_commitment_hash = data_packed_hash( - "FiberCandidateSettlementCommitmentV0", - pack_fiber_settlement_commitment( - { - "channel_id": old_cell["channel_id"], - "route_commitment_hash": route_commitment_hash, - "payment_hash": payment_hash, - "old_balance_commitment_hash": old_balance, - "new_balance_commitment_hash": new_balance, - "settlement_amount": settlement_amount, - "payout_commitment_hash": payout_commitment_hash, - } - ), - ) - receipt = { - "action": OP_FIBER_SETTLE, - "candidate_id": old_cell["candidate_id"], - "policy_hash": old_cell["policy_hash"], - "operator_authority_hash": old_cell["operator_authority_hash"], - "channel_id": old_cell["channel_id"], - "route_commitment_hash": route_commitment_hash, - "payment_hash": payment_hash, - "old_balance_commitment_hash": old_balance, - "new_balance_commitment_hash": new_balance, - "settlement_amount": settlement_amount, - "old_status": STATUS_ACTIVE, - "new_status": FIBER_STATUS_SETTLED, - "old_nonce": old_nonce, - "new_nonce": new_nonce, - "intent_core_hash": intent_core_hash, - "signed_intent_hash": signed_intent_hash, - "payout_commitment_hash": payout_commitment_hash, - "latest_receipt_hash": expected_receipt_hash, - "signer_authority_hash": old_cell["operator_authority_hash"], - "expiry": old_cell["expiry"], - } - receipt_data = pack_fiber_receipt(receipt) - return { - "old_cell": old_cell or zero_fiber_cell(), - "old_cell_data": pack_fiber_cell(old_cell or zero_fiber_cell()), - "new_cell": new_cell, - "new_cell_data": new_cell_data, - "receipt": receipt, - "receipt_data": receipt_data, - "signed_intent": signed_intent, - "signed_intent_hash": signed_intent_hash, - "signature_payload": bytes(sig_payload), - "settlement_commitment_hash": settlement_commitment_hash, - "latest_receipt_hash": expected_receipt_hash, - } - - -def build_fiber_initialize_tx( - funding: dict[str, Any], - lifecycle_data_hash: str, - cell_deps: list[dict[str, Any]], - header_hash: str, - material: dict[str, Any], -) -> dict[str, Any]: - change_capacity = funding["total_capacity"] - STATE_CAPACITY - if change_capacity <= 0: - raise LiveAcceptanceError("Fiber initialize funding capacity is too small") - witness = fiber_entry_witness( - OP_FIBER_INITIALIZE_ACTIVE_CANDIDATE, - material["old_cell_data"], - material["signed_intent"], - material["signature_payload"], - ) - return transaction( - funding, - [ - {"capacity": hex(STATE_CAPACITY), "lock": always_success_lock(), "type": lifecycle_type(lifecycle_data_hash)}, - {"capacity": hex(change_capacity), "lock": always_success_lock(), "type": None}, - ], - [hex0x(material["new_cell_data"]), "0x"], - cell_deps, - [witness] + ["0x" for _ in funding["cells"][1:]], - [header_hash], - ) - - -def build_fiber_settle_tx( - *, - old_ref: dict[str, Any], - funding: dict[str, Any], - lifecycle_data_hash: str, - cell_deps: list[dict[str, Any]], - header_hash: str, - material: dict[str, Any], -) -> dict[str, Any]: - change_capacity = funding["total_capacity"] - RECEIPT_CAPACITY - if change_capacity <= 0: - raise LiveAcceptanceError("Fiber settle funding capacity is too small") - witness = fiber_entry_witness(OP_FIBER_SETTLE, material["old_cell_data"], material["signed_intent"], material["signature_payload"]) - return transaction( - [old_ref] + funding["cells"], - [ - {"capacity": hex(old_ref["capacity"]), "lock": always_success_lock(), "type": lifecycle_type(lifecycle_data_hash)}, - {"capacity": hex(RECEIPT_CAPACITY), "lock": always_success_lock(), "type": None}, - {"capacity": hex(change_capacity), "lock": always_success_lock(), "type": None}, - ], - [hex0x(material["new_cell_data"]), hex0x(material["receipt_data"]), "0x"], - cell_deps, - [witness] + ["0x" for _ in funding["cells"]], - [header_hash], - ) - - -def compile_contract_lifecycle(repo_root: pathlib.Path, contract: ReportContract, output: pathlib.Path) -> None: - if contract.lifecycle_action is None: - raise LiveAcceptanceError(f"{contract.profile} has no lifecycle action") - cmd = [ - "cargo", - "run", - "--quiet", - "--bin", - "cellc", - "--", - contract.source, - "--target-profile", - "ckb", - "--target", - "riscv64-elf", - "--entry-action", - contract.lifecycle_action, - "-o", - str(output), - ] - subprocess.run(cmd, cwd=repo_root, check=True) - - -def run_fungible_xudt_live(args: argparse.Namespace, contract: ReportContract) -> dict[str, Any]: - repo_root = args.repo_root.resolve() - ckb_repo = args.ckb_repo.resolve() - ckb_bin = resolve_ckb_bin(ckb_repo, args.ckb_bin) - run_dir = (args.run_dir or (repo_root / "target/novaseal-fungible-xudt-devnet-stateful-live" / str(int(time.time())))).resolve() - run_dir.mkdir(parents=True, exist_ok=True) - lifecycle_elf = run_dir / "nova-fungible-xudt-lifecycle-type.elf" - compile_contract_lifecycle(repo_root, contract, lifecycle_elf) - verifier_elf = repo_root / "proposals/novaseal/v0-mvp-skeleton/target/novaseal-btc-verifier-riscv-shell-release.elf" - if not verifier_elf.is_file(): - raise LiveAcceptanceError(f"missing verifier ELF: {verifier_elf}") - - devnet = CkbDevnet(ckb_repo, ckb_bin, run_dir) - report: dict[str, Any] = { - "schema": "novaseal-planned-profile-devnet-stateful-live-v0.1", - "profile": contract.profile, - "status": "running", - "scenario": "fungible_xudt_issue_transfer_settle", - "repo_root": str(repo_root), - "ckb_repo": str(ckb_repo), - "ckb_bin": str(ckb_bin), - "run_dir": str(run_dir), - "expected_tx_hashes": named_pointer_rows(contract.tx_hashes, "pointer"), - "required_live_checks": named_pointer_rows(contract.live_checks, "pointer"), - "required_negative_cases": named_pointer_rows(contract.negative_cases, "key"), - } - stage = "initializing" - try: - stage = "start devnet" - devnet.start() - stage = "deploy artifacts" - genesis = devnet.get_block_by_number(0) - always_dep = always_success_dep(genesis["transactions"][0]["hash"]) - verifier = deploy_code_cell(devnet, "cellscript_btc_bip340_verifier_riscv", verifier_elf.read_bytes(), always_dep) - lifecycle = deploy_code_cell(devnet, "nova_fungible_xudt_lifecycle_type", lifecycle_elf.read_bytes(), always_dep) - cell_deps = [verifier["cell_dep"], lifecycle["cell_dep"], always_dep] - provenance = stateful_provenance( - repo_root, - [ - pathlib.Path("proposals/novaseal/fungible-xudt-profile-v0/Cell.toml"), - pathlib.Path("proposals/novaseal/fungible-xudt-profile-v0/src"), - pathlib.Path("proposals/novaseal/fungible-xudt-profile-v0/schemas"), - pathlib.Path("proposals/novaseal/v0-mvp-skeleton/verifier/novaseal_btc_verifier"), - pathlib.Path("scripts/novaseal_planned_profiles_devnet_stateful_live.py"), - pathlib.Path("scripts/novaseal_devnet_stateful_live.py"), - ], - {"verifier": verifier_elf, "lifecycle": lifecycle_elf}, - ) - base = xudt_base_state("live") - - stage = "valid issue" - issue_material = build_xudt_material(op=OP_ISSUE, base=base, old_cell=None) - issue_header = devnet.rpc("get_tip_header") - issue_funding = devnet.collect_spendable(STATE_CAPACITY + RECEIPT_CAPACITY + 100 * SHANNONS) - issue_tx = build_xudt_issue_tx(issue_funding, lifecycle["data_hash"], cell_deps, issue_header["hash"], issue_material) - issue_dry_run = devnet.rpc("dry_run_transaction", [issue_tx]) - issue_commit = devnet.submit_and_commit(issue_tx, "fungible xUDT issue") - issue_balance_live = devnet.assert_live_cell( - issue_commit["tx_hash"], - 0, - label="xUDT issued balance", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=issue_material["new_cell_data"], - ) - issue_receipt_live = devnet.assert_live_cell( - issue_commit["tx_hash"], - 1, - label="xUDT issue receipt", - expected_capacity=RECEIPT_CAPACITY, - expected_lock=always_success_lock(), - expected_type=None, - expected_data=issue_material["receipt_data"], - ) - issued_ref = {"tx_hash": issue_commit["tx_hash"], "index": 0, "capacity": STATE_CAPACITY} - - stage = "negative transfer wrong holder signature" - negative_header = devnet.rpc("get_tip_header") - wrong_sig_material = build_xudt_material( - op=OP_TRANSFER, - base=base, - old_cell=issue_material["new_cell"], - mutate_signature=True, - ) - wrong_sig_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - wrong_sig_tx = build_xudt_transfer_tx( - old_ref=issued_ref, - funding=wrong_sig_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=negative_header["hash"], - material=wrong_sig_material, - ) - wrong_holder_signature_reject = devnet.dry_run_rejects( - wrong_sig_tx, - "xUDT wrong holder signature transfer", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=BIP340_CHILD_REJECTED_ERROR_CODE, - ) - - stage = "negative transfer amount mismatch" - mismatch_material = build_xudt_material( - op=OP_TRANSFER, - base=base, - old_cell=issue_material["new_cell"], - transfer_amount_override=issue_material["new_cell"]["amount"] - 1, - ) - mismatch_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - mismatch_tx = build_xudt_transfer_tx( - old_ref=issued_ref, - funding=mismatch_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=negative_header["hash"], - material=mismatch_material, - ) - transfer_amount_mismatch_reject = devnet.dry_run_rejects( - mismatch_tx, - "xUDT transfer amount mismatch", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=5, - ) - post_transfer_negative_live = devnet.assert_live_cell( - issued_ref["tx_hash"], - issued_ref["index"], - label="post-negative xUDT issued balance", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=issue_material["new_cell_data"], - ) - - stage = "valid transfer" - transfer_header = devnet.rpc("get_tip_header") - transfer_material = build_xudt_material(op=OP_TRANSFER, base=base, old_cell=issue_material["new_cell"]) - transfer_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - transfer_tx = build_xudt_transfer_tx( - old_ref=issued_ref, - funding=transfer_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=transfer_header["hash"], - material=transfer_material, - ) - transfer_dry_run = devnet.rpc("dry_run_transaction", [transfer_tx]) - transfer_commit = devnet.submit_and_commit(transfer_tx, "fungible xUDT transfer") - old_balance_dead = devnet.wait_dead_cell(issued_ref["tx_hash"], issued_ref["index"]) - receiver_balance_live = devnet.assert_live_cell( - transfer_commit["tx_hash"], - 0, - label="xUDT receiver balance", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=transfer_material["new_cell_data"], - ) - transfer_receipt_live = devnet.assert_live_cell( - transfer_commit["tx_hash"], - 1, - label="xUDT transfer receipt", - expected_capacity=RECEIPT_CAPACITY, - expected_lock=always_success_lock(), - expected_type=None, - expected_data=transfer_material["receipt_data"], - ) - receiver_ref = {"tx_hash": transfer_commit["tx_hash"], "index": 0, "capacity": STATE_CAPACITY} - - stage = "negative settle wrong holder signature" - settle_negative_header = devnet.rpc("get_tip_header") - wrong_settle_material = build_xudt_material( - op=OP_SETTLE, - base=base, - old_cell=transfer_material["new_cell"], - mutate_signature=True, - ) - wrong_settle_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - wrong_settle_tx = build_xudt_settle_tx( - old_ref=receiver_ref, - funding=wrong_settle_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=settle_negative_header["hash"], - material=wrong_settle_material, - ) - settle_wrong_holder_signature_reject = devnet.dry_run_rejects( - wrong_settle_tx, - "xUDT wrong holder signature settle", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=BIP340_CHILD_REJECTED_ERROR_CODE, - ) - post_negative_state_live = devnet.assert_live_cell( - receiver_ref["tx_hash"], - receiver_ref["index"], - label="post-negative xUDT receiver balance", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=transfer_material["new_cell_data"], - ) - - stage = "valid settle" - settle_header = devnet.rpc("get_tip_header") - settle_material = build_xudt_material(op=OP_SETTLE, base=base, old_cell=transfer_material["new_cell"]) - settle_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - settle_tx = build_xudt_settle_tx( - old_ref=receiver_ref, - funding=settle_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=settle_header["hash"], - material=settle_material, - ) - settle_dry_run = devnet.rpc("dry_run_transaction", [settle_tx]) - settle_commit = devnet.submit_and_commit(settle_tx, "fungible xUDT settle") - receiver_balance_dead = devnet.wait_dead_cell(receiver_ref["tx_hash"], receiver_ref["index"]) - settlement_receipt_live = devnet.assert_live_cell( - settle_commit["tx_hash"], - 0, - label="xUDT settlement receipt", - expected_capacity=RECEIPT_CAPACITY, - expected_lock=always_success_lock(), - expected_type=None, - expected_data=settle_material["receipt_data"], - ) - - report.update( - { - "status": "passed", - "live_devnet_rpc_executed": True, - "stateful_lifecycle_executed": True, - "ckb_log": str(devnet.log_path), - "rpc_url": devnet.rpc_url, - "artifacts": {"verifier": verifier, "lifecycle": lifecycle}, - "provenance": provenance, - "issue": { - "dry_run_cycles": issue_dry_run.get("cycles"), - "commit": issue_commit, - "balance_live": issue_balance_live.get("status") == "live", - "receipt_live": issue_receipt_live.get("status") == "live", - "balance_data_hash": hex0x(cell_data_hash(issue_material["new_cell_data"])), - "receipt_hash": hex0x(issue_material["latest_receipt_hash"]), - }, - "transfer": { - "dry_run_cycles": transfer_dry_run.get("cycles"), - "commit": transfer_commit, - "old_balance_not_live": old_balance_dead.get("status") != "live", - "sender_balance_live": post_transfer_negative_live.get("status") == "live", - "receiver_balance_live": receiver_balance_live.get("status") == "live", - "receipt_live": transfer_receipt_live.get("status") == "live", - "amount_conserved": transfer_material["new_cell"]["amount"] == issue_material["new_cell"]["amount"], - "receipt_hash": hex0x(transfer_material["latest_receipt_hash"]), - }, - "settle": { - "dry_run_cycles": settle_dry_run.get("cycles"), - "commit": settle_commit, - "old_balance_not_live": receiver_balance_dead.get("status") != "live", - "settlement_receipt_live": settlement_receipt_live.get("status") == "live", - "receipt_hash": hex0x(settle_material["latest_receipt_hash"]), - }, - "negative_cases": { - "wrong_holder_signature_dry_run": wrong_holder_signature_reject, - "transfer_amount_mismatch_dry_run": transfer_amount_mismatch_reject, - "settle_wrong_holder_signature_dry_run": settle_wrong_holder_signature_reject, - "post_negative_state_still_live": post_negative_state_live.get("status") == "live", - }, - } - ) - return report - except Exception as error: - report.update( - { - "status": "failed", - "stage": stage, - "error": str(error), - "ckb_log": str(devnet.log_path), - "rpc_url": devnet.rpc_url, - } - ) - return report - finally: - if not args.keep_node: - devnet.stop() - - -def run_rwa_receipt_live(args: argparse.Namespace, contract: ReportContract) -> dict[str, Any]: - repo_root = args.repo_root.resolve() - ckb_repo = args.ckb_repo.resolve() - ckb_bin = resolve_ckb_bin(ckb_repo, args.ckb_bin) - run_dir = (args.run_dir or (repo_root / "target/novaseal-rwa-receipt-devnet-stateful-live" / str(int(time.time())))).resolve() - run_dir.mkdir(parents=True, exist_ok=True) - lifecycle_elf = run_dir / "nova-rwa-receipt-lifecycle-type.elf" - compile_contract_lifecycle(repo_root, contract, lifecycle_elf) - verifier_elf = repo_root / "proposals/novaseal/v0-mvp-skeleton/target/novaseal-btc-verifier-riscv-shell-release.elf" - if not verifier_elf.is_file(): - raise LiveAcceptanceError(f"missing verifier ELF: {verifier_elf}") - - devnet = CkbDevnet(ckb_repo, ckb_bin, run_dir) - report: dict[str, Any] = { - "schema": "novaseal-planned-profile-devnet-stateful-live-v0.1", - "profile": contract.profile, - "status": "running", - "scenario": "rwa_receipt_materialize_claim_settle", - "repo_root": str(repo_root), - "ckb_repo": str(ckb_repo), - "ckb_bin": str(ckb_bin), - "run_dir": str(run_dir), - "expected_tx_hashes": named_pointer_rows(contract.tx_hashes, "pointer"), - "required_live_checks": named_pointer_rows(contract.live_checks, "pointer"), - "required_negative_cases": named_pointer_rows(contract.negative_cases, "key"), - } - stage = "initializing" - try: - stage = "start devnet" - devnet.start() - stage = "deploy artifacts" - genesis = devnet.get_block_by_number(0) - always_dep = always_success_dep(genesis["transactions"][0]["hash"]) - verifier = deploy_code_cell(devnet, "cellscript_btc_bip340_verifier_riscv", verifier_elf.read_bytes(), always_dep) - lifecycle = deploy_code_cell(devnet, "nova_rwa_receipt_lifecycle_type", lifecycle_elf.read_bytes(), always_dep) - cell_deps = [verifier["cell_dep"], lifecycle["cell_dep"], always_dep] - provenance = stateful_provenance( - repo_root, - [ - pathlib.Path("proposals/novaseal/rwa-receipt-profile-v0/Cell.toml"), - pathlib.Path("proposals/novaseal/rwa-receipt-profile-v0/src"), - pathlib.Path("proposals/novaseal/rwa-receipt-profile-v0/schemas"), - pathlib.Path("proposals/novaseal/v0-mvp-skeleton/verifier/novaseal_btc_verifier"), - pathlib.Path("scripts/novaseal_planned_profiles_devnet_stateful_live.py"), - pathlib.Path("scripts/novaseal_devnet_stateful_live.py"), - ], - {"verifier": verifier_elf, "lifecycle": lifecycle_elf}, - ) - base = rwa_base_state("live") - - stage = "valid materialize" - materialize_material = build_rwa_material(op=OP_MATERIALIZE, base=base, old_cell=None) - materialize_header = devnet.rpc("get_tip_header") - materialize_funding = devnet.collect_spendable(STATE_CAPACITY + RECEIPT_CAPACITY + 100 * SHANNONS) - materialize_tx = build_rwa_state_event_tx( - op=OP_MATERIALIZE, - old_ref=None, - funding=materialize_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=materialize_header["hash"], - material=materialize_material, - ) - materialize_dry_run = devnet.rpc("dry_run_transaction", [materialize_tx]) - materialize_commit = devnet.submit_and_commit(materialize_tx, "RWA receipt materialize") - materialized_receipt_live = devnet.assert_live_cell( - materialize_commit["tx_hash"], - 0, - label="RWA materialized receipt", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=materialize_material["new_cell_data"], - ) - materialized_event_live = devnet.assert_live_cell( - materialize_commit["tx_hash"], - 1, - label="RWA materialized audit event", - expected_capacity=RECEIPT_CAPACITY, - expected_lock=always_success_lock(), - expected_type=None, - expected_data=materialize_material["event_data"], - ) - materialized_ref = {"tx_hash": materialize_commit["tx_hash"], "index": 0, "capacity": STATE_CAPACITY} - - stage = "negative claim wrong holder signature" - negative_header = devnet.rpc("get_tip_header") - wrong_holder_claim_material = build_rwa_material( - op=OP_CLAIM, - base=base, - old_cell=materialize_material["new_cell"], - mutate_holder_signature=True, - ) - wrong_holder_claim_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - wrong_holder_claim_tx = build_rwa_state_event_tx( - op=OP_CLAIM, - old_ref=materialized_ref, - funding=wrong_holder_claim_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=negative_header["hash"], - material=wrong_holder_claim_material, - ) - wrong_holder_claim_reject = devnet.dry_run_rejects( - wrong_holder_claim_tx, - "RWA wrong holder claim", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=BIP340_CHILD_REJECTED_ERROR_CODE, - ) - post_claim_negative_live = devnet.assert_live_cell( - materialized_ref["tx_hash"], - materialized_ref["index"], - label="post-negative RWA materialized receipt", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=materialize_material["new_cell_data"], - ) - - stage = "valid claim" - claim_header = devnet.rpc("get_tip_header") - claim_material = build_rwa_material(op=OP_CLAIM, base=base, old_cell=materialize_material["new_cell"]) - claim_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - claim_tx = build_rwa_state_event_tx( - op=OP_CLAIM, - old_ref=materialized_ref, - funding=claim_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=claim_header["hash"], - material=claim_material, - ) - claim_dry_run = devnet.rpc("dry_run_transaction", [claim_tx]) - claim_commit = devnet.submit_and_commit(claim_tx, "RWA receipt claim") - old_receipt_dead = devnet.wait_dead_cell(materialized_ref["tx_hash"], materialized_ref["index"]) - claimed_receipt_live = devnet.assert_live_cell( - claim_commit["tx_hash"], - 0, - label="RWA claimed receipt", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=claim_material["new_cell_data"], - ) - claim_event_live = devnet.assert_live_cell( - claim_commit["tx_hash"], - 1, - label="RWA claim event", - expected_capacity=RECEIPT_CAPACITY, - expected_lock=always_success_lock(), - expected_type=None, - expected_data=claim_material["event_data"], - ) - claimed_ref = {"tx_hash": claim_commit["tx_hash"], "index": 0, "capacity": STATE_CAPACITY} - - stage = "negative settlement wrong issuer signature" - settle_negative_header = devnet.rpc("get_tip_header") - wrong_issuer_settlement_material = build_rwa_material( - op=OP_RWA_SETTLE, - base=base, - old_cell=claim_material["new_cell"], - mutate_issuer_signature=True, - ) - wrong_issuer_settlement_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - wrong_issuer_settlement_tx = build_rwa_settle_tx( - old_ref=claimed_ref, - funding=wrong_issuer_settlement_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=settle_negative_header["hash"], - material=wrong_issuer_settlement_material, - ) - wrong_issuer_settlement_reject = devnet.dry_run_rejects( - wrong_issuer_settlement_tx, - "RWA wrong issuer settlement", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=BIP340_CHILD_REJECTED_ERROR_CODE, - ) - - stage = "negative settlement amount mutation" - amount_mutation_material = build_rwa_material( - op=OP_RWA_SETTLE, - base=base, - old_cell=claim_material["new_cell"], - settlement_amount_override=claim_material["new_cell"]["amount"] - 1, - ) - amount_mutation_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - amount_mutation_tx = build_rwa_settle_tx( - old_ref=claimed_ref, - funding=amount_mutation_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=settle_negative_header["hash"], - material=amount_mutation_material, - ) - amount_mutation_reject = devnet.dry_run_rejects( - amount_mutation_tx, - "RWA settlement amount mutation", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=5, - ) - post_negative_state_live = devnet.assert_live_cell( - claimed_ref["tx_hash"], - claimed_ref["index"], - label="post-negative RWA claimed receipt", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=claim_material["new_cell_data"], - ) - - stage = "valid settle" - settle_header = devnet.rpc("get_tip_header") - settle_material = build_rwa_material(op=OP_RWA_SETTLE, base=base, old_cell=claim_material["new_cell"]) - settle_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - settle_tx = build_rwa_settle_tx( - old_ref=claimed_ref, - funding=settle_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=settle_header["hash"], - material=settle_material, - ) - settle_dry_run = devnet.rpc("dry_run_transaction", [settle_tx]) - settle_commit = devnet.submit_and_commit(settle_tx, "RWA receipt settle") - old_claim_dead = devnet.wait_dead_cell(claimed_ref["tx_hash"], claimed_ref["index"]) - settlement_event_live = devnet.assert_live_cell( - settle_commit["tx_hash"], - 0, - label="RWA settlement event", - expected_capacity=RECEIPT_CAPACITY, - expected_lock=always_success_lock(), - expected_type=None, - expected_data=settle_material["event_data"], - ) - - report.update( - { - "status": "passed", - "live_devnet_rpc_executed": True, - "stateful_lifecycle_executed": True, - "ckb_log": str(devnet.log_path), - "rpc_url": devnet.rpc_url, - "artifacts": {"verifier": verifier, "lifecycle": lifecycle}, - "provenance": provenance, - "materialize": { - "dry_run_cycles": materialize_dry_run.get("cycles"), - "commit": materialize_commit, - "receipt_live": materialized_receipt_live.get("status") == "live", - "audit_event_live": materialized_event_live.get("status") == "live", - "event_hash": hex0x(materialize_material["latest_receipt_hash"]), - }, - "claim": { - "dry_run_cycles": claim_dry_run.get("cycles"), - "commit": claim_commit, - "old_receipt_not_live": old_receipt_dead.get("status") != "live", - "claimed_receipt_live": claimed_receipt_live.get("status") == "live", - "claim_event_live": claim_event_live.get("status") == "live", - "event_hash": hex0x(claim_material["latest_receipt_hash"]), - }, - "settle": { - "dry_run_cycles": settle_dry_run.get("cycles"), - "commit": settle_commit, - "old_claim_not_live": old_claim_dead.get("status") != "live", - "settlement_receipt_live": settlement_event_live.get("status") == "live", - "settlement_event_live": settlement_event_live.get("status") == "live", - "amount_conserved": settle_material["old_cell"]["amount"] == claim_material["new_cell"]["amount"], - "event_hash": hex0x(settle_material["latest_receipt_hash"]), - }, - "negative_cases": { - "wrong_holder_claim_dry_run": wrong_holder_claim_reject, - "wrong_issuer_settlement_dry_run": wrong_issuer_settlement_reject, - "amount_mutation_dry_run": amount_mutation_reject, - "post_negative_state_still_live": post_negative_state_live.get("status") == "live", - }, - } - ) - return report - except Exception as error: - report.update( - { - "status": "failed", - "stage": stage, - "error": str(error), - "ckb_log": str(devnet.log_path), - "rpc_url": devnet.rpc_url, - } - ) - return report - finally: - if not args.keep_node: - devnet.stop() - - -def run_btc_transaction_commitment_live(args: argparse.Namespace, contract: ReportContract) -> dict[str, Any]: - repo_root = args.repo_root.resolve() - ckb_repo = args.ckb_repo.resolve() - ckb_bin = resolve_ckb_bin(ckb_repo, args.ckb_bin) - run_dir = ( - args.run_dir - or (repo_root / "target/novaseal-btc-transaction-commitment-devnet-stateful-live" / str(int(time.time()))) - ).resolve() - run_dir.mkdir(parents=True, exist_ok=True) - lifecycle_elf = run_dir / "nova-btc-transaction-commitment-lifecycle-type.elf" - compile_contract_lifecycle(repo_root, contract, lifecycle_elf) - verifier_elf = repo_root / "proposals/novaseal/v0-mvp-skeleton/target/novaseal-btc-verifier-riscv-shell-release.elf" - if not verifier_elf.is_file(): - raise LiveAcceptanceError(f"missing verifier ELF: {verifier_elf}") - - devnet = CkbDevnet(ckb_repo, ckb_bin, run_dir) - report: dict[str, Any] = { - "schema": "novaseal-planned-profile-devnet-stateful-live-v0.1", - "profile": contract.profile, - "status": "running", - "scenario": "btc_transaction_commitment_initialize_then_commit", - "repo_root": str(repo_root), - "ckb_repo": str(ckb_repo), - "ckb_bin": str(ckb_bin), - "run_dir": str(run_dir), - "expected_tx_hashes": named_pointer_rows(contract.tx_hashes, "pointer"), - "required_live_checks": named_pointer_rows(contract.live_checks, "pointer"), - "required_negative_cases": named_pointer_rows(contract.negative_cases, "key"), - "btc_public_verification_scope": ( - "live CKB transition executes the BIP340 runtime verifier and binds a declared BTC txid/wtxid/output tuple; " - "SPV/indexer finality remains separate production evidence" - ), - } - stage = "initializing" - try: - stage = "start devnet" - devnet.start() - stage = "deploy artifacts" - genesis = devnet.get_block_by_number(0) - always_dep = always_success_dep(genesis["transactions"][0]["hash"]) - verifier = deploy_code_cell(devnet, "cellscript_btc_bip340_verifier_riscv", verifier_elf.read_bytes(), always_dep) - lifecycle = deploy_code_cell(devnet, "nova_btc_transaction_commitment_lifecycle_type", lifecycle_elf.read_bytes(), always_dep) - cell_deps = [verifier["cell_dep"], lifecycle["cell_dep"], always_dep] - provenance = stateful_provenance( - repo_root, - [ - pathlib.Path("proposals/novaseal/btc-transaction-commitment-profile-v0/Cell.toml"), - pathlib.Path("proposals/novaseal/btc-transaction-commitment-profile-v0/src"), - pathlib.Path("proposals/novaseal/btc-transaction-commitment-profile-v0/schemas"), - pathlib.Path("proposals/novaseal/v0-mvp-skeleton/verifier/novaseal_btc_verifier"), - pathlib.Path("scripts/novaseal_planned_profiles_devnet_stateful_live.py"), - pathlib.Path("scripts/novaseal_devnet_stateful_live.py"), - ], - {"verifier": verifier_elf, "lifecycle": lifecycle_elf}, - ) - base = btc_tx_base_state("live") - - stage = "valid initialize" - initialize_material = build_btc_tx_material(op=OP_BTC_INITIALIZE_ACTIVE_STATE, base=base, old_cell=None) - initialize_header = devnet.rpc("get_tip_header") - initialize_funding = devnet.collect_spendable(STATE_CAPACITY + 100 * SHANNONS) - initialize_tx = build_btc_tx_initialize_tx( - initialize_funding, - lifecycle["data_hash"], - cell_deps, - initialize_header["hash"], - initialize_material, - ) - initialize_dry_run = devnet.rpc("dry_run_transaction", [initialize_tx]) - initialize_commit = devnet.submit_and_commit(initialize_tx, "BTC transaction commitment initialize") - initial_state_live = devnet.assert_live_cell( - initialize_commit["tx_hash"], - 0, - label="BTC transaction active state", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=initialize_material["new_cell_data"], - ) - initial_ref = {"tx_hash": initialize_commit["tx_hash"], "index": 0, "capacity": STATE_CAPACITY} - - stage = "negative wrong committer signature" - negative_header = devnet.rpc("get_tip_header") - wrong_sig_material = build_btc_tx_material( - op=OP_BTC_COMMIT_TRANSACTION, - base=base, - old_cell=initialize_material["new_cell"], - mutate_signature=True, - ) - wrong_sig_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - wrong_sig_tx = build_btc_tx_commit_tx( - old_ref=initial_ref, - funding=wrong_sig_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=negative_header["hash"], - material=wrong_sig_material, - ) - wrong_committer_signature_reject = devnet.dry_run_rejects( - wrong_sig_tx, - "BTC transaction wrong committer signature", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=BIP340_CHILD_REJECTED_ERROR_CODE, - ) - - stage = "negative zero BTC txid" - zero_txid_material = build_btc_tx_material( - op=OP_BTC_COMMIT_TRANSACTION, - base=base, - old_cell=initialize_material["new_cell"], - zero_btc_txid=True, - ) - zero_txid_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - zero_txid_tx = build_btc_tx_commit_tx( - old_ref=initial_ref, - funding=zero_txid_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=negative_header["hash"], - material=zero_txid_material, - ) - zero_btc_txid_reject = devnet.dry_run_rejects( - zero_txid_tx, - "BTC transaction zero txid", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=5, - ) - - stage = "negative transition hash mismatch" - mismatch_material = build_btc_tx_material( - op=OP_BTC_COMMIT_TRANSACTION, - base=base, - old_cell=initialize_material["new_cell"], - transition_hash_mismatch=True, - ) - mismatch_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - mismatch_tx = build_btc_tx_commit_tx( - old_ref=initial_ref, - funding=mismatch_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=negative_header["hash"], - material=mismatch_material, - ) - transition_hash_mismatch_reject = devnet.dry_run_rejects( - mismatch_tx, - "BTC transaction transition hash mismatch", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=5, - ) - post_negative_state_live = devnet.assert_live_cell( - initial_ref["tx_hash"], - initial_ref["index"], - label="post-negative BTC transaction active state", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=initialize_material["new_cell_data"], - ) - - stage = "valid commit transaction" - commit_header = devnet.rpc("get_tip_header") - commit_material = build_btc_tx_material( - op=OP_BTC_COMMIT_TRANSACTION, - base=base, - old_cell=initialize_material["new_cell"], - ) - commit_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - commit_tx = build_btc_tx_commit_tx( - old_ref=initial_ref, - funding=commit_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=commit_header["hash"], - material=commit_material, - ) - commit_dry_run = devnet.rpc("dry_run_transaction", [commit_tx]) - commit_commit = devnet.submit_and_commit(commit_tx, "BTC transaction commitment transition") - old_state_dead = devnet.wait_dead_cell(initial_ref["tx_hash"], initial_ref["index"]) - committed_state_live = devnet.assert_live_cell( - commit_commit["tx_hash"], - 0, - label="BTC transaction committed state", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=commit_material["new_cell_data"], - ) - receipt_live = devnet.assert_live_cell( - commit_commit["tx_hash"], - 1, - label="BTC transaction commitment receipt", - expected_capacity=RECEIPT_CAPACITY, - expected_lock=always_success_lock(), - expected_type=None, - expected_data=commit_material["receipt_data"], - ) - - report.update( - { - "status": "passed", - "live_devnet_rpc_executed": True, - "stateful_lifecycle_executed": True, - "ckb_log": str(devnet.log_path), - "rpc_url": devnet.rpc_url, - "artifacts": {"verifier": verifier, "lifecycle": lifecycle}, - "provenance": provenance, - "initialize": { - "dry_run_cycles": initialize_dry_run.get("cycles"), - "commit": initialize_commit, - "state_live": initial_state_live.get("status") == "live", - "state_data_hash": hex0x(cell_data_hash(initialize_material["new_cell_data"])), - }, - "commit_transaction": { - "dry_run_cycles": commit_dry_run.get("cycles"), - "commit": commit_commit, - "old_state_not_live": old_state_dead.get("status") != "live", - "new_state_live": committed_state_live.get("status") == "live", - "receipt_live": receipt_live.get("status") == "live", - "btc_tx_tuple_bound": ( - commit_material["new_cell"]["btc_tx_commitment_hash"] == commit_material["btc_tx_commitment_hash"] - and commit_material["new_cell"]["btc_tx_commitment_hash"] != ZERO_HASH - ), - "transition_commitment_bound": commit_material["transition_commitment_hash"] == ckb_hash(base["committed_state_hash"]), - "public_btc_verification_executed": True, - "public_btc_verification_scope": "BIP340 runtime verifier execution over the signed BTC commitment intent", - "btc_tx_commitment_hash": hex0x(commit_material["btc_tx_commitment_hash"]), - "public_btc_anchor": { - "kind": "btc_transaction_commitment", - "anchor_source": BTC_ANCHOR_SOURCE_LOCAL, - "btc_txid": hex0x(commit_material["btc_txid"]), - "btc_wtxid": hex0x(commit_material["btc_wtxid"]), - "btc_output_index": commit_material["btc_output_index"], - "btc_amount_sats": commit_material["btc_amount_sats"], - "ckb_btc_commitment_hash": hex0x(commit_material["btc_tx_commitment_hash"]), - }, - "signed_intent_hash": hex0x(commit_material["signed_intent_hash"]), - "receipt_hash": hex0x(commit_material["latest_receipt_hash"]), - }, - "negative_cases": { - "wrong_committer_signature_dry_run": wrong_committer_signature_reject, - "zero_btc_txid_dry_run": zero_btc_txid_reject, - "transition_hash_mismatch_dry_run": transition_hash_mismatch_reject, - "post_negative_state_still_live": post_negative_state_live.get("status") == "live", - }, - } - ) - return report - except Exception as error: - report.update( - { - "status": "failed", - "stage": stage, - "error": str(error), - "ckb_log": str(devnet.log_path), - "rpc_url": devnet.rpc_url, - } - ) - return report - finally: - if not args.keep_node: - devnet.stop() - - -def run_btc_utxo_seal_live(args: argparse.Namespace, contract: ReportContract) -> dict[str, Any]: - repo_root = args.repo_root.resolve() - ckb_repo = args.ckb_repo.resolve() - ckb_bin = resolve_ckb_bin(ckb_repo, args.ckb_bin) - run_dir = (args.run_dir or (repo_root / "target/novaseal-btc-utxo-seal-devnet-stateful-live" / str(int(time.time())))).resolve() - run_dir.mkdir(parents=True, exist_ok=True) - lifecycle_elf = run_dir / "nova-btc-utxo-seal-lifecycle-type.elf" - compile_contract_lifecycle(repo_root, contract, lifecycle_elf) - verifier_elf = repo_root / "proposals/novaseal/v0-mvp-skeleton/target/novaseal-btc-verifier-riscv-shell-release.elf" - if not verifier_elf.is_file(): - raise LiveAcceptanceError(f"missing verifier ELF: {verifier_elf}") - - devnet = CkbDevnet(ckb_repo, ckb_bin, run_dir) - report: dict[str, Any] = { - "schema": "novaseal-planned-profile-devnet-stateful-live-v0.1", - "profile": contract.profile, - "status": "running", - "scenario": "btc_utxo_seal_initialize_then_close", - "repo_root": str(repo_root), - "ckb_repo": str(ckb_repo), - "ckb_bin": str(ckb_bin), - "run_dir": str(run_dir), - "expected_tx_hashes": named_pointer_rows(contract.tx_hashes, "pointer"), - "required_live_checks": named_pointer_rows(contract.live_checks, "pointer"), - "required_negative_cases": named_pointer_rows(contract.negative_cases, "key"), - "btc_public_verification_scope": ( - "live CKB closure executes the BIP340 runtime verifier and binds a declared BTC UTXO/spend tuple; " - "SPV/indexer spend-finality evidence remains separate production evidence" - ), - } - stage = "initializing" - try: - stage = "start devnet" - devnet.start() - stage = "deploy artifacts" - genesis = devnet.get_block_by_number(0) - always_dep = always_success_dep(genesis["transactions"][0]["hash"]) - verifier = deploy_code_cell(devnet, "cellscript_btc_bip340_verifier_riscv", verifier_elf.read_bytes(), always_dep) - lifecycle = deploy_code_cell(devnet, "nova_btc_utxo_seal_lifecycle_type", lifecycle_elf.read_bytes(), always_dep) - cell_deps = [verifier["cell_dep"], lifecycle["cell_dep"], always_dep] - provenance = stateful_provenance( - repo_root, - [ - pathlib.Path("proposals/novaseal/btc-utxo-seal-profile-v0/Cell.toml"), - pathlib.Path("proposals/novaseal/btc-utxo-seal-profile-v0/src"), - pathlib.Path("proposals/novaseal/btc-utxo-seal-profile-v0/schemas"), - pathlib.Path("proposals/novaseal/v0-mvp-skeleton/verifier/novaseal_btc_verifier"), - pathlib.Path("scripts/novaseal_planned_profiles_devnet_stateful_live.py"), - pathlib.Path("scripts/novaseal_devnet_stateful_live.py"), - ], - {"verifier": verifier_elf, "lifecycle": lifecycle_elf}, - ) - base = btc_utxo_base_state("live") - - stage = "valid initialize" - initialize_material = build_btc_utxo_material(op=OP_BTC_UTXO_INITIALIZE_ACTIVE_SEAL, base=base, old_cell=None) - initialize_header = devnet.rpc("get_tip_header") - initialize_funding = devnet.collect_spendable(STATE_CAPACITY + 100 * SHANNONS) - initialize_tx = build_btc_utxo_initialize_tx( - initialize_funding, - lifecycle["data_hash"], - cell_deps, - initialize_header["hash"], - initialize_material, - ) - initialize_dry_run = devnet.rpc("dry_run_transaction", [initialize_tx]) - initialize_commit = devnet.submit_and_commit(initialize_tx, "BTC UTXO seal initialize") - initial_state_live = devnet.assert_live_cell( - initialize_commit["tx_hash"], - 0, - label="BTC UTXO active seal", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=initialize_material["new_cell_data"], - ) - initial_ref = {"tx_hash": initialize_commit["tx_hash"], "index": 0, "capacity": STATE_CAPACITY} - - stage = "negative wrong owner signature" - negative_header = devnet.rpc("get_tip_header") - wrong_sig_material = build_btc_utxo_material( - op=OP_BTC_UTXO_CLOSE, - base=base, - old_cell=initialize_material["new_cell"], - mutate_signature=True, - ) - wrong_sig_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - wrong_sig_tx = build_btc_utxo_close_tx( - old_ref=initial_ref, - funding=wrong_sig_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=negative_header["hash"], - material=wrong_sig_material, - ) - wrong_owner_signature_reject = devnet.dry_run_rejects( - wrong_sig_tx, - "BTC UTXO wrong owner signature", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=BIP340_CHILD_REJECTED_ERROR_CODE, - ) - - stage = "negative UTXO commitment mismatch" - mismatch_material = build_btc_utxo_material( - op=OP_BTC_UTXO_CLOSE, - base=base, - old_cell=initialize_material["new_cell"], - utxo_commitment_mismatch=True, - ) - mismatch_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - mismatch_tx = build_btc_utxo_close_tx( - old_ref=initial_ref, - funding=mismatch_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=negative_header["hash"], - material=mismatch_material, - ) - utxo_commitment_mismatch_reject = devnet.dry_run_rejects( - mismatch_tx, - "BTC UTXO commitment mismatch", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=5, - ) - - stage = "negative zero spend txid" - zero_spend_material = build_btc_utxo_material( - op=OP_BTC_UTXO_CLOSE, - base=base, - old_cell=initialize_material["new_cell"], - zero_spend_txid=True, - ) - zero_spend_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - zero_spend_tx = build_btc_utxo_close_tx( - old_ref=initial_ref, - funding=zero_spend_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=negative_header["hash"], - material=zero_spend_material, - ) - zero_spend_txid_reject = devnet.dry_run_rejects( - zero_spend_tx, - "BTC UTXO zero spend txid", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=5, - ) - post_negative_state_live = devnet.assert_live_cell( - initial_ref["tx_hash"], - initial_ref["index"], - label="post-negative BTC UTXO active seal", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=initialize_material["new_cell_data"], - ) - - stage = "valid close UTXO seal" - close_header = devnet.rpc("get_tip_header") - close_material = build_btc_utxo_material( - op=OP_BTC_UTXO_CLOSE, - base=base, - old_cell=initialize_material["new_cell"], - ) - close_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - close_tx = build_btc_utxo_close_tx( - old_ref=initial_ref, - funding=close_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=close_header["hash"], - material=close_material, - ) - close_dry_run = devnet.rpc("dry_run_transaction", [close_tx]) - close_commit = devnet.submit_and_commit(close_tx, "BTC UTXO seal closure") - old_state_dead = devnet.wait_dead_cell(initial_ref["tx_hash"], initial_ref["index"]) - closed_state_live = devnet.assert_live_cell( - close_commit["tx_hash"], - 0, - label="BTC UTXO closed seal", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=close_material["new_cell_data"], - ) - receipt_live = devnet.assert_live_cell( - close_commit["tx_hash"], - 1, - label="BTC UTXO closure receipt", - expected_capacity=RECEIPT_CAPACITY, - expected_lock=always_success_lock(), - expected_type=None, - expected_data=close_material["receipt_data"], - ) - - report.update( - { - "status": "passed", - "live_devnet_rpc_executed": True, - "stateful_lifecycle_executed": True, - "ckb_log": str(devnet.log_path), - "rpc_url": devnet.rpc_url, - "artifacts": {"verifier": verifier, "lifecycle": lifecycle}, - "provenance": provenance, - "initialize": { - "dry_run_cycles": initialize_dry_run.get("cycles"), - "commit": initialize_commit, - "state_live": initial_state_live.get("status") == "live", - "state_data_hash": hex0x(cell_data_hash(initialize_material["new_cell_data"])), - }, - "close_utxo_seal": { - "dry_run_cycles": close_dry_run.get("cycles"), - "commit": close_commit, - "old_state_not_live": old_state_dead.get("status") != "live", - "new_state_live": closed_state_live.get("status") == "live", - "receipt_live": receipt_live.get("status") == "live", - "sealed_utxo_tuple_bound": ( - initialize_material["new_cell"]["sealed_utxo_commitment_hash"] == close_material["sealed_utxo_commitment_hash"] - ), - "spend_tuple_bound": close_material["closure_commitment_hash"] != ZERO_HASH, - "public_btc_spend_verification_executed": True, - "public_btc_verification_scope": "BIP340 runtime verifier execution over the signed BTC UTXO closure intent", - "sealed_utxo_commitment_hash": hex0x(close_material["sealed_utxo_commitment_hash"]), - "closure_commitment_hash": hex0x(close_material["closure_commitment_hash"]), - "public_btc_anchor": { - "kind": "btc_utxo_spend", - "anchor_source": BTC_ANCHOR_SOURCE_LOCAL, - "sealed_btc_txid": hex0x(close_material["btc_txid"]), - "sealed_btc_vout_index": close_material["btc_vout_index"], - "sealed_btc_amount_sats": close_material["btc_amount_sats"], - "script_pubkey_hash": hex0x(close_material["script_pubkey_hash"]), - "btc_txid": hex0x(close_material["spend_txid"]), - "btc_wtxid": hex0x(close_material["spend_wtxid"]), - "spend_input_index": close_material["spend_input_index"], - "ckb_btc_commitment_hash": hex0x(close_material["closure_commitment_hash"]), - "sealed_utxo_commitment_hash": hex0x(close_material["sealed_utxo_commitment_hash"]), - }, - "signed_intent_hash": hex0x(close_material["signed_intent_hash"]), - "receipt_hash": hex0x(close_material["latest_receipt_hash"]), - }, - "negative_cases": { - "wrong_owner_signature_dry_run": wrong_owner_signature_reject, - "utxo_commitment_mismatch_dry_run": utxo_commitment_mismatch_reject, - "zero_spend_txid_dry_run": zero_spend_txid_reject, - "post_negative_state_still_live": post_negative_state_live.get("status") == "live", - }, - } - ) - return report - except Exception as error: - report.update( - { - "status": "failed", - "stage": stage, - "error": str(error), - "ckb_log": str(devnet.log_path), - "rpc_url": devnet.rpc_url, - } - ) - return report - finally: - if not args.keep_node: - devnet.stop() - - -def run_dual_seal_live(args: argparse.Namespace, contract: ReportContract) -> dict[str, Any]: - repo_root = args.repo_root.resolve() - ckb_repo = args.ckb_repo.resolve() - ckb_bin = resolve_ckb_bin(ckb_repo, args.ckb_bin) - run_dir = (args.run_dir or (repo_root / "target/novaseal-dual-seal-devnet-stateful-live" / str(int(time.time())))).resolve() - run_dir.mkdir(parents=True, exist_ok=True) - lifecycle_elf = run_dir / "nova-dual-seal-lifecycle-type.elf" - compile_contract_lifecycle(repo_root, contract, lifecycle_elf) - verifier_elf = repo_root / "proposals/novaseal/v0-mvp-skeleton/target/novaseal-btc-verifier-riscv-shell-release.elf" - if not verifier_elf.is_file(): - raise LiveAcceptanceError(f"missing verifier ELF: {verifier_elf}") - - devnet = CkbDevnet(ckb_repo, ckb_bin, run_dir) - report: dict[str, Any] = { - "schema": "novaseal-planned-profile-devnet-stateful-live-v0.1", - "profile": contract.profile, - "status": "running", - "scenario": "dual_seal_initialize_then_finalize", - "repo_root": str(repo_root), - "ckb_repo": str(ckb_repo), - "ckb_bin": str(ckb_bin), - "run_dir": str(run_dir), - "expected_tx_hashes": named_pointer_rows(contract.tx_hashes, "pointer"), - "required_live_checks": named_pointer_rows(contract.live_checks, "pointer"), - "required_negative_cases": named_pointer_rows(contract.negative_cases, "key"), - "finality_scope": ( - "live CKB finalisation executes the maturity guard and both BIP340 authorities over a declared BTC closure commitment; " - "public BTC SPV/indexer closure evidence remains separate production evidence" - ), - } - stage = "initializing" - try: - stage = "start devnet" - devnet.start() - stage = "deploy artifacts" - genesis = devnet.get_block_by_number(0) - always_dep = always_success_dep(genesis["transactions"][0]["hash"]) - verifier = deploy_code_cell(devnet, "cellscript_btc_bip340_verifier_riscv", verifier_elf.read_bytes(), always_dep) - lifecycle = deploy_code_cell(devnet, "nova_dual_seal_lifecycle_type", lifecycle_elf.read_bytes(), always_dep) - cell_deps = [verifier["cell_dep"], lifecycle["cell_dep"], always_dep] - provenance = stateful_provenance( - repo_root, - [ - pathlib.Path("proposals/novaseal/dual-seal-profile-v0/Cell.toml"), - pathlib.Path("proposals/novaseal/dual-seal-profile-v0/src"), - pathlib.Path("proposals/novaseal/dual-seal-profile-v0/schemas"), - pathlib.Path("proposals/novaseal/v0-mvp-skeleton/verifier/novaseal_btc_verifier"), - pathlib.Path("scripts/novaseal_planned_profiles_devnet_stateful_live.py"), - pathlib.Path("scripts/novaseal_devnet_stateful_live.py"), - ], - {"verifier": verifier_elf, "lifecycle": lifecycle_elf}, - ) - base = dual_seal_base_state("live") - - stage = "valid initialize" - initialize_material = build_dual_seal_material(op=OP_DUAL_SEAL_INITIALIZE_ACTIVE, base=base, old_cell=None) - initialize_header = devnet.rpc("get_tip_header") - initialize_funding = devnet.collect_spendable(STATE_CAPACITY + 100 * SHANNONS) - initialize_tx = build_dual_seal_initialize_tx( - initialize_funding, - lifecycle["data_hash"], - cell_deps, - initialize_header["hash"], - initialize_material, - ) - initialize_dry_run = devnet.rpc("dry_run_transaction", [initialize_tx]) - initialize_commit = devnet.submit_and_commit(initialize_tx, "dual-seal initialize") - initial_state_live = devnet.assert_live_cell( - initialize_commit["tx_hash"], - 0, - label="dual-seal active state", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=initialize_material["new_cell_data"], - ) - initial_ref = {"tx_hash": initialize_commit["tx_hash"], "index": 0, "capacity": STATE_CAPACITY} - - stage = "negative wrong BTC owner signature" - negative_header = devnet.rpc("get_tip_header") - wrong_btc_owner_material = build_dual_seal_material( - op=OP_DUAL_SEAL_FINALIZE, - base=base, - old_cell=initialize_material["new_cell"], - mutate_btc_owner_signature=True, - ) - wrong_btc_owner_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - wrong_btc_owner_tx = build_dual_seal_finalize_tx( - old_ref=initial_ref, - funding=wrong_btc_owner_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=negative_header["hash"], - material=wrong_btc_owner_material, - ) - wrong_btc_owner_reject = devnet.dry_run_rejects( - wrong_btc_owner_tx, - "dual-seal wrong BTC owner signature", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=BIP340_CHILD_REJECTED_ERROR_CODE, - ) - - stage = "negative wrong CKB authority signature" - wrong_ckb_authority_material = build_dual_seal_material( - op=OP_DUAL_SEAL_FINALIZE, - base=base, - old_cell=initialize_material["new_cell"], - mutate_ckb_authority_signature=True, - ) - wrong_ckb_authority_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - wrong_ckb_authority_tx = build_dual_seal_finalize_tx( - old_ref=initial_ref, - funding=wrong_ckb_authority_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=negative_header["hash"], - material=wrong_ckb_authority_material, - ) - wrong_ckb_authority_reject = devnet.dry_run_rejects( - wrong_ckb_authority_tx, - "dual-seal wrong CKB authority signature", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=BIP340_CHILD_REJECTED_ERROR_CODE, - ) - - stage = "negative missing BTC closure" - missing_closure_material = build_dual_seal_material( - op=OP_DUAL_SEAL_FINALIZE, - base=base, - old_cell=initialize_material["new_cell"], - zero_btc_closure=True, - ) - missing_closure_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - missing_closure_tx = build_dual_seal_finalize_tx( - old_ref=initial_ref, - funding=missing_closure_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=negative_header["hash"], - material=missing_closure_material, - ) - missing_closure_reject = devnet.dry_run_rejects( - missing_closure_tx, - "dual-seal missing BTC closure commitment", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=5, - ) - post_negative_state_live = devnet.assert_live_cell( - initial_ref["tx_hash"], - initial_ref["index"], - label="post-negative dual-seal active state", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=initialize_material["new_cell_data"], - ) - - stage = "valid finalize" - finalize_header = devnet.rpc("get_tip_header") - finalize_material = build_dual_seal_material( - op=OP_DUAL_SEAL_FINALIZE, - base=base, - old_cell=initialize_material["new_cell"], - ) - finalize_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - finalize_tx = build_dual_seal_finalize_tx( - old_ref=initial_ref, - funding=finalize_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=finalize_header["hash"], - material=finalize_material, - ) - finalize_dry_run = devnet.rpc("dry_run_transaction", [finalize_tx]) - finalize_commit = devnet.submit_and_commit(finalize_tx, "dual-seal finalization") - old_state_dead = devnet.wait_dead_cell(initial_ref["tx_hash"], initial_ref["index"]) - receipt_live = devnet.assert_live_cell( - finalize_commit["tx_hash"], - 0, - label="dual-seal final receipt", - expected_capacity=RECEIPT_CAPACITY, - expected_lock=always_success_lock(), - expected_type=None, - expected_data=finalize_material["receipt_data"], - ) - - report.update( - { - "status": "passed", - "live_devnet_rpc_executed": True, - "stateful_lifecycle_executed": True, - "ckb_log": str(devnet.log_path), - "rpc_url": devnet.rpc_url, - "artifacts": {"verifier": verifier, "lifecycle": lifecycle}, - "provenance": provenance, - "initialize": { - "dry_run_cycles": initialize_dry_run.get("cycles"), - "commit": initialize_commit, - "state_live": initial_state_live.get("status") == "live", - "state_data_hash": hex0x(cell_data_hash(initialize_material["new_cell_data"])), - }, - "finalize_dual_seal": { - "dry_run_cycles": finalize_dry_run.get("cycles"), - "commit": finalize_commit, - "old_state_not_live": old_state_dead.get("status") != "live", - "receipt_live": receipt_live.get("status") == "live", - "btc_closure_bound": finalize_material["btc_closure_commitment_hash"] != ZERO_HASH, - "ckb_maturity_executed": base["maturity_timepoint"] == 0, - "dual_authority_executed": True, - "finality_commitment_hash": hex0x(finalize_material["finality_commitment_hash"]), - "btc_closure_commitment_hash": hex0x(finalize_material["btc_closure_commitment_hash"]), - "public_btc_anchor": { - "kind": "dual_seal_btc_closure", - "anchor_source": BTC_ANCHOR_SOURCE_LOCAL, - "sealed_btc_txid": hex0x(finalize_material["sealed_btc_txid"]), - "sealed_btc_vout_index": finalize_material["sealed_btc_vout_index"], - "sealed_btc_amount_sats": finalize_material["sealed_btc_amount_sats"], - "script_pubkey_hash": hex0x(finalize_material["script_pubkey_hash"]), - "btc_txid": hex0x(finalize_material["btc_txid"]), - "btc_wtxid": hex0x(finalize_material["btc_wtxid"]), - "spend_input_index": finalize_material["spend_input_index"], - "ckb_btc_commitment_hash": hex0x(finalize_material["btc_closure_commitment_hash"]), - "sealed_utxo_commitment_hash": hex0x(finalize_material["old_cell"]["sealed_utxo_commitment_hash"]), - }, - "signed_intent_hash": hex0x(finalize_material["signed_intent_hash"]), - "receipt_hash": hex0x(finalize_material["latest_receipt_hash"]), - }, - "negative_cases": { - "wrong_btc_owner_signature_dry_run": wrong_btc_owner_reject, - "wrong_ckb_authority_signature_dry_run": wrong_ckb_authority_reject, - "btc_closure_commitment_missing_dry_run": missing_closure_reject, - "post_negative_state_still_live": post_negative_state_live.get("status") == "live", - }, - } - ) - return report - except Exception as error: - report.update( - { - "status": "failed", - "stage": stage, - "error": str(error), - "ckb_log": str(devnet.log_path), - "rpc_url": devnet.rpc_url, - } - ) - return report - finally: - if not args.keep_node: - devnet.stop() - - -def run_fiber_candidate_live(args: argparse.Namespace, contract: ReportContract) -> dict[str, Any]: - repo_root = args.repo_root.resolve() - ckb_repo = args.ckb_repo.resolve() - ckb_bin = resolve_ckb_bin(ckb_repo, args.ckb_bin) - run_dir = (args.run_dir or (repo_root / "target/novaseal-fiber-candidate-devnet-stateful-live" / str(int(time.time())))).resolve() - run_dir.mkdir(parents=True, exist_ok=True) - lifecycle_elf = run_dir / "nova-fiber-candidate-lifecycle-type.elf" - compile_contract_lifecycle(repo_root, contract, lifecycle_elf) - verifier_elf = repo_root / "proposals/novaseal/v0-mvp-skeleton/target/novaseal-btc-verifier-riscv-shell-release.elf" - if not verifier_elf.is_file(): - raise LiveAcceptanceError(f"missing verifier ELF: {verifier_elf}") - - devnet = CkbDevnet(ckb_repo, ckb_bin, run_dir) - report: dict[str, Any] = { - "schema": "novaseal-planned-profile-devnet-stateful-live-v0.1", - "profile": contract.profile, - "status": "running", - "scenario": "fiber_candidate_initialize_then_settle", - "repo_root": str(repo_root), - "ckb_repo": str(ckb_repo), - "ckb_bin": str(ckb_bin), - "run_dir": str(run_dir), - "expected_tx_hashes": named_pointer_rows(contract.tx_hashes, "pointer"), - "required_live_checks": named_pointer_rows(contract.live_checks, "pointer"), - "required_negative_cases": named_pointer_rows(contract.negative_cases, "key"), - "fiber_execution_scope": "live CKB stateful settlement path; real Fiber node/channel execution remains a later external experiment", - } - stage = "initializing" - try: - stage = "start devnet" - devnet.start() - stage = "deploy artifacts" - genesis = devnet.get_block_by_number(0) - always_dep = always_success_dep(genesis["transactions"][0]["hash"]) - verifier = deploy_code_cell(devnet, "cellscript_btc_bip340_verifier_riscv", verifier_elf.read_bytes(), always_dep) - lifecycle = deploy_code_cell(devnet, "nova_fiber_candidate_lifecycle_type", lifecycle_elf.read_bytes(), always_dep) - cell_deps = [verifier["cell_dep"], lifecycle["cell_dep"], always_dep] - provenance = stateful_provenance( - repo_root, - [ - pathlib.Path("proposals/novaseal/fiber-candidate-profile-v0/Cell.toml"), - pathlib.Path("proposals/novaseal/fiber-candidate-profile-v0/src"), - pathlib.Path("proposals/novaseal/fiber-candidate-profile-v0/schemas"), - pathlib.Path("proposals/novaseal/v0-mvp-skeleton/verifier/novaseal_btc_verifier"), - pathlib.Path("scripts/novaseal_planned_profiles_devnet_stateful_live.py"), - pathlib.Path("scripts/novaseal_devnet_stateful_live.py"), - ], - {"verifier": verifier_elf, "lifecycle": lifecycle_elf}, - ) - base = fiber_base_state("live") - - stage = "valid initialize" - initialize_material = build_fiber_material(op=OP_FIBER_INITIALIZE_ACTIVE_CANDIDATE, base=base, old_cell=None) - initialize_header = devnet.rpc("get_tip_header") - initialize_funding = devnet.collect_spendable(STATE_CAPACITY + 100 * SHANNONS) - initialize_tx = build_fiber_initialize_tx( - initialize_funding, - lifecycle["data_hash"], - cell_deps, - initialize_header["hash"], - initialize_material, - ) - initialize_dry_run = devnet.rpc("dry_run_transaction", [initialize_tx]) - initialize_commit = devnet.submit_and_commit(initialize_tx, "Fiber candidate initialize") - initial_state_live = devnet.assert_live_cell( - initialize_commit["tx_hash"], - 0, - label="Fiber active candidate", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=initialize_material["new_cell_data"], - ) - initial_ref = {"tx_hash": initialize_commit["tx_hash"], "index": 0, "capacity": STATE_CAPACITY} - - stage = "negative wrong operator signature" - negative_header = devnet.rpc("get_tip_header") - wrong_sig_material = build_fiber_material( - op=OP_FIBER_SETTLE, - base=base, - old_cell=initialize_material["new_cell"], - mutate_signature=True, - ) - wrong_sig_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - wrong_sig_tx = build_fiber_settle_tx( - old_ref=initial_ref, - funding=wrong_sig_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=negative_header["hash"], - material=wrong_sig_material, - ) - wrong_operator_signature_reject = devnet.dry_run_rejects( - wrong_sig_tx, - "Fiber wrong operator signature", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=BIP340_CHILD_REJECTED_ERROR_CODE, - ) - - stage = "negative balance replay" - replay_material = build_fiber_material( - op=OP_FIBER_SETTLE, - base=base, - old_cell=initialize_material["new_cell"], - balance_replay=True, - ) - replay_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - replay_tx = build_fiber_settle_tx( - old_ref=initial_ref, - funding=replay_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=negative_header["hash"], - material=replay_material, - ) - balance_commitment_replay_reject = devnet.dry_run_rejects( - replay_tx, - "Fiber balance commitment replay", - expected_source="Inputs[0].Type", - expected_data_hash=lifecycle["data_hash"], - expected_error_code=5, - ) - post_negative_state_live = devnet.assert_live_cell( - initial_ref["tx_hash"], - initial_ref["index"], - label="post-negative Fiber active candidate", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=initialize_material["new_cell_data"], - ) - - stage = "valid settle" - settle_header = devnet.rpc("get_tip_header") - settle_material = build_fiber_material(op=OP_FIBER_SETTLE, base=base, old_cell=initialize_material["new_cell"]) - settle_funding = devnet.collect_spendable(RECEIPT_CAPACITY + 100 * SHANNONS) - settle_tx = build_fiber_settle_tx( - old_ref=initial_ref, - funding=settle_funding, - lifecycle_data_hash=lifecycle["data_hash"], - cell_deps=cell_deps, - header_hash=settle_header["hash"], - material=settle_material, - ) - settle_dry_run = devnet.rpc("dry_run_transaction", [settle_tx]) - settle_commit = devnet.submit_and_commit(settle_tx, "Fiber candidate settlement") - old_candidate_dead = devnet.wait_dead_cell(initial_ref["tx_hash"], initial_ref["index"]) - settled_candidate_live = devnet.assert_live_cell( - settle_commit["tx_hash"], - 0, - label="Fiber settled candidate", - expected_capacity=STATE_CAPACITY, - expected_lock=always_success_lock(), - expected_type=lifecycle_type(lifecycle["data_hash"]), - expected_data=settle_material["new_cell_data"], - ) - receipt_live = devnet.assert_live_cell( - settle_commit["tx_hash"], - 1, - label="Fiber settlement receipt", - expected_capacity=RECEIPT_CAPACITY, - expected_lock=always_success_lock(), - expected_type=None, - expected_data=settle_material["receipt_data"], - ) - - report.update( - { - "status": "passed", - "live_devnet_rpc_executed": True, - "stateful_lifecycle_executed": True, - "ckb_log": str(devnet.log_path), - "rpc_url": devnet.rpc_url, - "artifacts": {"verifier": verifier, "lifecycle": lifecycle}, - "provenance": provenance, - "initialize": { - "dry_run_cycles": initialize_dry_run.get("cycles"), - "commit": initialize_commit, - "candidate_live": initial_state_live.get("status") == "live", - "candidate_data_hash": hex0x(cell_data_hash(initialize_material["new_cell_data"])), - }, - "settle_fiber_candidate": { - "dry_run_cycles": settle_dry_run.get("cycles"), - "commit": settle_commit, - "old_candidate_not_live": old_candidate_dead.get("status") != "live", - "new_candidate_live": settled_candidate_live.get("status") == "live", - "receipt_live": receipt_live.get("status") == "live", - "balance_commitment_progressed": ( - settle_material["new_cell"]["balance_commitment_hash"] - != initialize_material["new_cell"]["balance_commitment_hash"] - ), - "fiber_execution_executed": True, - "fiber_execution_scope": "profile-level live CKB settlement path; external Fiber node experiment is still separate", - "settlement_commitment_hash": hex0x(settle_material["settlement_commitment_hash"]), - "signed_intent_hash": hex0x(settle_material["signed_intent_hash"]), - "receipt_hash": hex0x(settle_material["latest_receipt_hash"]), - }, - "negative_cases": { - "wrong_operator_signature_dry_run": wrong_operator_signature_reject, - "balance_commitment_replay_dry_run": balance_commitment_replay_reject, - "post_negative_state_still_live": post_negative_state_live.get("status") == "live", - }, - } - ) - return report - except Exception as error: - report.update( - { - "status": "failed", - "stage": stage, - "error": str(error), - "ckb_log": str(devnet.log_path), - "rpc_url": devnet.rpc_url, - } - ) - return report - finally: - if not args.keep_node: - devnet.stop() - - -def run_live(args: argparse.Namespace, contract: ReportContract) -> dict[str, Any]: - if contract.profile == "fungible-xudt": - return run_fungible_xudt_live(args, contract) - if contract.profile == "rwa-receipt": - return run_rwa_receipt_live(args, contract) - if contract.profile == "btc-transaction-commitment": - return run_btc_transaction_commitment_live(args, contract) - if contract.profile == "btc-utxo-seal": - return run_btc_utxo_seal_live(args, contract) - if contract.profile == "dual-seal": - return run_dual_seal_live(args, contract) - if contract.profile == "fiber-candidate": - return run_fiber_candidate_live(args, contract) - report = not_run_report(contract) - report["live_runner_gap"] = f"{contract.profile} live runner is not implemented yet" - return report - - -def main() -> int: - args = parse_args() - contract = REPORT_CONTRACTS[args.profile] - report = not_run_report(contract) - if args.prepare_artifacts: - prep = prepare_lifecycle_artifact(args.repo_root, contract, args.pretty) - print(json.dumps(prep, indent=2 if args.pretty else None, sort_keys=True)) - return 0 if prep["status"] == "passed" else 1 - if args.list_contract: - print(json.dumps(report, indent=2 if args.pretty else None, sort_keys=True)) - return 1 - - output = args.output or args.repo_root / contract.output - if args.live: - report = run_live(args, contract) - write_json(output, report, args.pretty) - print(f"wrote {output} status={report.get('status')} profile={args.profile}") - return 0 if report.get("status") == "passed" else 1 - - write_json(output, report, args.pretty) - print(f"wrote {output} status=not_run profile={args.profile}") - return 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/novaseal_profile_operator_fixtures.py b/scripts/novaseal_profile_operator_fixtures.py deleted file mode 100644 index bbb0e92f..00000000 --- a/scripts/novaseal_profile_operator_fixtures.py +++ /dev/null @@ -1,303 +0,0 @@ -#!/usr/bin/env python3 -"""Generate NovaSeal planned-profile operator signing fixtures. - -This report is the profile-specific companion to the core/agreement wallet -vectors. It binds each planned profile action to its fixture, current source -tree, schema set, invariant matrix, signing witnesses, display payload, and -live-report transaction skeleton where local stateful evidence exists. -""" - -from __future__ import annotations - -import argparse -import hashlib -import json -from pathlib import Path -from typing import Any - -from novaseal_btc_anchor_contract import public_btc_anchor_shape_matches_profile - - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_OUTPUT = ROOT / "target/novaseal-profile-operator-fixtures.json" - -CKB_HASH_PERSONAL = b"ckb-default-hash" -REPORT_PERSON = b"NovaProfileFxV0" -PACKED_DOMAIN = b"NovaSealProfileOperatorFixtureV0\x00" - - -def hex0x(data: bytes) -> str: - return "0x" + data.hex() - - -def ckb_blake2b256(data: bytes) -> bytes: - return hashlib.blake2b(data, digest_size=32, person=CKB_HASH_PERSONAL).digest() - - -def report_hash(label: str, value: Any) -> str: - h = hashlib.blake2b(digest_size=32, person=REPORT_PERSON) - h.update(label.encode("utf-8")) - h.update(b"\x00") - h.update(canonical_json(value)) - return hex0x(h.digest()) - - -def canonical_json(value: Any) -> bytes: - return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8") - - -def json_file_hash(path: Path) -> str: - return report_hash(path.name, json.loads(path.read_text(encoding="utf-8"))) - - -def file_set_hash(paths: list[Path]) -> str: - entries = [] - for path in sorted(paths): - if path.is_symlink() or not path.is_file(): - continue - entries.append({"path": str(path.relative_to(ROOT)), "sha256": hashlib.sha256(path.read_bytes()).hexdigest()}) - return report_hash("file_set", entries) - - -def source_tree_hash(root: Path) -> str: - return file_set_hash(sorted(root.glob("*.cell"))) - - -def schema_set_hash(root: Path) -> str: - return file_set_hash(sorted(root.glob("*.schema"))) - - -def packed_hash(type_name: str, packed: bytes) -> tuple[str, str]: - preimage = PACKED_DOMAIN + type_name.encode("utf-8") + b"\x00" + len(packed).to_bytes(4, "little") + packed - return hex0x(preimage), hex0x(ckb_blake2b256(preimage)) - - -def json_pointer(value: Any, pointer: str) -> Any: - current = value - for raw in pointer.strip("/").split("/"): - if raw == "": - continue - key = raw.replace("~1", "/").replace("~0", "~") - if isinstance(current, dict): - current = current.get(key) - else: - return None - return current - - -PROFILE_CASES: list[dict[str, Any]] = [ - { - "profile": "fungible-xudt-profile-v0", - "root": "proposals/novaseal/fungible-xudt-profile-v0", - "signed_type": "NovaFungibleXudtSignedIntentV0", - "live_report": "target/novaseal-fungible-xudt-devnet-stateful-live.json", - "cases": [ - ("issue_xudt", "issue_valid.json", ["issuer"], "/issue/commit/tx_hash"), - ("transfer_xudt", "transfer_valid.json", ["holder"], "/transfer/commit/tx_hash"), - ("settle_xudt", "settle_valid.json", ["holder"], "/settle/commit/tx_hash"), - ], - }, - { - "profile": "rwa-receipt-profile-v0", - "root": "proposals/novaseal/rwa-receipt-profile-v0", - "signed_type": "NovaRwaReceiptSignedIntentV0", - "live_report": "target/novaseal-rwa-receipt-devnet-stateful-live.json", - "cases": [ - ("materialize_rwa_receipt", "materialize_valid.json", ["issuer"], "/materialize/commit/tx_hash"), - ("claim_rwa_receipt", "claim_valid.json", ["holder"], "/claim/commit/tx_hash"), - ("settle_rwa_receipt", "settle_valid.json", ["issuer", "holder"], "/settle/commit/tx_hash"), - ], - }, - { - "profile": "btc-transaction-commitment-profile-v0", - "root": "proposals/novaseal/btc-transaction-commitment-profile-v0", - "signed_type": "NovaBtcTransactionCommitmentSignedIntentV0", - "live_report": "target/novaseal-btc-transaction-commitment-devnet-stateful-live.json", - "public_btc_anchor": "/commit_transaction/public_btc_anchor", - "cases": [ - ("commit_btc_transaction_transition", "commit_transaction_valid.json", ["committer"], "/commit_transaction/commit/tx_hash"), - ], - }, - { - "profile": "btc-utxo-seal-profile-v0", - "root": "proposals/novaseal/btc-utxo-seal-profile-v0", - "signed_type": "NovaBtcUtxoSealSignedIntentV0", - "live_report": "target/novaseal-btc-utxo-seal-devnet-stateful-live.json", - "public_btc_anchor": "/close_utxo_seal/public_btc_anchor", - "cases": [ - ("close_btc_utxo_seal", "close_utxo_seal_valid.json", ["owner"], "/close_utxo_seal/commit/tx_hash"), - ], - }, - { - "profile": "dual-seal-profile-v0", - "root": "proposals/novaseal/dual-seal-profile-v0", - "signed_type": "NovaDualSealSignedIntentV0", - "live_report": "target/novaseal-dual-seal-devnet-stateful-live.json", - "public_btc_anchor": "/finalize_dual_seal/public_btc_anchor", - "cases": [ - ("finalize_dual_seal", "finalize_dual_seal_valid.json", ["btc_owner", "ckb_authority"], "/finalize_dual_seal/commit/tx_hash"), - ], - }, - { - "profile": "fiber-candidate-profile-v0", - "root": "proposals/novaseal/fiber-candidate-profile-v0", - "signed_type": "NovaFiberCandidateSignedIntentV0", - "live_report": "target/novaseal-fiber-candidate-devnet-stateful-live.json", - "fiber_report": "target/novaseal-fiber-node-experiments.json", - "cases": [ - ("settle_fiber_candidate", "settle_fiber_candidate_valid.json", ["operator"], "/settle_fiber_candidate/commit/tx_hash"), - ], - }, -] - - -def build_case(profile: dict[str, Any], action: str, fixture_name: str, signers: list[str], tx_pointer: str | None) -> dict[str, Any]: - profile_root = ROOT / profile["root"] - fixture_path = profile_root / "fixtures" / fixture_name - fixture = json.loads(fixture_path.read_text(encoding="utf-8")) - source_hash = source_tree_hash(profile_root / "src") - schemas_hash = schema_set_hash(profile_root / "schemas") - proof_hash = json_file_hash(profile_root / "proofs/invariant_matrix.json") - live_report_path = ROOT / profile["live_report"] if profile.get("live_report") else None - live_report = json.loads(live_report_path.read_text(encoding="utf-8")) if live_report_path and live_report_path.is_file() else None - fiber_report_path = ROOT / profile["fiber_report"] if profile.get("fiber_report") else None - fiber_report = json.loads(fiber_report_path.read_text(encoding="utf-8")) if fiber_report_path and fiber_report_path.is_file() else None - live_tx_hash = json_pointer(live_report, tx_pointer) if live_report and tx_pointer else None - public_btc_anchor = json_pointer(live_report, profile.get("public_btc_anchor")) if live_report and profile.get("public_btc_anchor") else None - public_btc_required = profile["profile"] in { - "btc-transaction-commitment-profile-v0", - "btc-utxo-seal-profile-v0", - "dual-seal-profile-v0", - } - - display = { - "profile": profile["profile"], - "action": action, - "fixture": fixture_name, - "fixture_description": fixture.get("description"), - "signers": signers, - "signed_type": profile["signed_type"], - "source_tree_hash": source_hash, - "schema_set_hash": schemas_hash, - "proof_matrix_hash": proof_hash, - "live_devnet_tx_hash": live_tx_hash, - "public_btc_anchor": public_btc_anchor, - "external_boundary": profile.get("external_boundary"), - } - witness_shape = { - "signed_intent": profile["signed_type"], - "signature_witnesses": [f"{signer}_sig" for signer in signers], - "fixture_expected": fixture.get("expected"), - "live_report": profile.get("live_report"), - "fiber_report": profile.get("fiber_report"), - } - intent_body = { - "schema": "novaseal-profile-operator-intent-v0.1", - "profile": profile["profile"], - "action": action, - "fixture": fixture_name, - "fixture_hash": json_file_hash(fixture_path), - "source_tree_hash": source_hash, - "schema_set_hash": schemas_hash, - "proof_matrix_hash": proof_hash, - "signers": signers, - "witness_shape_hash": report_hash("witness_shape", witness_shape), - "live_report_hash": report_hash(profile["live_report"], live_report) if live_report is not None else None, - "fiber_report_hash": report_hash(profile["fiber_report"], fiber_report) if fiber_report is not None else None, - "live_tx_hash": live_tx_hash, - "public_btc_anchor": public_btc_anchor, - "external_boundary": profile.get("external_boundary"), - } - packed = canonical_json(intent_body) - preimage, digest = packed_hash(profile["signed_type"], packed) - tx_skeleton = { - "profile": profile["profile"], - "action": action, - "fixture": fixture_name, - "live_tx_hash": live_tx_hash, - "source_tree_hash": source_hash, - "witness_shape_hash": intent_body["witness_shape_hash"], - "public_btc_anchor": public_btc_anchor, - } - status_checks = { - "fixture_expected_accepted": fixture.get("expected") == "accepted", - "fixture_action_matches": fixture.get("action") == action, - "live_status_passed_or_external_boundary": bool(live_report and live_report.get("status") == "passed") - or profile.get("external_boundary") == "package_fixture_only_external_btc_and_ckb_finality_required", - "fiber_execution_passed_when_required": not fiber_report - or json_pointer(fiber_report, "/workflow_coverage/all_required_workflows_executed_passed") is True, - "public_btc_anchor_present_when_required": (not public_btc_required) or bool(public_btc_anchor), - "public_btc_anchor_shape_matches_profile": (not public_btc_required) - or public_btc_anchor_shape_matches_profile(profile["profile"], public_btc_anchor), - } - status = "passed" if all(status_checks.values()) else "failed" - return { - "profile": profile["profile"], - "action": action, - "fixture": fixture_name, - "status": status, - "checks": status_checks, - "signers": signers, - "signed_type": profile["signed_type"], - "signed_intent_hash": digest, - "signed_intent_hash_preimage_hex": preimage, - "signed_intent_body_hex": hex0x(packed), - "bip340_message_hash": digest, - "witness_shape_hash": intent_body["witness_shape_hash"], - "tx_skeleton_hash": report_hash("tx_skeleton", tx_skeleton), - "fixture_hash": intent_body["fixture_hash"], - "source_tree_hash": source_hash, - "schema_set_hash": schemas_hash, - "proof_matrix_hash": proof_hash, - "live_report_hash": intent_body["live_report_hash"], - "fiber_report_hash": intent_body["fiber_report_hash"], - "live_devnet_tx_hash": live_tx_hash, - "public_btc_anchor": public_btc_anchor, - "wallet_display": display, - "operator_witness_shape": witness_shape, - } - - -def build_report() -> dict[str, Any]: - cases = [] - for profile in PROFILE_CASES: - for action, fixture_name, signers, tx_pointer in profile["cases"]: - cases.append(build_case(profile, action, fixture_name, signers, tx_pointer)) - profiles = sorted({case["profile"] for case in cases}) - status = "passed" if cases and all(case["status"] == "passed" for case in cases) else "failed" - return { - "schema": "novaseal-profile-operator-fixtures-v0.1", - "status": status, - "hash_algorithm": "ckb_blake2b_256", - "signature_scheme": "BIP340 Schnorr over 32-byte signed profile intent hash", - "fixture_boundary": "wallet/service fixtures bind declared profile actions to source, schema, invariant, witness, and live-report evidence; external BTC/CellDep/TCB attestations remain separate production gates", - "summary": { - "total": len(cases), - "matched": len([case for case in cases if case["status"] == "passed"]), - "profile_count": len(profiles), - "profiles": profiles, - }, - "profiles": profiles, - "cases": cases, - } - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) - parser.add_argument("--pretty", action="store_true") - args = parser.parse_args() - - report = build_report() - args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") - if args.pretty: - print( - f"wrote {args.output} status={report['status']} " - f"profiles={report['summary']['profile_count']} cases={report['summary']['total']}" - ) - return 0 if report["status"] == "passed" else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/novaseal_service_builder_fixtures.py b/scripts/novaseal_service_builder_fixtures.py deleted file mode 100644 index a27d2420..00000000 --- a/scripts/novaseal_service_builder_fixtures.py +++ /dev/null @@ -1,212 +0,0 @@ -#!/usr/bin/env python3 -"""Generate NovaSeal service-builder fixtures from operator fixtures. - -The report models the wallet/service request and response boundary for every -planned NovaSeal profile action. It intentionally remains a deterministic JSON -builder fixture, not a claim that public BTC SPV, public CellDep, or external -TCB attestations have been collected. -""" - -from __future__ import annotations - -import argparse -import hashlib -import json -from pathlib import Path -from typing import Any - -from novaseal_btc_anchor_contract import public_btc_anchor_shape_matches_profile - - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_OPERATOR_FIXTURES = ROOT / "target/novaseal-profile-operator-fixtures.json" -DEFAULT_OUTPUT = ROOT / "target/novaseal-service-builder-fixtures.json" - -REPORT_PERSON = b"NovaSvcBuildV0" - - -def hex0x(data: bytes) -> str: - return "0x" + data.hex() - - -def canonical_json(value: Any) -> bytes: - return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8") - - -def report_hash(label: str, value: Any) -> str: - h = hashlib.blake2b(digest_size=32, person=REPORT_PERSON) - h.update(label.encode("utf-8")) - h.update(b"\x00") - h.update(canonical_json(value)) - return hex0x(h.digest()) - - -def is_hex32(value: Any) -> bool: - if not isinstance(value, str) or not value.startswith("0x") or len(value) != 66: - return False - try: - raw = bytes.fromhex(value[2:]) - except ValueError: - return False - return any(byte != 0 for byte in raw) - - -def external_inputs(profile: str) -> list[str]: - required = ["public_shared_cell_dep_attestation", "external_bip340_tcb_review_attestation"] - if profile in {"btc-transaction-commitment-profile-v0", "btc-utxo-seal-profile-v0", "dual-seal-profile-v0"}: - required.append("public_btc_spv_evidence") - if profile == "rwa-receipt-profile-v0": - required.append("legal_registry_review_evidence") - return required - - -def build_case(operator_case: dict[str, Any]) -> dict[str, Any]: - profile = operator_case["profile"] - action = operator_case["action"] - fixture = operator_case["fixture"] - signers = operator_case["signers"] - operator_fixture_hash = report_hash("operator_case", operator_case) - request = { - "schema": "novaseal-service-builder-request-v0.1", - "builder_name": "novaseal-profile-service-builder-v0", - "profile": profile, - "action": action, - "fixture": fixture, - "idempotency_key": report_hash("idempotency", [profile, action, fixture, operator_case["signed_intent_hash"]]), - "operator_fixture_hash": operator_fixture_hash, - "signers": signers, - "required_profile_inputs": { - "source_tree_hash": operator_case["source_tree_hash"], - "schema_set_hash": operator_case["schema_set_hash"], - "proof_matrix_hash": operator_case["proof_matrix_hash"], - "fixture_hash": operator_case["fixture_hash"], - }, - "required_live_inputs": { - "live_report_hash": operator_case.get("live_report_hash"), - "live_devnet_tx_hash": operator_case.get("live_devnet_tx_hash"), - "fiber_report_hash": operator_case.get("fiber_report_hash"), - "public_btc_anchor": operator_case.get("public_btc_anchor"), - }, - "production_external_inputs": external_inputs(profile), - } - tx_skeleton = { - "schema": "novaseal-service-builder-tx-skeleton-v0.1", - "profile": profile, - "action": action, - "fixture": fixture, - "builder_name": request["builder_name"], - "operator_fixture_hash": operator_fixture_hash, - "signed_intent_hash": operator_case["signed_intent_hash"], - "witness_shape_hash": operator_case["witness_shape_hash"], - "source_tree_hash": operator_case["source_tree_hash"], - "live_devnet_tx_hash": operator_case.get("live_devnet_tx_hash"), - "public_btc_anchor": operator_case.get("public_btc_anchor"), - } - response = { - "schema": "novaseal-service-builder-response-v0.1", - "builder_name": request["builder_name"], - "profile": profile, - "action": action, - "fixture": fixture, - "service_queue_key": report_hash("service_queue", [profile, action, fixture, request["idempotency_key"]]), - "tx_skeleton_hash": report_hash("tx_skeleton", tx_skeleton), - "witness_shape_hash": operator_case["witness_shape_hash"], - "signed_intent_hash": operator_case["signed_intent_hash"], - "bip340_message_hash": operator_case["bip340_message_hash"], - "receipt_binding_hash": report_hash( - "receipt_binding", - { - "profile": profile, - "action": action, - "fixture": fixture, - "signed_intent_hash": operator_case["signed_intent_hash"], - "tx_skeleton_hash": report_hash("tx_skeleton", tx_skeleton), - "operator_fixture_hash": operator_fixture_hash, - }, - ), - "builder_trace_hash": report_hash("builder_trace", {"request": request, "tx_skeleton": tx_skeleton}), - } - checks = { - "operator_case_passed": operator_case.get("status") == "passed", - "request_hashes_present": all(is_hex32(value) for value in request["required_profile_inputs"].values()), - "signed_intent_hash_bound": is_hex32(response["signed_intent_hash"]) - and response["signed_intent_hash"] == operator_case["signed_intent_hash"], - "bip340_message_hash_bound": is_hex32(response["bip340_message_hash"]) - and response["bip340_message_hash"] == operator_case["bip340_message_hash"], - "witness_shape_hash_bound": is_hex32(response["witness_shape_hash"]) - and response["witness_shape_hash"] == operator_case["witness_shape_hash"], - "tx_skeleton_hash_present": is_hex32(response["tx_skeleton_hash"]), - "receipt_binding_hash_present": is_hex32(response["receipt_binding_hash"]), - "service_queue_key_present": is_hex32(response["service_queue_key"]), - "external_requirements_named": bool(request["production_external_inputs"]), - "public_btc_anchor_bound_when_required": ( - "public_btc_spv_evidence" not in request["production_external_inputs"] - or bool(request["required_live_inputs"].get("public_btc_anchor")) - ), - "public_btc_anchor_shape_matches_profile": ( - "public_btc_spv_evidence" not in request["production_external_inputs"] - or public_btc_anchor_shape_matches_profile(profile, request["required_live_inputs"].get("public_btc_anchor")) - ), - "tx_skeleton_public_btc_anchor_shape_matches_profile": ( - "public_btc_spv_evidence" not in request["production_external_inputs"] - or public_btc_anchor_shape_matches_profile(profile, tx_skeleton.get("public_btc_anchor")) - ), - } - return { - "profile": profile, - "action": action, - "fixture": fixture, - "status": "passed" if all(checks.values()) else "failed", - "checks": checks, - "builder_name": request["builder_name"], - "operator_fixture_hash": operator_fixture_hash, - "signers": signers, - "request": request, - "response": response, - "tx_skeleton": tx_skeleton, - } - - -def build_report(operator_fixtures: dict[str, Any]) -> dict[str, Any]: - cases = [build_case(case) for case in operator_fixtures.get("cases", [])] - profiles = sorted({case["profile"] for case in cases}) - status = "passed" if cases and all(case["status"] == "passed" for case in cases) else "failed" - return { - "schema": "novaseal-service-builder-fixtures-v0.1", - "status": status, - "builder_name": "novaseal-profile-service-builder-v0", - "source_operator_fixture_report": str(DEFAULT_OPERATOR_FIXTURES.relative_to(ROOT)), - "source_operator_fixture_report_hash": report_hash("operator_report", operator_fixtures), - "fixture_boundary": "builder fixtures model reproducible service request/response hashes for local profile evidence; public BTC SPV, public CellDep, external TCB, and legal registry evidence remain production inputs", - "summary": { - "total": len(cases), - "matched": len([case for case in cases if case["status"] == "passed"]), - "profile_count": len(profiles), - "profiles": profiles, - }, - "profiles": profiles, - "cases": cases, - } - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--operator-fixtures", type=Path, default=DEFAULT_OPERATOR_FIXTURES) - parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) - parser.add_argument("--pretty", action="store_true") - args = parser.parse_args() - - operator_fixtures = json.loads(args.operator_fixtures.read_text(encoding="utf-8")) - report = build_report(operator_fixtures) - args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") - if args.pretty: - print( - f"wrote {args.output} status={report['status']} " - f"profiles={report['summary']['profile_count']} cases={report['summary']['total']}" - ) - return 0 if report["status"] == "passed" else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/novaseal_wallet_signing_vectors.py b/scripts/novaseal_wallet_signing_vectors.py deleted file mode 100644 index bc31581a..00000000 --- a/scripts/novaseal_wallet_signing_vectors.py +++ /dev/null @@ -1,409 +0,0 @@ -#!/usr/bin/env python3 -"""Generate NovaSeal wallet signing vectors. - -The output is a wallet-facing companion to the packed canonical vectors. It -freezes the exact 32-byte BIP340 message, the typed preimage, and the -fixed-width Molecule-equivalent byte layout a wallet must display/sign. -""" - -from __future__ import annotations - -import argparse -import hashlib -import json -from pathlib import Path -from typing import Any - - -ROOT = Path(__file__).resolve().parents[1] -CORE_ROOT = ROOT / "proposals/novaseal/v0-mvp-skeleton" -AGREEMENT_ROOT = ROOT / "proposals/novaseal/agreement-profile-v0" -DEFAULT_CORE_VECTORS = CORE_ROOT / "target/novaseal-canonical-vectors.json" -DEFAULT_OUTPUT = ROOT / "target/novaseal-wallet-signing-vectors.json" - -PACKED_HASH_DOMAIN = b"CellScriptPackedHashV0\x00" -CKB_HASH_PERSONAL = b"ckb-default-hash" -VECTOR_PERSON = b"NovaSealWalletV0" -ZERO_HASH = "0x" + "00" * 32 - -CKB = 100_000_000 -BORROWER_AUTHORITY = "0x" + "11" * 32 -LENDER_AUTHORITY = "0x" + "22" * 32 -COLLATERAL_AMOUNT = 1_000 * CKB -PRINCIPAL_AMOUNT = 700 * CKB -FIXED_FEE_AMOUNT = 30 * CKB -EXPIRY_TIMEPOINT = 200 - - -def hex0x(data: bytes) -> str: - return "0x" + data.hex() - - -def ckb_blake2b256(data: bytes) -> bytes: - return hashlib.blake2b(data, digest_size=32, person=CKB_HASH_PERSONAL).digest() - - -def stable_hash(label: str, value: Any) -> str: - h = hashlib.blake2b(digest_size=32, person=VECTOR_PERSON) - h.update(label.encode("utf-8")) - h.update(b"\x00") - h.update(str(value).encode("utf-8")) - return hex0x(h.digest()) - - -def as_bytes32(value: str) -> bytes: - raw = value[2:] if value.startswith("0x") else value - data = bytes.fromhex(raw) - if len(data) != 32: - raise ValueError(f"expected Byte32, got {len(data)} bytes") - return data - - -def uint(value: int, size: int) -> bytes: - if value < 0 or value >= 1 << (size * 8): - raise ValueError(f"{value} does not fit u{size * 8}") - return value.to_bytes(size, "little") - - -def packed_hash_preimage(type_name: str, packed_bytes: bytes) -> bytes: - return PACKED_HASH_DOMAIN + type_name.encode("utf-8") + b"\x00" + len(packed_bytes).to_bytes(4, "little") + packed_bytes - - -def packed_hash(type_name: str, packed_bytes: bytes) -> tuple[str, str]: - preimage = packed_hash_preimage(type_name, packed_bytes) - return hex0x(preimage), hex0x(ckb_blake2b256(preimage)) - - -def field_map(encoded: dict[str, Any]) -> dict[str, Any]: - result: dict[str, Any] = {} - for field in encoded.get("fields", []): - if "value" in field: - result[field["name"]] = field["value"] - elif field.get("type") in {"Byte32", "Hash"}: - result[field["name"]] = field["hex"] - elif field.get("type") == "OutPoint": - components = {component["name"]: component for component in field.get("components", [])} - result[field["name"]] = { - "tx_hash": components.get("tx_hash", {}).get("hex"), - "index": components.get("index", {}).get("value"), - } - elif "nested" in field: - result[field["name"]] = field_map(field["nested"]) - return result - - -def wallet_record( - *, - suite: str, - name: str, - action: str, - signers: list[str], - signed_intent: dict[str, Any], - display: dict[str, Any], - expected_receipt_hash: str, -) -> dict[str, Any]: - preimage = signed_intent["hash_preimage_hex"] - message = signed_intent["digest_blake2b_256"] - recomputed = hex0x(ckb_blake2b256(bytes.fromhex(preimage[2:]))) - status = "passed" if recomputed == message else "failed" - return { - "suite": suite, - "name": name, - "action": action, - "signers": signers, - "status": status, - "bip340_message_hash": message, - "signed_type": signed_intent["type"], - "signed_intent_packed_hex": signed_intent["hex"], - "signed_intent_hash_preimage_hex": preimage, - "molecule_fixed_equivalent_hex": signed_intent["hex"], - "molecule_profile": "fixed-width CellScript schema; equivalent to declared-field concatenation for these v0 structs", - "expected_receipt_hash": expected_receipt_hash, - "wallet_display": display, - } - - -def core_vectors(path: Path) -> list[dict[str, Any]]: - payload = json.loads(path.read_text(encoding="utf-8")) - vectors: list[dict[str, Any]] = [] - for vector in payload.get("vectors", []): - encoded = vector.get("encoded", {}) - resolved = encoded.get("resolved") - if not isinstance(resolved, dict): - continue - signed_intent = resolved.get("signed_intent") - if not isinstance(signed_intent, dict): - signed_intent = resolved.get("resolved_intent") or encoded.get("intent") - if not isinstance(signed_intent, dict): - continue - if not signed_intent.get("hash_preimage_hex") and isinstance(signed_intent.get("hex"), str): - preimage, digest = packed_hash(signed_intent.get("type", "NovaSealIntentV0"), bytes.fromhex(signed_intent["hex"][2:])) - signed_intent = {**signed_intent, "hash_preimage_hex": preimage, "digest_blake2b_256": digest} - if signed_intent.get("fields") and "nested" in signed_intent["fields"][0]: - core = field_map(signed_intent["fields"][0]["nested"]) - else: - core = field_map(signed_intent) - old_cell = field_map(encoded.get("old_cell", {})) - display = { - "protocol": "NovaSeal Core v0", - "fixture": vector.get("fixture"), - "action": core.get("action"), - "terminal_path": core.get("terminal_path"), - "btc_authority_hash": old_cell.get("btc_authority_hash"), - "btc_authority_hash_semantics": "legacy field name; for NovaSeal v0 this equals the 32-byte BIP340 x-only public key and is not a CKB recipient lock hash or payout script identifier", - "old_cell": core.get("old_cell"), - "old_state_hash": core.get("old_state_hash"), - "new_state_hash": core.get("new_state_hash"), - "old_nonce": core.get("old_nonce"), - "new_nonce": core.get("new_nonce"), - "expiry": core.get("expiry"), - "policy_hash": core.get("policy_hash"), - } - vectors.append( - wallet_record( - suite="novaseal-core-v0", - name=str(vector.get("name") or vector.get("fixture")), - action="key_auth_transition", - signers=["btc_authority"], - signed_intent=signed_intent, - display=display, - expected_receipt_hash=field_map(signed_intent).get("expected_receipt_hash") - or resolved.get("resolved_receipt_hash") - or vector.get("hashes", {}).get("resolved_receipt_hash"), - ) - ) - return vectors - - -def encode_native_payout(action: int, role: int, recipient: str, amount: int, terms_hash: str, agreement_id: str, nonce: int) -> dict[str, Any]: - packed = b"".join( - [ - uint(action, 1), - as_bytes32(agreement_id), - uint(role, 1), - as_bytes32(recipient), - uint(0, 1), - as_bytes32(ZERO_HASH), - uint(amount, 8), - as_bytes32(terms_hash), - uint(nonce, 8), - ] - ) - preimage, digest = packed_hash("NativeCkbPayoutV0", packed) - return {"type": "NativeCkbPayoutV0", "hex": hex0x(packed), "hash_preimage_hex": preimage, "digest_blake2b_256": digest} - - -def encode_agreement_intent_core( - action: int, - agreement_id: str, - terms_hash: str, - old_status: int, - new_status: int, - old_nonce: int, - new_nonce: int, - terminal_amount: int, - payout_commitment_hash: str, -) -> dict[str, Any]: - packed = b"".join( - [ - uint(action, 1), - as_bytes32(agreement_id), - as_bytes32(terms_hash), - as_bytes32(BORROWER_AUTHORITY), - as_bytes32(LENDER_AUTHORITY), - uint(old_status, 1), - uint(new_status, 1), - uint(old_nonce, 8), - uint(new_nonce, 8), - uint(terminal_amount, 8), - as_bytes32(payout_commitment_hash), - uint(EXPIRY_TIMEPOINT, 8), - ] - ) - preimage, digest = packed_hash("NovaAgreementIntentCoreV0", packed) - return {"type": "NovaAgreementIntentCoreV0", "hex": hex0x(packed), "hash_preimage_hex": preimage, "digest_blake2b_256": digest} - - -def encode_canonical_envelope( - action: int, - agreement_id: str, - terms_hash: str, - old_state_commitment: str, - new_state_commitment: str, - old_nonce: int, - new_nonce: int, - authority_hash: str, - profile_body_hash: str, - payout_commitment_hash: str, -) -> dict[str, Any]: - packed = b"".join( - [ - as_bytes32(agreement_id), - as_bytes32(terms_hash), - uint(action, 1), - uint(action, 1), - as_bytes32(agreement_id), - as_bytes32(old_state_commitment), - as_bytes32(new_state_commitment), - uint(old_nonce, 8), - uint(new_nonce, 8), - uint(EXPIRY_TIMEPOINT, 8), - as_bytes32(authority_hash), - as_bytes32(profile_body_hash), - as_bytes32(payout_commitment_hash), - ] - ) - preimage, digest = packed_hash("NovaSealCanonicalEnvelopeV0", packed) - return {"type": "NovaSealCanonicalEnvelopeV0", "hex": hex0x(packed), "hash_preimage_hex": preimage, "digest_blake2b_256": digest} - - -def encode_agreement_receipt_commitment( - action: int, - agreement_id: str, - terms_hash: str, - old_status: int, - new_status: int, - terminal_amount: int, - old_nonce: int, - new_nonce: int, - intent_core_hash: str, - payout_commitment_hash: str, -) -> dict[str, Any]: - packed = b"".join( - [ - uint(action, 1), - as_bytes32(agreement_id), - uint(old_status, 1), - uint(new_status, 1), - as_bytes32(terms_hash), - as_bytes32(BORROWER_AUTHORITY), - as_bytes32(LENDER_AUTHORITY), - uint(terminal_amount, 8), - uint(old_nonce, 8), - uint(new_nonce, 8), - as_bytes32(intent_core_hash), - as_bytes32(payout_commitment_hash), - ] - ) - preimage, digest = packed_hash("NovaAgreementReceiptCommitmentV0", packed) - return {"type": "NovaAgreementReceiptCommitmentV0", "hex": hex0x(packed), "hash_preimage_hex": preimage, "digest_blake2b_256": digest} - - -def encode_agreement_signed_intent(core: dict[str, Any], canonical_envelope_hash: str, expected_receipt_hash: str) -> dict[str, Any]: - packed = bytes.fromhex(core["hex"][2:]) + as_bytes32(canonical_envelope_hash) + as_bytes32(expected_receipt_hash) - preimage, digest = packed_hash("NovaAgreementSignedIntentV0", packed) - return {"type": "NovaAgreementSignedIntentV0", "hex": hex0x(packed), "hash_preimage_hex": preimage, "digest_blake2b_256": digest} - - -def agreement_case(name: str, action: int, old_status: int, new_status: int, old_nonce: int, new_nonce: int, terminal_amount: int, signers: list[str]) -> dict[str, Any]: - agreement_id = stable_hash("agreement_id", "mvb-starter-v0") - terms_hash = stable_hash("terms_hash", "ckb-ckb-fixed-fee-v0") - if action == 0: - payout_hash = encode_native_payout(action, 0, BORROWER_AUTHORITY, PRINCIPAL_AMOUNT, terms_hash, agreement_id, 0)[ - "digest_blake2b_256" - ] - elif action == 1: - lender = encode_native_payout(action, 1, LENDER_AUTHORITY, PRINCIPAL_AMOUNT + FIXED_FEE_AMOUNT, terms_hash, agreement_id, 1) - borrower = encode_native_payout(action, 2, BORROWER_AUTHORITY, COLLATERAL_AMOUNT, terms_hash, agreement_id, 1) - packed = as_bytes32(lender["digest_blake2b_256"]) + as_bytes32(borrower["digest_blake2b_256"]) - _, payout_hash = packed_hash("RepayPayoutCommitmentV0", packed) - else: - payout_hash = encode_native_payout(action, 3, LENDER_AUTHORITY, COLLATERAL_AMOUNT, terms_hash, agreement_id, 1)[ - "digest_blake2b_256" - ] - core = encode_agreement_intent_core( - action, agreement_id, terms_hash, old_status, new_status, old_nonce, new_nonce, terminal_amount, payout_hash - ) - receipt = encode_agreement_receipt_commitment( - action, agreement_id, terms_hash, old_status, new_status, terminal_amount, old_nonce, new_nonce, core["digest_blake2b_256"], payout_hash - ) - authority_hash = LENDER_AUTHORITY if action == 2 else BORROWER_AUTHORITY - canonical = encode_canonical_envelope( - action, - agreement_id, - terms_hash, - ZERO_HASH if action == 0 else stable_hash("previous_receipt_hash", "agreement-active-v0"), - receipt["digest_blake2b_256"], - old_nonce, - new_nonce, - authority_hash, - core["digest_blake2b_256"], - payout_hash, - ) - signed = encode_agreement_signed_intent(core, canonical["digest_blake2b_256"], receipt["digest_blake2b_256"]) - action_name = {0: "originate_agreement", 1: "repay_before_expiry", 2: "claim_after_expiry"}[action] - return wallet_record( - suite="novaseal-agreement-profile-v0", - name=name, - action=action_name, - signers=signers, - signed_intent=signed, - expected_receipt_hash=receipt["digest_blake2b_256"], - display={ - "protocol": "NovaSeal Agreement Profile v0", - "action": action_name, - "agreement_id": agreement_id, - "terms_hash": terms_hash, - "borrower_authority_hash": BORROWER_AUTHORITY, - "lender_authority_hash": LENDER_AUTHORITY, - "old_status": old_status, - "new_status": new_status, - "old_nonce": old_nonce, - "new_nonce": new_nonce, - "terminal_amount_shannons": terminal_amount, - "canonical_envelope_hash": canonical["digest_blake2b_256"], - "payout_commitment_hash": payout_hash, - "expiry_timepoint": EXPIRY_TIMEPOINT, - }, - ) - - -def agreement_vectors() -> list[dict[str, Any]]: - return [ - agreement_case("originate_valid", 0, 0, 1, 0, 0, PRINCIPAL_AMOUNT, ["borrower", "lender"]), - agreement_case("repay_before_expiry_valid", 1, 1, 2, 0, 1, PRINCIPAL_AMOUNT + FIXED_FEE_AMOUNT, ["borrower"]), - agreement_case("claim_after_expiry_valid", 2, 1, 3, 0, 1, COLLATERAL_AMOUNT, ["lender"]), - ] - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--core-vectors", type=Path, default=DEFAULT_CORE_VECTORS) - parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) - parser.add_argument("--pretty", action="store_true") - args = parser.parse_args() - - vectors = core_vectors(args.core_vectors) + agreement_vectors() - status = "passed" if vectors and all(vector["status"] == "passed" for vector in vectors) else "failed" - payload = { - "schema": "novaseal-wallet-signing-vectors-v0.1", - "status": status, - "hash_algorithm": "ckb_blake2b_256", - "signature_scheme": "BIP340 Schnorr over 32-byte signed intent hash", - "authority_identifier_semantics": { - "btc_authority_hash": "legacy-named NovaSeal core field; in v0 it equals the 32-byte BIP340 x-only public key", - "not_ckb_recipient_lock_hash": True, - "not_payout_script_identifier": True, - "agreement_payout_mapping": "profile/builder surface; payout recipients must not be inferred from the core BTC authority field", - }, - "molecule_alignment": "fixed-width v0 structs use declared-field little-endian concatenation; no dynamic tables/vectors in these signing objects", - "summary": { - "total": len(vectors), - "core_vectors": len([vector for vector in vectors if vector["suite"] == "novaseal-core-v0"]), - "agreement_vectors": len([vector for vector in vectors if vector["suite"] == "novaseal-agreement-profile-v0"]), - "matched": len([vector for vector in vectors if vector["status"] == "passed"]), - }, - "vectors": vectors, - } - args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") - if args.pretty: - print( - f"wrote {args.output} status={payload['status']} total={payload['summary']['total']} " - f"core={payload['summary']['core_vectors']} agreement={payload['summary']['agreement_vectors']}" - ) - return 0 if status == "passed" else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/validate_cellscript_tooling_release.py b/scripts/validate_cellscript_tooling_release.py deleted file mode 100755 index 8eeecc9d..00000000 --- a/scripts/validate_cellscript_tooling_release.py +++ /dev/null @@ -1,364 +0,0 @@ -#!/usr/bin/env python3 -"""Validate CellScript package/LSP/tooling release boundaries.""" - -from __future__ import annotations - -import json -import re -import tomllib -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[1] - - -def read(path: str) -> str: - return (ROOT / path).read_text(encoding="utf-8") - - -def require(condition: bool, message: str) -> None: - if not condition: - raise SystemExit(f"invalid CellScript tooling release boundary: {message}") - - -def require_contains(path: str, tokens: list[str]) -> None: - text = read(path) - for token in tokens: - require(token in text, f"{path} is missing {token!r}") - - -def main() -> int: - cargo_toml = read("Cargo.toml") - cargo = tomllib.loads(cargo_toml) - cargo_lock = tomllib.loads(read("Cargo.lock")) - package_json = json.loads(read("editors/vscode-cellscript/package.json")) - changelog = read("CHANGELOG.md") - extension_changelog = read("editors/vscode-cellscript/CHANGELOG.md") - extension_readme = read("editors/vscode-cellscript/README.md") - - crate_version = cargo["package"]["version"] - lock_versions = [ - package.get("version") - for package in cargo_lock.get("package", []) - if package.get("name") == "cellscript" - ] - release_surface = ".".join(crate_version.split("-", 1)[0].split(".")[:2]) - changelog_match = re.search(r"^## ([0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?) - ", changelog, re.MULTILINE) - - require(lock_versions == [crate_version], "Cargo.lock cellscript version must match Cargo.toml package.version") - require(package_json["version"] == crate_version, "VS Code extension version must match Cargo.toml package.version") - require(changelog_match is not None, "CHANGELOG.md must start with a semver release heading") - require(changelog_match.group(1) == crate_version, "CHANGELOG.md current release heading must match Cargo.toml package.version") - require(f"## {crate_version}" in extension_changelog, "VS Code extension changelog must include the current package version") - require(f"current {release_surface} authoring surface" in extension_readme, "VS Code extension README must name the current authoring surface") - require("current 0.15 authoring surface" not in extension_readme, "VS Code extension README must not describe the current surface as 0.15") - require_contains( - "src/lib.rs", - ['pub const VERSION: &str = env!("CARGO_PKG_VERSION");'], - ) - require_contains( - "src/main.rs", - ["#[command(version = cellscript::VERSION)]"], - ) - require_contains("README.md", [f'version = "{crate_version}"']) - for wiki_path in [ - "docs/wiki/Tutorial-01-Getting-Started.md", - "docs/wiki/Cookbook-Recipes.md", - "docs/wiki/Tutorial-03-Resources-and-Cell-Effects.md", - "docs/wiki/Tutorial-08-Bundled-Example-Contracts.md", - "docs/wiki/Tutorial-11-Scoped-Invariants-and-ProofPlan.md", - ]: - require("--primitive-strict 0.15" not in read(wiki_path), f"{wiki_path} must use the current 0.16 assurance gate in command examples") - require("--primitive-strict=0.15" not in read(wiki_path), f"{wiki_path} must use the current 0.16 assurance gate in command examples") - - ckb_acceptance = read("scripts/ckb_cellscript_acceptance.sh") - require('"--primitive-strict", "0.15"' not in ckb_acceptance, "CKB acceptance runner must not use the retired 0.15 assurance gate") - require('"--primitive-strict", "0.16"' in ckb_acceptance, "CKB acceptance runner must use the current 0.16 assurance gate") - require("ORIGINAL_SCOPED_ACTION_FAIL_CLOSED = {}" in ckb_acceptance, "CKB acceptance runner must keep token/AMM/launch out of strict 0.16 fail-closed coverage") - require('"token.cell": ["mint_with_authority", "transfer_token", "burn", "merge"]' in ckb_acceptance, "CKB acceptance runner must compile token actions as original strict scoped actions") - require('"amm_pool.cell": ["seed_pool", "swap_a_for_b", "add_liquidity", "remove_liquidity"]' in ckb_acceptance, "CKB acceptance runner must compile AMM actions as original strict scoped actions") - require('"launch.cell": ["launch_token", "bootstrap_token"]' in ckb_acceptance, "CKB acceptance runner must compile launch actions as original strict scoped actions") - require("mapfile" not in ckb_acceptance and "readarray" not in ckb_acceptance, "CKB acceptance runner must remain compatible with macOS Bash 3.2") - require("while IFS= read -r value" in ckb_acceptance, "CKB acceptance pin parsing must use the portable read loop") - - tutorial_08 = read("docs/wiki/Tutorial-08-Bundled-Example-Contracts.md") - require("strict v0.16 ProofPlan gate" in tutorial_08, "bundled example tutorial must document the strict 0.16 ProofPlan gate") - require('for f in examples/*.cell; do\n echo "==> $f"\n cellc "$f" --target riscv64-elf --target-profile ckb -o' in tutorial_08, "bundled example compile-all loop must not claim every example passes strict 0.16") - - require(package_json["name"] == "cellscript-vscode", "VS Code extension package name changed") - require(package_json["main"] == "./dist/extension.js", "VS Code extension entrypoint changed") - require("vscode-languageclient" in package_json.get("devDependencies", {}), "VS Code extension must build with vscode-languageclient") - require("esbuild" in package_json.get("devDependencies", {}), "VS Code extension must bundle with esbuild") - require("@vscode/vsce" in package_json.get("devDependencies", {}), "VS Code extension must pin vsce for package dry runs") - require("build" in package_json.get("scripts", {}), "VS Code extension must expose a build script") - require("vscode:prepublish" in package_json.get("scripts", {}), "VS Code extension must build before publish") - require("package" in package_json.get("scripts", {}), "VS Code extension must expose a package script") - require("publish:dry-run" in package_json.get("scripts", {}), "VS Code extension must expose a publish dry-run script") - require( - "vsce package --no-dependencies --out /tmp/cellscript-vscode-dry-run.vsix" - in package_json["scripts"]["publish:dry-run"], - "VS Code publish dry-run must package a local VSIX instead of using an unsupported publish --dry-run flag", - ) - commands = {command.get("command") for command in package_json.get("contributes", {}).get("commands", [])} - for command in [ - "cellscript.compileCurrentFile", - "cellscript.showMetadata", - "cellscript.showConstraints", - "cellscript.showAbi", - "cellscript.showActionBuildPlan", - "cellscript.generateTypescriptBuilder", - "cellscript.verifyPackage", - "cellscript.verifyRegistry", - "cellscript.verifyLiveRegistry", - "cellscript.showProductionReport", - ]: - require(command in commands, f"VS Code extension must contribute {command}") - require( - f"onCommand:{command}" in package_json.get("activationEvents", []), - f"VS Code extension must activate for {command}", - ) - settings = package_json.get("contributes", {}).get("configuration", {}).get("properties", {}) - for setting in [ - "cellscript.compilerPath", - "cellscript.useCargoRunFallback", - "cellscript.commandTimeoutMs", - "cellscript.maxOutputBytes", - "cellscript.target", - "cellscript.builderOutputDir", - "cellscript.ckbRpcUrl", - "cellscript.deploymentNetwork", - "cellscript.registryRequirePublisherSignature", - "cellscript.registryRequireAuditReport", - ]: - require(setting in settings, f"VS Code extension must expose {setting}") - - require_contains( - "src/main.rs", - [ - "Start the language server (JSON-RPC over stdio).", - "cellscript::lsp::server::run_lsp_server_blocking();", - ], - ) - require_contains( - "src/lsp/server.rs", - [ - "tower_lsp::LanguageServer", - "JSON-RPC", - "completion_provider", - "hover_provider", - "definition_provider", - "references_provider", - "rename_provider", - "document_formatting_provider", - "signature_help_provider", - "folding_range_provider", - "selection_range_provider", - ], - ) - require_contains( - "editors/vscode-cellscript/extension.js", - [ - "LanguageClient", - "TransportKind.stdio", - "--lsp", - "selectMetadataEntry", - "findPackageRootForDocument", - "cellscript.showConstraints", - "cellscript.showAbi", - "cellscript.showActionBuildPlan", - "cellscript.generateTypescriptBuilder", - "cellscript.verifyPackage", - "cellscript.verifyRegistry", - "cellscript.verifyLiveRegistry", - "cellscript.showProductionReport", - "gen-builder", - "package", - "verify", - "registry", - "ckbRpcUrl", - "registryRequirePublisherSignature", - "registryRequireAuditReport", - "--require-publisher-signature", - "--require-audit-report", - ], - ) - require_contains( - "editors/vscode-cellscript/scripts/validate.mjs", - [ - "LanguageClient", - "TransportKind.stdio", - "cellscript.generateTypescriptBuilder", - "cellscript.verifyLiveRegistry", - "cellscript.builderOutputDir", - "extension README must describe the production local tooling surface", - ], - ) - require_contains( - "scripts/cellscript_ckb_release_gate.sh", - [ - # The legacy release gate is now a thin shim to the unified gate - # script; assert the delegation contract rather than the deleted - # dead-code function bodies. - "exec \"$ROOT_DIR/scripts/cellscript_gate.sh\" release", - "exec \"$ROOT_DIR/scripts/cellscript_gate.sh\" release-quick", - ], - ) - require_contains( - "README.md", - [ - "cellc action build", - "cellc gen-builder --target typescript", - "cellc package verify", - "cellc registry verify --live", - ], - ) - require_contains( - "website/package.json", - [ - '"prepare:registry": "python3 scripts/generate-registry-data.py"', - '"build": "npm run prepare:registry && astro check && astro build && npm run check:docs && npm run check:dist"', - '"check:docs": "node scripts/check-doc-links.mjs"', - '"check:dist": "node scripts/check-dist-regressions.mjs"', - ], - ) - require_contains( - "website/src/pages/index.astro", - [ - 'href="/registry"', - 'data-i18n="nav.registryBrowse"', - ], - ) - require_contains( - "scripts/cellscript_gate.sh", - [ - "run_in_dir", - "run_website_build_check", - "website registry data is stale", - "run_in_dir website npm exec -- astro check", - "run_in_dir website npm exec -- astro build", - "run_in_dir editors/vscode-cellscript npm exec -- vsce package --no-dependencies --out /tmp/cellscript-vscode-dry-run.vsix", - "node editors/vscode-cellscript/scripts/validate.mjs", - ], - ) - gate_script = read("scripts/cellscript_gate.sh") - tx_measure_gate = gate_script.split("check_ckb_tx_measure_tool() {", 1)[1].split( - "check_novaseal_rust_tooling() {", 1 - )[0] - require( - "cargo test --manifest-path tools/ckb-tx-measure/Cargo.toml --locked" in tx_measure_gate, - "CKB transaction measure tooling must be tested by the release gate", - ) - require( - "RUSTUP_TOOLCHAIN" not in tx_measure_gate, - "CKB transaction measure tooling must use CellScript's pinned Rust toolchain", - ) - require( - 'print(manifest["package"]["version"])' in gate_script, - "release source identity must read the root package version from Cargo.toml", - ) - require( - 'manifest["workspace"]["package"]' not in gate_script, - "release source identity must not assume a virtual workspace package table", - ) - require_contains( - ".github/workflows/website-build.yml", - [ - "workflow_dispatch:", - "Generate registry website data", - "Check generated registry data is committed", - "Upload website dist", - ], - ) - website_build_workflow = read(".github/workflows/website-build.yml") - require("pull_request:" not in website_build_workflow, "website artifact workflow must not duplicate the unified CI gate on pull requests") - require("push:" not in website_build_workflow, "website artifact workflow must not duplicate the unified CI gate on pushes") - require_contains( - "src/main.rs", - [ - "cellc_cli_command().get_subcommands()", - "cellscript::cli::run()", - ], - ) - require_contains( - "src/cli/mod.rs", - [ - "mod novaseal_certification;", - ], - ) - require_contains( - "src/cli/commands.rs", - [ - "Command::Certify", - "novaseal-profile-v0", - ], - ) - require_contains( - "docs/wiki/Tutorial-07-LSP-and-Tooling.md", - [ - "CellScript: Generate TypeScript Action Builder", - "cellscript.builderOutputDir", - "cellc registry verify --live", - "cellscript.registryRequirePublisherSignature", - "cellscript.registryRequireAuditReport", - "npm test", - ], - ) - require_contains( - "docs/archive/0.20/CELLSCRIPT_0_20_ROADMAP.md", - [ - "VS Code extension", - "check_action_builder_toolchain", - "CellFabric is frozen", - ], - ) - require_contains( - "src/package/mod.rs", - [ - "failed to resolve registry dependency '{}/{}@{}' via discovery index '{}': {}", - "registry package '{}/{}@{}' has no source_hash in registry.json", - "source_hash mismatch for '{}/{}@{}': expected '{}', got '{}'", - "Git { url: String, revision: String }", - "pub fn consistency_issues(&self, manifest: &PackageManifest) -> Vec", - "pub fn replace_with_resolved(&mut self, resolved: &HashMap)", - ], - ) - require_contains( - "tests/cli.rs", - [ - "cellc_rejects_registry_dependency_without_namespace", - "cellc_build_resolves_registry_dependency_and_writes_phase1_lockfile", - "cellc_install_path_updates_lockfile_and_remove_prunes_it", - "cellc_fmt_subcommand_formats_sources", - "cellc_run_subcommand_executes_pure_elf_package", - "cellc_gen_builder_typescript_emits_package_scaffold", - "cellc_gen_builder_lockfile_identity_fails_closed", - ], - ) - require_contains( - "tests/registry.rs", - [ - "package_manager_resolves_registry_dependency_with_source_hash_from_local_git_fixture", - "package_manager_rejects_registry_source_hash_mismatch", - "lockfile_consistency_accepts_matching_registry_source", - ], - ) - - for excluded in [ - '".github/"', - '"docs/"', - '"docs/wiki/"', - '"editors/"', - '"proposals/"', - '"scripts/__pycache__/"', - ]: - require(excluded in cargo_toml, f"Cargo.toml package exclude is missing {excluded}") - - require("__pycache__/" in read(".gitignore"), ".gitignore must ignore generated Python bytecode directories") - require("*.py[cod]" in read(".gitignore"), ".gitignore must ignore generated Python bytecode files") - - print("valid CellScript tooling release boundary") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/validate_ckb_cellscript_production_evidence.py b/scripts/validate_ckb_cellscript_production_evidence.py deleted file mode 100755 index e0791ce8..00000000 --- a/scripts/validate_ckb_cellscript_production_evidence.py +++ /dev/null @@ -1,1058 +0,0 @@ -#!/usr/bin/env python3 -"""Validate CKB CellScript production acceptance evidence before release.""" - -from __future__ import annotations - -import argparse -import hashlib -import json -from pathlib import Path -import subprocess -from typing import Any - - -SOURCE_PROVENANCE_SCHEMA = "cellscript-ckb-acceptance-source-provenance-v0.22" -BUILD_REPORT_SCHEMA = "cellscript-ckb-build-report-v0.20" -SOURCE_PROVENANCE_PATHS = [ - "Cargo.lock", - "Cargo.toml", - "rust-toolchain.toml", - ".github/workflows/release.yml", - "src", - "examples", - "scripts/cellscript_gate.sh", - "scripts/cellscript_ckb_release_gate.sh", - "scripts/ckb_acceptance_pin.json", - "scripts/ckb_cellscript_acceptance.sh", - "scripts/validate_ckb_cellscript_production_evidence.py", -] - -EXPECTED_EXAMPLES = [ - "amm_pool.cell", - "launch.cell", - "multisig.cell", - "nft.cell", - "timelock.cell", - "token.cell", - "vesting.cell", -] -EXPECTED_NON_PRODUCTION_EXAMPLES = ["registry.cell", "atomic_swap.cell", "multi_phase_dao.cell"] -EXPECTED_LANGUAGE_EXAMPLES = [ - "canonical_style.cell", - "order_book.cell", - "registry.cell", - "stdlib.cell", - "v0_14_capacity_time.cell", - "v0_14_ckb_type_id_create.cell", - "v0_14_delegate_verify.cell", - "v0_14_hash_blake2b.cell", - "v0_14_multi_step_pipeline.cell", - "v0_14_witness_source.cell", - "v0_15_identity_lifecycle.cell", - "v0_15_scoped_invariant.cell", - "v0_22_borrow.cell", - "v0_22_bounded_lifecycle.cell", - "v0_22_transaction_views.cell", -] -EXPECTED_ACTION_COUNT = 43 -EXPECTED_STATUS = "passed" -EXPECTED_MODE = "production" -EXPECTED_LOCK_SPEND_MATRIX = { - "multisig.cell": ["is_signer_lock", "can_execute", "can_cancel", "has_enough_approvals", "not_expired"], - "nft.cell": ["nft_ownership", "listing_seller", "offer_buyer", "valid_royalty", "collection_creator"], - "timelock.cell": ["can_unlock_lock", "is_owner", "lock_id_commitment", "asset_matches", "not_expired", "emergency_approved"], - "vesting.cell": ["vesting_admin"], -} -EXPECTED_LOCK_COUNT = sum(len(locks) for locks in EXPECTED_LOCK_SPEND_MATRIX.values()) -EXPECTED_LOCK_NAMES = [ - f"{example}:{lock}" - for example, locks in EXPECTED_LOCK_SPEND_MATRIX.items() - for lock in locks -] -EXPECTED_CRITICAL_ELF_ABI_EXAMPLES = ["launch.cell", "token.cell", "amm_pool.cell"] - -ACTION_RUN_KEYS = [ - "token_action_runs", - "nft_action_runs", - "timelock_action_runs", - "multisig_action_runs", - "vesting_action_runs", - "amm_action_runs", - "launch_action_runs", -] - -EXPECTED_ACTIONS_BY_RUN_KEY = { - "token_action_runs": ["mint_with_authority", "transfer_token", "burn", "merge"], - "nft_action_runs": [ - "create_collection", - "mint", - "transfer", - "create_listing", - "cancel_listing", - "buy_from_listing", - "create_offer", - "accept_offer", - "burn", - "batch_mint", - ], - "timelock_action_runs": [ - "create_absolute_lock", - "create_relative_lock", - "lock_asset", - "request_release", - "request_emergency_release", - "approve_emergency_release", - "extend_lock", - "execute_release", - "execute_emergency_release", - "batch_create_locks", - ], - "multisig_action_runs": [ - "create_wallet", - "propose_transfer", - "record_approval", - "execute_proposal", - "cancel_proposal", - "propose_add_signer", - "propose_remove_signer", - "propose_change_threshold", - ], - "vesting_action_runs": ["create_vesting_config", "grant_vesting", "claim_vested", "claim_fully_vested", "revoke_grant"], - "amm_action_runs": ["seed_pool", "swap_a_for_b", "add_liquidity", "remove_liquidity"], - "launch_action_runs": ["launch_token", "bootstrap_token"], -} -EXPECTED_ACTION_IDS = sorted( - f"{example}:{action}" - for run_key, actions in EXPECTED_ACTIONS_BY_RUN_KEY.items() - for example in [{ - "token_action_runs": "token.cell", - "nft_action_runs": "nft.cell", - "timelock_action_runs": "timelock.cell", - "multisig_action_runs": "multisig.cell", - "vesting_action_runs": "vesting.cell", - "amm_action_runs": "amm_pool.cell", - "launch_action_runs": "launch.cell", - }[run_key]] - for action in actions -) -EXPECTED_PUBLIC_ACTIONS_BY_EXAMPLE = { - "token.cell": EXPECTED_ACTIONS_BY_RUN_KEY["token_action_runs"], - "nft.cell": EXPECTED_ACTIONS_BY_RUN_KEY["nft_action_runs"], - "timelock.cell": [ - "create_absolute_lock", - "create_relative_lock", - "lock_asset", - "request_release", - "execute_release", - "request_emergency_release", - "approve_emergency_release", - "execute_emergency_release", - "extend_lock", - "batch_create_locks", - ], - "multisig.cell": EXPECTED_ACTIONS_BY_RUN_KEY["multisig_action_runs"], - "vesting.cell": EXPECTED_ACTIONS_BY_RUN_KEY["vesting_action_runs"], - "amm_pool.cell": EXPECTED_ACTIONS_BY_RUN_KEY["amm_action_runs"], - "launch.cell": EXPECTED_ACTIONS_BY_RUN_KEY["launch_action_runs"], -} -EXPECTED_END_TO_END_STATEFUL_SCENARIOS = [ - "token.mint-with-authority-transfer-mint-with-authority-merge-burn", - "nft.mint-list-transfer-by-listing", - "timelock.create-lock-lock-asset-request-release-execute", - "launch.launch-token-then-mint-with-authority", - "amm.seed-add-swap-remove", - "vesting.create-config-grant-revoke", - "multisig.create-propose-approve-approve-execute", -] - - -def load_json(path: Path) -> dict[str, Any]: - try: - with path.open("r", encoding="utf-8") as fh: - value = json.load(fh) - except FileNotFoundError as exc: - raise SystemExit(f"missing CKB production evidence: {path}") from exc - except json.JSONDecodeError as exc: - raise SystemExit(f"invalid JSON in {path}: {exc}") from exc - if not isinstance(value, dict): - raise SystemExit(f"{path} must contain a JSON object") - return value - - -def require(condition: bool, message: str) -> None: - if not condition: - raise SystemExit(f"invalid CKB CellScript production evidence: {message}") - - -def require_field(mapping: dict[str, Any], key: str, expected: Any, context: str = "") -> None: - actual = mapping.get(key) - prefix = f"{context}." if context else "" - require(actual == expected, f"{prefix}{key} must be {expected!r}, got {actual!r}") - - -def require_empty(mapping: dict[str, Any], key: str, context: str = "") -> None: - value = mapping.get(key) - prefix = f"{context}." if context else "" - require(value == [], f"{prefix}{key} must be empty, got {value!r}") - - -def require_positive_int(value: Any, context: str) -> int: - require(isinstance(value, int) and value > 0, f"{context} must be a positive integer, got {value!r}") - return value - - -def require_bool(value: Any, context: str) -> bool: - require(isinstance(value, bool), f"{context} must be a boolean, got {value!r}") - return value - -def require_hex_hash(value: Any, context: str) -> str: - require( - isinstance(value, str) - and value.startswith("0x") - and len(value) == 66 - and all(ch in "0123456789abcdefABCDEF" for ch in value[2:]), - f"{context} must be a 32-byte 0x-prefixed hex hash, got {value!r}", - ) - return value - - -def validate_elf_entry_abi_gate(report: dict[str, Any]) -> None: - gate = report.get("ckb_elf_entry_abi_gate") - require(isinstance(gate, dict), "ckb_elf_entry_abi_gate must be an object") - require_field(gate, "schema", "cellscript-ckb-elf-entry-abi-gate-v0.22", "ckb_elf_entry_abi_gate") - require_field(gate, "status", EXPECTED_STATUS, "ckb_elf_entry_abi_gate") - require_field(gate, "requires_ckb_vm_stack_pointer_preserved", True, "ckb_elf_entry_abi_gate") - require_field(gate, "requires_entry_trampoline_call_sequence", True, "ckb_elf_entry_abi_gate") - require_field(gate, "requires_rx_only_executable_segment", True, "ckb_elf_entry_abi_gate") - require_field(gate, "requires_no_fake_stack_load_segment", True, "ckb_elf_entry_abi_gate") - require_field(gate, "critical_examples", EXPECTED_CRITICAL_ELF_ABI_EXAMPLES, "ckb_elf_entry_abi_gate") - require_empty(gate, "failures", "ckb_elf_entry_abi_gate") - require_positive_int(gate.get("audited_artifact_count"), "ckb_elf_entry_abi_gate.audited_artifact_count") - - critical = gate.get("critical_example_gate") - require(isinstance(critical, dict), "ckb_elf_entry_abi_gate.critical_example_gate must be an object") - for example in EXPECTED_CRITICAL_ELF_ABI_EXAMPLES: - row = critical.get(example) - require(isinstance(row, dict), f"ckb_elf_entry_abi_gate.critical_example_gate.{example} must be an object") - require_field(row, "status", EXPECTED_STATUS, f"ckb_elf_entry_abi_gate.critical_example_gate.{example}") - require_field(row, "missing", False, f"ckb_elf_entry_abi_gate.critical_example_gate.{example}") - require_empty(row, "failures", f"ckb_elf_entry_abi_gate.critical_example_gate.{example}") - require_positive_int(row.get("artifact_count"), f"ckb_elf_entry_abi_gate.critical_example_gate.{example}.artifact_count") - - rows = gate.get("rows") - require(isinstance(rows, list) and rows, "ckb_elf_entry_abi_gate.rows must be a non-empty list") - for index, row in enumerate(rows): - require(isinstance(row, dict), f"ckb_elf_entry_abi_gate.rows[{index}] must be an object") - context = f"ckb_elf_entry_abi_gate.rows[{index}]" - require_field(row, "status", EXPECTED_STATUS, context) - require_field(row, "preserves_ckb_vm_stack_pointer", True, context) - require_field(row, "entry_trampoline_calls_with_ra", True, context) - require_field(row, "executable_segment_rx_only", True, context) - require_field(row, "executable_segment_file_size_equals_memory_size", True, context) - require(isinstance(row.get("artifact"), str) and row["artifact"], f"{context}.artifact must be a non-empty string") - require_field(row, "first_instruction_le_hex", "0x00000097", context) - require_field( - row, - "trampoline_instructions_le_hex", - ["0x00000097", "0x014080e7", "0x000008b7", "0x05d88893", "0x00000073"], - context, - ) - require_field(row, "trampoline_bytes_hex", "97000000e7804001b70800009388d80573000000", context) - require_field(row, "call_target", row.get("expected_call_target"), context) - require_field(row, "exit_syscall_number", 93, context) - require_field(row, "exit_sequence_exact", True, context) - - -def git_stdout(repo_root: Path, args: list[str]) -> str: - try: - return subprocess.check_output(["git", *args], cwd=repo_root, text=True).strip() - except (OSError, subprocess.CalledProcessError) as exc: - raise SystemExit(f"failed to query git source provenance in {repo_root}: {exc}") from exc - - -def tracked_source_files(repo_root: Path) -> list[str]: - output = git_stdout(repo_root, ["ls-files", "--", *SOURCE_PROVENANCE_PATHS]) - return [ - line - for line in output.splitlines() - if line and (repo_root / line).is_file() - ] - - -def file_sha256(path: Path) -> str: - h = hashlib.sha256() - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - h.update(chunk) - return h.hexdigest() - -def ckb_data_hash_hex(data: bytes) -> str: - return "0x" + hashlib.blake2b(data, digest_size=32, person=b"ckb-default-hash").hexdigest() - - -def tracked_source_sha256(repo_root: Path, files: list[str]) -> str: - h = hashlib.sha256() - for rel in files: - h.update(rel.encode("utf-8")) - h.update(b"\0") - h.update(file_sha256(repo_root / rel).encode("ascii")) - h.update(b"\n") - return "0x" + h.hexdigest() - - -def current_source_provenance(repo_root: Path) -> dict[str, Any]: - files = tracked_source_files(repo_root) - return { - "repo_commit": git_stdout(repo_root, ["rev-parse", "HEAD"]), - "git_dirty": bool(git_stdout(repo_root, ["status", "--porcelain", "--untracked-files=all"])), - "tracked_source_paths": SOURCE_PROVENANCE_PATHS, - "tracked_source_files": files, - "tracked_source_file_count": len(files), - "tracked_source_sha256": tracked_source_sha256(repo_root, files), - "acceptance_script_sha256": "0x" + file_sha256(repo_root / "scripts/ckb_cellscript_acceptance.sh"), - "validator_script_sha256": "0x" + file_sha256(repo_root / "scripts/validate_ckb_cellscript_production_evidence.py"), - } - - -def validate_source_provenance(report: dict[str, Any], repo_root: Path) -> None: - provenance = report.get("source_provenance") - require(isinstance(provenance, dict), "source_provenance must be an object") - require_field(provenance, "schema", SOURCE_PROVENANCE_SCHEMA, "source_provenance") - require(isinstance(provenance.get("generated_at_utc"), str), "source_provenance.generated_at_utc must be a timestamp string") - require_field(provenance, "git_dirty", False, "source_provenance") - - current = current_source_provenance(repo_root) - for key in ( - "repo_commit", - "git_dirty", - "tracked_source_paths", - "tracked_source_files", - "tracked_source_file_count", - "tracked_source_sha256", - "acceptance_script_sha256", - "validator_script_sha256", - ): - require_field(provenance, key, current[key], "source_provenance") - - -def validate_public_builder_contracts(report: dict[str, Any]) -> None: - gate = report.get("public_builder_contracts") - require(isinstance(gate, dict), "public_builder_contracts must be an object") - require_field(gate, "schema", "cellscript-public-builder-contract-gate-v0.22", "public_builder_contracts") - require_field(gate, "status", EXPECTED_STATUS, "public_builder_contracts") - require_field(gate, "example_count", len(EXPECTED_EXAMPLES), "public_builder_contracts") - require_field(gate, "action_count", EXPECTED_ACTION_COUNT, "public_builder_contracts") - require_field(gate, "requires_gen_builder", True, "public_builder_contracts") - require_field(gate, "requires_action_build", True, "public_builder_contracts") - require_field( - gate, - "transaction_origin_claim", - "acceptance-python-harness-not-generated-builder", - "public_builder_contracts", - ) - contracts = gate.get("contracts") - require(isinstance(contracts, list), "public_builder_contracts.contracts must be a list") - require([contract.get("example") for contract in contracts] == EXPECTED_EXAMPLES, "public builder examples must match exact release scope") - seen_action_ids: list[str] = [] - for contract in contracts: - example = contract["example"] - context = f"public_builder_contracts.{example}" - expected_actions = EXPECTED_PUBLIC_ACTIONS_BY_EXAMPLE[example] - require_field(contract, "status", EXPECTED_STATUS, context) - require_field(contract, "generator_schema", "cellscript-generated-builder-summary-v0.20", context) - require_field(contract, "builder_manifest_schema", "cellscript-generated-action-builder-v0.20", context) - require_field(contract, "target", "typescript", context) - require_field(contract, "target_profile", "ckb", context) - require_field(contract, "actions", expected_actions, context) - require_field(contract, "action_count", len(expected_actions), context) - require_field(contract, "runtime_adapter_execution", "not-proven-by-this-contract-gate", context) - require_hex_hash(contract.get("manifest_sha256"), f"{context}.manifest_sha256") - require_hex_hash(contract.get("generated_tree_sha256"), f"{context}.generated_tree_sha256") - require_positive_int(contract.get("generated_file_count"), f"{context}.generated_file_count") - manifest_path = Path(contract.get("manifest_path", "")) - require(manifest_path.is_file(), f"{context}.manifest_path does not exist: {manifest_path}") - require("0x" + file_sha256(manifest_path) == contract["manifest_sha256"], f"{context}.manifest_sha256 does not match file") - manifest = load_json(manifest_path) - require([action.get("name") for action in manifest.get("actions", [])] == expected_actions, f"{context} manifest action mismatch") - generated_files = sorted(path for path in manifest_path.parent.rglob("*") if path.is_file()) - tree_hash = hashlib.sha256() - for path in generated_files: - relative = path.relative_to(manifest_path.parent).as_posix() - tree_hash.update(relative.encode("utf-8")) - tree_hash.update(b"\0") - tree_hash.update(hashlib.sha256(path.read_bytes()).digest()) - require_field(contract, "generated_file_count", len(generated_files), context) - require_field(contract, "generated_tree_sha256", "0x" + tree_hash.hexdigest(), context) - plans = contract.get("action_plans") - require(isinstance(plans, list) and len(plans) == len(expected_actions), f"{context}.action_plans must cover every action") - for plan, action in zip(plans, expected_actions, strict=True): - plan_context = f"{context}.action_plans.{action}" - require_field(plan, "action", action, plan_context) - require_field(plan, "contract_id", f"{example}:{action}", plan_context) - require_field(plan, "policy", "cellscript-action-builder-plan-v1", plan_context) - require_field(plan, "status", EXPECTED_STATUS, plan_context) - require_hex_hash(plan.get("plan_sha256"), f"{plan_context}.plan_sha256") - plan_path = Path(plan.get("plan_path", "")) - require(plan_path.is_file(), f"{plan_context}.plan_path does not exist: {plan_path}") - require_field(plan, "plan_sha256", "0x" + file_sha256(plan_path), plan_context) - plan_json = load_json(plan_path) - require_field(plan_json, "status", "ok", f"{plan_context}.file") - require_field(plan_json, "policy", "cellscript-action-builder-plan-v1", f"{plan_context}.file") - require_field(plan_json, "action", action, f"{plan_context}.file") - require_field(plan_json, "target_profile", "ckb", f"{plan_context}.file") - seen_action_ids.append(plan["contract_id"]) - require(sorted(seen_action_ids) == EXPECTED_ACTION_IDS, "public builder action contracts must match the exact production action matrix") - - -def validate_ckb_runtime_provenance(report: dict[str, Any], repo_root: Path, report_dir: Path) -> None: - pin_path = repo_root / "scripts/ckb_acceptance_pin.json" - pin = load_json(pin_path) - require_field(pin, "schema", "cellscript-ckb-acceptance-pin-v0.22", "ckb_acceptance_pin") - provenance = report.get("ckb_runtime_provenance") - require(isinstance(provenance, dict), "ckb_runtime_provenance must be an object") - context = "ckb_runtime_provenance" - require_field(provenance, "schema", "cellscript-ckb-runtime-provenance-v0.22", context) - require_field(provenance, "pin_schema", pin["schema"], context) - require_field(provenance, "pin_file_sha256", "0x" + file_sha256(pin_path), context) - require_field(provenance, "repository", pin["repository"], context) - require_field(provenance, "revision", pin["revision"], context) - require_field(provenance, "repo_head", pin["revision"], context) - require_field(provenance, "repo_dirty", False, context) - require_field(provenance, "version", pin["version"], context) - require_field(provenance, "build_mode", "fresh-dedicated-cargo-target", context) - require_field(provenance, "binary_archived_with_report", True, context) - version_output = provenance.get("version_output") - require( - isinstance(version_output, str) - and pin["version"] in version_output - and pin["revision"][:7] in version_output, - f"{context}.version_output must bind version and revision, got {version_output!r}", - ) - - ckb_repo = Path(report.get("ckb_repo", "")).resolve() - require(ckb_repo.is_dir(), f"ckb_repo does not exist: {ckb_repo}") - require(git_stdout(ckb_repo, ["rev-parse", "HEAD"]) == pin["revision"], "current CKB checkout does not match pin") - require(not git_stdout(ckb_repo, ["status", "--porcelain", "--untracked-files=all"]), "current CKB checkout must be clean") - binary_path = Path(provenance.get("binary_path", "")).resolve() - require(binary_path.is_file(), f"{context}.binary_path does not exist: {binary_path}") - require_field(provenance, "binary_path", str((report_dir / "ckb-runtime" / "ckb").resolve()), context) - require_field(provenance, "binary_sha256", "0x" + file_sha256(binary_path), context) - require_field(provenance, "version_output", subprocess.check_output([binary_path, "--version"], text=True).strip(), context) - - expected_paths = { - "source_template_path": ckb_repo / pin["template_paths"][0], - "source_spec_path": ckb_repo / pin["template_paths"][1], - } - for key, path in expected_paths.items(): - require_field(provenance, key, str(path), context) - require(path.is_file(), f"{context}.{key} does not exist: {path}") - require_field(provenance, key.replace("_path", "_sha256"), "0x" + file_sha256(path), context) - for key in ("effective_config", "effective_spec"): - path = Path(provenance.get(f"{key}_path", "")) - require(path.is_file(), f"{context}.{key}_path does not exist: {path}") - require_field(provenance, f"{key}_sha256", "0x" + file_sha256(path), context) - require_hex_hash(provenance.get("genesis_hash"), f"{context}.genesis_hash") - require_field(provenance, "genesis_hash", report.get("onchain", {}).get("genesis_hash"), context) - -def validate_build_reports(report: dict[str, Any], *, compile_only: bool) -> None: - build_index = report.get("cellscript_build_reports") - require(isinstance(build_index, dict), "cellscript_build_reports must be an object") - require_field(build_index, "schema", "cellscript-ckb-build-report-index-v0.20", "cellscript_build_reports") - require_field(build_index, "target_profile", "ckb", "cellscript_build_reports") - require_field(build_index, "vm_profile", "ckb-vm", "cellscript_build_reports") - require_field(build_index, "artifact_format", "riscv64-elf", "cellscript_build_reports") - require_field(build_index, "artifact_hash_algorithm", "ckb-blake2b256", "cellscript_build_reports") - require_field(build_index, "requires_exact_artifact_hash", True, "cellscript_build_reports") - require_field(build_index, "requires_elf_entry_abi_gate", True, "cellscript_build_reports") - require_field(build_index, "requires_live_code_cell_data_hash_match", True, "cellscript_build_reports") - require_field(build_index, "status", EXPECTED_STATUS, "cellscript_build_reports") - - rows = build_index.get("reports") - require(isinstance(rows, list) and rows, "cellscript_build_reports.reports must be a non-empty list") - require_field(build_index, "artifact_count", len(rows), "cellscript_build_reports") - - elf_gate = report.get("ckb_elf_entry_abi_gate") or {} - require_field(build_index, "artifact_count", elf_gate.get("audited_artifact_count"), "cellscript_build_reports") - - seen_artifacts: set[str] = set() - for index, row in enumerate(rows): - require(isinstance(row, dict), f"cellscript_build_reports.reports[{index}] must be an object") - context = f"cellscript_build_reports.reports[{index}]" - require_field(row, "schema", BUILD_REPORT_SCHEMA, context) - require_field(row, "target_profile", "ckb", context) - require_field(row, "vm_profile", "ckb-vm", context) - require_field(row, "artifact_format", "riscv64-elf", context) - require_field(row, "artifact_hash_algorithm", "ckb-blake2b256", context) - require_field(row, "deployment_hash_type_used_by_gate", "data1", context) - require_field(row, "verify_artifact_status", "passed", context) - require_field(row, "verify_target_profile", "ckb", context) - require_field(row, "elf_entry_abi_status", "passed", context) - require_field(row, "abi_trailer_stripped", True, context) - require_positive_int(row.get("artifact_size_bytes"), f"{context}.artifact_size_bytes") - require_hex_hash(row.get("deployable_elf_hash"), f"{context}.deployable_elf_hash") - require_hex_hash(row.get("artifact_sha256"), f"{context}.artifact_sha256") - artifact_path = row.get("artifact_path") - require(isinstance(artifact_path, str) and artifact_path, f"{context}.artifact_path must be present") - require(artifact_path not in seen_artifacts, f"duplicate build report artifact_path: {artifact_path}") - seen_artifacts.add(artifact_path) - artifact = Path(artifact_path) - require(artifact.exists(), f"{context}.artifact_path does not exist: {artifact}") - artifact_bytes = artifact.read_bytes() - require(len(artifact_bytes) == row["artifact_size_bytes"], f"{context}.artifact_size_bytes does not match artifact") - require(ckb_data_hash_hex(artifact_bytes) == row["deployable_elf_hash"], f"{context}.deployable_elf_hash does not match artifact") - require("0x" + hashlib.sha256(artifact_bytes).hexdigest() == row["artifact_sha256"], f"{context}.artifact_sha256 does not match artifact") - onchain_deployments = row.get("onchain_deployments") - require(isinstance(onchain_deployments, list), f"{context}.onchain_deployments must be a list") - if compile_only: - require(onchain_deployments == [], f"{context}.onchain_deployments must be empty for compile-only reports") - else: - require(onchain_deployments, f"{context}.onchain_deployments must contain live deployment evidence") - for deployment_index, deployment in enumerate(onchain_deployments): - deployment_context = f"{context}.onchain_deployments[{deployment_index}]" - require(isinstance(deployment, dict), f"{deployment_context} must be an object") - require_field(deployment, "code_cell_live", True, deployment_context) - require_field(deployment, "live_code_cell_data_hash_matches_artifact", True, deployment_context) - require_field( - deployment, - "artifact_ckb_data_hash_blake2b", - row["deployable_elf_hash"], - deployment_context, - ) - require_field( - deployment, - "live_code_cell_data_hash", - row["deployable_elf_hash"], - deployment_context, - ) - out_point = deployment.get("out_point") - require(isinstance(out_point, dict), f"{deployment_context}.out_point must be an object") - require(isinstance(out_point.get("tx_hash"), str) and out_point["tx_hash"].startswith("0x"), f"{deployment_context}.out_point.tx_hash must be hex") - require(isinstance(out_point.get("index"), str) and out_point["index"].startswith("0x"), f"{deployment_context}.out_point.index must be hex") - - if compile_only: - require(build_index.get("onchain_deployed_artifact_count") in (None, 0), "compile-only build reports must not record onchain deployments") - else: - require_field(build_index, "onchain_deployed_artifact_count", len(rows), "cellscript_build_reports") - require_field(build_index, "live_code_cell_data_hash_match_count", len(rows), "cellscript_build_reports") - require_empty(build_index, "missing_onchain_deployments", "cellscript_build_reports") - require_empty(build_index, "live_code_cell_data_hash_mismatches", "cellscript_build_reports") - require_empty(build_index, "unexpected_onchain_artifacts", "cellscript_build_reports") - - -def all_action_runs(report: dict[str, Any]) -> list[dict[str, Any]]: - onchain = report.get("onchain") - require(isinstance(onchain, dict), "onchain section must be present") - runs: list[dict[str, Any]] = [] - for key in ACTION_RUN_KEYS: - value = onchain.get(key) - require(isinstance(value, list), f"onchain.{key} must be a list") - expected_actions = EXPECTED_ACTIONS_BY_RUN_KEY[key] - actual_actions = [row.get("action") for row in value if isinstance(row, dict)] - require( - sorted(actual_actions) == sorted(expected_actions) and len(actual_actions) == len(expected_actions), - f"onchain.{key} actions must be {expected_actions!r}, got {actual_actions!r}", - ) - require( - len(set(actual_actions)) == len(actual_actions), - f"onchain.{key} must not contain duplicate actions, got {actual_actions!r}", - ) - for row in value: - require(isinstance(row, dict), f"onchain.{key} entries must be objects") - runs.append(row) - return runs - - -def validate_compile_gate(report: dict[str, Any], *, compile_only: bool = False) -> None: - require_field(report, "acceptance_mode", EXPECTED_MODE) - require_field(report, "status", EXPECTED_STATUS) - if compile_only: - require_field(report, "production_ready", False) - else: - require_field(report, "production_ready", True) - require_field(report, "bundled_examples_count", len(EXPECTED_EXAMPLES)) - require_field(report, "bundled_examples_exact_order", EXPECTED_EXAMPLES) - require_field(report, "non_production_examples", EXPECTED_NON_PRODUCTION_EXAMPLES) - require_field(report, "language_examples_count", len(EXPECTED_LANGUAGE_EXAMPLES)) - require_field(report, "language_examples_exact_order", EXPECTED_LANGUAGE_EXAMPLES) - require_field(report, "original_scoped_action_count", EXPECTED_ACTION_COUNT) - require_field(report, "original_scoped_lock_count", EXPECTED_LOCK_COUNT) - require_field(report, "original_scoped_action_fail_closed_count", 0) - require_field(report, "original_scoped_lock_fail_closed_count", 0) - require_empty(report, "strict_original_ckb_compile_policy_fail_closed") - require_empty(report, "strict_original_ckb_compile_unexpected_failures") - require_empty(report, "original_scoped_action_fail_closed") - require_empty(report, "original_scoped_lock_fail_closed") - - gate = report.get("production_gate") - require(isinstance(gate, dict), "production_gate must be an object") - require_field(gate, "status", EXPECTED_STATUS, "production_gate") - require_empty(gate, "failures", "production_gate") - require_field(gate, "requires_original_scoped_harnesses", True, "production_gate") - require_field(gate, "requires_no_expected_fail_closed_entries", True, "production_gate") - require_field(gate, "requires_all_bundled_examples_strict_original_ckb", True, "production_gate") - require_field(gate, "requires_ckb_elf_entry_abi_gate", True, "production_gate") - require_field(gate, "requires_cellscript_build_reports", True, "production_gate") - require_field(gate, "requires_public_builder_contracts", True, "production_gate") - validate_elf_entry_abi_gate(report) - validate_build_reports(report, compile_only=compile_only) - - coverage = report.get("ckb_business_coverage") - require(isinstance(coverage, dict), "ckb_business_coverage must be an object") - require_field(coverage, "strict_compile_coverage_complete", True, "ckb_business_coverage") - require_field(coverage, "expected_fail_closed_action_count", 0, "ckb_business_coverage") - require_field(coverage, "expected_fail_closed_lock_count", 0, "ckb_business_coverage") - if compile_only: - require_field(coverage, "status", "incomplete", "ckb_business_coverage") - require_field(coverage, "onchain_action_coverage_complete", False, "ckb_business_coverage") - require_field(coverage, "ckb_onchain_action_count", 0, "ckb_business_coverage") - onchain = report.get("onchain") - require(isinstance(onchain, dict), "onchain section must be present") - require_field(onchain, "status", "skipped", "onchain") - require_field(onchain, "reason", "compile-only", "onchain") - else: - require_field(coverage, "status", "complete", "ckb_business_coverage") - require_field(coverage, "onchain_action_coverage_complete", True, "ckb_business_coverage") - require_field(coverage, "ckb_onchain_action_count", EXPECTED_ACTION_COUNT, "ckb_business_coverage") - missing = coverage.get("missing_ckb_onchain_actions") - require(missing in ({}, None), f"ckb_business_coverage.missing_ckb_onchain_actions must be empty, got {missing!r}") - - example_scope = report.get("example_scope") - require(isinstance(example_scope, dict), "example_scope must be an object") - require_field(example_scope, "production_bundled_examples", EXPECTED_EXAMPLES, "example_scope") - require_field(example_scope, "non_production_top_level_examples", EXPECTED_NON_PRODUCTION_EXAMPLES, "example_scope") - require_field(example_scope, "non_production_language_examples", EXPECTED_LANGUAGE_EXAMPLES, "example_scope") - scope_note = example_scope.get("production_scope_note") - require( - isinstance(scope_note, str) - and "Only production_bundled_examples" in scope_note - and "non_production_top_level_examples" in scope_note - and "non_production_language_examples" in scope_note, - "example_scope.production_scope_note must state the production/non-production example boundary", - ) - source_layout = report.get("example_source_layout") - require(isinstance(source_layout, dict), "example_source_layout must be an object") - require(isinstance(source_layout.get("canonical_bundled_examples"), str), "example_source_layout must record canonical_bundled_examples") - require(isinstance(source_layout.get("language_examples"), str), "example_source_layout must record language_examples") - require( - "production_acceptance_examples" not in source_layout - and "canonical_business_examples" not in source_layout - and "flat_business_compatibility_examples" not in source_layout, - "example_source_layout must not advertise the removed business/acceptance split", - ) - layout_note = source_layout.get("canonical_examples_note") - require( - isinstance(layout_note, str) - and "top-level examples/*.cell directly" in layout_note - and "examples/business and examples/acceptance" in layout_note, - "example_source_layout.canonical_examples_note must state the single-source example layout", - ) - - lock_scope = report.get("lock_acceptance_scope") - require(isinstance(lock_scope, dict), "lock_acceptance_scope must be an object") - if lock_scope.get("onchain_lock_spend_matrix") is True: - require_field(lock_scope, "strict_compile_only", False, "lock_acceptance_scope") - require_field(lock_scope, "onchain_lock_spend_matrix_scope", EXPECTED_LOCK_SPEND_MATRIX, "lock_acceptance_scope") - require_field(lock_scope, "required_cases_per_lock", ["valid_spend", "invalid_spend"], "lock_acceptance_scope") - else: - require_field(lock_scope, "strict_compile_only", True, "lock_acceptance_scope") - require_field(lock_scope, "onchain_lock_spend_matrix", False, "lock_acceptance_scope") - require_field(lock_scope, "pending_onchain_lock_spend_matrix", EXPECTED_LOCK_SPEND_MATRIX, "lock_acceptance_scope") - require_field( - lock_scope, - "required_cases_per_lock_when_promoted", - ["valid_spend", "invalid_spend"], - "lock_acceptance_scope", - ) - lock_scope_note = lock_scope.get("scope_note") - require(isinstance(lock_scope_note, str) and "strict-compiled" in lock_scope_note, "lock_acceptance_scope.scope_note must mention strict compilation") - - -def validate_onchain_gate(report: dict[str, Any]) -> None: - onchain = report.get("onchain") - require(isinstance(onchain, dict), "onchain section must be present") - require_field(onchain, "status", EXPECTED_STATUS, "onchain") - require_field(onchain, "all_artifacts_deployed_and_spent", True, "onchain") - require_field(onchain, "all_bundled_examples_deployed", True, "onchain") - require_field(onchain, "bundled_examples_deployed", EXPECTED_EXAMPLES, "onchain") - require_field(onchain, "all_token_actions_exercised", True, "onchain") - require_field(onchain, "all_nft_actions_exercised", True, "onchain") - require_field(onchain, "all_timelock_actions_exercised", True, "onchain") - require_field(onchain, "all_multisig_actions_exercised", True, "onchain") - require_field(onchain, "all_vesting_actions_exercised", True, "onchain") - require_field(onchain, "all_amm_actions_exercised", True, "onchain") - require_field(onchain, "all_launch_actions_exercised", True, "onchain") - require_field(onchain, "builder_backed_action_count", 0, "onchain") - require_field(onchain, "acceptance_harness_action_count", EXPECTED_ACTION_COUNT, "onchain") - require_field(onchain, "public_builder_contract_action_count", EXPECTED_ACTION_COUNT, "onchain") - require_field(onchain, "measured_cycles_action_count", EXPECTED_ACTION_COUNT, "onchain") - require_field(onchain, "tx_size_measured_action_count", EXPECTED_ACTION_COUNT, "onchain") - require_field(onchain, "occupied_capacity_measured_action_count", EXPECTED_ACTION_COUNT, "onchain") - require_field(onchain, "lock_spend_matrix_count", EXPECTED_LOCK_COUNT, "onchain") - require_field(onchain, "builder_backed_lock_spend_matrix_count", 0, "onchain") - require_field(onchain, "acceptance_harness_lock_spend_matrix_count", EXPECTED_LOCK_COUNT, "onchain") - require_field(onchain, "lock_valid_spend_count", EXPECTED_LOCK_COUNT, "onchain") - require_field(onchain, "lock_invalid_spend_count", EXPECTED_LOCK_COUNT, "onchain") - require_field(onchain, "measured_cycles_lock_count", EXPECTED_LOCK_COUNT, "onchain") - require_field(onchain, "tx_size_measured_lock_count", EXPECTED_LOCK_COUNT, "onchain") - require_field(onchain, "occupied_capacity_measured_lock_count", EXPECTED_LOCK_COUNT, "onchain") - require_field(onchain, "all_locks_behavior_exercised", True, "onchain") - resource_scope = onchain.get("resource_identity_evidence_scope") - require(isinstance(resource_scope, dict), "onchain.resource_identity_evidence_scope must be an object") - require_field(resource_scope, "status", "fixture-only", "onchain.resource_identity_evidence_scope") - require_field(resource_scope, "always_success_resource_types", True, "onchain.resource_identity_evidence_scope") - require_field(resource_scope, "production_resource_identity_proven", False, "onchain.resource_identity_evidence_scope") - - deployment_runs = onchain.get("bundled_example_deployment_runs") - require(isinstance(deployment_runs, list), "onchain.bundled_example_deployment_runs must be a list") - require( - len(deployment_runs) == len(EXPECTED_EXAMPLES), - f"expected {len(EXPECTED_EXAMPLES)} bundled example deployment runs, got {len(deployment_runs)}", - ) - deployment_names = [run.get("name") for run in deployment_runs if isinstance(run, dict)] - require( - deployment_names == EXPECTED_EXAMPLES, - f"bundled example deployment order must be {EXPECTED_EXAMPLES!r}, got {deployment_names!r}", - ) - for run in deployment_runs: - require(isinstance(run, dict), "bundled example deployment run entries must be objects") - name = run.get("name") - require(isinstance(name, str) and name, "bundled example deployment run is missing name") - require_field(run, "status", EXPECTED_STATUS, name) - require_field(run, "kind", "bundled-example-strict-original", name) - require_bool(run.get("code_cell_live"), f"{name}.code_cell_live") - require_positive_int(run.get("artifact_size_bytes"), f"{name}.artifact_size_bytes") - require_field(run, "live_code_cell_data_hash_matches_artifact", True, name) - require_hex_hash(run.get("artifact_ckb_data_hash_blake2b"), f"{name}.artifact_ckb_data_hash_blake2b") - require_field(run, "live_code_cell_data_hash", run["artifact_ckb_data_hash_blake2b"], name) - valid_deploy_dry_run = run.get("valid_deploy_dry_run") - require(isinstance(valid_deploy_dry_run, dict), f"{name} missing valid_deploy_dry_run") - require( - isinstance(valid_deploy_dry_run.get("cycles"), str) and valid_deploy_dry_run["cycles"].startswith("0x"), - f"{name} missing hex deploy dry-run cycles", - ) - - final_gate = report.get("final_production_hardening_gate") - require(isinstance(final_gate, dict), "final_production_hardening_gate must be an object") - require_field(final_gate, "status", EXPECTED_STATUS, "final_production_hardening_gate") - require_field(final_gate, "ready", True, "final_production_hardening_gate") - require_field(final_gate, "requires_builder_generated_transactions", False, "final_production_hardening_gate") - require_field(final_gate, "requires_public_builder_contracts", True, "final_production_hardening_gate") - require_field(final_gate, "requires_acceptance_harness_transactions", True, "final_production_hardening_gate") - require_field(final_gate, "requires_measured_cycles", True, "final_production_hardening_gate") - require_field(final_gate, "requires_consensus_serialized_tx_size", True, "final_production_hardening_gate") - require_field(final_gate, "requires_exact_occupied_capacity", True, "final_production_hardening_gate") - require_field(final_gate, "requires_stateful_action_coverage", True, "final_production_hardening_gate") - require_field(final_gate, "production_resource_identity_claim", False, "final_production_hardening_gate") - require_field(final_gate, "resource_identity_evidence_scope", "always-success-fixture-only", "final_production_hardening_gate") - require_field(final_gate, "requires_build_report_live_artifact_linkage", True, "final_production_hardening_gate") - require_empty(final_gate, "failures", "final_production_hardening_gate") - - stateful = onchain.get("stateful_scenarios") - require(isinstance(stateful, dict), "onchain.stateful_scenarios must be an object") - require_field(stateful, "status", EXPECTED_STATUS, "onchain.stateful_scenarios") - require_positive_int(stateful.get("scenario_count"), "onchain.stateful_scenarios.scenario_count") - require_positive_int(stateful.get("step_count"), "onchain.stateful_scenarios.step_count") - require_field( - stateful, - "end_to_end_scenario_count", - len(EXPECTED_END_TO_END_STATEFUL_SCENARIOS), - "onchain.stateful_scenarios", - ) - require_field( - stateful, - "action_branch_scenario_count", - stateful["scenario_count"] - len(EXPECTED_END_TO_END_STATEFUL_SCENARIOS), - "onchain.stateful_scenarios", - ) - coverage = stateful.get("stateful_action_coverage") - require(isinstance(coverage, dict), "onchain.stateful_scenarios.stateful_action_coverage must be an object") - require_field(coverage, "status", EXPECTED_STATUS, "stateful_action_coverage") - require_field(coverage, "required_action_count", EXPECTED_ACTION_COUNT, "stateful_action_coverage") - require_field(coverage, "covered_action_count", EXPECTED_ACTION_COUNT, "stateful_action_coverage") - require_field(coverage, "required_action_ids", EXPECTED_ACTION_IDS, "stateful_action_coverage") - require_field(coverage, "covered_action_ids", EXPECTED_ACTION_IDS, "stateful_action_coverage") - require_empty(coverage, "missing_action_ids", "stateful_action_coverage") - require_empty(coverage, "missing_artifact_ids", "stateful_action_coverage") - require_empty(coverage, "unexpected_artifact_ids", "stateful_action_coverage") - stateful_runs = stateful.get("runs") - require(isinstance(stateful_runs, list) and len(stateful_runs) == stateful["scenario_count"], "stateful scenario runs must match scenario_count") - require( - [run.get("name") for run in stateful_runs[: len(EXPECTED_END_TO_END_STATEFUL_SCENARIOS)]] - == EXPECTED_END_TO_END_STATEFUL_SCENARIOS, - "stateful end-to-end scenario names/order must match the production matrix", - ) - seen_stateful_names: set[str] = set() - main_action_ids: set[str] = set() - branch_action_ids: list[str] = [] - observed_step_count = 0 - for index, stateful_run in enumerate(stateful_runs): - context = f"onchain.stateful_scenarios.runs[{index}]" - require(isinstance(stateful_run, dict), f"{context} must be an object") - name = stateful_run.get("name") - require(isinstance(name, str) and name, f"{context}.name must be a non-empty string") - require(name not in seen_stateful_names, f"duplicate stateful scenario name: {name}") - seen_stateful_names.add(name) - require_field(stateful_run, "status", EXPECTED_STATUS, context) - require_field(stateful_run, "builder_backed", False, context) - require_field(stateful_run, "transaction_origin", "acceptance-python-harness", context) - require_field(stateful_run, "harness_origin", "handwritten-python-acceptance-transaction", context) - require(isinstance(stateful_run.get("acceptance_harness_name"), str) and stateful_run["acceptance_harness_name"], f"{context} missing acceptance_harness_name") - action_ids = stateful_run.get("action_ids") - require(isinstance(action_ids, list) and action_ids, f"{context}.action_ids must be a non-empty list") - require(set(action_ids).issubset(EXPECTED_ACTION_IDS), f"{context}.action_ids contains actions outside the production matrix") - steps = stateful_run.get("steps") - require(isinstance(steps, list) and steps, f"{context}.steps must be a non-empty list") - observed_step_count += len(steps) - if index < len(EXPECTED_END_TO_END_STATEFUL_SCENARIOS): - require_field(stateful_run, "kind", "stateful-scenario", context) - require(len(steps) >= 2, f"{context} end-to-end scenario must contain at least two committed steps") - main_action_ids.update(action_ids) - else: - require_field(stateful_run, "kind", "stateful-action-branch", context) - require(len(action_ids) == 1 and len(steps) == 1, f"{context} branch scenario must bind exactly one action and one step") - branch_action_ids.extend(action_ids) - - for step_index, step in enumerate(steps): - step_context = f"{context}.steps[{step_index}]" - require(isinstance(step, dict), f"{step_context} must be an object") - require(isinstance(step.get("step"), str) and step["step"], f"{step_context}.step must be a non-empty string") - require_field(step, "status", EXPECTED_STATUS, step_context) - dry_run = step.get("dry_run") - require(isinstance(dry_run, dict), f"{step_context}.dry_run must be an object") - require( - isinstance(dry_run.get("cycles"), str) and dry_run["cycles"].startswith("0x"), - f"{step_context}.dry_run.cycles must be a hex quantity", - ) - commit = step.get("commit") - require(isinstance(commit, dict), f"{step_context}.commit must be an object") - require_hex_hash(commit.get("tx_hash"), f"{step_context}.commit.tx_hash") - commit_status = commit.get("status") - require(isinstance(commit_status, dict), f"{step_context}.commit.status must be an object") - require_field(commit_status, "status", "committed", f"{step_context}.commit.status") - constraints = step.get("measured_constraints") - require(isinstance(constraints, dict), f"{step_context}.measured_constraints must be an object") - require_positive_int(constraints.get("measured_cycles"), f"{step_context}.measured_constraints.measured_cycles") - require_positive_int( - constraints.get("consensus_serialized_tx_size_bytes"), - f"{step_context}.measured_constraints.consensus_serialized_tx_size_bytes", - ) - require_positive_int( - constraints.get("occupied_capacity_shannons"), - f"{step_context}.measured_constraints.occupied_capacity_shannons", - ) - require_field(constraints, "capacity_is_sufficient", True, f"{step_context}.measured_constraints") - require_empty(constraints, "under_capacity_output_indexes", f"{step_context}.measured_constraints") - consumed_inputs = step.get("consumed_inputs") - require(isinstance(consumed_inputs, list), f"{step_context}.consumed_inputs must be a list") - require( - all(isinstance(cell, dict) and cell.get("status") != "live" for cell in consumed_inputs), - f"{step_context}.consumed_inputs contains a still-live or malformed cell", - ) - outputs_live = step.get("outputs_live") - require(isinstance(outputs_live, dict), f"{step_context}.outputs_live must be an object") - require(all(value is True for value in outputs_live.values()), f"{step_context}.outputs_live contains a dead output") - - require_field(stateful, "step_count", observed_step_count, "onchain.stateful_scenarios") - expected_branch_ids = sorted(set(EXPECTED_ACTION_IDS) - main_action_ids) - require(sorted(branch_action_ids) == expected_branch_ids, "stateful branch scenarios must cover every action absent from end-to-end flows exactly once") - - runs = all_action_runs(report) - require(len(runs) == EXPECTED_ACTION_COUNT, f"expected {EXPECTED_ACTION_COUNT} action runs, got {len(runs)}") - seen_names: set[str] = set() - for run in runs: - name = run.get("name") - require(isinstance(name, str) and name, "action run is missing name") - require(name not in seen_names, f"duplicate action run name: {name}") - seen_names.add(name) - action = run.get("action") - require(isinstance(action, str) and action, f"{name} is missing action") - require(name.endswith(f":{action}"), f"{name} must end with action suffix :{action}") - require_field(run, "status", EXPECTED_STATUS, name) - require_field(run, "builder_backed", False, name) - require_field(run, "transaction_origin", "acceptance-python-harness", name) - require_field(run, "harness_origin", "handwritten-python-acceptance-transaction", name) - require(isinstance(run.get("acceptance_harness_name"), str) and run["acceptance_harness_name"], f"{name} missing acceptance_harness_name") - require(isinstance(run.get("acceptance_harness_implementation"), str) and run["acceptance_harness_implementation"], f"{name} missing acceptance_harness_implementation") - require_field(run, "public_builder_contract_id", name, name) - require_field(run, "public_builder_contract_verified", True, name) - - code = run.get("code") - require(isinstance(code, dict), f"{name} missing code section") - require_bool(code.get("code_cell_live"), f"{name}.code.code_cell_live") - require_positive_int(code.get("artifact_size_bytes"), f"{name}.code.artifact_size_bytes") - require_field(code, "live_code_cell_data_hash_matches_artifact", True, f"{name}.code") - require_hex_hash(code.get("artifact_ckb_data_hash_blake2b"), f"{name}.code.artifact_ckb_data_hash_blake2b") - require_field(code, "live_code_cell_data_hash", code["artifact_ckb_data_hash_blake2b"], f"{name}.code") - - valid_dry_run = run.get("valid_dry_run") - require(isinstance(valid_dry_run, dict), f"{name} missing valid_dry_run") - require(isinstance(valid_dry_run.get("cycles"), str) and valid_dry_run["cycles"].startswith("0x"), f"{name} missing hex dry-run cycles") - require(isinstance(run.get("valid_commit"), dict), f"{name} missing valid_commit") - - malformed = run.get("malformed_transaction") - require(isinstance(malformed, dict), f"{name} missing malformed_transaction evidence") - require_field(malformed, "status", "rejected", f"{name}.malformed_transaction") - require_field(malformed, "expected_reason_matched", True, f"{name}.malformed_transaction") - require_field(malformed, "policy_or_capacity_reason", False, f"{name}.malformed_transaction") - - measured = run.get("measured_constraints") - require(isinstance(measured, dict), f"{name} missing measured_constraints") - require_field(measured, "cycles_status", "dry-run-measured", f"{name}.measured_constraints") - require_field(measured, "tx_size_status", "measured-by-cellscript-ckb-tx-measure", f"{name}.measured_constraints") - require_field( - measured, - "occupied_capacity_status", - "derived-by-cellscript-ckb-tx-measure", - f"{name}.measured_constraints", - ) - require_positive_int(measured.get("measured_cycles"), f"{name}.measured_constraints.measured_cycles") - require_positive_int( - measured.get("consensus_serialized_tx_size_bytes"), - f"{name}.measured_constraints.consensus_serialized_tx_size_bytes", - ) - occupied = require_positive_int( - measured.get("occupied_capacity_shannons"), - f"{name}.measured_constraints.occupied_capacity_shannons", - ) - output_capacity = require_positive_int( - measured.get("output_capacity_shannons"), - f"{name}.measured_constraints.output_capacity_shannons", - ) - require(output_capacity >= occupied, f"{name} output capacity is below occupied capacity") - output_count = require_positive_int(measured.get("output_count"), f"{name}.measured_constraints.output_count") - output_caps = measured.get("measured_output_capacity_shannons") - output_occupied = measured.get("output_occupied_capacity_shannons") - require(isinstance(output_caps, list), f"{name}.measured_constraints.measured_output_capacity_shannons must be a list") - require(isinstance(output_occupied, list), f"{name}.measured_constraints.output_occupied_capacity_shannons must be a list") - require(len(output_caps) == output_count, f"{name} measured output capacity count does not match output_count") - require(len(output_occupied) == output_count, f"{name} occupied output capacity count does not match output_count") - for index, (cap, occ) in enumerate(zip(output_caps, output_occupied)): - cap_int = require_positive_int(cap, f"{name}.measured_constraints.measured_output_capacity_shannons[{index}]") - occ_int = require_positive_int(occ, f"{name}.measured_constraints.output_occupied_capacity_shannons[{index}]") - require(cap_int >= occ_int, f"{name} output {index} capacity is below occupied capacity") - require(measured.get("capacity_is_sufficient") is True, f"{name} has insufficient capacity") - require(measured.get("under_capacity_output_indexes") == [], f"{name} has under-capacity outputs") - - lock_runs = onchain.get("lock_spend_matrix_runs") - require(isinstance(lock_runs, list), "onchain.lock_spend_matrix_runs must be a list") - lock_names = [run.get("name") for run in lock_runs if isinstance(run, dict)] - require( - sorted(lock_names) == sorted(EXPECTED_LOCK_NAMES) and len(lock_names) == EXPECTED_LOCK_COUNT, - f"lock spend matrix must cover {EXPECTED_LOCK_NAMES!r}, got {lock_names!r}", - ) - require(len(set(lock_names)) == len(lock_names), f"lock spend matrix must not contain duplicates, got {lock_names!r}") - for run in lock_runs: - require(isinstance(run, dict), "lock spend matrix entries must be objects") - name = run.get("name") - require(isinstance(name, str) and name, "lock run is missing name") - lock = run.get("lock") - require(isinstance(lock, str) and lock, f"{name} is missing lock") - require(name.endswith(f":{lock}"), f"{name} must end with lock suffix :{lock}") - require_field(run, "status", EXPECTED_STATUS, name) - require_field(run, "builder_backed", False, name) - require_field(run, "transaction_origin", "acceptance-python-harness", name) - require_field(run, "harness_origin", "handwritten-python-acceptance-transaction", name) - require(isinstance(run.get("acceptance_harness_name"), str) and run["acceptance_harness_name"], f"{name} missing acceptance_harness_name") - require(isinstance(run.get("acceptance_harness_implementation"), str) and run["acceptance_harness_implementation"], f"{name} missing acceptance_harness_implementation") - - code = run.get("code") - require(isinstance(code, dict), f"{name} missing code section") - require_bool(code.get("code_cell_live"), f"{name}.code.code_cell_live") - require_positive_int(code.get("artifact_size_bytes"), f"{name}.code.artifact_size_bytes") - require_field(code, "live_code_cell_data_hash_matches_artifact", True, f"{name}.code") - require_hex_hash(code.get("artifact_ckb_data_hash_blake2b"), f"{name}.code.artifact_ckb_data_hash_blake2b") - require_field(code, "live_code_cell_data_hash", code["artifact_ckb_data_hash_blake2b"], f"{name}.code") - - valid_spend = run.get("valid_spend") - require(isinstance(valid_spend, dict), f"{name} missing valid_spend evidence") - require_field(valid_spend, "status", EXPECTED_STATUS, f"{name}.valid_spend") - require_field(valid_spend, "output_live", True, f"{name}.valid_spend") - valid_dry_run = valid_spend.get("dry_run") - require(isinstance(valid_dry_run, dict), f"{name}.valid_spend missing dry_run") - require( - isinstance(valid_dry_run.get("cycles"), str) and valid_dry_run["cycles"].startswith("0x"), - f"{name}.valid_spend missing hex dry-run cycles", - ) - require(isinstance(valid_spend.get("commit"), dict), f"{name}.valid_spend missing commit") - - invalid_spend = run.get("invalid_spend") - require(isinstance(invalid_spend, dict), f"{name} missing invalid_spend evidence") - require_field(invalid_spend, "status", "rejected", f"{name}.invalid_spend") - rejection = invalid_spend.get("rejection") - require(isinstance(rejection, dict), f"{name}.invalid_spend missing rejection") - require_field(rejection, "status", "rejected", f"{name}.invalid_spend.rejection") - require_field(rejection, "expected_reason_matched", True, f"{name}.invalid_spend.rejection") - require_field(rejection, "policy_or_capacity_reason", False, f"{name}.invalid_spend.rejection") - reason = rejection.get("reason") - require(isinstance(reason, str) and reason, f"{name}.invalid_spend.rejection missing reason") - for fragment in ("source: Inputs[0].Lock", "ValidationFailure", "error code 5"): - require(fragment in reason, f"{name}.invalid_spend.rejection must show lock predicate error fragment {fragment!r}") - live_after_rejection = invalid_spend.get("input_cells_live_after_rejection") - require( - isinstance(live_after_rejection, list) and live_after_rejection and all(value is True for value in live_after_rejection), - f"{name}.invalid_spend must keep rejected input cells live", - ) - - measured = run.get("measured_constraints") - require(isinstance(measured, dict), f"{name} missing measured_constraints") - require_field(measured, "cycles_status", "dry-run-measured", f"{name}.measured_constraints") - require_field(measured, "tx_size_status", "measured-by-cellscript-ckb-tx-measure", f"{name}.measured_constraints") - require_field( - measured, - "occupied_capacity_status", - "derived-by-cellscript-ckb-tx-measure", - f"{name}.measured_constraints", - ) - require_positive_int(measured.get("measured_cycles"), f"{name}.measured_constraints.measured_cycles") - require_positive_int( - measured.get("consensus_serialized_tx_size_bytes"), - f"{name}.measured_constraints.consensus_serialized_tx_size_bytes", - ) - occupied = require_positive_int( - measured.get("occupied_capacity_shannons"), - f"{name}.measured_constraints.occupied_capacity_shannons", - ) - output_capacity = require_positive_int( - measured.get("output_capacity_shannons"), - f"{name}.measured_constraints.output_capacity_shannons", - ) - require(output_capacity >= occupied, f"{name} output capacity is below occupied capacity") - require(measured.get("capacity_is_sufficient") is True, f"{name} has insufficient capacity") - require(measured.get("under_capacity_output_indexes") == [], f"{name} has under-capacity outputs") - - -def main() -> int: - parser = argparse.ArgumentParser( - description="Validate production CKB CellScript acceptance evidence emitted by CellScript scripts/ckb_cellscript_acceptance.sh.", - ) - parser.add_argument("report", type=Path, help="Path to ckb-cellscript-acceptance-report.json") - parser.add_argument( - "--repo-root", - type=Path, - default=Path(__file__).resolve().parents[1], - help="CellScript checkout used to recompute source provenance. Defaults to this script's repository.", - ) - parser.add_argument( - "--compile-only", - action="store_true", - help="Only validate strict compile and scoped-entry production gates. This is not sufficient for external release.", - ) - args = parser.parse_args() - - report_path = args.report.resolve() - repo_root = args.repo_root.resolve() - report = load_json(report_path) - validate_source_provenance(report, repo_root) - validate_public_builder_contracts(report) - validate_compile_gate(report, compile_only=args.compile_only) - if not args.compile_only: - validate_ckb_runtime_provenance(report, repo_root, report_path.parent) - validate_onchain_gate(report) - - mode = "compile-only " if args.compile_only else "" - print(f"valid CKB CellScript {mode}production evidence: {report_path}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/services/registry-api/.dockerignore b/services/registry-api/.dockerignore new file mode 100644 index 00000000..051ddd27 --- /dev/null +++ b/services/registry-api/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +dist-node +.env +wrangler.toml +test +npm-debug.log* diff --git a/services/registry-api/Dockerfile b/services/registry-api/Dockerfile new file mode 100644 index 00000000..a557af45 --- /dev/null +++ b/services/registry-api/Dockerfile @@ -0,0 +1,23 @@ +FROM node:22-bookworm-slim AS build + +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY tsconfig.json ./ +COPY src ./src +RUN npm run check && npm run build:node + +FROM node:22-bookworm-slim AS runtime + +ENV NODE_ENV=production \ + NODE_OPTIONS=--enable-source-maps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev && npm cache clean --force +COPY --from=build /app/dist-node ./dist-node +COPY migrations ./migrations +COPY scripts ./scripts + +USER 1000:101 +EXPOSE 8787 +CMD ["sh", "-c", "node scripts/migrate.mjs && exec node dist-node/server.mjs"] diff --git a/services/registry-api/Dockerfile.verifier b/services/registry-api/Dockerfile.verifier new file mode 100644 index 00000000..1ace43aa --- /dev/null +++ b/services/registry-api/Dockerfile.verifier @@ -0,0 +1,33 @@ +FROM rust:1.97.1-bookworm AS rust-build + +WORKDIR /source +COPY . . +RUN cargo build --locked --release --manifest-path services/registry-verifier/Cargo.toml +RUN cargo build --locked --release --manifest-path services/registry-artifact-verifier/Cargo.toml + +FROM node:22-bookworm-slim AS node-build + +WORKDIR /app +COPY services/registry-api/package.json services/registry-api/package-lock.json ./ +RUN npm ci +COPY services/registry-api/tsconfig.json ./ +COPY services/registry-api/src ./src +RUN npm run check && npm run build:node:verifier + +FROM node:22-bookworm-slim AS runtime + +ENV NODE_ENV=production \ + NODE_OPTIONS=--enable-source-maps \ + REGISTRY_VERIFIER_BINARY=/usr/local/bin/cellscript-registry-verify \ + REGISTRY_ARTIFACT_VERIFIER_BINARY=/usr/local/bin/cellscript-registry-artifact-verify \ + HOME=/tmp/verifier-home \ + XDG_CACHE_HOME=/tmp/verifier-cache +WORKDIR /app +COPY services/registry-api/package.json services/registry-api/package-lock.json ./ +RUN npm ci --omit=dev && npm cache clean --force +COPY --from=node-build /app/dist-node/verification-worker.mjs* ./dist-node/ +COPY --from=rust-build /source/services/registry-verifier/target/release/cellscript-registry-verify /usr/local/bin/cellscript-registry-verify +COPY --from=rust-build /source/services/registry-artifact-verifier/target/release/cellscript-registry-artifact-verify /usr/local/bin/cellscript-registry-artifact-verify + +USER 1000:101 +CMD ["node", "dist-node/verification-worker.mjs"] diff --git a/services/registry-api/README.md b/services/registry-api/README.md index ded70db9..81731554 100644 --- a/services/registry-api/README.md +++ b/services/registry-api/README.md @@ -1,256 +1,588 @@ # CellScript Registry API -Cloudflare Workers write API for the public CellScript registry. - -This service is the production write boundary behind: - -- `https://api.registry.cellscript.dev` for authenticated writes; -- `https://registry.cellscript.dev` for static/CDN reads. - -The same Worker can also serve `https://registry.cellscript.dev/packages/*` -directly from R2, while the rest of the website may stay on Pages/static -hosting. - -It intentionally does not use D1 as the primary database. Runtime state is -stored in Neon Postgres through Cloudflare Hyperdrive, while immutable source -snapshots and static registry read objects are stored in R2. - -## Implemented Boundaries - -- JoyID-rooted capability authorisation with `@joyid/ckb` `verifySignature`. -- Challenge binding against canonical `cellscript-registry-auth-v1` payloads. -- `principal_type = joyid_ckb` only. -- `principal_id` binding against the JoyID signer key; display addresses are - not accepted as ACL keys. -- Scoped capability records with expiry and revocation fields. -- Namespace claim path with reserved/short-name review state. -- Seeded reserved namespace list for core ecosystem, hostname, security, and - support namespaces. -- Namespace claim cooldown for newly claimed namespaces by the same JoyID - principal; invalid JoyID signatures do not consume principal quota. -- Publish admission path for source packages. -- Namespace owner ACL check before publish admission. -- P-256 capability-signature verification for daily publish payloads. -- One-time signed nonce consumption for capability creation, capability - revocation, and package publish. -- `Idempotency-Key` support for package publish retries. A completed matching - request returns the stored response with `x-idempotency-status: replayed`; the - same key with different request content is rejected. If admission fails after - a publish key is reserved but before the version is accepted, the processing - reservation is released. -- Existing package versions are rejected before source snapshot writes. -- Immutable R2 source snapshot and static package-version JSON writes before - package-version admission; if the static read object cannot be persisted, the - version is not accepted into the registry store. -- Static package-version JSON write to R2 at - `/packages/:namespace/:name/versions/:version.json`; this is the direct URL - served by `https://registry.cellscript.dev`. -- Initial package-version status: `source_published`. -- Per-IP, per-ASN, per-principal, per-capability, and per-package quota hooks. -- Future `policy_hooks` and `bond_policy_hooks` tables for later bond or - refundable-deposit policies; no on-chain fee or bond is enforced now. -- Token-gated admin operations for reserved namespaces, namespace review - status, and package-version status transitions. -- Suppressive package-version admin transitions (`deprecated`, `yanked`, - `quarantined`) update the static read object before changing the write-store - status, so public reads fail conservative during incident response. -- Token-gated audit-event read path for review, incident response, and - production debugging. -- Audit/event log records for capability, namespace, auth failure, rate-limit, - and publish transitions, including admin review/quarantine/yank overrides. -- Successful capability use updates `last_used_at` and writes a - `capability.used` audit event. -- Scheduled cleanup for expired nonces, idempotency records, and old quota - events. +Typed production API for the public CellScript artifact Registry. The same +application runs as a Cloudflare Worker or through the bundled Node HTTP +adapter. + +- `https://api.registry.cellscript.dev` is the authenticated write and dynamic + read boundary. +- `https://registry.cellscript.dev` serves immutable bundles and static release + JSON independently from the write database. + +The Pudge test environment is a separate, ephemeral service: + +- `https://api.testnet.registry.cellscript.dev` is the sandbox API; +- `https://objects.testnet.registry.cellscript.dev` is its object origin; +- `https://testnet.registry.cellscript.dev/registry` is its `noindex` UI. + +It uses a different Postgres volume, object volume, signing origin, wallet +storage key, RPC identity, and Compose project. Do not put a network selector in +the production Registry. `REGISTRY_ENVIRONMENT=testnet-sandbox` requires the +dedicated origins and accepts only a Pudge/Testnet RPC. Unknown environments +fail closed. + +Postgres is authoritative for publisher capabilities, namespace ownership, +artifact releases, orthogonal release states, evidence, jobs, idempotency, and +audit events. R2 or the filesystem adapter stores immutable content and static +read objects. + +## Artifact Contract + +The API has one public resource family: `/v1/artifacts`. Every release declares +an artifact descriptor: + +```ts +{ + kind: "source_library" | "profile_library" | "runtime_verifier" | + "deployable_contract" | "reproducible_binary" | "template"; + profile: "cellscript_source" | "ckb_executable" | + "reproducible_build" | "copy_material"; + consumption_mode: "dependency" | "deployment" | "tcb" | "copy"; + language: "cellscript" | "rust" | "c" | "javascript" | + "other" | "unspecified"; +} +``` + +Profile/kind/language/consumption combinations are closed and validated. The +single extension point is the exported +`cellscript-registry-profile-catalog-v1`: every profile names a versioned +validator, allowed kind/language/consumption contracts, whether a profile +contract is required, and a `dependency` or `non_resolving` capability. Only +`cellscript_source` is dependency-resolving. Unknown profiles and attempts to +use CKB executables, reproducible builds, or copy material as dependencies fail +closed. + +The +generic profiles additionally carry a closed +`cellscript-registry-profile-contract-v1` object. Admission, the publisher CLI, +and the isolated verifier independently canonicalize it, bind its hash, reject +unknown fields, and verify the typed build/security/CKB/verifier/reproduction +or copy fields against immutable object hashes. The independent verifier then +applies a profile-specific object contract: + +- `cellscript_source`: compile the canonical CellScript snapshot; +- `ckb_executable`: hash-bind source, executable, ABI, and any optional + reproducible build recipe; when a CellScript bundle supplies one verified + sidecar, require the complete metadata/lowering-record/source-map set and run + the compiler-independent structural checker; +- `reproducible_build`: hash-bind source, executable, and build recipe, then + require external reproducibility evidence; +- `copy_material`: hash-bind a `cellscript-template-file-map-v1` source and + never treat it as a dependency. + +A deployable `ckb_executable` Lock Script may additionally carry the closed +`cellscript-registry-ls-idl-interface-v1` contract. Admission requires exactly +one ABI object, validates the bounded LS-IDL 0.1 document, hashes the original +ABI bytes with SHA-256, and checks that digest against both the interface +contract and the executable's final 32 bytes. The response path returns those +stored bytes directly; it does not parse and reserialise JSON. See +[`docs/CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md`](../../docs/CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md). + +Release state is split across: + +```text +verification_status = pending | hash_bound | verified | evidence_required | rejected +deployment_status = not_applicable | undeployed | deployed | chain_verified +availability_status = active | deprecated | yanked | quarantined +``` + +Publisher input can create only the initial states. Verification and deployment +states are derived from accepted evidence. Availability is the operator safety +axis and does not rewrite identity or evidence. + +Legacy `status` values such as `source_published` and `verified_build` are a +compatibility projection of those three axes, not an additional trust claim. +New clients must read `verification_status`, `deployment_status`, and +`availability_status` independently; in particular, `hash_bound` means object +integrity only, not semantic correctness or security review. + +For a reproducible profile, `verified_build` with level `evidence_required` is +only the hash-bound predecessor. An admin promotion to `reproduced_build` +requires two to sixteen P-256-signed `cellscript-reproduction-report-v2` +reports. Each builder ID, public key, and trust domain must be distinct; every +builder must match `REGISTRY_REPRODUCER_POLICY_JSON`; and the reports must span +the policy's minimum number of trust domains. Reports bind the signed +environment, source hash, build-recipe hash, artifact hash, build-log hash, +timestamp, and predecessor evidence. Deployment admission rejects a +reproducible artifact until this transition succeeds. Accepted evidence also +stores the canonical policy SHA-256 and the minimum trust-domain threshold used +at acceptance time. + +Trust-domain independence is an operator-governance fact, not something the API +can infer from two different strings. Production policy must use builders under +separate administrative control and separate private-key custody. Two keys +created or controlled by the same Registry operator must not be labelled as two +independent trust domains. `/ready` validates policy shape, key importability, +and configured threshold only; it is not an organizational-independence +attestation. + +Each independent operator can create its own key and public enrollment record +without contacting the Registry write API: + +```bash +cellc auth reproducer create \ + --builder-id \ + --trust-domain \ + --json > builder-enrollment.json +``` + +The operator sends only `policy_builder` to the Registry administrator. By +default the private key remains in that builder's OS keychain. The explicit +`--private-key-output ` mode exists on Unix for transfer into that +builder's CI secret manager; it creates a new mode-0600 PKCS#8-base64 file and +refuses to overwrite an existing path. ## Endpoints ```text GET /health GET /ready -GET /packages/:namespace/:name/versions/:version.json +GET /artifacts/:namespace/:name/releases/:release.json +GET /v1/artifacts +GET /v1/artifacts/:namespace/:name +GET /v1/artifacts/:namespace/:name/releases/:release/evidence +GET /v1/artifacts/:namespace/:name/releases/:release/commitment +GET /v1/ckb/scripts/:code_hash/interfaces/ls-idl?network=:network&hash_type=:hash_type[&data_hash=:data_hash] +GET /idl/:code_hash +POST /v1/artifacts/:namespace/:name/releases +POST /v1/artifacts/:namespace/:name/releases/:release/deployments +POST /v1/artifacts/:namespace/:name/releases/:release/availability + POST /v1/capabilities +GET /v1/capabilities/:key_id/check?namespace=:namespace&name=:name POST /v1/capabilities/:key_id/revoke POST /v1/namespaces/claim -POST /v1/packages/:namespace/:name/versions +POST /v1/authorisation-sessions +GET /v1/authorisation-sessions/:session_id +POST /v1/authorisation-sessions/:session_id/challenge +POST /v1/authorisation-sessions/:session_id/complete + GET /v1/admin/audit-events +GET /v1/admin/verification-queue +POST /v1/admin/verification-jobs/:job_id/retry POST /v1/admin/reserved-namespaces POST /v1/admin/namespaces/:namespace/status -POST /v1/admin/packages/:namespace/:name/versions/:version/status +POST /v1/admin/artifacts/:namespace/:name/releases/:release/availability +POST /v1/admin/artifacts/:namespace/:name/releases/:release/promote ``` -## Deploy Setup - -1. Create a Neon Postgres database. -2. Apply database migrations: +List filters are `q`, `namespace`, `kind`, `verification`, `deployment`, +`availability`, `limit`, and `offset`. Quarantined releases are absent from +public detail and evidence reads. + +The canonical LS-IDL lookup returns +`application/vnd.ckb.ls-idl+json` plus digest, coordinate, commitment, and +verification headers. `data_hash` is required for `hash_type=type`; ambiguous +matches return `409`. `/idl/:code_hash` is a compatibility route for existing +clients and returns the same exact raw bytes. + +## Publisher Authorisation + +Wallet-rooted capability authorisation supports: + +- JoyID signatures under `principal_type = joyid_ckb`; +- recoverable CKB secp256k1 message signatures under + `principal_type = ckb_secp256k1`. + +The signature public key is bound to `principal_id`; a display address is not +an ACL key. The delegated P-256 capability is expiring, revocable, and stored +separately from the wallet root. Namespace ownership must match the capability +principal. Each write family has its own exact-coordinate or namespace-wide +scope: + +- `publish:namespace/name` admits immutable releases; +- `deployment:namespace/name` attaches verified CKB deployment evidence; +- `availability:namespace/name` deprecates, yanks, or restores a release. + +Each form also accepts `namespace/*`. Possessing one action does not imply either +of the others. + +`POST /v1/authorisation-sessions/:session_id/complete` is idempotent after a +successful completion. Its first successful call commits nonce use, capability +registration, namespace claim or review state, session completion, and audit +events in one store transaction. A concurrent call returns the committed +session instead of creating a second capability use. Expired sessions, stale +challenge tokens, and conflicting namespace owners leave the session pending +and create none of those records. The 15-minute expiry applies only while a +session is pending. `authorised` and `review_pending` results remain readable +to the polling CLI for 24 hours, then cleanup removes them; this lets a CLI +recover a wallet approval committed immediately before the approval window +closed. + +For an interactive first publish, `cellc publish --authorise` creates a +15-minute, exact-coordinate browser session and opens the matching Registry +site. The CLI generates the delegated P-256 key first and keeps its private key +in the OS keychain as pending before opening the browser, then promotes it to +active only after `authorised` or `review_pending` returns the same key ID. +Only Registry-confirmed cancellation or pending-session expiry removes the +pending entry. A local polling deadline performs one final Registry read and +otherwise leaves the pending key recoverable through the key ID printed before +the browser opens. The API stores only the public key plus hashes of separate +one-time CLI-polling and browser-approval tokens. The browser token travels in +the URL fragment, not the query string, so it is absent from HTTP logs and +Referer headers; browser reads never return the polling token or resulting +capability key ID. After the wallet approves the +server-built challenge, the Registry records the capability, claims the +namespace, and the polling CLI continues the original publish automatically. +Use `--no-open` to print the browser URL without launching it. + +The explicit commands below remain the auditable/manual route for CI, external +wallet signing, and recovery: ```bash -DATABASE_URL='postgres://...' npm run migrate +cellc auth capability create --principal-type --principal-id \ + --scope publish:ns/name \ + --expires 90d --json > capability-payload.json +# Sign the canonical payload in a supported CKB wallet. +cellc auth capability submit --payload capability-payload.json --wallet-signature wallet-signature.json +cellc auth namespace claim --namespace ns --payload capability-payload.json --wallet-signature wallet-signature.json ``` -3. Create a Cloudflare R2 bucket for source snapshots and static registry JSON - objects. -4. Create a Cloudflare Hyperdrive config pointing at Neon. -5. Copy `wrangler.example.toml` to `wrangler.toml`. -6. Replace `REPLACE_WITH_CLOUDFLARE_HYPERDRIVE_ID`. -7. Confirm `[triggers]` is enabled in `wrangler.toml`; the example schedules a - cleanup run every 15 minutes. -8. Configure admin auth as a Cloudflare secret: +The browser defaults to the single exact `publish:ns/name` scope. Add +`deployment:ns/name` or `availability:ns/name` only when the delegated key must +perform those later maintenance actions; they are not required to publish a +release. + +The read-only capability check returns public status, expiry and scopes plus an +artifact-specific evaluation of publish/deployment/availability access and +namespace ownership. It never returns the delegated public key, wallet +signature or capability signature. The Submit UI uses this endpoint before it +reveals a publish command for either a newly authorised or existing cellc key. + +Capability registration does not silently claim a namespace. Publish remains +blocked until the claim is active. Signed nonces are one-use; publish requests +also use an `Idempotency-Key` so exact retries replay safely and conflicting +content fails. + +The browser wallet directory lists Neuron, JoyID, imToken, CKBull, SafePal, +Ledger, imKey, OneKey, UTXO Global, Rei Wallet, Gate, and QuantumPurse. Runtime +connectivity is determined by CCC discovery. Directory entries without a live +connector use the external signed-payload handoff and never bypass backend +signature verification. Production accepts only mainnet authorisation and +deployment evidence. The isolated Pudge Sandbox accepts only testnet evidence; +the two origins make wallet challenges and capability signatures non-replayable +across environments. + +## Pudge Sandbox Retention + +Every sandbox release stores `registry_environment = testnet-sandbox`, +`network = testnet`, `expires_at = created_at + 72h`, and +`purge_after = expires_at + 24h`. Public SQL and in-memory reads filter by +`expires_at` even if maintenance is delayed. At expiry, the version-addressed +static JSON is deleted; after the grace period, a source object is deleted only +when no non-expired release references its snapshot hash. Database identity and +audit rows remain as tombstones so abuse and replay investigations are not +erased. Reads never extend TTL. + +The sandbox additionally limits a wallet principal to 20 accepted publish +attempts per 24 hours and one package coordinate to five; the ordinary IP, +capability, namespace-cooldown, request-size, and snapshot-size controls still +apply. + +This policy cannot delete Pudge chain history or consume a deployed code Cell. +It only removes the Registry index and its off-chain object bytes. + +## Release Admission + +Daily publish signs canonical JSON for: -```bash -npx wrangler secret put REGISTRY_ADMIN_TOKEN --config wrangler.toml +```text +cellscript-registry-publish-v1 / publish ``` -9. Deploy with: +Admission requires: -```bash -npm install -npm run check -npm test -npm run build -npx wrangler deploy --config wrangler.toml -``` +- an active, unexpired, unrevoked capability with matching scope; +- an active namespace owned by the same principal; +- matching route, signed payload, artifact descriptor, coordinate, release, + source hash, manifest hash, and single-release nested entry; +- a valid capability signature and unused nonce; +- a new release coordinate; +- a non-empty immutable snapshot/bundle no larger than 5 MiB; +- successful immutable-bundle and initial static-object writes. + +Generic artifact profile contracts are closed and hash-bound. In particular, +an `audited` security declaration requires an immutable `audit_report` bundle +object bound by `security.audit_report_hash`; the isolated verifier recomputes +that hash before it emits evidence. -`wrangler.example.toml` is intentionally safe to commit. The real -`wrangler.toml` should not contain secrets; secrets must be configured through -Cloudflare bindings/secrets. +An LS-IDL profile also requires `artifact.kind = deployable_contract`, +`artifact.profile = ckb_executable`, `consumption_mode = deployment`, and +`profile_contract.ckb.script_role = lock`. Both the compiler-backed worker and +the artifact-only verifier recompute the raw ABI SHA-256 and executable suffix; +neither accepts a detached or JSON-equivalent-but-byte-different interface. -`npm run migrate` creates and uses a local `schema_migrations` table. Re-running -it is safe; already-applied migration files are skipped. +The database transaction stores the release, job, capability use, audit event, +nonce, and completed idempotency record. The verifier job is created in the +same transaction. An admission response does not claim verification. -`GET /health` is a liveness check. `GET /ready` is the production readiness -check and returns `503` until Hyperdrive, R2, and `REGISTRY_ADMIN_TOKEN` are all -configured. `NAMESPACE_CLAIM_COOLDOWN_SECONDS` defaults to `3600`; lower it only -for controlled staging tests. +CellScript packages publish with `cellc publish`; profile libraries add +`--artifact-kind profile_library`. Other artifacts publish with: -## Admin Governance Boundary +```bash +cellc publish --artifact-manifest Artifact.toml --dry-run +cellc publish --artifact-manifest Artifact.toml +``` -Admin operations require `Authorization: Bearer ` or -`x-registry-admin-token`. The optional `x-registry-admin-actor` header is stored -in audit logs so manual review, reserved namespace changes, quarantine, yanks, -deprecations, and verification promotions are attributable. +`CELLSCRIPT_REGISTRY_API_URL` overrides the API base URL. +`CELLSCRIPT_CAPABILITY_PRIVATE_KEY_PKCS8_B64` supplies the delegated key in CI. +`CELLSCRIPT_REGISTRY_IDEMPOTENCY_KEY` pins the exact retry key. -Supported package-version status transitions through the admin API are: +## Network-Bound Deployment Evidence + +Executable publication begins at `deployment_status = undeployed`. A publisher +records a deployment by signing canonical JSON for: ```text -source_published -indexed_pending -verified_build -deployed -deprecated -yanked -quarantined +cellscript-registry-deployment / record_deployment ``` -Audit events can be queried with: +The request must identify the network fixed by the Registry environment +(`mainnet` in production, `testnet` in the Pudge Sandbox), the published +executable hash, equal Cell data hash, code hash, hash type, dep type, and +OutPoint. Prior verified-build evidence is mandatory. + +The API confirms the configured RPC chain identity, calls +`get_live_cell(out_point, true, false)` to prove the OutPoint is live, then +reads `get_transaction(tx_hash).tx_status` to require a committed creation +transaction and obtain the block hash used for confirmation counting. It fails +closed unless the Cell is live and its data hash equals the published +executable. For `hash_type = type`, it serializes the returned Type Script with +Molecule and verifies its CKB Script hash against `code_hash`. Data-hash modes +require `code_hash` to equal the data hash. The service does not depend on the +proxy-specific `get_live_cell.block_hash` extension. Success appends +hash-addressed evidence and sets only `deployment_status = chain_verified`. + +`CKB_RPC_URL` configures the environment RPC. `CKB_MAINNET_RPC_URL` remains a +production compatibility alias. The Docker deployment sets +`CKB_RPC_MAX_RESPONSE_BYTES=8388608`: canonical secp256k1 DepGroup validation +must read the genesis data Cell, whose JSON-RPC hex encoding exceeds the +conservative 2 MiB library default. The API still enforces its 8 MiB hard +ceiling and bounded RPC timeout. + +## Registry Chain Commitments + +After deployment evidence exists, the public commitment endpoint returns the +canonical payload, `CSREGv1 || commitment_hash` Cell data, and—when fully +configured—a mainnet transaction intent containing the fixed output Lock, Type +Script, data, and both required code CellDeps. The publisher's wallet supplies +capacity, inputs, change, fee, witnesses, signatures, and broadcast. + +The four Script configuration values are all-or-nothing: ```text -GET /v1/admin/audit-events?event_type=namespace.claimed&namespace=cellscript&limit=50 +REGISTRY_TYPE_SCRIPT_JSON +REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON +REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON +REGISTRY_COMMITMENT_LOCK_CELL_DEP_JSON ``` -The endpoint requires the same admin token and supports filters for -`event_type`, `principal_type`, `principal_id`, `namespace`, `name`, `version`, -`before`, and `limit`. `limit` is capped at 200. - -## Capability Registration And Revocation +In `ENVIRONMENT=production`, configuration is additionally pinned to the +tracked immutable Registry Type Script release in +`contracts/registry-type-script/release-manifest.json`. Its Type args must be +the CKB Script hash of the complete custody Lock, the custody Lock must be the +mainnet `secp256k1_blake160_sighash_all` Script with 20-byte signer args, and +its CellDep must be the canonical genesis DepGroup. The Type Script requires a +custody-locked input for creation as well as update/destruction, so merely +creating an output addressed to the Registry cannot forge an official +commitment. + +`CKB_REGISTRY_SCAN_MAX_CELLS` bounds the scheduled indexer scan (default 1000, +allowed range 100–10000). `CKB_MIN_CONFIRMATIONS` defaults to 24 and applies to +deployment Cells, commitment Cells, and both configured Script code CellDeps. +Maintenance queries exact Type Script matches with a `CSREGv1` data prefix, +verifies the configured commitment Lock, and reconciles current lifecycle +state. A matching sufficiently confirmed live Cell promotes to +`on_chain_committed`; a spent or immature commitment returns to `deployed`; and +a stale deployment returns to `verification_status = verified` with +`deployment_status = undeployed` (projected as `verified_build`). Historical +evidence is retained. + +Leaving all four Script values unset deliberately disables transaction-intent +construction and chain reconciliation; maintenance then clears any prior +current-commitment pointer because it can no longer re-observe that claim. +Setting only some of them is a service misconfiguration. Invalid, spent, or insufficiently confirmed code CellDeps +also fail readiness. Deploying and pinning the canonical mainnet Registry Type +and commitment Lock Scripts remains an operator action; checked-in code does +not itself prove that a public commitment exists. + +### Commitment custody boundary and incident response + +The currently pinned production policy uses one standard +`secp256k1_blake160_sighash_all` custody Lock. This is deliberately simple, but +it is a single-key trust boundary: whoever can satisfy that Lock can create, +replace, or destroy commitment Cells. The Type Script has no independent +multisig, timelock, or revocation mechanism, and the API never holds that +private key. Do not describe a commitment as consensus over Registry +operators; it is an attributable statement by the configured custody key. + +Operators must keep the custody key outside the API and verifier hosts, review +the complete transaction intent in the signing wallet, monitor the configured +Type Script for unexpected spends, and retain the prior commitment evidence in +the Registry audit store. On suspected compromise, stop issuing commitment +intents, remove the four commitment configuration values from traffic-serving +instances, preserve the last observed Cells and audit events, rotate to a new +custody Lock and therefore a new Type Script identity, and publish that +transition explicitly. Rotating the 20-byte signer args changes the custody +Script hash embedded in Type args; it is not an in-place key revocation. + +## Verification Worker + +The leased Postgres queue uses `FOR UPDATE SKIP LOCKED`, three-attempt bounded +retry/dead-letter handling, crash recovery, and a static-publication checkpoint. +The verifier subprocess has timeout, output, CPU, memory, process, capability, +filesystem, and temporary-storage bounds. + +Verifier rejection output uses stable machine-readable codes. Current boundary +codes include `invalid_arguments`, `snapshot_unavailable`, `snapshot_invalid`, +`snapshot_authentication_failed`, `unsupported_profile`, +`artifact_identity_mismatch`, `identity_hash_mismatch`, +`cellscript_compilation_failed`, `artifact_bundle_invalid`, +`profile_contract_invalid`, `manifest_invalid`, and +`verifier_internal_error`. The Node worker preserves terminal verifier codes in +the job record; transport, timeout, malformed-output, and store failures remain +retryable infrastructure errors. + +For CellScript source, the verifier compiles the authenticated snapshot using +the current real compiler. For generic artifact bundles it validates the +coordinate/profile and required objects, recomputes all hashes, and emits the +profile-specific verification level. Generic CKB bundles remain `hash_bound`. +A CKB bundle that supplies the complete compile metadata, lowering record, and +source map is processed by the separate least-privilege artifact worker and +may become `structurally_verified`; checker version, policy, and report hash +are persisted. Partial sidecar sets fail closed. Evidence insertion and the job +publishing checkpoint commit atomically; a crash after that point retries only +the static object write. + +Queue operations require the admin token: -`cellc auth capability create` only creates the local delegated key and prints -the JoyID challenge. It does not register the key until the JoyID-signed payload -is submitted to the write API: - -```bash -cellc auth capability create --principal-id --scope publish:ns/pkg --expires 90d --json > capability-payload.json -# Sign capability-payload.json with the production JoyID path exposed through CCC. -cellc auth capability submit --payload capability-payload.json --joyid-signature joyid-signature.json +```text +GET /v1/admin/verification-queue +POST /v1/admin/verification-jobs/:job_id/retry ``` -The registry submit page can sign the same payload through the CCC JoyID CKB -signer and submit it directly to `/v1/capabilities`. The signed response can -also be copied as `joyid-signature.json` for the CLI submit path. +## Admin Boundary -The submit page derives the preferred `principal_id` from the connected JoyID -signer and exposes a copy action. The API verifies that the JoyID signature's -public key and key type match the `principal_id` embedded in the payload before -recording the capability. +Admin requests require `Authorization: Bearer ` or +`x-registry-admin-token`. `x-registry-admin-actor` is stored in audit events. -Capability revocation follows the same challenge/submit shape so that the -revocation is also bound to the JoyID root principal: +The generic availability endpoint accepts only `active`, `deprecated`, +`yanked`, or `quarantined`. It cannot manufacture verification or deployment +claims. Evidence-specific promotions validate required hashes and predecessor +evidence. Ordinary verified-build promotion is performed by the automatic +worker; the token-gated promotion path is for attributable recovery and +operations. -```bash -cellc auth capability revoke --principal-id --capability-key-id --json > revoke-payload.json -# Sign revoke-payload.json with JoyID. -cellc auth capability revoke --payload revoke-payload.json --joyid-signature joyid-signature.json --reason "rotate delegated key" -``` +Audit events support filters for event type, principal, namespace, name, +release, time cursor, and bounded limit. -## Publish Payload Boundary +## Self-hosted Production -Capability creation signs the canonical JSON form of: +The checked-in stack uses Postgres 17, the Node 22 adapter, an isolated Rust +verification worker, a shared object volume, and read-only nginx. TLS is +terminated outside the compose stack. Build immutable linux/amd64 API and +verifier images before transferring them to production; do not compile the +full Rust verifier on a resource-shared production host. -```text -cellscript-registry-auth-v1 / authorize_capability +```bash +cp deploy/.env.example deploy/.env +chmod 600 deploy/.env +docker compose --env-file deploy/.env -f deploy/docker-compose.production.yml config +docker compose --env-file deploy/.env -f deploy/docker-compose.production.yml up -d --no-build ``` -Daily publish signs the canonical JSON form of: +Required runtime configuration: ```text -cellscript-registry-publish-v1 / publish +DATABASE_URL +REGISTRY_OBJECTS_DIR +REGISTRY_ADMIN_TOKEN +REGISTRY_ORIGIN +STATIC_REGISTRY_ORIGIN +REGISTRY_API_IMAGE +REGISTRY_VERIFIER_IMAGE ``` -The API rejects a publish unless: - -- the capability exists; -- the capability is unrevoked and unexpired; -- the capability scope covers `publish:namespace/package`; -- the namespace exists and is active; -- the capability principal owns the namespace; -- the capability signature verifies; -- the signed publish nonce has not already been consumed; -- the package version does not already exist; -- a source snapshot is provided and persisted to R2; -- a static package-version JSON object is persisted to R2 for the CDN read path. +Mainnet deployment checks use `CKB_MAINNET_RPC_URL`. Chain commitments remain +disabled unless `REGISTRY_TYPE_SCRIPT_JSON`, +`REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON`, `REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON`, +and `REGISTRY_COMMITMENT_LOCK_CELL_DEP_JSON` are supplied together. Set +`REGISTRY_REPRODUCER_POLICY_JSON` before accepting reproduction promotions and +use `CKB_MIN_CONFIRMATIONS` to raise or lower the default 24-block confirmation +floor. The Node adapter, production Compose file, and Worker example pass the +same settings. + +The API container applies tracked additive migrations before serving traffic. +`0001_initial.sql` is the frozen deployed baseline. `0002` adds the verifier +queue; `0003` adds multi-wallet principals; `0004` converts an empty legacy +release table to the artifact/state model and intentionally fails if rows exist +so operators cannot perform a lossy implicit migration; `0005` separates +hash-integrity evidence from semantic verification with `hash_bound`; and +`0006` admits the independent `reproduced_build` evidence kind; and `0007` +renames historical chain evidence, adds the current-commitment pointer and +status projection constraints, and deliberately demotes legacy current claims +until the mainnet indexer re-observes a sufficiently confirmed live Cell. +`0008` adds isolated sandbox retention, `0009` adds wallet-authorisation +sessions, and `0010` adds the bounded partial lookup index used to resolve +LS-IDL from active public chain-verified deployment evidence. Apply `0010` +before enabling either LS-IDL read route. + +`GET /health` is process liveness and is the Compose container healthcheck. +`GET /ready` is the traffic and operator gate: it checks store/object access, +admin configuration, CKB/commitment dependencies, and—when +`REQUIRE_REGISTRY_VERIFIER_READY=true`—a fresh verifier heartbeat. External +load balancers and deployment automation should use `/ready`; a transient RPC, +database, object-store, or verifier dependency failure must not be mistaken for +a dead Node process by the container runtime. + +## Backups + +`deploy/backup.sh` creates a Postgres custom dump, object archive, Postgres +image identity, and SHA-256 manifest under the bounded retention policy. -Clients that need safe retry semantics should send an `Idempotency-Key` header -with at least 16 visible token characters. The key is not an auth credential; it -only scopes response replay and conflict detection for the exact publish -request body. - -`cellc publish` sends this header by default using a hash of the exact publish -request. It can be pinned with `--idempotency-key` or -`CELLSCRIPT_REGISTRY_IDEMPOTENCY_KEY` for CI jobs that intentionally retry the -same request. +```bash +(cd /data/cellscript-registry/backups/ && sha256sum --check SHA256SUMS) +docker run --rm --network none \ + -v /data/cellscript-registry/backups/:/backup:ro \ + postgres:17-alpine pg_restore --list /backup/postgres.dump > /dev/null +tar -tzf /data/cellscript-registry/backups//objects.tar.gz > /dev/null +``` -If publish admission fails before the package version is accepted, the write API -releases the matching `processing` idempotency reservation. The signed publish -nonce may already have been consumed, so a later retry with the same CI retry key -must use a freshly generated publish payload and capability signature. +Restore rehearsals use new empty database/object volumes and require `/ready` +plus static artifact reads before traffic cut-over. Never overwrite live +volumes with an untested restore. -Successful publish returns a direct static read URL shaped as: +## Cloudflare -```text -https://registry.cellscript.dev/packages/:namespace/:name/versions/:version.json -``` +The Worker and the isolated verifier are different processes. Cloudflare +Workers cannot spawn the Rust verifier binary. A Worker-only deployment can +serve the API, write R2 objects, and enqueue Postgres jobs, but it cannot advance +those jobs to `hash_bound` or `verified`; releases will remain pending. -The route is served from R2 and sets short CDN cache headers. It does not -require Hyperdrive or the write store, so ordinary package reads stay isolated -from authenticated write-path dependencies. +The checked-in Node verifier currently consumes a Postgres queue and a shared +filesystem object store. It does not yet include an R2/S3 object adapter. +Therefore the supported production write topology is the self-hosted Node API + +Rust verifier Compose stack above. Treat the Worker configuration as an edge/API +deployment template until an external verifier is given both the same database +and an implemented immutable R2 object adapter. Do not route production publish +traffic to a Worker deployment that has no queue consumer. -CLI publish has two supported signing shapes: +For an API-only or development Worker deployment, configure Neon, R2, +Hyperdrive, the scheduled cleanup trigger, and `REGISTRY_ADMIN_TOKEN`; then +apply migrations and deploy: ```bash -# Daily local use: key was generated by auth capability create and stored in keychain. -cellc publish - -# CI or external signer: sign the canonical payload, then submit it unchanged. -cellc publish --print-payload --json > publish-payload.json -cellc publish --payload publish-payload.json --capability-signature +DATABASE_URL='postgres://...' npm run migrate +npm install +npm run check +npm test +npm run build +npx wrangler deploy --config wrangler.toml ``` -`CELLSCRIPT_REGISTRY_API_URL` overrides the write API base URL. CI may set -`CELLSCRIPT_CAPABILITY_PRIVATE_KEY_PKCS8_B64` to let the CLI sign with a -delegated capability key without JoyID or keychain access. -`CELLSCRIPT_REGISTRY_IDEMPOTENCY_KEY` pins the publish retry key; otherwise the -CLI derives one from the publish request and reuses it for transient retry of -the same HTTP submission. +The checked-in `wrangler.example.toml` contains no secret. Re-running +`npm run migrate` is safe because applied migration filenames are recorded in +`schema_migrations`. ## Local Verification @@ -258,7 +590,11 @@ the same HTTP submission. npm run check npm test npm run build +npm run build:node +cargo test --locked --manifest-path ../registry-verifier/Cargo.toml +cargo clippy --locked --manifest-path ../registry-verifier/Cargo.toml --all-targets -- -D warnings ``` -`npm run build` performs a wrangler dry-run bundle against the example -configuration. It does not deploy. +The repository-wide `dev` and `ci` gates exercise these surfaces together with +the compiler, CLI, website, and independent verifier. None of the commands in +this section deploys production. diff --git a/services/registry-api/deploy/.env.example b/services/registry-api/deploy/.env.example new file mode 100644 index 00000000..555ce299 --- /dev/null +++ b/services/registry-api/deploy/.env.example @@ -0,0 +1,27 @@ +REGISTRY_DB_PASSWORD=replace-with-a-generated-secret +REGISTRY_ADMIN_TOKEN=replace-with-a-generated-secret +# Pin immutable, prebuilt linux/amd64 image tags or digests in production. +# REGISTRY_API_IMAGE=cellscript-registry-api:latest +# REGISTRY_VERIFIER_IMAGE=cellscript-registry-verifier:latest +# Set this to an absolute checkout/build-context path when the deployment +# directory is not nested under the CellScript repository and Compose will +# build the images locally. +# CELLSCRIPT_REGISTRY_SOURCE_ROOT=/data/cellscript-registry/source +# REGISTRY_ORIGIN=https://api.registry.cellscript.dev +# STATIC_REGISTRY_ORIGIN=https://registry.cellscript.dev +# CKB_MAINNET_RPC_URL=https://mainnet.ckb.dev/rpc +# Enable chain commitments only after deploying and pinning the canonical +# mainnet Registry Type Script, its CellDep, and the commitment custody Lock. +# All four JSON values are required together; leaving them unset keeps +# commitment transaction construction and chain-index reconciliation disabled. +# REGISTRY_TYPE_SCRIPT_JSON={"code_hash":"0x8b6de99567accdca438818a55c16534ed10fc335f117709b1487fd2666808bfb","hash_type":"data1","args":"0x"} +# REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON={"out_point":{"tx_hash":"0x","index":"0x0"},"dep_type":"code"} +# REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON={"code_hash":"0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8","hash_type":"type","args":"0x"} +# REGISTRY_COMMITMENT_LOCK_CELL_DEP_JSON={"out_point":{"tx_hash":"0x71a7ba8fc96349fea0ed3a5c47992e3b4084b031a42264a018e0072e8172e46c","index":"0x0"},"dep_type":"dep_group"} +# CKB_REGISTRY_SCAN_MAX_CELLS=1000 +# CKB_MIN_CONFIRMATIONS=24 +# Signed reproduction promotion remains disabled until this policy names at +# least two active builders under genuinely separate administrative control and +# private-key custody. Different labels for keys controlled by one operator are +# not independent trust domains. +# REGISTRY_REPRODUCER_POLICY_JSON={"schema":"cellscript-reproducer-policy-v1","minimum_trust_domains":2,"builders":[{"builder_id":"builder-a","trust_domain":"org-a","public_key":"p256-spki:..."},{"builder_id":"builder-b","trust_domain":"org-b","public_key":"p256-spki:..."}]} diff --git a/services/registry-api/deploy/backup.sh b/services/registry-api/deploy/backup.sh new file mode 100755 index 00000000..9ac3f050 --- /dev/null +++ b/services/registry-api/deploy/backup.sh @@ -0,0 +1,90 @@ +#!/bin/sh +set -eu + +backup_root="${REGISTRY_BACKUP_DIR:-/data/cellscript-registry/backups}" +postgres_container="${REGISTRY_POSTGRES_CONTAINER:-cellscript-registry-postgres-1}" +objects_volume="${REGISTRY_OBJECTS_VOLUME:-cellscript-registry_registry-objects}" +retention_days="${REGISTRY_BACKUP_RETENTION_DAYS:-7}" + +case "$backup_root" in + /*) ;; + *) + echo "REGISTRY_BACKUP_DIR must be an absolute path" >&2 + exit 2 + ;; +esac + +if [ "$backup_root" = "/" ]; then + echo "REGISTRY_BACKUP_DIR must not be the filesystem root" >&2 + exit 2 +fi + +case "$retention_days" in + ''|*[!0-9]*) + echo "REGISTRY_BACKUP_RETENTION_DAYS must be an integer" >&2 + exit 2 + ;; +esac + +if [ "$retention_days" -lt 1 ] || [ "$retention_days" -gt 365 ]; then + echo "REGISTRY_BACKUP_RETENTION_DAYS must be between 1 and 365" >&2 + exit 2 +fi + +command -v docker >/dev/null 2>&1 || { + echo "docker is required" >&2 + exit 2 +} + +install -d -m 750 "$backup_root" +stamp="$(date -u +%Y%m%dT%H%M%SZ)" +final_dir="$backup_root/$stamp" + +if [ -e "$final_dir" ]; then + echo "backup already exists: $final_dir" >&2 + exit 2 +fi + +temporary="$(mktemp -d "$backup_root/.tmp-$stamp.XXXXXX")" +cleanup() { + if [ -n "${temporary:-}" ] && [ -d "$temporary" ]; then + rm -rf -- "$temporary" + fi +} +trap cleanup EXIT HUP INT TERM + +docker exec "$postgres_container" pg_dump \ + --username cellscript_registry \ + --dbname cellscript_registry \ + --format custom \ + --no-owner \ + --no-privileges > "$temporary/postgres.dump" + +docker run --rm \ + --network none \ + --read-only \ + --security-opt no-new-privileges:true \ + --volume "$objects_volume:/objects:ro" \ + --volume "$temporary:/backup" \ + alpine:3.22 \ + tar -czf /backup/objects.tar.gz -C /objects . + +docker inspect --format '{{.Image}}' "$postgres_container" > "$temporary/postgres-image.txt" +( + cd "$temporary" + sha256sum postgres.dump objects.tar.gz postgres-image.txt > SHA256SUMS +) + +chmod 640 "$temporary"/* +mv "$temporary" "$final_dir" +temporary="" + +find "$backup_root" \ + -mindepth 1 \ + -maxdepth 1 \ + -type d \ + -name '20??????T??????Z' \ + -mtime "+$retention_days" \ + -exec rm -rf -- {} + + +echo "$final_dir" diff --git a/services/registry-api/deploy/cellscript-registry-backup.service b/services/registry-api/deploy/cellscript-registry-backup.service new file mode 100644 index 00000000..db2d9ed3 --- /dev/null +++ b/services/registry-api/deploy/cellscript-registry-backup.service @@ -0,0 +1,21 @@ +[Unit] +Description=Back up the CellScript Registry database and object volume +Requires=docker.service +After=docker.service + +[Service] +Type=oneshot +ExecStartPre=/usr/bin/install -d -m 0750 /data/cellscript-registry/backups +ExecStart=/data/cellscript-registry/app/deploy/backup.sh +User=root +Group=root +UMask=0027 +Nice=10 +IOSchedulingClass=best-effort +IOSchedulingPriority=7 +NoNewPrivileges=true +PrivateTmp=true +ProtectHome=true +ProtectSystem=strict +ReadWritePaths=/data/cellscript-registry +ReadOnlyPaths=-/data/cellscript-registry/app -/data/cellscript-registry/releases diff --git a/services/registry-api/deploy/cellscript-registry-backup.timer b/services/registry-api/deploy/cellscript-registry-backup.timer new file mode 100644 index 00000000..7ed2605c --- /dev/null +++ b/services/registry-api/deploy/cellscript-registry-backup.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Run the CellScript Registry backup daily + +[Timer] +OnCalendar=*-*-* 03:17:00 +Persistent=true +RandomizedDelaySec=15m +Unit=cellscript-registry-backup.service + +[Install] +WantedBy=timers.target diff --git a/services/registry-api/deploy/docker-compose.production.yml b/services/registry-api/deploy/docker-compose.production.yml new file mode 100644 index 00000000..37d6b417 --- /dev/null +++ b/services/registry-api/deploy/docker-compose.production.yml @@ -0,0 +1,184 @@ +name: cellscript-registry + +services: + postgres: + image: postgres:17-alpine + restart: unless-stopped + environment: + POSTGRES_DB: cellscript_registry + POSTGRES_USER: cellscript_registry + POSTGRES_PASSWORD: ${REGISTRY_DB_PASSWORD:?REGISTRY_DB_PASSWORD is required} + volumes: + - registry-postgres:/var/lib/postgresql/data + networks: + - registry-internal + healthcheck: + test: ["CMD-SHELL", "pg_isready -U cellscript_registry -d cellscript_registry"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 20s + security_opt: + - no-new-privileges:true + logging: &logging + driver: json-file + options: + max-size: "10m" + max-file: "3" + + object-store-init: + image: alpine:3.22 + command: ["sh", "-c", "chown -R 1000:101 /objects && find /objects -type d -exec chmod 2750 '{}' ';' && find /objects -type f -exec chmod 0640 '{}' ';'"] + volumes: + - registry-objects:/objects + restart: "no" + security_opt: + - no-new-privileges:true + + api: + image: ${REGISTRY_API_IMAGE:-cellscript-registry-api:latest} + build: + context: .. + dockerfile: Dockerfile + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + object-store-init: + condition: service_completed_successfully + environment: + PORT: "8787" + DATABASE_URL: postgresql://cellscript_registry:${REGISTRY_DB_PASSWORD}@postgres:5432/cellscript_registry + REGISTRY_OBJECTS_DIR: /objects + REGISTRY_ADMIN_TOKEN: ${REGISTRY_ADMIN_TOKEN:?REGISTRY_ADMIN_TOKEN is required} + REGISTRY_ORIGIN: ${REGISTRY_ORIGIN:-https://api.registry.cellscript.dev} + STATIC_REGISTRY_ORIGIN: ${STATIC_REGISTRY_ORIGIN:-https://registry.cellscript.dev} + REGISTRY_WEBSITE_ORIGIN: ${REGISTRY_WEBSITE_ORIGIN:-https://cellscript.dev} + CKB_MAINNET_RPC_URL: ${CKB_MAINNET_RPC_URL:-https://mainnet.ckb.dev/rpc} + CKB_RPC_URL: ${CKB_RPC_URL:-https://mainnet.ckb.dev/rpc} + CKB_RPC_MAX_RESPONSE_BYTES: "8388608" + REGISTRY_TYPE_SCRIPT_JSON: ${REGISTRY_TYPE_SCRIPT_JSON:-} + REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON: ${REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON:-} + REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON: ${REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON:-} + REGISTRY_COMMITMENT_LOCK_CELL_DEP_JSON: ${REGISTRY_COMMITMENT_LOCK_CELL_DEP_JSON:-} + REGISTRY_REPRODUCER_POLICY_JSON: ${REGISTRY_REPRODUCER_POLICY_JSON:-} + CKB_REGISTRY_SCAN_MAX_CELLS: ${CKB_REGISTRY_SCAN_MAX_CELLS:-1000} + CKB_MIN_CONFIRMATIONS: ${CKB_MIN_CONFIRMATIONS:-24} + ENVIRONMENT: production + REGISTRY_ENVIRONMENT: production + MAX_INCOMING_BODY_BYTES: "7340032" + MAX_JSON_BODY_BYTES: "6291456" + MAX_SNAPSHOT_BYTES: "5242880" + REQUIRE_REGISTRY_VERIFIER_READY: "true" + REGISTRY_VERIFIER_SHARED_HEARTBEAT: /objects/.health/verifier-ready + REGISTRY_VERIFIER_HEARTBEAT_MAX_AGE_SECONDS: "120" + VIRTUAL_HOST: api.registry.cellscript.dev + VIRTUAL_PORT: "8787" + expose: + - "8787" + volumes: + - registry-objects:/objects + networks: + - registry-internal + - stack-network + read_only: true + tmpfs: + - /tmp:size=32m,mode=1777 + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8787/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"] + interval: 15s + timeout: 5s + retries: 10 + start_period: 30s + logging: *logging + + verifier: + image: ${REGISTRY_VERIFIER_IMAGE:-cellscript-registry-verifier:latest} + build: + context: ${CELLSCRIPT_REGISTRY_SOURCE_ROOT:-../../..} + dockerfile: services/registry-api/Dockerfile.verifier + restart: unless-stopped + init: true + depends_on: + api: + condition: service_started + postgres: + condition: service_healthy + object-store-init: + condition: service_completed_successfully + environment: + DATABASE_URL: postgresql://cellscript_registry:${REGISTRY_DB_PASSWORD}@postgres:5432/cellscript_registry + REGISTRY_OBJECTS_DIR: /objects + STATIC_REGISTRY_ORIGIN: ${STATIC_REGISTRY_ORIGIN:-https://registry.cellscript.dev} + CELLSCRIPT_REGISTRY_API_URL: ${REGISTRY_ORIGIN:-https://api.registry.cellscript.dev} + ENVIRONMENT: production + REGISTRY_VERIFIER_POLL_INTERVAL_MS: "2000" + REGISTRY_VERIFIER_JOB_TIMEOUT_SECONDS: "240" + REGISTRY_VERIFIER_LEASE_SECONDS: "300" + REGISTRY_VERIFIER_HEALTH_FILE: /tmp/registry-verifier-ready + REGISTRY_VERIFIER_SHARED_HEARTBEAT: /objects/.health/verifier-ready + volumes: + - registry-objects:/objects + networks: + - registry-internal + - stack-network + read_only: true + tmpfs: + - /tmp:size=512m,mode=1777 + pids_limit: 128 + mem_limit: 1g + cpus: 1.0 + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD", "node", "-e", "const s=require('node:fs').statSync('/tmp/registry-verifier-ready');if(Date.now()-s.mtimeMs>120000)process.exit(1)"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + logging: *logging + + static-registry: + image: nginx:1.27-alpine + restart: unless-stopped + depends_on: + object-store-init: + condition: service_completed_successfully + environment: + VIRTUAL_HOST: registry.cellscript.dev + VIRTUAL_PORT: "8080" + expose: + - "8080" + volumes: + - registry-objects:/srv/registry:ro + - ./registry-static.nginx.conf:/etc/nginx/conf.d/default.conf:ro + networks: + - stack-network + read_only: true + tmpfs: + - /var/cache/nginx:size=16m + - /var/run:size=1m + - /tmp:size=4m + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:8080/health"] + interval: 15s + timeout: 5s + retries: 5 + logging: *logging + +volumes: + registry-postgres: + registry-objects: + +networks: + registry-internal: + internal: true + stack-network: + external: true + name: stack-network diff --git a/services/registry-api/deploy/docker-compose.testnet.yml b/services/registry-api/deploy/docker-compose.testnet.yml new file mode 100644 index 00000000..c263dea0 --- /dev/null +++ b/services/registry-api/deploy/docker-compose.testnet.yml @@ -0,0 +1,148 @@ +name: cellscript-registry-testnet + +services: + postgres: + image: postgres:17-alpine + restart: unless-stopped + environment: + POSTGRES_DB: cellscript_registry_testnet + POSTGRES_USER: cellscript_registry_testnet + POSTGRES_PASSWORD: ${REGISTRY_TESTNET_DB_PASSWORD:?REGISTRY_TESTNET_DB_PASSWORD is required} + volumes: + - registry-testnet-postgres:/var/lib/postgresql/data + networks: [registry-testnet-internal] + healthcheck: + test: ["CMD-SHELL", "pg_isready -U cellscript_registry_testnet -d cellscript_registry_testnet"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 20s + security_opt: [no-new-privileges:true] + + object-store-init: + image: alpine:3.22 + command: ["sh", "-c", "chown -R 1000:101 /objects && find /objects -type d -exec chmod 2750 '{}' ';' && find /objects -type f -exec chmod 0640 '{}' ';'"] + volumes: + - registry-testnet-objects:/objects + restart: "no" + security_opt: [no-new-privileges:true] + + api: + image: ${REGISTRY_API_IMAGE:-cellscript-registry-api:latest} + build: + context: .. + dockerfile: Dockerfile + restart: unless-stopped + depends_on: + postgres: { condition: service_healthy } + object-store-init: { condition: service_completed_successfully } + environment: + PORT: "8787" + DATABASE_URL: postgresql://cellscript_registry_testnet:${REGISTRY_TESTNET_DB_PASSWORD}@postgres:5432/cellscript_registry_testnet + REGISTRY_OBJECTS_DIR: /objects + REGISTRY_ADMIN_TOKEN: ${REGISTRY_TESTNET_ADMIN_TOKEN:?REGISTRY_TESTNET_ADMIN_TOKEN is required} + REGISTRY_ORIGIN: ${REGISTRY_TESTNET_ORIGIN:-https://api.testnet.registry.cellscript.dev} + STATIC_REGISTRY_ORIGIN: ${STATIC_REGISTRY_TESTNET_ORIGIN:-https://objects.testnet.registry.cellscript.dev} + REGISTRY_WEBSITE_ORIGIN: ${REGISTRY_TESTNET_WEBSITE_ORIGIN:-https://testnet.registry.cellscript.dev} + CKB_RPC_URL: ${CKB_TESTNET_RPC_URL:-https://testnet.ckb.dev/rpc} + CKB_RPC_MAX_RESPONSE_BYTES: "8388608" + CKB_MIN_CONFIRMATIONS: "4" + REGISTRY_TYPE_SCRIPT_JSON: ${REGISTRY_TESTNET_TYPE_SCRIPT_JSON:-} + REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON: ${REGISTRY_TESTNET_TYPE_SCRIPT_CELL_DEP_JSON:-} + REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON: ${REGISTRY_TESTNET_COMMITMENT_LOCK_SCRIPT_JSON:-} + REGISTRY_COMMITMENT_LOCK_CELL_DEP_JSON: ${REGISTRY_TESTNET_COMMITMENT_LOCK_CELL_DEP_JSON:-} + REGISTRY_REPRODUCER_POLICY_JSON: ${REGISTRY_REPRODUCER_POLICY_JSON:-} + CKB_REGISTRY_SCAN_MAX_CELLS: "1000" + ENVIRONMENT: testnet-sandbox + REGISTRY_ENVIRONMENT: testnet-sandbox + MAX_INCOMING_BODY_BYTES: "7340032" + MAX_JSON_BODY_BYTES: "6291456" + MAX_SNAPSHOT_BYTES: "5242880" + REQUIRE_REGISTRY_VERIFIER_READY: "true" + REGISTRY_VERIFIER_SHARED_HEARTBEAT: /objects/.health/verifier-ready + REGISTRY_VERIFIER_HEARTBEAT_MAX_AGE_SECONDS: "120" + VIRTUAL_HOST: ${REGISTRY_TESTNET_API_HOST:-api.testnet.registry.cellscript.dev} + VIRTUAL_PORT: "8787" + expose: ["8787"] + volumes: + - registry-testnet-objects:/objects + networks: [registry-testnet-internal, stack-network] + read_only: true + tmpfs: ["/tmp:size=32m,mode=1777"] + security_opt: [no-new-privileges:true] + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8787/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"] + interval: 15s + timeout: 5s + retries: 10 + start_period: 30s + + verifier: + image: ${REGISTRY_VERIFIER_IMAGE:-cellscript-registry-verifier:latest} + build: + context: ${CELLSCRIPT_REGISTRY_SOURCE_ROOT:-../../..} + dockerfile: services/registry-api/Dockerfile.verifier + restart: unless-stopped + init: true + depends_on: + api: { condition: service_started } + postgres: { condition: service_healthy } + object-store-init: { condition: service_completed_successfully } + environment: + DATABASE_URL: postgresql://cellscript_registry_testnet:${REGISTRY_TESTNET_DB_PASSWORD}@postgres:5432/cellscript_registry_testnet + REGISTRY_OBJECTS_DIR: /objects + STATIC_REGISTRY_ORIGIN: ${STATIC_REGISTRY_TESTNET_ORIGIN:-https://objects.testnet.registry.cellscript.dev} + CELLSCRIPT_REGISTRY_API_URL: ${REGISTRY_TESTNET_ORIGIN:-https://api.testnet.registry.cellscript.dev} + ENVIRONMENT: testnet-sandbox + REGISTRY_VERIFIER_POLL_INTERVAL_MS: "2000" + REGISTRY_VERIFIER_JOB_TIMEOUT_SECONDS: "240" + REGISTRY_VERIFIER_LEASE_SECONDS: "300" + REGISTRY_VERIFIER_HEALTH_FILE: /tmp/registry-verifier-ready + REGISTRY_VERIFIER_SHARED_HEARTBEAT: /objects/.health/verifier-ready + volumes: + - registry-testnet-objects:/objects + networks: [registry-testnet-internal, stack-network] + read_only: true + tmpfs: ["/tmp:size=512m,mode=1777"] + pids_limit: 128 + mem_limit: 1g + cpus: 1.0 + cap_drop: [ALL] + security_opt: [no-new-privileges:true] + healthcheck: + test: ["CMD", "node", "-e", "const s=require('node:fs').statSync('/tmp/registry-verifier-ready');if(Date.now()-s.mtimeMs>120000)process.exit(1)"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + + static-registry: + image: nginx:1.27-alpine + restart: unless-stopped + depends_on: + object-store-init: { condition: service_completed_successfully } + environment: + VIRTUAL_HOST: ${STATIC_REGISTRY_TESTNET_HOST:-objects.testnet.registry.cellscript.dev} + VIRTUAL_PORT: "8080" + expose: ["8080"] + volumes: + - registry-testnet-objects:/srv/registry:ro + - ./registry-static.nginx.conf:/etc/nginx/conf.d/default.conf:ro + networks: [stack-network] + read_only: true + tmpfs: + - /var/cache/nginx:size=16m + - /var/run:size=1m + - /tmp:size=4m + security_opt: [no-new-privileges:true] + +volumes: + registry-testnet-postgres: + registry-testnet-objects: + +networks: + registry-testnet-internal: + internal: true + stack-network: + external: true + name: stack-network diff --git a/services/registry-api/deploy/registry-static.nginx.conf b/services/registry-api/deploy/registry-static.nginx.conf new file mode 100644 index 00000000..6d03b346 --- /dev/null +++ b/services/registry-api/deploy/registry-static.nginx.conf @@ -0,0 +1,61 @@ +server { + listen 8080; + server_name _; + root /srv/registry; + + server_tokens off; + charset utf-8; + + add_header Content-Security-Policy "default-src 'none'; base-uri 'none'; frame-ancestors 'none'" always; + add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" always; + add_header Referrer-Policy "no-referrer" always; + add_header Strict-Transport-Security "max-age=31536000" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "DENY" always; + add_header X-Permitted-Cross-Domain-Policies "none" always; + + location = /health { + default_type application/json; + return 200 '{"status":"ok"}\n'; + } + + location /artifacts/ { + limit_except GET HEAD { deny all; } + try_files $uri =404; + default_type application/json; + add_header Access-Control-Allow-Origin "*" always; + add_header Cache-Control "public, max-age=60, stale-while-revalidate=300" always; + add_header Content-Security-Policy "default-src 'none'; base-uri 'none'; frame-ancestors 'none'" always; + add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" always; + add_header Referrer-Policy "no-referrer" always; + add_header Strict-Transport-Security "max-age=31536000" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "DENY" always; + add_header X-Permitted-Cross-Domain-Policies "none" always; + } + + location /source-snapshots/ { + limit_except GET HEAD { deny all; } + try_files $uri =404; + default_type application/octet-stream; + types { + application/json json; + application/x-tar tar; + application/gzip gz; + } + add_header Access-Control-Allow-Origin "*" always; + add_header Cache-Control "public, max-age=31536000, immutable" always; + add_header Content-Security-Policy "default-src 'none'; base-uri 'none'; frame-ancestors 'none'" always; + add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" always; + add_header Referrer-Policy "no-referrer" always; + add_header Strict-Transport-Security "max-age=31536000" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "DENY" always; + add_header X-Permitted-Cross-Domain-Policies "none" always; + } + + location / { + default_type application/json; + return 404 '{"error":{"code":"not_found","message":"route not found"}}\n'; + } +} diff --git a/services/registry-api/migrations/0001_initial.sql b/services/registry-api/migrations/0001_initial.sql index 0b11ea49..a441a8db 100644 --- a/services/registry-api/migrations/0001_initial.sql +++ b/services/registry-api/migrations/0001_initial.sql @@ -92,7 +92,9 @@ create table if not exists package_versions ( version text not null, status text not null, source_hash text not null, - manifest_hash text, + manifest_hash text not null, + edition text not null, + compatibility_profile_hash text not null, capability_key_id text not null references capabilities(key_id), principal_type text not null, principal_id text not null, @@ -117,9 +119,36 @@ create table if not exists package_versions ( 'deprecated', 'yanked', 'quarantined' - )) + )), + check (source_hash ~ '^(0x)?[0-9A-Fa-f]{64}$'), + check (manifest_hash ~ '^(0x)?[0-9A-Fa-f]{64}$'), + check (edition = '2026'), + check (compatibility_profile_hash ~ '^(0x)?[0-9A-Fa-f]{64}$') ); +create index if not exists package_versions_public_idx + on package_versions(status, created_at desc, namespace, name); + +create table if not exists package_version_evidence ( + namespace text not null, + name text not null, + version text not null, + kind text not null, + evidence_hash text not null, + evidence jsonb not null, + request_id text not null, + admin_actor text not null, + created_at timestamptz not null default now(), + primary key (namespace, name, version, kind, evidence_hash), + foreign key (namespace, name, version) + references package_versions(namespace, name, version), + check (kind in ('verified_build', 'deployed', 'on_chain_attested')), + check (evidence_hash ~ '^sha256:[0-9A-Fa-f]{64}$') +); + +create index if not exists package_version_evidence_lookup_idx + on package_version_evidence(namespace, name, version, created_at); + create table if not exists idempotency_keys ( key text primary key, request_hash text not null, diff --git a/services/registry-api/migrations/0002_verification_jobs.sql b/services/registry-api/migrations/0002_verification_jobs.sql new file mode 100644 index 00000000..98dc0330 --- /dev/null +++ b/services/registry-api/migrations/0002_verification_jobs.sql @@ -0,0 +1,58 @@ +create table if not exists verification_jobs ( + id uuid primary key default gen_random_uuid(), + namespace text not null, + name text not null, + version text not null, + status text not null default 'queued', + attempt_count integer not null default 0, + max_attempts integer not null default 3, + available_at timestamptz not null default now(), + lease_owner text, + lease_expires_at timestamptz, + evidence_hash text, + evidence jsonb, + last_error_code text, + last_error_message text, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + started_at timestamptz, + completed_at timestamptz, + unique (namespace, name, version), + foreign key (namespace, name, version) + references package_versions(namespace, name, version), + check (status in ('queued', 'running', 'publishing', 'retry_wait', 'succeeded', 'dead_letter')), + check (attempt_count >= 0), + check (max_attempts between 1 and 20), + check ( + (status in ('running', 'publishing') and lease_owner is not null and lease_expires_at is not null) + or + (status not in ('running', 'publishing') and lease_owner is null and lease_expires_at is null) + ), + check ( + (evidence_hash is null and evidence is null) + or + (evidence_hash ~ '^sha256:[0-9A-Fa-f]{64}$' and evidence is not null) + ), + check ((status = 'succeeded') = (completed_at is not null)) +); + +create index if not exists verification_jobs_claim_idx + on verification_jobs(status, available_at, lease_expires_at, created_at); + +create index if not exists verification_jobs_dead_letter_idx + on verification_jobs(updated_at desc) + where status = 'dead_letter'; + +insert into verification_jobs(namespace, name, version) +select pv.namespace, pv.name, pv.version +from package_versions pv +where pv.status in ('source_published', 'indexed_pending') + and not exists ( + select 1 + from package_version_evidence pve + where pve.namespace = pv.namespace + and pve.name = pv.name + and pve.version = pv.version + and pve.kind = 'verified_build' + ) +on conflict (namespace, name, version) do nothing; diff --git a/services/registry-api/migrations/0003_multi_wallet_principals.sql b/services/registry-api/migrations/0003_multi_wallet_principals.sql new file mode 100644 index 00000000..15c6721f --- /dev/null +++ b/services/registry-api/migrations/0003_multi_wallet_principals.sql @@ -0,0 +1,9 @@ +alter table principals + drop constraint if exists principals_principal_type_check; + +alter table principals + add constraint principals_principal_type_check + check (principal_type in ('joyid_ckb', 'ckb_secp256k1')); + +comment on column capabilities.joyid_signature is + 'Legacy column name; stores the verified root-wallet signature envelope for joyid_ckb or ckb_secp256k1 principals.'; diff --git a/services/registry-api/migrations/0004_artifact_model.sql b/services/registry-api/migrations/0004_artifact_model.sql new file mode 100644 index 00000000..0e453fe2 --- /dev/null +++ b/services/registry-api/migrations/0004_artifact_model.sql @@ -0,0 +1,39 @@ +do $$ +begin + if exists (select 1 from package_versions limit 1) then + raise exception 'artifact model cut requires an empty unreleased package_versions table'; + end if; +end $$; + +alter table package_versions + add column artifact jsonb, + add column verification_status text, + add column deployment_status text, + add column availability_status text, + alter column edition drop not null, + alter column compatibility_profile_hash drop not null; + +alter table package_versions + alter column artifact set not null, + alter column verification_status set not null, + alter column deployment_status set not null, + alter column availability_status set not null, + add constraint package_versions_artifact_object_check check (jsonb_typeof(artifact) = 'object'), + add constraint package_versions_verification_status_check + check (verification_status in ('pending', 'verified', 'evidence_required', 'rejected')), + add constraint package_versions_deployment_status_check + check (deployment_status in ('not_applicable', 'undeployed', 'deployed', 'chain_verified')), + add constraint package_versions_availability_status_check + check (availability_status in ('active', 'deprecated', 'yanked', 'quarantined')); + +alter table package_versions + drop constraint if exists package_versions_edition_check, + drop constraint if exists package_versions_compatibility_profile_hash_check; + +alter table package_versions + add constraint package_versions_edition_check check (edition is null or edition = '2026'), + add constraint package_versions_compatibility_profile_hash_check + check (compatibility_profile_hash is null or compatibility_profile_hash ~ '^(0x)?[0-9A-Fa-f]{64}$'); + +create index package_versions_artifact_public_idx + on package_versions(availability_status, verification_status, deployment_status, (artifact->>'kind'), created_at desc); diff --git a/services/registry-api/migrations/0005_truthful_verification_status.sql b/services/registry-api/migrations/0005_truthful_verification_status.sql new file mode 100644 index 00000000..23080053 --- /dev/null +++ b/services/registry-api/migrations/0005_truthful_verification_status.sql @@ -0,0 +1,6 @@ +alter table package_versions + drop constraint if exists package_versions_verification_status_check; + +alter table package_versions + add constraint package_versions_verification_status_check + check (verification_status in ('pending', 'hash_bound', 'verified', 'evidence_required', 'rejected')); diff --git a/services/registry-api/migrations/0006_reproducibility_evidence.sql b/services/registry-api/migrations/0006_reproducibility_evidence.sql new file mode 100644 index 00000000..3a633730 --- /dev/null +++ b/services/registry-api/migrations/0006_reproducibility_evidence.sql @@ -0,0 +1,6 @@ +alter table package_version_evidence + drop constraint if exists package_version_evidence_kind_check; + +alter table package_version_evidence + add constraint package_version_evidence_kind_check + check (kind in ('verified_build', 'reproduced_build', 'deployed', 'on_chain_attested')); diff --git a/services/registry-api/migrations/0007_current_commitment_state.sql b/services/registry-api/migrations/0007_current_commitment_state.sql new file mode 100644 index 00000000..079e697b --- /dev/null +++ b/services/registry-api/migrations/0007_current_commitment_state.sql @@ -0,0 +1,67 @@ +alter table package_version_evidence + drop constraint if exists package_version_evidence_kind_check; + +alter table package_versions + drop constraint if exists package_versions_status_check; + +update package_version_evidence +set kind = 'on_chain_committed' +where kind = 'on_chain_attested'; + +update package_versions +set status = 'on_chain_committed' +where status = 'on_chain_attested'; + +alter table package_versions + add column current_commitment_kind text not null default 'on_chain_committed', + add column current_commitment_evidence_hash text; + +-- Historical attestation evidence is not proof that its Cell is still live. +-- Preserve the evidence after renaming it, but fail closed until the mainnet +-- reconciliation job observes a sufficiently confirmed live commitment again. +update package_versions +set status = case + when availability_status <> 'active' then availability_status + when deployment_status in ('deployed', 'chain_verified') then 'deployed' + when verification_status in ('hash_bound', 'verified', 'evidence_required') then 'verified_build' + when status = 'indexed_pending' then 'indexed_pending' + else 'source_published' +end; + +alter table package_version_evidence + add constraint package_version_evidence_kind_check + check (kind in ('verified_build', 'reproduced_build', 'deployed', 'on_chain_committed')); + +alter table package_versions + add constraint package_versions_status_check + check (status in ( + 'source_published', + 'indexed_pending', + 'verified_build', + 'deployed', + 'on_chain_committed', + 'deprecated', + 'yanked', + 'quarantined' + )), + add constraint package_versions_current_commitment_kind_check + check (current_commitment_kind = 'on_chain_committed'), + add constraint package_versions_current_commitment_evidence_fk + foreign key (namespace, name, version, current_commitment_kind, current_commitment_evidence_hash) + references package_version_evidence(namespace, name, version, kind, evidence_hash), + add constraint package_versions_status_projection_check + check ( + (availability_status <> 'active' and status = availability_status) + or + (availability_status = 'active' and ( + (current_commitment_evidence_hash is not null and status = 'on_chain_committed') + or + (current_commitment_evidence_hash is null and deployment_status in ('deployed', 'chain_verified') and status = 'deployed') + or + (current_commitment_evidence_hash is null and deployment_status in ('not_applicable', 'undeployed') + and verification_status in ('hash_bound', 'verified', 'evidence_required') and status = 'verified_build') + or + (current_commitment_evidence_hash is null and deployment_status in ('not_applicable', 'undeployed') + and verification_status in ('pending', 'rejected') and status in ('source_published', 'indexed_pending')) + )) + ); diff --git a/services/registry-api/migrations/0008_testnet_sandbox_retention.sql b/services/registry-api/migrations/0008_testnet_sandbox_retention.sql new file mode 100644 index 00000000..d7c7267f --- /dev/null +++ b/services/registry-api/migrations/0008_testnet_sandbox_retention.sql @@ -0,0 +1,30 @@ +alter table package_versions + add column if not exists registry_environment text not null default 'production', + add column if not exists chain_network text not null default 'mainnet', + add column if not exists expires_at timestamptz, + add column if not exists expired_at timestamptz, + add column if not exists purge_after timestamptz, + add column if not exists static_purged_at timestamptz, + add column if not exists source_purged_at timestamptz; + +alter table package_versions + add constraint package_versions_registry_environment_check + check (registry_environment in ('production', 'testnet-sandbox')), + add constraint package_versions_chain_network_check + check (chain_network in ('mainnet', 'testnet')), + add constraint package_versions_environment_network_check + check ( + (registry_environment = 'production' and chain_network = 'mainnet' + and expires_at is null and purge_after is null) + or + (registry_environment = 'testnet-sandbox' and chain_network = 'testnet' + and expires_at is not null and purge_after is not null and purge_after > expires_at) + ); + +create index if not exists package_versions_expiry_idx + on package_versions(expires_at) + where expires_at is not null; + +create index if not exists package_versions_object_purge_idx + on package_versions(purge_after, snapshot_hash) + where purge_after is not null and source_purged_at is null; diff --git a/services/registry-api/migrations/0009_authorisation_sessions.sql b/services/registry-api/migrations/0009_authorisation_sessions.sql new file mode 100644 index 00000000..f2b348f1 --- /dev/null +++ b/services/registry-api/migrations/0009_authorisation_sessions.sql @@ -0,0 +1,45 @@ +create table if not exists authorisation_sessions ( + session_id text primary key, + poll_token_hash text not null, + browser_token_hash text not null, + registry_origin text not null, + website_origin text not null, + capability_pubkey text not null, + requested_scopes text[] not null, + capability_expires_at timestamptz not null, + cli_version text not null, + namespace text not null, + name text not null, + artifact_kind text not null, + status text not null default 'pending', + principal_type text, + principal_id text, + payload jsonb, + challenge_token_hash text, + capability_key_id text references capabilities(key_id), + namespace_status text, + audit_request_id text not null, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + expires_at timestamptz not null, + completed_at timestamptz, + check (session_id ~ '^auth_[0-9a-f]{32}$'), + check (poll_token_hash ~ '^sha256:[0-9a-f]{64}$'), + check (browser_token_hash ~ '^sha256:[0-9a-f]{64}$'), + check (cardinality(requested_scopes) > 0), + check (artifact_kind in ( + 'source_library', 'profile_library', 'runtime_verifier', + 'deployable_contract', 'reproducible_binary', 'template' + )), + check (status in ('pending', 'authorised', 'review_pending')), + check (namespace_status is null or namespace_status in ('active', 'review_pending')), + check ( + (status = 'pending' and capability_key_id is null and namespace_status is null and completed_at is null) + or + (status in ('authorised', 'review_pending') and capability_key_id is not null + and namespace_status is not null and completed_at is not null) + ) +); + +create index if not exists authorisation_sessions_expiry_idx + on authorisation_sessions(expires_at); diff --git a/services/registry-api/migrations/0010_ls_idl_interfaces.sql b/services/registry-api/migrations/0010_ls_idl_interfaces.sql new file mode 100644 index 00000000..87edc3e9 --- /dev/null +++ b/services/registry-api/migrations/0010_ls_idl_interfaces.sql @@ -0,0 +1,9 @@ +create index package_version_evidence_ls_idl_lookup_idx + on package_version_evidence ( + lower(regexp_replace(evidence->>'code_hash', '^0x', '', 'i')), + (evidence->>'network'), + (evidence->>'hash_type'), + lower(regexp_replace(evidence->>'data_hash', '^0x', '', 'i')), + created_at desc + ) + where kind = 'deployed'; diff --git a/services/registry-api/package-lock.json b/services/registry-api/package-lock.json index 8e7496bb..8085e36b 100644 --- a/services/registry-api/package-lock.json +++ b/services/registry-api/package-lock.json @@ -7,13 +7,20 @@ "": { "name": "@cellscript/registry-api", "version": "0.1.0", + "engines": { + "node": ">=22 <23" + }, "dependencies": { "@joyid/ckb": "^1.1.4", + "@noble/curves": "2.2.0", + "@noble/hashes": "2.2.0", "pg": "^8.13.1" }, "devDependencies": { "@cloudflare/workers-types": "^4.20250617.0", + "@types/node": "^22.20.1", "@types/pg": "^8.11.10", + "esbuild": "^0.25.12", "typescript": "^5.8.3", "vitest": "^3.2.4", "wrangler": "^4.20.5" @@ -170,9 +177,9 @@ "optional": true }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -187,9 +194,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -204,9 +211,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -221,9 +228,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -238,9 +245,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -255,9 +262,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -272,9 +279,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -289,9 +296,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -306,9 +313,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -323,9 +330,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -340,9 +347,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -357,9 +364,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -374,9 +381,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -391,9 +398,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -408,9 +415,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -425,9 +432,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -442,9 +449,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -459,9 +466,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], @@ -476,9 +483,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -493,9 +500,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], @@ -510,9 +517,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -527,9 +534,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" ], @@ -544,9 +551,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -561,9 +568,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -578,9 +585,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -595,9 +602,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -1218,6 +1225,33 @@ "integrity": "sha512-5jQNjFw76YCd+Ppl+0RvBWzxwvWaKfWC5wjVFFdNAieX7xksCHfZFIeow8je7AF8uVypwe56WlLBlblxw9NBBQ==", "license": "MIT" }, + "node_modules/@noble/curves": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/@noble/curves/-/curves-2.2.0.tgz", + "integrity": "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.2.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@poppinss/colors": { "version": "4.1.6", "resolved": "https://registry.npmmirror.com/@poppinss/colors/-/colors-4.1.6.tgz", @@ -1682,13 +1716,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.0.0", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-26.0.0.tgz", - "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", + "version": "22.20.1", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~8.3.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/pg": { @@ -1999,9 +2033,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2012,32 +2046,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/estree-walker": { @@ -2724,9 +2758,9 @@ } }, "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, @@ -2838,6 +2872,490 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, "node_modules/vitest": { "version": "3.2.6", "resolved": "https://registry.npmmirror.com/vitest/-/vitest-3.2.6.tgz", diff --git a/services/registry-api/package.json b/services/registry-api/package.json index 8e8b200f..8c732e4e 100644 --- a/services/registry-api/package.json +++ b/services/registry-api/package.json @@ -3,20 +3,32 @@ "version": "0.1.0", "private": true, "type": "module", + "engines": { + "node": ">=22 <23" + }, "scripts": { "check": "tsc --noEmit", "test": "vitest run", "build": "wrangler deploy --dry-run --config wrangler.example.toml --outdir dist", + "build:node": "npm run build:node:server && npm run build:node:verifier", + "build:node:server": "esbuild src/node-server.ts --bundle --platform=node --format=esm --packages=external --target=node22 --sourcemap --outfile=dist-node/server.mjs", + "build:node:verifier": "esbuild src/verification-worker.ts --bundle --platform=node --format=esm --packages=external --target=node22 --sourcemap --outfile=dist-node/verification-worker.mjs", + "start:node": "node dist-node/server.mjs", + "start:verifier": "node dist-node/verification-worker.mjs", "migrate": "node scripts/migrate.mjs", "deploy": "wrangler deploy --config wrangler.toml" }, "dependencies": { "@joyid/ckb": "^1.1.4", + "@noble/curves": "2.2.0", + "@noble/hashes": "2.2.0", "pg": "^8.13.1" }, "devDependencies": { "@cloudflare/workers-types": "^4.20250617.0", + "@types/node": "^22.20.1", "@types/pg": "^8.11.10", + "esbuild": "^0.25.12", "typescript": "^5.8.3", "vitest": "^3.2.4", "wrangler": "^4.20.5" diff --git a/services/registry-api/src/domain.ts b/services/registry-api/src/domain.ts index 8651262c..7d0ee9e1 100644 --- a/services/registry-api/src/domain.ts +++ b/services/registry-api/src/domain.ts @@ -1,21 +1,79 @@ import type { SignChallengeResponseData } from "@joyid/ckb"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { blake2b } from "@noble/hashes/blake2.js"; export const AUTH_PROTOCOL = "cellscript-registry-auth-v1"; export const AUTH_ACTION = "authorize_capability"; export const AUTH_REVOKE_CAPABILITY_ACTION = "revoke_capability"; export const PUBLISH_PROTOCOL = "cellscript-registry-publish-v1"; export const PUBLISH_ACTION = "publish"; +export const DEPLOYMENT_PROTOCOL = "cellscript-registry-deployment"; +export const DEPLOYMENT_ACTION = "record_deployment"; +export const AVAILABILITY_PROTOCOL = "cellscript-registry-availability-v1"; +export const AVAILABILITY_ACTION = "set_availability"; +export const REGISTRY_SCHEMA_VERSION = 1; +export const ARTIFACT_PROFILE_CONTRACT_SCHEMA = "cellscript-registry-profile-contract-v1"; +export const ARTIFACT_PROFILE_CATALOG_SCHEMA = "cellscript-registry-profile-catalog-v1"; +export const LS_IDL_INTERFACE_SCHEMA = "cellscript-registry-ls-idl-interface-v1"; +export const LS_IDL_CONTENT_TYPE = "application/vnd.ckb.ls-idl+json"; +export const LS_IDL_FORMAT_VERSION = "0.1"; +export const CELLSCRIPT_EDITION = "2026"; export const DEFAULT_REGISTRY_ORIGIN = "https://api.registry.cellscript.dev"; export const DEFAULT_STATIC_REGISTRY_ORIGIN = "https://registry.cellscript.dev"; -export const ACCEPTED_PRINCIPAL_TYPE = "joyid_ckb"; +export const JOYID_PRINCIPAL_TYPE = "joyid_ckb"; +export const CKB_SECP256K1_PRINCIPAL_TYPE = "ckb_secp256k1"; +export const ACCEPTED_PRINCIPAL_TYPES = [JOYID_PRINCIPAL_TYPE, CKB_SECP256K1_PRINCIPAL_TYPE] as const; +export const ARTIFACT_KINDS = [ + "source_library", + "profile_library", + "runtime_verifier", + "deployable_contract", + "reproducible_binary", + "template", +] as const; +export const ARTIFACT_PROFILES = ["cellscript_source", "ckb_executable", "reproducible_build", "copy_material"] as const; +export const ARTIFACT_LANGUAGES = ["cellscript", "rust", "c", "javascript", "other", "unspecified"] as const; +export const CONSUMPTION_MODES = ["dependency", "tcb", "deployment", "copy"] as const; +export const CAPABILITY_SCOPE_ACTIONS = ["publish", "deployment", "availability"] as const; export const JOYID_CKB_PRINCIPAL_BINDING_CONTEXT = "cellscript-registry-joyid-ckb-principal-v1"; +export const CKB_SECP256K1_PRINCIPAL_BINDING_CONTEXT = "cellscript-registry-ckb-secp256k1-principal-v1"; + +export type PrincipalType = (typeof ACCEPTED_PRINCIPAL_TYPES)[number]; +export type ArtifactKind = (typeof ARTIFACT_KINDS)[number]; +export type ArtifactProfile = (typeof ARTIFACT_PROFILES)[number]; +export type ArtifactLanguage = (typeof ARTIFACT_LANGUAGES)[number]; +export type ConsumptionMode = (typeof CONSUMPTION_MODES)[number]; +export type CapabilityScopeAction = (typeof CAPABILITY_SCOPE_ACTIONS)[number]; +export type VerificationStatus = "pending" | "hash_bound" | "verified" | "evidence_required" | "rejected"; +export type DeploymentStatus = "not_applicable" | "undeployed" | "deployed" | "chain_verified"; +export type AvailabilityStatus = "active" | "deprecated" | "yanked" | "quarantined"; + +export interface ArtifactDescriptor { + kind: ArtifactKind; + profile: ArtifactProfile; + consumption_mode: ConsumptionMode; + language: ArtifactLanguage; +} + +export interface ArtifactProfileDefinition { + schema: typeof ARTIFACT_PROFILE_CATALOG_SCHEMA; + profile: ArtifactProfile; + validator_id: + | "cellscript-source-package-v1" + | "ckb-executable-profile-v1" + | "reproducible-build-profile-v1" + | "copy-material-profile-v1"; + resolver_capability: "dependency" | "non_resolving"; + requires_profile_contract: boolean; + contracts: Partial>; +} export type RegistryEntryStatus = | "source_published" | "indexed_pending" | "verified_build" | "deployed" - | "on_chain_attested" + | "on_chain_committed" | "deprecated" | "yanked" | "quarantined"; @@ -24,7 +82,7 @@ export interface CapabilityAuthorisationPayload { protocol: typeof AUTH_PROTOCOL; action: typeof AUTH_ACTION; registry_origin: string; - principal_type: typeof ACCEPTED_PRINCIPAL_TYPE; + principal_type: PrincipalType; principal_id: string; capability_pubkey: string; requested_scopes: string[]; @@ -39,7 +97,7 @@ export interface CapabilityRevocationPayload { protocol: typeof AUTH_PROTOCOL; action: typeof AUTH_REVOKE_CAPABILITY_ACTION; registry_origin: string; - principal_type: typeof ACCEPTED_PRINCIPAL_TYPE; + principal_type: PrincipalType; principal_id: string; capability_key_id: string; nonce: string; @@ -56,13 +114,80 @@ export interface PublishPayload { name: string; version: string; source_hash: string; - manifest_hash?: string; + manifest_hash: string; capability_key_id: string; nonce: string; issued_at: string; expires_at: string; cli_version: string; - registry_entry: Record; + artifact: ArtifactDescriptor; + registry_entry: RegistryIndexEntry; +} + +export interface DeploymentPayload { + protocol: typeof DEPLOYMENT_PROTOCOL; + action: typeof DEPLOYMENT_ACTION; + registry_origin: string; + namespace: string; + name: string; + release: string; + network: "mainnet" | "testnet"; + artifact_hash: string; + data_hash: string; + code_hash: string; + hash_type: "data" | "data1" | "data2" | "type"; + dep_type: "code" | "dep_group"; + out_point: { tx_hash: string; index: number }; + capability_key_id: string; + nonce: string; + issued_at: string; + expires_at: string; + cli_version: string; +} + +export interface AvailabilityPayload { + protocol: typeof AVAILABILITY_PROTOCOL; + action: typeof AVAILABILITY_ACTION; + registry_origin: string; + namespace: string; + name: string; + release: string; + availability_status: Exclude; + reason?: string; + capability_key_id: string; + nonce: string; + issued_at: string; + expires_at: string; + cli_version: string; +} + +export interface RegistryVersionEntry { + version: string; + tag: string; + source_hash: string; + cellscript_version?: string; + /** Source-language semantics only; target/ABI/schema identity is separate. */ + edition?: typeof CELLSCRIPT_EDITION; + /** Hash of the resolved edition + target + assurance + ABI + schema axes. */ + compatibility_profile_hash?: string; + artifact_hash?: string; + build_recipe_hash?: string; + abi_hash?: string; + profile_contract?: Record; + dependencies?: Record; + verification_status: "pending"; + deployment_status: DeploymentStatus; + availability_status: "active"; + [key: string]: unknown; +} + +export interface RegistryIndexEntry { + schema_version: typeof REGISTRY_SCHEMA_VERSION; + namespace: string; + name: string; + artifact: ArtifactDescriptor; + versions: [RegistryVersionEntry]; + [key: string]: unknown; } export interface SourceSnapshotInput { @@ -81,6 +206,15 @@ export interface JoyidVerifier { verifySignature(signature: SignChallengeResponseData): Promise; } +export interface CkbSecp256k1Signature { + scheme: typeof CKB_SECP256K1_PRINCIPAL_TYPE; + challenge: string; + signature: string; + public_key: string; +} + +export type PrincipalSignature = SignChallengeResponseData | CkbSecp256k1Signature; + export interface CapabilitySignatureVerifier { verify(canonicalPayload: string, capabilityPubkey: string, signature: CapabilitySignature): Promise; } @@ -123,6 +257,15 @@ export async function sha256Hex(input: string | Uint8Array | ArrayBuffer): Promi return [...new Uint8Array(hash)].map((byte) => byte.toString(16).padStart(2, "0")).join(""); } +export function ckbBlake2bHex(input: string | Uint8Array): string { + const data = typeof input === "string" ? new TextEncoder().encode(input) : input; + const digest = blake2b(data, { + dkLen: 32, + personalization: new TextEncoder().encode("ckb-default-hash"), + }); + return `0x${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`; +} + export function base64ToBytes(value: string): Uint8Array { const binary = atob(value); const out = new Uint8Array(binary.length); @@ -180,6 +323,39 @@ export function requireStringArray(value: Record, key: string): return item.map((entry) => entry.trim()); } +export function isPrincipalType(value: string): value is PrincipalType { + return ACCEPTED_PRINCIPAL_TYPES.some((principalType) => principalType === value); +} + +export function validatePrincipalType(value: string): PrincipalType { + if (!isPrincipalType(value)) { + throw new ApiError( + 400, + "unsupported_principal_type", + `principal_type must be one of: ${ACCEPTED_PRINCIPAL_TYPES.join(", ")}`, + ); + } + return value; +} + +function validatePrincipalId(value: string, principalType: PrincipalType): string { + const principalId = value.trim().toLowerCase(); + if (principalType === CKB_SECP256K1_PRINCIPAL_TYPE) { + if (!/^0x[0-9a-f]{64}$/.test(principalId)) { + throw new ApiError( + 400, + "invalid_principal_id", + "ckb_secp256k1 principal_id must be the 32-byte CellScript public-key binding", + ); + } + return principalId; + } + if (!/^0x[0-9a-f]{40,64}$/.test(principalId) && !/^ck[bt]1[0-9a-z]+$/.test(principalId)) { + throw new ApiError(400, "invalid_principal_id", "principal_id must be a normalized JoyID/CKB identity binding"); + } + return principalId; +} + export function parseTimestamp(value: string, key: string): Date { const date = new Date(value); if (!Number.isFinite(date.getTime())) { @@ -190,20 +366,199 @@ export function parseTimestamp(value: string, key: string): Date { export function validatePackageIdent(value: string, field: string): string { const trimmed = value.trim(); - if (!/^[a-z0-9][a-z0-9_-]{1,62}$/.test(trimmed)) { - throw new ApiError(400, "invalid_package_identifier", `${field} must be lowercase ascii, 2-63 chars`); + if (!/^[a-z0-9](?:[a-z0-9_-]{0,62}[a-z0-9])?$/.test(trimmed)) { + throw new ApiError( + 400, + "invalid_package_identifier", + `${field} must be 1-64 lowercase letters or numbers, with _ or - only between characters`, + ); } return trimmed; } export function validateVersion(value: string): string { const trimmed = value.trim(); - if (!/^[0-9]+[.][0-9]+[.][0-9]+(?:[-+][0-9A-Za-z.-]+)?$/.test(trimmed)) { - throw new ApiError(400, "invalid_version", "version must be semver-like"); + const match = /^(0|[1-9][0-9]*)[.](0|[1-9][0-9]*)[.](0|[1-9][0-9]*)(?:-([0-9A-Za-z-]+(?:[.][0-9A-Za-z-]+)*))?(?:[+]([0-9A-Za-z-]+(?:[.][0-9A-Za-z-]+)*))?$/.exec(trimmed); + if (!match) { + throw new ApiError(400, "invalid_version", "version must be valid SemVer"); + } + const prerelease = match[4]?.split(".") ?? []; + if (prerelease.some((identifier) => /^[0-9]+$/.test(identifier) && identifier.length > 1 && identifier.startsWith("0"))) { + throw new ApiError(400, "invalid_version", "numeric SemVer prerelease identifiers must not contain leading zeroes"); } return trimmed; } +export function validateDeploymentPayload( + input: unknown, + registryOrigin: string, + now: Date, + expectedNetwork: DeploymentPayload["network"] = "mainnet", +): DeploymentPayload { + const value = assertPlainObject(input, "invalid_deployment_payload"); + if (requireString(value, "protocol") !== DEPLOYMENT_PROTOCOL || requireString(value, "action") !== DEPLOYMENT_ACTION) { + throw new ApiError(400, "invalid_deployment_action", "deployment payload has the wrong protocol or action"); + } + if (requireString(value, "registry_origin") !== registryOrigin) { + throw new ApiError(400, "invalid_registry_origin", "deployment payload registry_origin does not match this API"); + } + const network = requireString(value, "network"); + if (network !== expectedNetwork) { + throw new ApiError( + 400, + "unsupported_deployment_network", + `Registry deployment records for this environment must use ${expectedNetwork}`, + ); + } + const artifactHash = requireString(value, "artifact_hash"); + const dataHash = requireString(value, "data_hash"); + const codeHash = requireString(value, "code_hash"); + validateHash(artifactHash, "artifact_hash", "invalid_artifact_hash"); + validateHash(dataHash, "data_hash", "invalid_data_hash"); + validateHash(codeHash, "code_hash", "invalid_code_hash"); + if (!sameCkbHash(artifactHash, dataHash)) { + throw new ApiError(400, "deployment_data_hash_mismatch", "data_hash must equal the published executable artifact_hash"); + } + const hashType = requireString(value, "hash_type"); + if (!(hashType === "data" || hashType === "data1" || hashType === "data2" || hashType === "type")) { + throw new ApiError(400, "invalid_hash_type", "hash_type must be data, data1, data2, or type"); + } + const depType = requireString(value, "dep_type"); + if (!(depType === "code" || depType === "dep_group")) { + throw new ApiError(400, "invalid_dep_type", "dep_type must be code or dep_group"); + } + const outPoint = assertPlainObject(value["out_point"], "invalid_deployment_out_point"); + const txHash = requireString(outPoint, "tx_hash"); + validateHash(txHash, "out_point.tx_hash", "invalid_deployment_out_point"); + const index = outPoint["index"]; + if (!Number.isSafeInteger(index) || Number(index) < 0 || Number(index) > 0xffff_ffff) { + throw new ApiError(400, "invalid_deployment_out_point", "out_point.index must be a non-negative u32 integer"); + } + const nonce = requireString(value, "nonce"); + if (!/^0x[0-9a-fA-F]{16,}$/.test(nonce)) { + throw new ApiError(400, "invalid_nonce", "nonce must be hex and at least 8 bytes"); + } + const issuedAt = requireString(value, "issued_at"); + const expiresAt = requireString(value, "expires_at"); + parseTimestamp(issuedAt, "issued_at"); + if (parseTimestamp(expiresAt, "expires_at").getTime() <= now.getTime()) { + throw new ApiError(401, "deployment_payload_expired", "deployment payload has expired"); + } + return { + protocol: DEPLOYMENT_PROTOCOL, + action: DEPLOYMENT_ACTION, + registry_origin: registryOrigin, + namespace: validatePackageIdent(requireString(value, "namespace"), "namespace"), + name: validatePackageIdent(requireString(value, "name"), "name"), + release: validateVersion(requireString(value, "release")), + network: expectedNetwork, + artifact_hash: artifactHash, + data_hash: dataHash, + code_hash: codeHash, + hash_type: hashType, + dep_type: depType, + out_point: { tx_hash: txHash, index: Number(index) }, + capability_key_id: requireString(value, "capability_key_id"), + nonce, + issued_at: issuedAt, + expires_at: expiresAt, + cli_version: requireString(value, "cli_version"), + }; +} + +export function validateAvailabilityPayload( + input: unknown, + registryOrigin: string, + now: Date, +): AvailabilityPayload { + const value = assertPlainObject(input, "invalid_availability_payload"); + if (requireString(value, "protocol") !== AVAILABILITY_PROTOCOL || requireString(value, "action") !== AVAILABILITY_ACTION) { + throw new ApiError(400, "invalid_availability_action", "availability payload has the wrong protocol or action"); + } + if (requireString(value, "registry_origin") !== registryOrigin) { + throw new ApiError(400, "invalid_registry_origin", "availability payload registry_origin does not match this API"); + } + const availabilityStatus = requireString(value, "availability_status"); + if (!(availabilityStatus === "active" || availabilityStatus === "deprecated" || availabilityStatus === "yanked")) { + throw new ApiError(400, "invalid_publisher_availability_status", "publishers may set availability_status to active, deprecated, or yanked"); + } + const reason = value["reason"] === undefined ? undefined : requireString(value, "reason").trim(); + if (availabilityStatus === "yanked" && !reason) { + throw new ApiError(400, "availability_reason_required", "yanking a release requires a reason"); + } + if (reason && reason.length > 500) { + throw new ApiError(400, "invalid_availability_reason", "availability reason must be no longer than 500 characters"); + } + const capabilityKeyId = requireString(value, "capability_key_id"); + if (!/^cap_[0-9a-f]{32}$/.test(capabilityKeyId)) { + throw new ApiError(400, "invalid_capability_key_id", "capability_key_id is malformed"); + } + const nonce = requireString(value, "nonce"); + if (!/^0x[0-9a-fA-F]{16,}$/.test(nonce)) { + throw new ApiError(400, "invalid_nonce", "nonce must be hex and at least 8 bytes"); + } + const issuedAt = requireString(value, "issued_at"); + const expiresAt = requireString(value, "expires_at"); + parseTimestamp(issuedAt, "issued_at"); + if (parseTimestamp(expiresAt, "expires_at").getTime() <= now.getTime()) { + throw new ApiError(401, "availability_payload_expired", "availability payload has expired"); + } + return { + protocol: AVAILABILITY_PROTOCOL, + action: AVAILABILITY_ACTION, + registry_origin: registryOrigin, + namespace: validatePackageIdent(requireString(value, "namespace"), "namespace"), + name: validatePackageIdent(requireString(value, "name"), "name"), + release: validateVersion(requireString(value, "release")), + availability_status: availabilityStatus, + ...(reason ? { reason } : {}), + capability_key_id: capabilityKeyId, + nonce, + issued_at: issuedAt, + expires_at: expiresAt, + cli_version: requireString(value, "cli_version"), + }; +} + +export function sameCkbHash(left: string, right: string): boolean { + return left.replace(/^0x/, "").toLowerCase() === right.replace(/^0x/, "").toLowerCase(); +} + +export function ckbScriptHash(value: unknown): string { + const script = assertPlainObject(value, "invalid_ckb_script"); + const codeHash = hexToBytes(requireString(script, "code_hash")); + if (codeHash.length !== 32) { + throw new ApiError(502, "invalid_ckb_rpc_response", "CKB RPC returned a script with a non-Byte32 code_hash"); + } + const hashType = requireString(script, "hash_type"); + const hashTypeByte = ({ data: 0, type: 1, data1: 2, data2: 4 } as const)[hashType as "data" | "type" | "data1" | "data2"]; + if (hashTypeByte === undefined) { + throw new ApiError(502, "invalid_ckb_rpc_response", "CKB RPC returned an unknown script hash_type"); + } + const args = hexToBytes(requireString(script, "args")); + const totalSize = 53 + args.length; + const serialized = new Uint8Array(totalSize); + writeU32Le(serialized, 0, totalSize); + writeU32Le(serialized, 4, 16); + writeU32Le(serialized, 8, 48); + writeU32Le(serialized, 12, 49); + serialized.set(codeHash, 16); + serialized[48] = hashTypeByte; + // Molecule Bytes is a byte FixVec: the u32 header stores the item count, + // while the enclosing Script table stores the total byte size. + writeU32Le(serialized, 49, args.length); + serialized.set(args, 53); + const digest = blake2b(serialized, { + dkLen: 32, + personalization: new TextEncoder().encode("ckb-default-hash"), + }); + return `0x${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`; +} + +function writeU32Le(target: Uint8Array, offset: number, value: number): void { + new DataView(target.buffer, target.byteOffset, target.byteLength).setUint32(offset, value, true); +} + export function validateCapabilityPayload( payload: unknown, registryOrigin: string, @@ -212,10 +567,14 @@ export function validateCapabilityPayload( const obj = assertPlainObject(payload, "invalid_capability_payload"); const protocol = requireString(obj, "protocol"); const action = requireString(obj, "action"); - const principalType = requireString(obj, "principal_type"); - const principalId = requireString(obj, "principal_id"); + const principalType = validatePrincipalType(requireString(obj, "principal_type")); + const principalId = validatePrincipalId(requireString(obj, "principal_id"), principalType); const capabilityPubkey = requireString(obj, "capability_pubkey"); - const requestedScopes = requireStringArray(obj, "requested_scopes"); + const requestedScopesValue = obj["requested_scopes"]; + if (!Array.isArray(requestedScopesValue) || requestedScopesValue.some((scope) => typeof scope !== "string" || scope.trim() === "")) { + throw new ApiError(400, "invalid_field", "requested_scopes must be a string array"); + } + const requestedScopes = requestedScopesValue.map((scope) => scope.trim()); const capabilityExpiresAt = requireString(obj, "capability_expires_at"); const nonce = requireString(obj, "nonce"); const issuedAt = requireString(obj, "issued_at"); @@ -228,14 +587,18 @@ export function validateCapabilityPayload( if (requireString(obj, "registry_origin") !== registryOrigin) { throw new ApiError(400, "invalid_registry_origin", "capability payload registry_origin does not match this API"); } - if (principalType !== ACCEPTED_PRINCIPAL_TYPE) { - throw new ApiError(400, "unsupported_principal_type", "only joyid_ckb principals are accepted"); - } - if (!/^0x[0-9a-fA-F]{40,64}$/.test(principalId) && !/^ck[bt]1[0-9a-z]+$/.test(principalId)) { - throw new ApiError(400, "invalid_principal_id", "principal_id must be a normalized JoyID/CKB identity binding"); + const ident = "[a-z0-9](?:[a-z0-9_-]{0,62}[a-z0-9])?"; + const scopeActionsPattern = CAPABILITY_SCOPE_ACTIONS.join("|"); + const scopePattern = new RegExp(`^(?:${scopeActionsPattern}):${ident}/(?:${ident}|\\*)$`); + if (requestedScopes.length === 0 || requestedScopes.some((scope) => !scopePattern.test(scope))) { + throw new ApiError( + 400, + "invalid_scope", + "requested_scopes must contain publish, deployment, or availability scopes for namespace/package or namespace/*", + ); } - if (requestedScopes.some((scope) => !/^publish:[a-z0-9][a-z0-9_-]{1,62}\/[a-z0-9][a-z0-9_-]{1,62}$/.test(scope))) { - throw new ApiError(400, "invalid_scope", "requested_scopes may only contain publish:namespace/package scopes"); + if (new Set(requestedScopes).size !== requestedScopes.length) { + throw new ApiError(400, "duplicate_scope", "requested_scopes must not contain duplicates"); } if (!/^0x[0-9a-fA-F]{16,}$/.test(nonce)) { throw new ApiError(400, "invalid_nonce", "nonce must be hex and at least 8 bytes"); @@ -254,7 +617,7 @@ export function validateCapabilityPayload( protocol: AUTH_PROTOCOL, action: AUTH_ACTION, registry_origin: registryOrigin, - principal_type: ACCEPTED_PRINCIPAL_TYPE, + principal_type: principalType, principal_id: principalId, capability_pubkey: capabilityPubkey, requested_scopes: requestedScopes, @@ -274,8 +637,8 @@ export function validateCapabilityRevocationPayload( const obj = assertPlainObject(payload, "invalid_capability_revocation_payload"); const protocol = requireString(obj, "protocol"); const action = requireString(obj, "action"); - const principalType = requireString(obj, "principal_type"); - const principalId = requireString(obj, "principal_id"); + const principalType = validatePrincipalType(requireString(obj, "principal_type")); + const principalId = validatePrincipalId(requireString(obj, "principal_id"), principalType); const capabilityKeyId = requireString(obj, "capability_key_id"); const nonce = requireString(obj, "nonce"); const issuedAt = requireString(obj, "issued_at"); @@ -288,12 +651,6 @@ export function validateCapabilityRevocationPayload( if (requireString(obj, "registry_origin") !== registryOrigin) { throw new ApiError(400, "invalid_registry_origin", "capability revocation registry_origin does not match this API"); } - if (principalType !== ACCEPTED_PRINCIPAL_TYPE) { - throw new ApiError(400, "unsupported_principal_type", "only joyid_ckb principals are accepted"); - } - if (!/^0x[0-9a-fA-F]{40,64}$/.test(principalId) && !/^ck[bt]1[0-9a-z]+$/.test(principalId)) { - throw new ApiError(400, "invalid_principal_id", "principal_id must be a normalized JoyID/CKB identity binding"); - } if (!/^cap_[0-9a-f]{32}$/.test(capabilityKeyId)) { throw new ApiError(400, "invalid_capability_key_id", "capability_key_id is malformed"); } @@ -309,7 +666,7 @@ export function validateCapabilityRevocationPayload( protocol: AUTH_PROTOCOL, action: AUTH_REVOKE_CAPABILITY_ACTION, registry_origin: registryOrigin, - principal_type: ACCEPTED_PRINCIPAL_TYPE, + principal_type: principalType, principal_id: principalId, capability_key_id: capabilityKeyId, nonce, @@ -333,15 +690,16 @@ export function validatePublishPayload(payload: unknown, registryOrigin: string, const name = validatePackageIdent(requireString(obj, "name"), "name"); const version = validateVersion(requireString(obj, "version")); const sourceHash = requireString(obj, "source_hash"); - if (!/^([a-z0-9_-]+:)?0x[0-9a-fA-F]{32,128}$/.test(sourceHash) && !/^[0-9a-fA-F]{32,128}$/.test(sourceHash)) { - throw new ApiError(400, "invalid_source_hash", "source_hash must be a hex content hash"); - } + validateHash(sourceHash, "source_hash", "invalid_source_hash"); + const manifestHash = requireString(obj, "manifest_hash"); + validateHash(manifestHash, "manifest_hash", "invalid_manifest_hash"); const capabilityKeyId = requireString(obj, "capability_key_id"); const nonce = requireString(obj, "nonce"); const issuedAt = requireString(obj, "issued_at"); const expiresAt = requireString(obj, "expires_at"); const cliVersion = requireString(obj, "cli_version"); - const registryEntry = assertPlainObject(obj["registry_entry"], "invalid_registry_entry"); + const artifact = validateArtifactDescriptor(obj["artifact"]); + const registryEntry = validateRegistryEntry(obj["registry_entry"], { namespace, name, version, sourceHash, manifestHash, artifact }); parseTimestamp(issuedAt, "issued_at"); if (parseTimestamp(expiresAt, "expires_at").getTime() <= now.getTime()) { throw new ApiError(401, "publish_payload_expired", "publish payload has expired"); @@ -358,19 +716,377 @@ export function validatePublishPayload(payload: unknown, registryOrigin: string, name, version, source_hash: sourceHash, + manifest_hash: manifestHash, capability_key_id: capabilityKeyId, nonce, issued_at: issuedAt, expires_at: expiresAt, cli_version: cliVersion, + artifact, registry_entry: registryEntry, }; - if (typeof obj["manifest_hash"] === "string") { - result.manifest_hash = obj["manifest_hash"]; - } return result; } +function validateHash(value: string, field: string, code: string): void { + if (!/^(?:0x)?[0-9a-fA-F]{64}$/.test(value)) { + throw new ApiError(400, code, `${field} must be a 32-byte hex content hash`); + } +} + +function validateRegistryEntry( + input: unknown, + outer: { namespace: string; name: string; version: string; sourceHash: string; manifestHash: string; artifact: ArtifactDescriptor }, +): RegistryIndexEntry { + const entry = assertPlainObject(input, "invalid_registry_entry"); + if (entry["schema_version"] !== REGISTRY_SCHEMA_VERSION) { + throw new ApiError( + 400, + "unsupported_registry_schema", + `registry_entry.schema_version must be ${REGISTRY_SCHEMA_VERSION}`, + ); + } + if (requireString(entry, "namespace") !== outer.namespace || requireString(entry, "name") !== outer.name) { + throw new ApiError(400, "registry_identity_mismatch", "registry_entry namespace/name must match the signed publish identity"); + } + const artifact = validateArtifactDescriptor(entry["artifact"]); + if (canonicalJson(artifact) !== canonicalJson(outer.artifact)) { + throw new ApiError(400, "artifact_identity_mismatch", "registry_entry artifact descriptor must match the signed publish identity"); + } + + const versions = entry["versions"]; + if (!Array.isArray(versions) || versions.length !== 1) { + throw new ApiError(400, "invalid_registry_versions", "registry_entry.versions must contain exactly the published version"); + } + const published = assertPlainObject(versions[0], "invalid_registry_version"); + const version = validateVersion(requireString(published, "version")); + const sourceHash = requireString(published, "source_hash"); + if (version !== outer.version || sourceHash !== outer.sourceHash) { + throw new ApiError(400, "registry_identity_mismatch", "registry version and source_hash must match the signed publish identity"); + } + if (requireString(published, "tag") !== `v${outer.version}`) { + throw new ApiError(400, "invalid_registry_tag", "registry version tag must be v"); + } + const initialStates = initialArtifactStates(artifact); + if ( + published["verification_status"] !== initialStates.verification_status + || published["deployment_status"] !== initialStates.deployment_status + || published["availability_status"] !== initialStates.availability_status + ) { + throw new ApiError(400, "invalid_initial_artifact_state", "new releases must use the profile's initial verification, deployment, and availability states"); + } + + const profileDefinition = ARTIFACT_PROFILE_CATALOG[artifact.profile]; + switch (profileDefinition.validator_id) { + case "cellscript-source-package-v1": { + if (published["profile_contract"] !== undefined) { + throw new ApiError(400, "invalid_profile_contract", "CellScript source releases do not use profile_contract"); + } + requireString(published, "cellscript_version"); + if (published["edition"] !== CELLSCRIPT_EDITION) { + throw new ApiError(400, "unsupported_cellscript_edition", `registry version edition must be ${CELLSCRIPT_EDITION}`); + } + const compatibilityProfileHash = requireString(published, "compatibility_profile_hash"); + validateHash(compatibilityProfileHash, "compatibility_profile_hash", "invalid_compatibility_profile_hash"); + const dependencies = assertPlainObject(published["dependencies"], "invalid_registry_dependencies"); + for (const [dependencyName, dependencyValue] of Object.entries(dependencies)) { + validatePackageIdent(dependencyName, "dependency name"); + const dependency = assertPlainObject(dependencyValue, "invalid_registry_dependency"); + validatePackageIdent(requireString(dependency, "namespace"), "dependency namespace"); + validateVersion(requireString(dependency, "version")); + } + break; + } + case "ckb-executable-profile-v1": + validateHash(requireString(published, "artifact_hash"), "artifact_hash", "invalid_artifact_hash"); + validateHash(requireString(published, "abi_hash"), "abi_hash", "invalid_abi_hash"); + break; + case "reproducible-build-profile-v1": + validateHash(requireString(published, "artifact_hash"), "artifact_hash", "invalid_artifact_hash"); + validateHash(requireString(published, "build_recipe_hash"), "build_recipe_hash", "invalid_build_recipe_hash"); + break; + case "copy-material-profile-v1": + break; + } + if (profileDefinition.requires_profile_contract) { + validateArtifactProfileContract(published["profile_contract"], artifact, published, outer.manifestHash); + } + + return entry as unknown as RegistryIndexEntry; +} + +function validateArtifactProfileContract( + input: unknown, + artifact: ArtifactDescriptor, + release: Record, + manifestHash: string, +): void { + let contract: Record; + try { + contract = assertPlainObject(input, "invalid_profile_contract"); + } catch { + throw new ApiError(400, "invalid_profile_contract", "profile_contract must be a JSON object"); + } + exactKeys( + contract, + ["schema", "artifact_kind", "profile", "build", "security", "ckb", "interface", "verifier", "reproduction", "copy"], + "profile_contract", + ); + requireLiteral(contract, "schema", ARTIFACT_PROFILE_CONTRACT_SCHEMA, "profile_contract"); + requireLiteral(contract, "artifact_kind", artifact.kind, "profile_contract"); + requireLiteral(contract, "profile", artifact.profile, "profile_contract"); + requireSameContentHash(ckbBlake2bHex(canonicalJson(contract)), manifestHash, "profile_contract manifest_hash"); + + if (artifact.kind === "runtime_verifier" || artifact.kind === "deployable_contract") { + const reproducible = validateBuildContract(contract); + validateSecurityContract(contract); + const ckb = requiredObject(contract, "ckb", "profile_contract"); + exactKeys(ckb, ["vm_version", "script_role", "hash_type", "dep_type", "abi_hash"], "profile_contract.ckb"); + requireOneOf(ckb, "vm_version", ["0", "1", "2"], "profile_contract.ckb"); + requireOneOf(ckb, "script_role", ["lock", "type", "dual_role", "helper"], "profile_contract.ckb"); + requireOneOf(ckb, "hash_type", ["data", "data1", "data2", "type"], "profile_contract.ckb"); + requireOneOf(ckb, "dep_type", ["code", "dep_group"], "profile_contract.ckb"); + requireBoundHash(ckb, "abi_hash", release["abi_hash"], "profile_contract.ckb"); + validateLsIdlInterfaceContract(contract, artifact); + validateReproductionContract(contract, release, reproducible); + forbidKeys(contract, ["copy"], "profile_contract"); + if (artifact.kind === "runtime_verifier") { + forbidKeys(contract, ["interface"], "profile_contract"); + const verifier = requiredObject(contract, "verifier", "profile_contract"); + exactKeys(verifier, ["verifier_id", "ipc_abi", "ipc_abi_hash"], "profile_contract.verifier"); + requireString(verifier, "verifier_id"); + requireString(verifier, "ipc_abi"); + requireBoundHash(verifier, "ipc_abi_hash", release["abi_hash"], "profile_contract.verifier"); + } else { + forbidKeys(contract, ["verifier"], "profile_contract"); + } + return; + } + if (artifact.kind === "reproducible_binary") { + validateBuildContract(contract, true); + validateSecurityContract(contract); + forbidKeys(contract, ["ckb", "interface", "verifier", "copy"], "profile_contract"); + validateReproductionContract(contract, release, true); + return; + } + if (artifact.kind === "template") { + forbidKeys(contract, ["build", "security", "ckb", "interface", "verifier", "reproduction"], "profile_contract"); + const copy = requiredObject(contract, "copy", "profile_contract"); + exactKeys(copy, ["format", "entrypoint"], "profile_contract.copy"); + requireOneOf(copy, "format", ["file_map_v1"], "profile_contract.copy"); + requireString(copy, "entrypoint"); + return; + } + throw new ApiError(400, "invalid_profile_contract", "profile_contract is not valid for this artifact kind"); +} + +function validateLsIdlInterfaceContract(contract: Record, artifact: ArtifactDescriptor): void { + if (contract["interface"] === undefined) return; + if (artifact.kind !== "deployable_contract") { + throw new ApiError(400, "invalid_profile_contract", "LS-IDL is valid only for deployable_contract artifacts"); + } + const ckb = requiredObject(contract, "ckb", "profile_contract"); + requireLiteral(ckb, "script_role", "lock", "profile_contract.ckb"); + const interfaceContract = requiredObject(contract, "interface", "profile_contract"); + exactKeys( + interfaceContract, + ["schema", "format", "format_version", "object_role", "content_type", "encoding", "commitment"], + "profile_contract.interface", + ); + requireLiteral(interfaceContract, "schema", LS_IDL_INTERFACE_SCHEMA, "profile_contract.interface"); + requireLiteral(interfaceContract, "format", "ls-idl", "profile_contract.interface"); + requireLiteral(interfaceContract, "format_version", LS_IDL_FORMAT_VERSION, "profile_contract.interface"); + requireLiteral(interfaceContract, "object_role", "abi", "profile_contract.interface"); + requireLiteral(interfaceContract, "content_type", LS_IDL_CONTENT_TYPE, "profile_contract.interface"); + requireLiteral(interfaceContract, "encoding", "linear-le-v0", "profile_contract.interface"); + const commitment = requiredObject(interfaceContract, "commitment", "profile_contract.interface"); + exactKeys(commitment, ["algorithm", "placement", "digest"], "profile_contract.interface.commitment"); + requireLiteral(commitment, "algorithm", "sha256", "profile_contract.interface.commitment"); + requireLiteral(commitment, "placement", "code-cell-data-suffix-32", "profile_contract.interface.commitment"); + validateHash( + requireString(commitment, "digest"), + "profile_contract.interface.commitment.digest", + "invalid_profile_contract", + ); +} + +function validateBuildContract(contract: Record, expectedReproducible?: boolean): boolean { + const build = requiredObject(contract, "build", "profile_contract"); + exactKeys(build, ["target", "toolchain", "profile", "source_revision", "reproducible"], "profile_contract.build"); + for (const field of ["target", "toolchain", "profile", "source_revision"]) requireString(build, field); + if (typeof build["reproducible"] !== "boolean") { + throw new ApiError(400, "invalid_profile_contract", "profile_contract.build.reproducible must be a boolean"); + } + if (expectedReproducible !== undefined && build["reproducible"] !== expectedReproducible) { + throw new ApiError(400, "invalid_profile_contract", `profile_contract.build.reproducible must be ${expectedReproducible}`); + } + return build["reproducible"]; +} + +function validateReproductionContract( + contract: Record, + release: Record, + reproducible: boolean, +): void { + if (!reproducible) { + forbidKeys(contract, ["reproduction"], "profile_contract"); + if (release["build_recipe_hash"] !== undefined) { + throw new ApiError(400, "invalid_profile_contract", "build_recipe_hash requires profile_contract.build.reproducible=true"); + } + return; + } + const reproduction = requiredObject(contract, "reproduction", "profile_contract"); + exactKeys(reproduction, ["environment", "command", "recipe_hash", "expected_artifact_hash"], "profile_contract.reproduction"); + requireString(reproduction, "environment"); + requireString(reproduction, "command"); + requireBoundHash(reproduction, "recipe_hash", release["build_recipe_hash"], "profile_contract.reproduction"); + requireBoundHash(reproduction, "expected_artifact_hash", release["artifact_hash"], "profile_contract.reproduction"); +} + +function validateSecurityContract(contract: Record): void { + const security = requiredObject(contract, "security", "profile_contract"); + exactKeys(security, ["status", "audit_report_hash"], "profile_contract.security"); + const status = requireOneOf(security, "status", ["unaudited", "review_required", "audited", "rejected"], "profile_contract.security"); + if (status === "audited" && security["audit_report_hash"] === undefined) { + throw new ApiError(400, "invalid_profile_contract", "profile_contract.security.audit_report_hash is required for audited artifacts"); + } + if (security["audit_report_hash"] !== undefined) { + validateHash(requireString(security, "audit_report_hash"), "profile_contract.security.audit_report_hash", "invalid_profile_contract"); + } +} + +function exactKeys(object: Record, allowed: string[], label: string): void { + const unexpected = Object.keys(object).find((key) => !allowed.includes(key)); + if (unexpected) throw new ApiError(400, "invalid_profile_contract", `${label}.${unexpected} is not recognised`); +} + +function forbidKeys(object: Record, forbidden: string[], label: string): void { + const present = forbidden.find((key) => object[key] !== undefined); + if (present) throw new ApiError(400, "invalid_profile_contract", `${label}.${present} is not valid for this artifact kind`); +} + +function requiredObject(object: Record, key: string, label: string): Record { + try { + return assertPlainObject(object[key], "invalid_profile_contract"); + } catch { + throw new ApiError(400, "invalid_profile_contract", `${label}.${key} must be a JSON object`); + } +} + +function requireLiteral(object: Record, key: string, expected: string, label: string): void { + if (requireString(object, key) !== expected) { + throw new ApiError(400, "invalid_profile_contract", `${label}.${key} must be '${expected}'`); + } +} + +function requireOneOf(object: Record, key: string, allowed: string[], label: string): string { + const value = requireString(object, key); + if (!allowed.includes(value)) { + throw new ApiError(400, "invalid_profile_contract", `${label}.${key} must be one of ${allowed.join(", ")}`); + } + return value; +} + +function requireBoundHash(object: Record, key: string, expected: unknown, label: string): void { + const value = requireString(object, key); + validateHash(value, `${label}.${key}`, "invalid_profile_contract"); + if (typeof expected !== "string") { + throw new ApiError(400, "invalid_profile_contract", `${label}.${key} has no release hash to bind`); + } + requireSameContentHash(value, expected, `${label}.${key}`); +} + +function requireSameContentHash(actual: string, expected: string, label: string): void { + const normalize = (value: string) => value.replace(/^0x/i, "").toLowerCase(); + if (normalize(actual) !== normalize(expected)) { + throw new ApiError(400, "invalid_profile_contract", `${label} does not match the signed immutable object hash`); + } +} + +export const ARTIFACT_PROFILE_CATALOG = { + cellscript_source: { + schema: ARTIFACT_PROFILE_CATALOG_SCHEMA, + profile: "cellscript_source", + validator_id: "cellscript-source-package-v1", + resolver_capability: "dependency", + requires_profile_contract: false, + contracts: { + source_library: { consumption_mode: "dependency", languages: ["cellscript"] }, + profile_library: { consumption_mode: "dependency", languages: ["cellscript"] }, + }, + }, + ckb_executable: { + schema: ARTIFACT_PROFILE_CATALOG_SCHEMA, + profile: "ckb_executable", + validator_id: "ckb-executable-profile-v1", + resolver_capability: "non_resolving", + requires_profile_contract: true, + contracts: { + runtime_verifier: { consumption_mode: "tcb", languages: ["cellscript", "rust", "c", "javascript", "other"] }, + deployable_contract: { consumption_mode: "deployment", languages: ["cellscript", "rust", "c", "javascript", "other"] }, + }, + }, + reproducible_build: { + schema: ARTIFACT_PROFILE_CATALOG_SCHEMA, + profile: "reproducible_build", + validator_id: "reproducible-build-profile-v1", + resolver_capability: "non_resolving", + requires_profile_contract: true, + contracts: { + reproducible_binary: { consumption_mode: "tcb", languages: ["rust", "c", "other"] }, + }, + }, + copy_material: { + schema: ARTIFACT_PROFILE_CATALOG_SCHEMA, + profile: "copy_material", + validator_id: "copy-material-profile-v1", + resolver_capability: "non_resolving", + requires_profile_contract: true, + contracts: { + template: { consumption_mode: "copy", languages: ["cellscript", "rust", "c", "javascript", "other", "unspecified"] }, + }, + }, +} as const satisfies Record; + +export function artifactProfileSupportsDependencyResolution(profile: ArtifactProfile): boolean { + return ARTIFACT_PROFILE_CATALOG[profile].resolver_capability === "dependency"; +} + +export function validateArtifactDescriptor(input: unknown): ArtifactDescriptor { + const value = assertPlainObject(input, "invalid_artifact_descriptor"); + const kind = requireString(value, "kind") as ArtifactKind; + const profile = requireString(value, "profile") as ArtifactProfile; + const consumptionMode = requireString(value, "consumption_mode") as ConsumptionMode; + const language = requireString(value, "language") as ArtifactLanguage; + if (!ARTIFACT_KINDS.includes(kind)) { + throw new ApiError(400, "invalid_artifact_kind", `artifact.kind must be one of ${ARTIFACT_KINDS.join(", ")}`); + } + if (!ARTIFACT_PROFILES.includes(profile)) { + throw new ApiError(400, "invalid_artifact_profile", `artifact.profile must be one of ${ARTIFACT_PROFILES.join(", ")}`); + } + const profileDefinition = ARTIFACT_PROFILE_CATALOG[profile]; + const contracts = profileDefinition.contracts as Partial< + Record + >; + const contract = contracts[kind]; + if (!contract || consumptionMode !== contract.consumption_mode || !(contract.languages as readonly string[]).includes(language)) { + throw new ApiError(400, "invalid_artifact_contract", "artifact profile, consumption mode, and language do not match its kind"); + } + return { kind, profile, consumption_mode: consumptionMode, language }; +} + +export function initialArtifactStates(artifact: ArtifactDescriptor): { + verification_status: VerificationStatus; + deployment_status: DeploymentStatus; + availability_status: AvailabilityStatus; +} { + return { + verification_status: "pending", + deployment_status: artifact.profile === "ckb_executable" ? "undeployed" : "not_applicable", + availability_status: "active", + }; +} + export function validateSnapshot(input: unknown, payload: PublishPayload, maxBytes: number): SourceSnapshotInput { const obj = assertPlainObject(input, "invalid_source_snapshot"); const contentBase64 = requireString(obj, "content_base64"); @@ -394,6 +1110,43 @@ export async function verifyJoyidAuthorisationPayload( return verifyJoyidPayloadSignature(payload, signature, verifier); } +export async function verifyPrincipalAuthorisationPayload( + payload: CapabilityAuthorisationPayload, + signature: PrincipalSignature, + joyidVerifier: JoyidVerifier, +): Promise { + return verifyPrincipalPayloadSignature(payload, signature, joyidVerifier); +} + +export async function verifyPrincipalPayloadSignature( + payload: unknown, + signature: PrincipalSignature, + joyidVerifier: JoyidVerifier, +): Promise { + const principalType = principalTypeFromPayload(payload); + if (principalType === JOYID_PRINCIPAL_TYPE) { + if (isCkbSecp256k1Signature(signature)) { + throw new ApiError(400, "signature_scheme_mismatch", "joyid_ckb requires a JoyID signature"); + } + return verifyJoyidPayloadSignature(payload, signature, joyidVerifier); + } + if (!isCkbSecp256k1Signature(signature)) { + throw new ApiError(400, "signature_scheme_mismatch", "ckb_secp256k1 requires a CKB secp256k1 signature"); + } + return verifyCkbSecp256k1PayloadSignature(payload, signature); +} + +function principalTypeFromPayload(payload: unknown): PrincipalType { + if (!payload || typeof payload !== "object" || Array.isArray(payload)) { + throw new ApiError(400, "invalid_principal_payload", "signed payload must be a JSON object"); + } + return validatePrincipalType(requireString(payload as Record, "principal_type")); +} + +export function isCkbSecp256k1Signature(signature: PrincipalSignature): signature is CkbSecp256k1Signature { + return "scheme" in signature && signature.scheme === CKB_SECP256K1_PRINCIPAL_TYPE; +} + export async function verifyJoyidPayloadSignature( payload: unknown, signature: SignChallengeResponseData, @@ -417,7 +1170,7 @@ async function verifyJoyidPrincipalBinding(payload: unknown, signature: SignChal return; } const obj = payload as Record; - if (obj["principal_type"] !== ACCEPTED_PRINCIPAL_TYPE || typeof obj["principal_id"] !== "string") { + if (obj["principal_type"] !== JOYID_PRINCIPAL_TYPE || typeof obj["principal_id"] !== "string") { return; } const principalId = obj["principal_id"].trim().toLowerCase(); @@ -453,33 +1206,137 @@ function normalizeJoyidPubkey(pubkey: string): string { return value.startsWith("0x") ? value.slice(2) : value; } -export function scopeAllowsPublish(scopes: string[], namespace: string, name: string): boolean { - return scopes.includes(`publish:${namespace}/${name}`) || scopes.includes(`publish:${namespace}/*`); +export async function ckbSecp256k1PrincipalIdFromPublicKey(publicKey: string): Promise { + const normalized = normalizeCkbSecp256k1PublicKey(publicKey); + const material = `${CKB_SECP256K1_PRINCIPAL_BINDING_CONTEXT}\n${normalized}`; + return `0x${await sha256Hex(material)}`; +} + +export async function verifyCkbSecp256k1PayloadSignature( + payload: unknown, + signature: CkbSecp256k1Signature, +): Promise { + const expectedChallenge = canonicalJson(payload); + if (signature.challenge !== expectedChallenge) { + throw new ApiError(401, "ckb_challenge_mismatch", "CKB signature challenge does not match the payload"); + } + const publicKey = normalizeCkbSecp256k1PublicKey(signature.public_key); + if (!payload || typeof payload !== "object" || Array.isArray(payload)) { + throw new ApiError(400, "invalid_principal_payload", "signed payload must be a JSON object"); + } + const obj = payload as Record; + if (obj["principal_type"] !== CKB_SECP256K1_PRINCIPAL_TYPE || typeof obj["principal_id"] !== "string") { + throw new ApiError(400, "signature_scheme_mismatch", "payload is not bound to ckb_secp256k1"); + } + const expectedPrincipalId = await ckbSecp256k1PrincipalIdFromPublicKey(publicKey); + if (obj["principal_id"].trim().toLowerCase() !== expectedPrincipalId) { + throw new ApiError(401, "ckb_principal_mismatch", "CKB public key does not match payload principal_id"); + } + + const signatureBytes = hexToBytes(signature.signature); + const recoveryId = signatureBytes[64]; + if (signatureBytes.length !== 65 || recoveryId === undefined || recoveryId > 3) { + throw new ApiError(401, "ckb_signature_invalid", "CKB signature must be a 65-byte recoverable secp256k1 signature"); + } + const message = new TextEncoder().encode(`Nervos Message:${expectedChallenge}`); + const messageHash = blake2b(message, { + dkLen: 32, + personalization: new TextEncoder().encode("ckb-default-hash"), + }); + const recoveredSignature = new Uint8Array(65); + recoveredSignature[0] = recoveryId; + recoveredSignature.set(signatureBytes.subarray(0, 64), 1); + let verified = false; + try { + verified = secp256k1.verify(recoveredSignature, messageHash, hexToBytes(publicKey), { + format: "recovered", + prehash: false, + }); + } catch { + verified = false; + } + if (!verified) { + throw new ApiError(401, "ckb_signature_invalid", "CKB secp256k1 signature verification failed"); + } +} + +function normalizeCkbSecp256k1PublicKey(publicKey: string): string { + const normalized = publicKey.trim().toLowerCase(); + const clean = normalized.startsWith("0x") ? normalized.slice(2) : normalized; + if (!/^(02|03)[0-9a-f]{64}$/.test(clean)) { + throw new ApiError(400, "invalid_ckb_public_key", "CKB public key must be a compressed 33-byte secp256k1 key"); + } + return `0x${clean}`; +} + +export function scopeAllows( + scopes: string[], + action: CapabilityScopeAction, + namespace: string, + name: string, +): boolean { + return scopes.includes(`${action}:${namespace}/${name}`) || scopes.includes(`${action}:${namespace}/*`); } export async function capabilityKeyId(capabilityPubkey: string): Promise { return `cap_${(await sha256Hex(capabilityPubkey)).slice(0, 32)}`; } -export class WebCryptoP256Verifier implements CapabilitySignatureVerifier { - async verify(canonicalPayload: string, capabilityPubkey: string, signature: CapabilitySignature): Promise { - if (signature.algorithm !== "p256-sha256" || !capabilityPubkey.startsWith("p256-spki:")) { - return false; - } - const spki = base64UrlToBytes(capabilityPubkey.slice("p256-spki:".length)); - const sig = parseSignatureBytes(signature.signature); - const key = await crypto.subtle.importKey( +const P256_SPKI_PREFIX = Uint8Array.from([ + 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, + 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, +]); + +export function isCanonicalP256SpkiPublicKey(value: string): boolean { + if (!value.startsWith("p256-spki:")) return false; + try { + const bytes = base64UrlToBytes(value.slice("p256-spki:".length)); + if (bytes.length !== P256_SPKI_PREFIX.length + 65 || bytes[P256_SPKI_PREFIX.length] !== 0x04) return false; + return P256_SPKI_PREFIX.every((byte, index) => bytes[index] === byte); + } catch { + return false; + } +} + +export async function isImportableP256SpkiPublicKey(value: string): Promise { + if (!isCanonicalP256SpkiPublicKey(value)) return false; + try { + await crypto.subtle.importKey( "spki", - toArrayBuffer(spki), + toArrayBuffer(base64UrlToBytes(value.slice("p256-spki:".length))), { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"], ); - return crypto.subtle.verify( - { name: "ECDSA", hash: "SHA-256" }, - key, - toArrayBuffer(sig), - new TextEncoder().encode(canonicalPayload), - ); + return true; + } catch { + return false; + } +} + +export class WebCryptoP256Verifier implements CapabilitySignatureVerifier { + async verify(canonicalPayload: string, capabilityPubkey: string, signature: CapabilitySignature): Promise { + if (signature.algorithm !== "p256-sha256" || !isCanonicalP256SpkiPublicKey(capabilityPubkey)) { + return false; + } + try { + const spki = base64UrlToBytes(capabilityPubkey.slice("p256-spki:".length)); + const sig = parseSignatureBytes(signature.signature); + const key = await crypto.subtle.importKey( + "spki", + toArrayBuffer(spki), + { name: "ECDSA", namedCurve: "P-256" }, + false, + ["verify"], + ); + return crypto.subtle.verify( + { name: "ECDSA", hash: "SHA-256" }, + key, + toArrayBuffer(sig), + new TextEncoder().encode(canonicalPayload), + ); + } catch { + return false; + } } } diff --git a/services/registry-api/src/filesystem-object-store.ts b/services/registry-api/src/filesystem-object-store.ts new file mode 100644 index 00000000..b74a7101 --- /dev/null +++ b/services/registry-api/src/filesystem-object-store.ts @@ -0,0 +1,65 @@ +import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises"; +import { randomUUID } from "node:crypto"; +import { dirname, resolve, sep } from "node:path"; + +import { sha256Hex } from "./domain"; +import type { RegistryObjectRead, RegistryObjectReader, SnapshotWriter } from "./index"; + +export class FilesystemObjectStore implements SnapshotWriter, RegistryObjectReader { + constructor(private readonly root: string) {} + + async put( + key: string, + body: Uint8Array, + _options: { contentType: string; metadata: Record }, + ): Promise { + const path = this.pathFor(key); + await mkdir(dirname(path), { recursive: true, mode: 0o750 }); + const temporary = `${path}.tmp-${randomUUID()}`; + try { + await writeFile(temporary, body, { mode: 0o640, flag: "wx" }); + await rename(temporary, path); + } catch (error) { + await unlink(temporary).catch(() => undefined); + throw error; + } + } + + async get(key: string): Promise { + try { + const body = await readFile(this.pathFor(key)); + return { + body, + contentType: contentTypeFor(key), + etag: `"sha256-${await sha256Hex(body)}"`, + }; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; + throw error; + } + } + + async delete(key: string): Promise { + await unlink(this.pathFor(key)).catch((error: NodeJS.ErrnoException) => { + if (error.code !== "ENOENT") throw error; + }); + } + + pathFor(key: string): string { + if (!/^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,1023}$/.test(key) || key.split("/").includes("..")) { + throw new Error("registry object key is invalid"); + } + const path = resolve(this.root, key); + if (path !== this.root && !path.startsWith(`${this.root}${sep}`)) { + throw new Error("registry object key escapes the configured root"); + } + return path; + } +} + +function contentTypeFor(key: string): string { + if (key.endsWith(".json")) return "application/json; charset=utf-8"; + if (key.endsWith(".tar.gz")) return "application/gzip"; + if (key.endsWith(".tar")) return "application/x-tar"; + return "application/octet-stream"; +} diff --git a/services/registry-api/src/index.ts b/services/registry-api/src/index.ts index 51af51a5..84232a58 100644 --- a/services/registry-api/src/index.ts +++ b/services/registry-api/src/index.ts @@ -1,29 +1,63 @@ import { verifySignature, type SignChallengeResponseData } from "@joyid/ckb"; import { - ACCEPTED_PRINCIPAL_TYPE, + ARTIFACT_KINDS, + CKB_SECP256K1_PRINCIPAL_TYPE, + JOYID_PRINCIPAL_TYPE, ApiError, + DEPLOYMENT_ACTION, + DEPLOYMENT_PROTOCOL, DEFAULT_REGISTRY_ORIGIN, DEFAULT_STATIC_REGISTRY_ORIGIN, + REGISTRY_SCHEMA_VERSION, WebCryptoP256Verifier, + assertPlainObject, base64ToBytes, canonicalJson, capabilityKeyId, - scopeAllowsPublish, + ckbBlake2bHex, + ckbScriptHash, + hexToBytes, + initialArtifactStates, + isCanonicalP256SpkiPublicKey, + isImportableP256SpkiPublicKey, + isPrincipalType, + scopeAllows, sha256Hex, + sameCkbHash, validateCapabilityPayload, validateCapabilityRevocationPayload, + validateDeploymentPayload, + validateAvailabilityPayload, validatePackageIdent, validatePublishPayload, validateSnapshot, validateVersion, - verifyJoyidAuthorisationPayload, - verifyJoyidPayloadSignature, + verifyPrincipalAuthorisationPayload, + verifyPrincipalPayloadSignature, type CapabilitySignature, type CapabilitySignatureVerifier, + type CkbSecp256k1Signature, type JoyidVerifier, + type PrincipalSignature, + type PrincipalType, + type ArtifactKind, + type AvailabilityStatus, + type DeploymentStatus, + type DeploymentPayload, type SourceSnapshotInput, + type VerificationStatus, } from "./domain"; -import { MemoryRegistryStore, type IdempotencyRecord, type RegistryStore, type SnapshotRecord } from "./store"; +import { + MemoryRegistryStore, + deriveRegistryEntryStatus, + packageVersionRequiresReproduction, + type IdempotencyRecord, + type PackageEvidenceKind, + type PackageEvidenceRecord, + type PackageVersionRecord, + type RegistryStore, + type SnapshotRecord, +} from "./store"; import { SqlRegistryStore, type HyperdriveLike } from "./sql-store"; export interface Env { @@ -32,16 +66,31 @@ export interface Env { SOURCE_SNAPSHOTS?: R2Bucket; REGISTRY_ORIGIN?: string; STATIC_REGISTRY_ORIGIN?: string; + REGISTRY_WEBSITE_ORIGIN?: string; MAX_JSON_BODY_BYTES?: string; MAX_SNAPSHOT_BYTES?: string; REGISTRY_ADMIN_TOKEN?: string; ENVIRONMENT?: string; + REGISTRY_ENVIRONMENT?: string; CLEANUP_QUOTA_EVENT_RETENTION_HOURS?: string; NAMESPACE_CLAIM_COOLDOWN_SECONDS?: string; + CKB_MAINNET_RPC_URL?: string; + CKB_RPC_URL?: string; + CKB_RPC_TIMEOUT_MS?: string; + CKB_RPC_MAX_RESPONSE_BYTES?: string; + CKB_DEP_GROUP_MAX_MEMBERS?: string; + REGISTRY_TYPE_SCRIPT_JSON?: string; + REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON?: string; + REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON?: string; + REGISTRY_COMMITMENT_LOCK_CELL_DEP_JSON?: string; + REGISTRY_REPRODUCER_POLICY_JSON?: string; + CKB_REGISTRY_SCAN_MAX_CELLS?: string; + CKB_MIN_CONFIRMATIONS?: string; } export interface SnapshotWriter { put(key: string, body: Uint8Array, options: { contentType: string; metadata: Record }): Promise; + delete?(key: string): Promise; } export interface RegistryObjectRead { @@ -60,13 +109,119 @@ export interface AppDeps { capabilityVerifier?: CapabilitySignatureVerifier; snapshotWriter?: SnapshotWriter; registryObjectReader?: RegistryObjectReader; + readinessCheck?: () => Promise>; + verifyDeployment?: (payload: DeploymentPayload) => Promise; + /** @deprecated Use verifyDeployment. */ + verifyMainnetDeployment?: (payload: DeploymentPayload) => Promise; + verifyRegistryCommitment?: ( + evidence: Record, + version: PackageVersionRecord, + deployed: PackageEvidenceRecord, + ) => Promise>; + /** @deprecated Use verifyRegistryCommitment. */ + verifyMainnetCommitment?: ( + evidence: Record, + version: PackageVersionRecord, + deployed: PackageEvidenceRecord, + ) => Promise>; + verifyRegistryCommitmentConfiguration?: (configuration: RegistryCommitmentConfiguration) => Promise; + listRegistryCommitmentCells?: (configuration: RegistryCommitmentConfiguration) => Promise; + /** @deprecated Use listRegistryCommitmentCells. */ + listMainnetCommitmentCells?: (configuration: RegistryCommitmentConfiguration) => Promise; now?: () => Date; } +export interface RegistryCommitmentConfiguration { + type_script: Record; + type_script_hash: string; + type_script_cell_dep: Record; + commitment_lock_script: Record; + commitment_lock_hash: string; + commitment_lock_cell_dep: Record; +} + +export interface RegistryCommitmentCell { + commitment_hash: string; + out_point: { tx_hash: string; index: number }; + block_number: string; + tip_block_number?: string; + confirmations?: number; + output: Record; +} + const DEFAULT_MAX_JSON_BODY_BYTES = 6 * 1024 * 1024; const DEFAULT_MAX_SNAPSHOT_BYTES = 5 * 1024 * 1024; const DEFAULT_QUOTA_EVENT_RETENTION_HOURS = 48; const DEFAULT_NAMESPACE_CLAIM_COOLDOWN_SECONDS = 60 * 60; +const TESTNET_SANDBOX_TTL_HOURS = 72; +const TESTNET_SANDBOX_PURGE_GRACE_HOURS = 24; +const AUTHORISATION_SESSION_TTL_MINUTES = 15; + +export type RegistryEnvironment = "production" | "testnet-sandbox"; + +export interface RegistryRuntimeConfig { + environment: RegistryEnvironment; + network: DeploymentPayload["network"]; + rpc_url: string; + record_ttl_hours: number | null; + object_purge_grace_hours: number | null; +} + +export function registryRuntimeConfig(env: Env): RegistryRuntimeConfig { + const value = (env.REGISTRY_ENVIRONMENT ?? "production").trim().toLowerCase(); + if (value === "production") { + return { + environment: "production", + network: "mainnet", + rpc_url: env.CKB_RPC_URL?.trim() || env.CKB_MAINNET_RPC_URL?.trim() || "https://mainnet.ckb.dev/rpc", + record_ttl_hours: null, + object_purge_grace_hours: null, + }; + } + if (value === "testnet-sandbox") { + const registryOrigin = (env.REGISTRY_ORIGIN ?? "").trim(); + const staticOrigin = (env.STATIC_REGISTRY_ORIGIN ?? "").trim(); + if (!registryOrigin || !staticOrigin + || registryOrigin === DEFAULT_REGISTRY_ORIGIN + || staticOrigin === DEFAULT_STATIC_REGISTRY_ORIGIN) { + throw new ApiError( + 503, + "testnet_sandbox_not_isolated", + "testnet-sandbox requires dedicated Registry API and object origins", + ); + } + return { + environment: "testnet-sandbox", + network: "testnet", + rpc_url: env.CKB_RPC_URL?.trim() || "https://testnet.ckb.dev/rpc", + record_ttl_hours: TESTNET_SANDBOX_TTL_HOURS, + object_purge_grace_hours: TESTNET_SANDBOX_PURGE_GRACE_HOURS, + }; + } + throw new ApiError(503, "invalid_registry_environment", "REGISTRY_ENVIRONMENT must be production or testnet-sandbox"); +} +export const CANONICAL_REGISTRY_TYPE_SCRIPT = Object.freeze({ + code_hash: "0x0dd596ade29e06e5bcc00f56abf36ecbe9afaa09f1b26a64436aa37854da622b", + hash_type: "data1", +}); +export const CKB_MAINNET_SIGHASH_LOCK = Object.freeze({ + code_hash: "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + hash_type: "type", +}); +export const CKB_MAINNET_SIGHASH_DEP_GROUP = Object.freeze({ + out_point: Object.freeze({ + tx_hash: "0x71a7ba8fc96349fea0ed3a5c47992e3b4084b031a42264a018e0072e8172e46c", + index: "0x0", + }), + dep_type: "dep_group", +}); +export const CKB_TESTNET_SIGHASH_DEP_GROUP = Object.freeze({ + out_point: Object.freeze({ + tx_hash: "0xf8de3bb47d055cdf460d93a2a6e1b05f7432f9777c8c474abf4eec1d4aee5d37", + index: "0x0", + }), + dep_type: "dep_group", +}); export function createApp(deps: AppDeps = {}) { return { @@ -87,7 +242,14 @@ export function createApp(deps: AppDeps = {}) { async function runScheduledMaintenance(env: Env, deps: AppDeps): Promise { const store = deps.store ?? getProductionStore(env); + await store.withMaintenanceLease("cellscript-registry:scheduled-maintenance", async () => { + await runScheduledMaintenanceUnderLease(env, deps, store); + }); +} + +async function runScheduledMaintenanceUnderLease(env: Env, deps: AppDeps, store: RegistryStore): Promise { const now = deps.now?.() ?? new Date(); + const runtime = registryRuntimeConfig(env); const requestId = `scheduled:${now.toISOString()}`; const quotaCutoff = new Date(now.getTime() - quotaEventRetentionHours(env) * 60 * 60 * 1000).toISOString(); const result = await store.cleanupExpiredState({ @@ -102,6 +264,97 @@ async function runScheduledMaintenance(env: Env, deps: AppDeps): Promise { ...result, }, }); + if (runtime.environment === "testnet-sandbox") { + await purgeExpiredSandboxObjects(env, deps, store, now, requestId, result); + } + let configuration: RegistryCommitmentConfiguration | null; + try { + configuration = registryCommitmentConfiguration(env, false); + if (!configuration) { + const demoted = await demoteCurrentCommitments( + env, + deps, + store, + requestId, + "registry_commitment_unconfigured", + ); + await store.appendAuditEvent({ + request_id: requestId, + event_type: "maintenance.registry_commitment_disabled", + data: { demoted_commitments: demoted }, + }); + return; + } + await requireLiveRegistryCommitmentConfiguration(env, deps, configuration); + } catch (error) { + const code = error instanceof ApiError ? error.code : "registry_commitment_configuration_check_failed"; + const deterministic = error instanceof ApiError && [ + "registry_commitment_misconfigured", + "registry_commitment_cell_dep_invalid", + "registry_commitment_code_hash_unresolved", + "ckb_rpc_not_mainnet", + "ckb_rpc_not_testnet", + "deployment_cell_not_live", + "invalid_dep_group", + "chain_observation_uncommitted", + "chain_confirmation_depth_insufficient", + ].includes(error.code); + const demoted = deterministic + ? await demoteCurrentCommitments(env, deps, store, requestId, code) + : 0; + await store.appendAuditEvent({ + request_id: requestId, + event_type: "maintenance.registry_commitment_configuration_failed", + data: { + error_code: code, + error: error instanceof Error ? error.message : String(error), + deterministic, + demoted_commitments: demoted, + }, + }); + return; + } + await reconcileRegistryChainState(env, deps, store, now, requestId); +} + +async function purgeExpiredSandboxObjects( + env: Env, + deps: AppDeps, + store: RegistryStore, + now: Date, + requestId: string, + result: Awaited>, +): Promise { + const staticCandidates = result.static_objects ?? []; + const sourceCandidates = result.source_objects ?? []; + if (staticCandidates.length === 0 && sourceCandidates.length === 0) return; + const writer = deps.snapshotWriter ?? r2SnapshotWriter(env); + if (!writer.delete) { + throw new ApiError(503, "registry_object_delete_unconfigured", "testnet-sandbox requires an object store with delete support"); + } + const deletedStatic = []; + const deletedSource = []; + for (const candidate of staticCandidates) { + await writer.delete(candidate.key); + deletedStatic.push(candidate); + } + for (const candidate of sourceCandidates) { + await writer.delete(candidate.key); + deletedSource.push(candidate); + } + await store.markSandboxObjectsPurged({ + static_objects: deletedStatic, + source_objects: deletedSource, + purged_at: now.toISOString(), + }); + await store.appendAuditEvent({ + request_id: requestId, + event_type: "maintenance.testnet_sandbox_objects_purged", + data: { + static_objects_deleted: deletedStatic.length, + source_objects_deleted: deletedSource.length, + }, + }); } async function routeRequest( @@ -117,16 +370,28 @@ async function routeRequest( return new Response(null, { status: 204, headers }); } if (request.method === "GET" && url.pathname === "/health") { - return json({ status: "ok", request_id: requestId }, 200, headers); + const runtime = registryRuntimeConfig(env); + return json({ + status: "ok", + request_id: requestId, + registry_environment: runtime.environment, + network: runtime.network, + record_ttl_hours: runtime.record_ttl_hours, + }, 200, headers); } if (request.method === "GET" && url.pathname === "/ready") { return handleReadiness(env, deps, requestId, headers); } - const staticPackageVersionMatch = url.pathname.match(/^\/packages\/([^/]+)\/([^/]+)\/versions\/([^/]+)[.]json$/); + const staticPackageVersionMatch = url.pathname.match(/^\/artifacts\/([^/]+)\/([^/]+)\/releases\/([^/]+)[.]json$/); if (request.method === "GET" && staticPackageVersionMatch) { + const runtime = registryRuntimeConfig(env); + const staticStore = runtime.environment === "testnet-sandbox" + ? deps.store ?? getProductionStore(env) + : deps.store; return handleStaticPackageVersionRead( env, deps, + staticStore, requestId, decodeURIComponent(staticPackageVersionMatch[1] ?? ""), decodeURIComponent(staticPackageVersionMatch[2] ?? ""), @@ -136,13 +401,177 @@ async function routeRequest( const store = deps.store ?? getProductionStore(env); const now = deps.now?.() ?? new Date(); + const runtime = registryRuntimeConfig(env); const registryOrigin = env.REGISTRY_ORIGIN ?? DEFAULT_REGISTRY_ORIGIN; const staticOrigin = env.STATIC_REGISTRY_ORIGIN ?? DEFAULT_STATIC_REGISTRY_ORIGIN; + const lsIdlInterfaceMatch = url.pathname.match(/^\/v1\/ckb\/scripts\/([^/]+)\/interfaces\/ls-idl$/); + if (request.method === "GET" && lsIdlInterfaceMatch) { + return handleLsIdlRead( + request, + env, + deps, + store, + requestId, + headers, + decodeURIComponent(lsIdlInterfaceMatch[1] ?? ""), + false, + ); + } + const lsIdlCompatibilityMatch = url.pathname.match(/^\/idl\/([^/]+)$/); + if (request.method === "GET" && lsIdlCompatibilityMatch) { + return handleLsIdlRead( + request, + env, + deps, + store, + requestId, + headers, + decodeURIComponent(lsIdlCompatibilityMatch[1] ?? ""), + true, + ); + } + + if (request.method === "POST" && url.pathname === "/v1/authorisation-sessions") { + return handleCreateAuthorisationSession(request, env, store, requestId, registryOrigin, now, headers); + } + + const authorisationSessionMatch = url.pathname.match(/^\/v1\/authorisation-sessions\/([^/]+)$/); + if (request.method === "GET" && authorisationSessionMatch) { + return handleGetAuthorisationSession( + request, + store, + requestId, + now, + headers, + decodeURIComponent(authorisationSessionMatch[1] ?? ""), + ); + } + + const authorisationChallengeMatch = url.pathname.match(/^\/v1\/authorisation-sessions\/([^/]+)\/challenge$/); + if (request.method === "POST" && authorisationChallengeMatch) { + return handlePrepareAuthorisationSession( + request, + env, + store, + requestId, + registryOrigin, + now, + headers, + decodeURIComponent(authorisationChallengeMatch[1] ?? ""), + ); + } + + const authorisationCompleteMatch = url.pathname.match(/^\/v1\/authorisation-sessions\/([^/]+)\/complete$/); + if (request.method === "POST" && authorisationCompleteMatch) { + return handleCompleteAuthorisationSession( + request, + env, + store, + requestId, + registryOrigin, + now, + deps, + headers, + decodeURIComponent(authorisationCompleteMatch[1] ?? ""), + ); + } + + if (request.method === "GET" && url.pathname === "/v1/artifacts") { + return handleListPackages(request, store, requestId, staticOrigin, headers); + } + + const publicEvidenceMatch = url.pathname.match(/^\/v1\/artifacts\/([^/]+)\/([^/]+)\/releases\/([^/]+)\/evidence$/); + if (request.method === "GET" && publicEvidenceMatch) { + return handlePublicPackageEvidence( + store, + requestId, + headers, + decodeURIComponent(publicEvidenceMatch[1] ?? ""), + decodeURIComponent(publicEvidenceMatch[2] ?? ""), + decodeURIComponent(publicEvidenceMatch[3] ?? ""), + ); + } + + const publicCommitmentMatch = url.pathname.match(/^\/v1\/artifacts\/([^/]+)\/([^/]+)\/releases\/([^/]+)\/commitment$/); + if (request.method === "GET" && publicCommitmentMatch) { + return handlePublicRegistryCommitment( + env, + deps, + store, + requestId, + headers, + decodeURIComponent(publicCommitmentMatch[1] ?? ""), + decodeURIComponent(publicCommitmentMatch[2] ?? ""), + decodeURIComponent(publicCommitmentMatch[3] ?? ""), + ); + } + + const deploymentMatch = url.pathname.match(/^\/v1\/artifacts\/([^/]+)\/([^/]+)\/releases\/([^/]+)\/deployments$/); + if (request.method === "POST" && deploymentMatch) { + return handleRecordDeployment( + request, + env, + store, + requestId, + registryOrigin, + staticOrigin, + now, + deps, + runtime, + headers, + decodeURIComponent(deploymentMatch[1] ?? ""), + decodeURIComponent(deploymentMatch[2] ?? ""), + decodeURIComponent(deploymentMatch[3] ?? ""), + ); + } + + const availabilityMatch = url.pathname.match(/^\/v1\/artifacts\/([^/]+)\/([^/]+)\/releases\/([^/]+)\/availability$/); + if (request.method === "POST" && availabilityMatch) { + return handlePublisherAvailability( + request, + env, + store, + requestId, + registryOrigin, + staticOrigin, + now, + deps, + headers, + decodeURIComponent(availabilityMatch[1] ?? ""), + decodeURIComponent(availabilityMatch[2] ?? ""), + decodeURIComponent(availabilityMatch[3] ?? ""), + ); + } + + const publicPackageMatch = url.pathname.match(/^\/v1\/artifacts\/([^/]+)\/([^/]+)$/); + if (request.method === "GET" && publicPackageMatch) { + return handlePublicPackageDetail( + store, + requestId, + staticOrigin, + headers, + decodeURIComponent(publicPackageMatch[1] ?? ""), + decodeURIComponent(publicPackageMatch[2] ?? ""), + ); + } + if (request.method === "POST" && url.pathname === "/v1/capabilities") { return handleCreateCapability(request, env, store, requestId, registryOrigin, now, deps, headers); } + const capabilityCheckMatch = url.pathname.match(/^\/v1\/capabilities\/([^/]+)\/check$/); + if (request.method === "GET" && capabilityCheckMatch) { + return handleCapabilityCheck( + request, + store, + requestId, + now, + headers, + decodeURIComponent(capabilityCheckMatch[1] ?? ""), + ); + } + if (request.method === "POST" && url.pathname === "/v1/admin/reserved-namespaces") { return handleAdminReservedNamespace(request, env, store, requestId, headers); } @@ -151,12 +580,28 @@ async function routeRequest( return handleAdminAuditEvents(request, env, store, requestId, headers); } + if (request.method === "GET" && url.pathname === "/v1/admin/verification-queue") { + return handleAdminVerificationQueue(request, env, store, requestId, headers); + } + + const adminVerificationRetryMatch = url.pathname.match(/^\/v1\/admin\/verification-jobs\/([^/]+)\/retry$/); + if (request.method === "POST" && adminVerificationRetryMatch) { + return handleAdminVerificationRetry( + request, + env, + store, + requestId, + headers, + decodeURIComponent(adminVerificationRetryMatch[1] ?? ""), + ); + } + const adminNamespaceStatusMatch = url.pathname.match(/^\/v1\/admin\/namespaces\/([^/]+)\/status$/); if (request.method === "POST" && adminNamespaceStatusMatch) { return handleAdminNamespaceStatus(request, env, store, requestId, headers, decodeURIComponent(adminNamespaceStatusMatch[1] ?? "")); } - const adminVersionStatusMatch = url.pathname.match(/^\/v1\/admin\/packages\/([^/]+)\/([^/]+)\/versions\/([^/]+)\/status$/); + const adminVersionStatusMatch = url.pathname.match(/^\/v1\/admin\/artifacts\/([^/]+)\/([^/]+)\/releases\/([^/]+)\/availability$/); if (request.method === "POST" && adminVersionStatusMatch) { return handleAdminPackageVersionStatus( request, @@ -172,6 +617,22 @@ async function routeRequest( ); } + const adminPromotionMatch = url.pathname.match(/^\/v1\/admin\/artifacts\/([^/]+)\/([^/]+)\/releases\/([^/]+)\/promote$/); + if (request.method === "POST" && adminPromotionMatch) { + return handleAdminPackageVersionPromotion( + request, + env, + store, + requestId, + staticOrigin, + deps, + headers, + decodeURIComponent(adminPromotionMatch[1] ?? ""), + decodeURIComponent(adminPromotionMatch[2] ?? ""), + decodeURIComponent(adminPromotionMatch[3] ?? ""), + ); + } + const revokeMatch = url.pathname.match(/^\/v1\/capabilities\/([^/]+)\/revoke$/); if (request.method === "POST" && revokeMatch) { return handleRevokeCapability( @@ -191,7 +652,7 @@ async function routeRequest( return handleClaimNamespace(request, env, store, requestId, registryOrigin, now, deps, headers); } - const publishMatch = url.pathname.match(/^\/v1\/packages\/([^/]+)\/([^/]+)\/versions$/); + const publishMatch = url.pathname.match(/^\/v1\/artifacts\/([^/]+)\/([^/]+)\/releases$/); if (request.method === "POST" && publishMatch) { return handlePublishVersion( request, @@ -211,9 +672,145 @@ async function routeRequest( throw new ApiError(404, "not_found", "route not found"); } +async function handleLsIdlRead( + request: Request, + env: Env, + deps: AppDeps, + store: RegistryStore, + requestId: string, + headers: Headers, + codeHashInput: string, + compatibilityRoute: boolean, +): Promise { + const codeHash = canonicalLookupHash(codeHashInput, "code_hash"); + const params = new URL(request.url).searchParams; + const runtime = registryRuntimeConfig(env); + const network = optionalPublicQuery(params, "network") ?? runtime.network; + if (network !== "mainnet" && network !== "testnet") { + throw new ApiError(400, "invalid_network", "network must be mainnet or testnet"); + } + const hashTypeRaw = optionalPublicQuery(params, "hash_type"); + const hashType = hashTypeRaw + ? requireOneOf(hashTypeRaw, ["data", "data1", "data2", "type"] as const, "invalid_hash_type") as + "data" | "data1" | "data2" | "type" + : undefined; + const dataHashRaw = optionalPublicQuery(params, "data_hash"); + const dataHash = dataHashRaw ? canonicalLookupHash(dataHashRaw, "data_hash") : undefined; + if (!compatibilityRoute && hashType === "type" && !dataHash) { + throw new ApiError( + 400, + "ls_idl_data_hash_required", + "Type-hash LS-IDL lookup requires data_hash so an upgrade cannot resolve to ambiguous interface bytes", + ); + } + const candidates = await store.findScriptInterfaceCandidates({ + code_hash: codeHash, + network, + ...(hashType ? { hash_type: hashType } : {}), + ...(dataHash ? { data_hash: dataHash } : {}), + limit: 17, + }); + if (candidates.length === 0) { + throw new ApiError(404, "ls_idl_not_found", "no active chain-verified LS-IDL release matches this script identity"); + } + if (candidates.length !== 1) { + throw new ApiError( + 409, + "ls_idl_ambiguous", + "multiple active LS-IDL releases match this code hash; provide hash_type and data_hash on the versioned endpoint", + ); + } + const candidate = candidates[0]!; + const deployment = candidate.deployment.evidence; + if (!compatibilityRoute && deployment["hash_type"] === "type" && !dataHash) { + throw new ApiError( + 409, + "ls_idl_data_hash_required", + "this Type-hash deployment requires data_hash to bind the current code Cell bytes", + ); + } + const signedRelease = candidate.version.registry_entry.versions.find((entry) => entry.version === candidate.version.version); + const profileContract = signedRelease?.profile_contract as Record | undefined; + const interfaceContract = profileContract?.["interface"] as Record | undefined; + const commitment = interfaceContract?.["commitment"] as Record | undefined; + if (interfaceContract?.["format"] !== "ls-idl" || commitment?.["algorithm"] !== "sha256") { + throw new ApiError(500, "ls_idl_contract_inconsistent", "stored release no longer has a readable LS-IDL contract"); + } + const expectedDigest = canonicalLookupHash(String(commitment["digest"] ?? ""), "interface commitment digest"); + const snapshot = await requireSnapshot(store, candidate.version); + const reader = deps.registryObjectReader ?? r2RegistryObjectReader(env); + const object = await reader.get(snapshot.r2_key); + if (!object) { + throw new ApiError(503, "ls_idl_bundle_unavailable", "the immutable LS-IDL bundle is temporarily unavailable"); + } + const bundleBytes = new Uint8Array(await new Response(object.body).arrayBuffer()); + if (bundleBytes.length === 0 || bundleBytes.length > DEFAULT_MAX_SNAPSHOT_BYTES) { + throw new ApiError(500, "ls_idl_bundle_invalid", "the immutable LS-IDL bundle violates its size contract"); + } + let bundle: Record; + try { + bundle = assertPlainObject(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bundleBytes)), "ls_idl_bundle_invalid"); + } catch { + throw new ApiError(500, "ls_idl_bundle_invalid", "the immutable LS-IDL bundle is not valid UTF-8 JSON"); + } + if (bundle["schema"] !== "cellscript-registry-bundle" + || bundle["namespace"] !== candidate.version.namespace + || bundle["name"] !== candidate.version.name + || bundle["release"] !== candidate.version.version + || bundle["profile"] !== "ckb_executable") { + throw new ApiError(500, "ls_idl_bundle_invalid", "the immutable LS-IDL bundle identity does not match its Registry release"); + } + const objects = bundle["objects"]; + if (!Array.isArray(objects)) { + throw new ApiError(500, "ls_idl_bundle_invalid", "the immutable LS-IDL bundle has no object list"); + } + const abiObjects = objects.filter((value) => { + try { return assertPlainObject(value, "ls_idl_bundle_invalid")["role"] === "abi"; } catch { return false; } + }); + if (abiObjects.length !== 1) { + throw new ApiError(500, "ls_idl_bundle_invalid", "the immutable LS-IDL bundle must contain exactly one abi object"); + } + const abiObject = assertPlainObject(abiObjects[0], "ls_idl_bundle_invalid"); + if (typeof abiObject["content_base64"] !== "string") { + throw new ApiError(500, "ls_idl_bundle_invalid", "the immutable LS-IDL abi object is not base64 encoded"); + } + let idlBytes: Uint8Array; + try { + idlBytes = base64ToBytes(abiObject["content_base64"]); + } catch { + throw new ApiError(500, "ls_idl_bundle_invalid", "the immutable LS-IDL abi object is malformed base64"); + } + if (idlBytes.length === 0 || idlBytes.length > 256 * 1024) { + throw new ApiError(500, "ls_idl_bundle_invalid", "the immutable LS-IDL document violates its size contract"); + } + const actualDigest = await sha256Hex(idlBytes); + if (actualDigest !== expectedDigest.slice(2)) { + throw new ApiError(500, "ls_idl_digest_mismatch", "stored LS-IDL bytes no longer match the admitted SHA-256 commitment"); + } + const out = new Headers(headers); + out.set("content-type", "application/vnd.ckb.ls-idl+json"); + out.set("cache-control", "public, max-age=300, stale-while-revalidate=3600"); + out.set("etag", `"sha256-${actualDigest}"`); + out.set("x-ls-idl-format-version", String(interfaceContract["format_version"] ?? "0.1")); + out.set("x-ls-idl-sha256", actualDigest); + out.set("x-ls-idl-coordinate", `${candidate.version.namespace}/${candidate.version.name}@${candidate.version.version}`); + out.set("x-ls-idl-commitment", "code-cell-data-suffix-32"); + out.set("x-ls-idl-verification", "schema-and-suffix-bound"); + return new Response(idlBytes.slice().buffer as ArrayBuffer, { status: 200, headers: out }); +} + +function canonicalLookupHash(value: string, label: string): string { + const bare = value.replace(/^0x/i, ""); + if (!/^[0-9a-fA-F]{64}$/.test(bare)) { + throw new ApiError(400, "invalid_script_hash", `${label} must be a 32-byte hexadecimal hash`); + } + return `0x${bare.toLowerCase()}`; +} + async function handleStaticPackageVersionRead( env: Env, deps: AppDeps, + store: RegistryStore | undefined, requestId: string, namespaceFromPath: string, nameFromPath: string, @@ -222,11 +819,14 @@ async function handleStaticPackageVersionRead( const namespace = validatePackageIdent(namespaceFromPath, "namespace"); const name = validatePackageIdent(nameFromPath, "name"); const version = validateVersion(versionFromPath); + if (store && !await store.getPackageVersion(namespace, name, version)) { + throw new ApiError(404, "registry_object_not_found", "artifact release registry object was not found"); + } const key = staticPackageVersionKey(namespace, name, version); const reader = deps.registryObjectReader ?? r2RegistryObjectReader(env); const object = await reader.get(key); if (!object) { - throw new ApiError(404, "registry_object_not_found", "package version registry object was not found"); + throw new ApiError(404, "registry_object_not_found", "artifact release registry object was not found"); } const headers = corsHeaders(requestId); headers.set("content-type", object.contentType ?? "application/json; charset=utf-8"); @@ -237,48 +837,1721 @@ async function handleStaticPackageVersionRead( return new Response(object.body, { status: 200, headers }); } -function handleReadiness(env: Env, deps: AppDeps, requestId: string, headers: Headers): Response { - const storeConfigured = !!deps.store || !!env.HYPERDRIVE; - const objectStoreConfigured = - (!!deps.snapshotWriter && !!deps.registryObjectReader) - || !!env.REGISTRY_OBJECTS - || !!env.SOURCE_SNAPSHOTS; - const adminConfigured = typeof env.REGISTRY_ADMIN_TOKEN === "string" && env.REGISTRY_ADMIN_TOKEN.trim() !== ""; - const ready = storeConfigured && objectStoreConfigured && adminConfigured; +async function handleListPackages( + request: Request, + store: RegistryStore, + requestId: string, + staticOrigin: string, + headers: Headers, +): Promise { + const params = new URL(request.url).searchParams; + const query = optionalPublicQuery(params, "q"); + const namespaceRaw = optionalPublicQuery(params, "namespace"); + const kindRaw = optionalPublicQuery(params, "kind"); + const verificationRaw = optionalPublicQuery(params, "verification"); + const deploymentRaw = optionalPublicQuery(params, "deployment"); + const availabilityRaw = optionalPublicQuery(params, "availability"); + const namespace = namespaceRaw ? validatePackageIdent(namespaceRaw, "namespace") : undefined; + const artifactKind = kindRaw ? requireOneOf(kindRaw, ARTIFACT_KINDS, "invalid_artifact_kind") as ArtifactKind : undefined; + const verificationStatus = verificationRaw + ? requireOneOf(verificationRaw, ["pending", "hash_bound", "verified", "evidence_required", "rejected"] as const, "invalid_verification_status") as VerificationStatus + : undefined; + const deploymentStatus = deploymentRaw + ? requireOneOf(deploymentRaw, ["not_applicable", "undeployed", "deployed", "chain_verified"] as const, "invalid_deployment_status") as DeploymentStatus + : undefined; + const availabilityStatus = availabilityRaw + ? requireOneOf(availabilityRaw, ["active", "deprecated", "yanked", "quarantined"] as const, "invalid_availability_status") as AvailabilityStatus + : "active"; + const limit = publicListInteger(params, "limit", 50, 1, 100); + const offset = publicListInteger(params, "offset", 0, 0, 10_000); + const page = await store.listArtifactPackagePage({ + ...(query ? { query } : {}), + ...(namespace ? { namespace } : {}), + ...(artifactKind ? { artifact_kind: artifactKind } : {}), + ...(verificationStatus ? { verification_status: verificationStatus } : {}), + ...(!verificationStatus ? { verification_statuses: ["hash_bound", "verified", "evidence_required"] as VerificationStatus[] } : {}), + ...(deploymentStatus ? { deployment_status: deploymentStatus } : {}), + ...(availabilityStatus ? { availability_status: availabilityStatus } : {}), + limit, + offset, + }); + const records = page.records; + const visible = records.filter((record) => record.availability_status !== "quarantined"); + const grouped = new Map(); + for (const record of visible) { + const key = `${record.namespace}/${record.name}`; + const versions = grouped.get(key) ?? []; + versions.push(record); + grouped.set(key, versions); + } + const snapshots = await requireSnapshots(store, visible); + const packages = [...grouped.entries()].map(([coordinate, versions]) => { + const latest = versions[0]!; + const entry = latest.registry_entry as Record; + return { + coordinate, + namespace: latest.namespace, + name: latest.name, + latest_release: latest.version, + artifact: latest.artifact, + verification_status: latest.verification_status, + deployment_status: latest.deployment_status, + availability_status: latest.availability_status, + description: typeof entry["description"] === "string" ? entry["description"] : null, + repository: typeof entry["repository"] === "string" ? entry["repository"] : null, + keywords: Array.isArray(entry["keywords"]) ? entry["keywords"] : [], + categories: Array.isArray(entry["categories"]) ? entry["categories"] : [], + releases: versions.map((version) => staticRegistryVersionPayload(version, snapshotForVersion(snapshots, version), staticOrigin)), + updated_at: latest.created_at, + registry_environment: latest.registry_environment ?? "production", + network: latest.network ?? "mainnet", + }; + }); return json( { - status: ready ? "ready" : "not_ready", + schema: "cellscript-registry-artifact-index", request_id: requestId, - checks: { - store: storeConfigured ? "configured" : "missing_hyperdrive", - object_store: objectStoreConfigured ? "configured" : "missing_r2", - admin_token: adminConfigured ? "configured" : "missing_secret", - }, + artifacts: packages, + count: packages.length, + offset, + limit, + ...(page.has_more ? { next_offset: offset + packages.length } : {}), }, - ready ? 200 : 503, + 200, headers, ); } -async function handleAdminReservedNamespace( - request: Request, - env: Env, +async function handlePublicPackageDetail( store: RegistryStore, requestId: string, + staticOrigin: string, headers: Headers, + namespaceFromPath: string, + nameFromPath: string, ): Promise { - const adminActor = requireAdminActor(request, env); - const body = await readJson(request, maxJsonBytes(env)); - const namespace = validatePackageIdent(String(body["namespace"] ?? ""), "namespace"); - const matchType = requireOneOf(String(body["match_type"] ?? "exact"), ["exact", "prefix", "typosquat"], "invalid_reserved_match_type"); - const reason = requireNonEmptyAdminString(body["reason"], "reason"); - const record = await store.upsertReservedNamespace({ - namespace, - match_type: matchType, - reason, - request_id: requestId, - admin_actor: adminActor, - }); + const namespace = validatePackageIdent(namespaceFromPath, "namespace"); + const name = validatePackageIdent(nameFromPath, "name"); + const versions = await store.listPackageVersions({ namespace, name, limit: 200, offset: 0 }); + const visible = versions.filter((version) => version.availability_status !== "quarantined"); + if (visible.length === 0) { + throw new ApiError(404, "artifact_not_found", "artifact is not known to the public registry"); + } + const snapshots = await requireSnapshots(store, visible); + const evidenceByVersion = new Map(); + for (const evidence of await store.listPackageEvidenceForPackage(namespace, name)) { + const records = evidenceByVersion.get(evidence.version) ?? []; + records.push(evidence); + evidenceByVersion.set(evidence.version, records); + } + const payloads = visible.map((version) => staticRegistryVersionPayload( + version, + snapshotForVersion(snapshots, version), + staticOrigin, + evidenceByVersion.get(version.version) ?? [], + )); + const latest = visible[0]!; + const entry = latest.registry_entry as Record; + return json( + { + schema: "cellscript-registry-artifact", + request_id: requestId, + coordinate: `${namespace}/${name}`, + namespace, + name, + description: typeof entry["description"] === "string" ? entry["description"] : null, + repository: typeof entry["repository"] === "string" ? entry["repository"] : null, + homepage: typeof entry["homepage"] === "string" ? entry["homepage"] : null, + documentation: typeof entry["documentation"] === "string" ? entry["documentation"] : null, + keywords: Array.isArray(entry["keywords"]) ? entry["keywords"] : [], + categories: Array.isArray(entry["categories"]) ? entry["categories"] : [], + latest_release: latest.version, + artifact: latest.artifact, + verification_status: latest.verification_status, + deployment_status: latest.deployment_status, + availability_status: latest.availability_status, + registry_environment: latest.registry_environment ?? "production", + network: latest.network ?? "mainnet", + releases: payloads, + }, + 200, + headers, + ); +} + +async function handlePublicPackageEvidence( + store: RegistryStore, + requestId: string, + headers: Headers, + namespaceFromPath: string, + nameFromPath: string, + versionFromPath: string, +): Promise { + const namespace = validatePackageIdent(namespaceFromPath, "namespace"); + const name = validatePackageIdent(nameFromPath, "name"); + const version = validateVersion(versionFromPath); + const record = await store.getPackageVersion(namespace, name, version); + if (!record || record.availability_status === "quarantined") { + throw new ApiError(404, "artifact_release_not_found", "artifact release is not known to the public registry"); + } + const evidence = await store.listPackageEvidence(namespace, name, version); + return json({ schema: "cellscript-registry-evidence-list", request_id: requestId, namespace, name, release: version, evidence }, 200, headers); +} + +async function handlePublicRegistryCommitment( + env: Env, + deps: AppDeps, + store: RegistryStore, + requestId: string, + headers: Headers, + namespaceFromPath: string, + nameFromPath: string, + versionFromPath: string, +): Promise { + const namespace = validatePackageIdent(namespaceFromPath, "namespace"); + const name = validatePackageIdent(nameFromPath, "name"); + const version = validateVersion(versionFromPath); + const record = await store.getPackageVersion(namespace, name, version); + if (!record || record.availability_status === "quarantined") { + throw new ApiError(404, "artifact_release_not_found", "artifact release is not known to the public Registry"); + } + const evidence = await store.listPackageEvidence(namespace, name, version); + const deployed = evidence.filter((item) => item.kind === "deployed").at(-1); + if (!deployed) { + throw new ApiError(409, "deployment_evidence_missing", "Registry commitment requires accepted deployment evidence for this environment"); + } + if (!deployed.evidence["chain_verification"]) { + throw new ApiError(409, "deployment_chain_evidence_missing", "Registry commitment requires RPC-verified deployment evidence"); + } + const commitmentEvidence = record.status === "on_chain_committed" + ? evidence + .filter((item) => item.kind === "on_chain_committed" + && item.evidence_hash === record.current_commitment_evidence_hash + && item.evidence["deployed_evidence_hash"] === deployed.evidence_hash + && [ + "get_live_cell+type_index", + "get_live_cell+configured_type_index", + "get_cells+configured_type_index", + "get_transaction+get_live_cell+configured_type_index", + ] + .includes(String(item.evidence["chain_verification"]))) + .at(-1) + : undefined; + const commitmentHash = registryCommitmentHash(record, deployed.evidence_hash); + const configuration = registryCommitmentConfiguration(env, false); + if (configuration) { + await requireLiveRegistryCommitmentConfiguration(env, deps, configuration); + } + const committed = configuration ? commitmentEvidence : undefined; + return json( + { + schema: "cellscript-registry-commitment-proof-v1", + request_id: requestId, + namespace, + name, + release: version, + status: committed + ? "on_chain_committed" + : commitmentEvidence + ? "commitment_unconfigured" + : "commitment_ready", + payload: registryCommitmentPayload(record, deployed.evidence_hash), + commitment_hash: commitmentHash, + cell_data: registryCommitmentCellData(commitmentHash), + deployed_evidence_hash: deployed.evidence_hash, + ...(configuration + ? { + transaction_intent: { + schema: "cellscript-registry-commitment-transaction-intent-v1", + network: registryRuntimeConfig(env).network, + output: { + lock: configuration.commitment_lock_script, + type: configuration.type_script, + data: registryCommitmentCellData(commitmentHash), + }, + required_cell_deps: [configuration.type_script_cell_dep], + custody_cell_dep: configuration.commitment_lock_cell_dep, + wallet_completes: ["capacity", "inputs", "change", "fee", "witnesses", "signatures", "broadcast"], + }, + registry_type_hash: configuration.type_script_hash, + commitment_lock_hash: configuration.commitment_lock_hash, + } + : { transaction_intent: null, configuration_status: "registry_commitment_scripts_unconfigured" }), + ...(committed + ? { + commitment_evidence_hash: committed.evidence_hash, + commitment: committed.evidence, + } + : {}), + }, + 200, + headers, + ); +} + +async function handleRecordDeployment( + request: Request, + env: Env, + store: RegistryStore, + requestId: string, + registryOrigin: string, + staticOrigin: string, + now: Date, + deps: AppDeps, + runtime: RegistryRuntimeConfig, + headers: Headers, + namespaceFromPath: string, + nameFromPath: string, + releaseFromPath: string, +): Promise { + await throttleRequestSource(store, request, requestId, "deployment", 40, 60 * 60, now); + const body = await readJson(request, Math.min(maxJsonBytes(env), 512 * 1024)); + const payload = validateDeploymentPayload(body["payload"], registryOrigin, now, runtime.network); + const namespace = validatePackageIdent(namespaceFromPath, "namespace"); + const name = validatePackageIdent(nameFromPath, "name"); + const release = validateVersion(releaseFromPath); + if (payload.namespace !== namespace || payload.name !== name || payload.release !== release) { + throw new ApiError(400, "route_payload_mismatch", "artifact route and deployment payload do not match"); + } + const version = await store.getPackageVersion(namespace, name, release); + if (!version) { + throw new ApiError(404, "artifact_release_not_found", "artifact release is not known to the registry"); + } + if (version.artifact.profile !== "ckb_executable" || version.deployment_status === "not_applicable") { + throw new ApiError(409, "deployment_not_applicable", "this artifact profile cannot have a CKB deployment"); + } + const signedRelease = version.registry_entry.versions.find((entry) => entry.version === release); + if (!signedRelease?.artifact_hash || !sameCkbHash(signedRelease.artifact_hash, payload.artifact_hash)) { + throw new ApiError(400, "deployment_artifact_mismatch", "deployment artifact_hash does not match the published release"); + } + requireDeploymentProfileContract(version, payload.hash_type, payload.dep_type); + const capability = await store.getCapability(payload.capability_key_id); + if (!capability || capability.revoked_at || new Date(capability.expires_at).getTime() <= now.getTime()) { + throw new ApiError(401, "capability_inactive", "deployment capability is missing, revoked, or expired"); + } + if (!scopeAllows(capability.scopes, "deployment", namespace, name)) { + throw new ApiError(403, "capability_scope_denied", "capability scope does not allow this artifact deployment"); + } + const namespaceRecord = await store.getNamespace(namespace); + if ( + !namespaceRecord + || namespaceRecord.status !== "active" + || namespaceRecord.owner_principal_type !== capability.principal_type + || namespaceRecord.owner_principal_id !== capability.principal_id + ) { + throw new ApiError(403, "namespace_owner_mismatch", "capability principal does not own the active namespace"); + } + const signature = requireCapabilitySignature(body["capability_signature"]); + const verifier = deps.capabilityVerifier ?? new WebCryptoP256Verifier(); + if (!(await verifier.verify(canonicalJson(payload), capability.capability_pubkey, signature))) { + throw new ApiError(401, "capability_signature_invalid", "capability signature verification failed"); + } + await throttle(store, requestId, `capability:${capability.key_id}`, "deployment", 20, 60 * 60, now); + await throttle(store, requestId, `artifact:${namespace}/${name}`, "deployment", 20, 60 * 60, now); + + const requestHash = await sha256Hex(canonicalJson({ + route: "record_deployment", + payload, + capability_signature: signature, + })); + const idempotencyKey = requestIdempotencyKey(request, "deployment") ?? `deployment:auto:${requestHash}`; + const replay = await idempotencyReplayResponse(store, idempotencyKey, requestHash, headers); + if (replay) return replay; + const reservation = await store.reserveIdempotencyKey({ + key: idempotencyKey, + request_hash: requestHash, + request_id: requestId, + expires_at: payload.expires_at, + }); + if (reservation.state === "conflict") { + throw new ApiError(409, "idempotency_key_conflict", "deployment command identity conflicts with an earlier request"); + } + if (reservation.state === "in_progress") { + throw new ApiError(409, "idempotency_request_in_progress", "matching deployment command is already processing"); + } + if (reservation.state === "completed") return idempotencyResponse(reservation.record, headers); + + let nonceKey: string | undefined; + let commandCommitted = false; + try { + nonceKey = await consumeSignedNonce(store, requestId, { + protocol: payload.protocol, + action: payload.action, + nonce: payload.nonce, + expires_at: payload.expires_at, + principal_type: capability.principal_type, + principal_id: capability.principal_id, + capability_key_id: capability.key_id, + }); + const deploymentVerifier = deps.verifyDeployment ?? deps.verifyMainnetDeployment; + const chain = deploymentVerifier + ? await deploymentVerifier(payload) + : await verifyDeployment(env, payload); + const previousEvidence = await store.listPackageEvidence(namespace, name, release); + const buildEvidence = latestBuildEvidence(previousEvidence, version); + const lsIdlInterface = releaseLsIdlInterface(version); + const evidence = { + schema: "cellscript-registry-evidence", + kind: "deployed", + producer: `publisher:${capability.principal_type}`, + generated_at: now.toISOString(), + verification_status: "passed", + source_hash: version.source_hash, + manifest_hash: version.manifest_hash, + verified_build_evidence_hash: buildEvidence.evidence_hash, + network: runtime.network, + artifact_hash: payload.artifact_hash, + data_hash: payload.data_hash, + code_hash: payload.code_hash, + hash_type: payload.hash_type, + dep_type: payload.dep_type, + out_point: payload.out_point, + deployment_status: "live", + chain_verification: "get_transaction+get_live_cell", + ...(lsIdlInterface ? { interface: lsIdlInterface } : {}), + ...(chain.block_hash ? { block_hash: chain.block_hash } : {}), + ...(chain.block_number ? { block_number: chain.block_number } : {}), + ...(chain.tip_block_number ? { observed_tip_block_number: chain.tip_block_number } : {}), + ...(chain.confirmations !== undefined ? { confirmations: chain.confirmations } : {}), + ...(chain.resolved_code_out_point ? { resolved_code_out_point: chain.resolved_code_out_point } : {}), + ...(chain.dep_group_size !== undefined ? { dep_group_size: chain.dep_group_size } : {}), + }; + const evidenceHash = `sha256:${await sha256Hex(canonicalJson(evidence))}`; + const responseBody = { + request_id: requestId, + coordinate: `${namespace}/${name}@${release}`, + deployment_status: "chain_verified", + evidence_hash: evidenceHash, + evidence: { + kind: "deployed" as const, + evidence_hash: evidenceHash, + evidence, + }, + }; + const snapshot = await requireSnapshot(store, version); + const recorded = await store.recordChainVerifiedDeployment({ + namespace, + name, + version: release, + kind: "deployed", + evidence_hash: evidenceHash, + evidence, + request_id: requestId, + admin_actor: `publisher:${capability.principal_id}`, + capability_usage: { + key_id: capability.key_id, + principal_type: capability.principal_type, + principal_id: capability.principal_id, + request_id: requestId, + action: "record_deployment", + namespace, + name, + version: release, + }, + idempotency: { + key: idempotencyKey, + request_hash: requestHash, + response_status: 201, + response_body: responseBody, + }, + }); + commandCommitted = true; + const allEvidence = await store.listPackageEvidence(namespace, name, release); + await tryWriteStaticRegistryVersionObject( + env, + deps, + store, + requestId, + recorded.version, + snapshot, + staticOrigin, + allEvidence, + ); + return json(responseBody, 201, headers); + } catch (error) { + if (!commandCommitted) { + if (nonceKey) await store.releaseNonce({ nonce_key: nonceKey, request_id: requestId }); + await store.releaseProcessingIdempotencyKey({ key: idempotencyKey, request_hash: requestHash }); + } + throw error; + } +} + +async function handlePublisherAvailability( + request: Request, + env: Env, + store: RegistryStore, + requestId: string, + registryOrigin: string, + staticOrigin: string, + now: Date, + deps: AppDeps, + headers: Headers, + namespaceFromPath: string, + nameFromPath: string, + releaseFromPath: string, +): Promise { + await throttleRequestSource(store, request, requestId, "availability", 40, 60 * 60, now); + const body = await readJson(request, Math.min(maxJsonBytes(env), 128 * 1024)); + const payload = validateAvailabilityPayload(body["payload"], registryOrigin, now); + const namespace = validatePackageIdent(namespaceFromPath, "namespace"); + const name = validatePackageIdent(nameFromPath, "name"); + const release = validateVersion(releaseFromPath); + if (payload.namespace !== namespace || payload.name !== name || payload.release !== release) { + throw new ApiError(400, "route_payload_mismatch", "artifact route and availability payload do not match"); + } + const version = await store.getPackageVersion(namespace, name, release); + if (!version) { + throw new ApiError(404, "artifact_release_not_found", "artifact release is not known to the registry"); + } + if (version.availability_status === "quarantined") { + throw new ApiError(403, "quarantine_admin_required", "a publisher cannot change an administratively quarantined release"); + } + const capability = await store.getCapability(payload.capability_key_id); + if (!capability || capability.revoked_at || new Date(capability.expires_at).getTime() <= now.getTime()) { + throw new ApiError(401, "capability_inactive", "availability capability is missing, revoked, or expired"); + } + if (!scopeAllows(capability.scopes, "availability", namespace, name)) { + throw new ApiError(403, "capability_scope_denied", "capability scope does not allow this artifact update"); + } + const namespaceRecord = await store.getNamespace(namespace); + if ( + !namespaceRecord + || namespaceRecord.status !== "active" + || namespaceRecord.owner_principal_type !== capability.principal_type + || namespaceRecord.owner_principal_id !== capability.principal_id + ) { + throw new ApiError(403, "namespace_owner_mismatch", "capability principal does not own the active namespace"); + } + const signature = requireCapabilitySignature(body["capability_signature"]); + const verifier = deps.capabilityVerifier ?? new WebCryptoP256Verifier(); + if (!(await verifier.verify(canonicalJson(payload), capability.capability_pubkey, signature))) { + throw new ApiError(401, "capability_signature_invalid", "capability signature verification failed"); + } + await throttle(store, requestId, `capability:${capability.key_id}`, "availability", 30, 60 * 60, now); + await throttle(store, requestId, `artifact:${namespace}/${name}`, "availability", 20, 60 * 60, now); + + const requestHash = await sha256Hex(canonicalJson({ + route: "set_availability", + payload, + capability_signature: signature, + })); + const idempotencyKey = requestIdempotencyKey(request, "availability") ?? `availability:auto:${requestHash}`; + const replay = await idempotencyReplayResponse(store, idempotencyKey, requestHash, headers); + if (replay) return replay; + const reservation = await store.reserveIdempotencyKey({ + key: idempotencyKey, + request_hash: requestHash, + request_id: requestId, + expires_at: payload.expires_at, + }); + if (reservation.state === "conflict") { + throw new ApiError(409, "idempotency_key_conflict", "availability command identity conflicts with an earlier request"); + } + if (reservation.state === "in_progress") { + throw new ApiError(409, "idempotency_request_in_progress", "matching availability command is already processing"); + } + if (reservation.state === "completed") return idempotencyResponse(reservation.record, headers); + + let nonceKey: string | undefined; + let commandCommitted = false; + try { + nonceKey = await consumeSignedNonce(store, requestId, { + protocol: payload.protocol, + action: payload.action, + nonce: payload.nonce, + expires_at: payload.expires_at, + principal_type: capability.principal_type, + principal_id: capability.principal_id, + capability_key_id: capability.key_id, + }); + const snapshot = await requireSnapshot(store, version); + const evidence = await store.listPackageEvidence(namespace, name, release); + const directUrl = staticPackageVersionUrl(staticOrigin, namespace, name, release); + const prospective = { ...version, availability_status: payload.availability_status }; + prospective.status = deriveRegistryEntryStatus(prospective, version.status); + const responseBody = { + request_id: requestId, + coordinate: `${namespace}/${name}@${release}`, + availability_status: payload.availability_status, + status: prospective.status, + }; + if (isSuppressivePackageVersionStatus(payload.availability_status)) { + await writeStaticRegistryVersionObject( + env, + deps, + { + ...version, + status: payload.availability_status === "active" ? version.status : payload.availability_status, + availability_status: payload.availability_status, + direct_url: directUrl, + }, + snapshot, + staticOrigin, + evidence, + ); + } + const record = await store.updatePackageVersionStatus({ + namespace, + name, + version: release, + status: payload.availability_status, + ...(payload.reason ? { reason: payload.reason } : {}), + request_id: requestId, + admin_actor: `publisher:${capability.principal_id}`, + audit_event_type: "publisher.package_version.availability_updated", + capability_usage: { + key_id: capability.key_id, + principal_type: capability.principal_type, + principal_id: capability.principal_id, + request_id: requestId, + action: "set_availability", + namespace, + name, + version: release, + }, + idempotency: { + key: idempotencyKey, + request_hash: requestHash, + response_status: 200, + response_body: responseBody, + }, + }); + commandCommitted = true; + if (!isSuppressivePackageVersionStatus(payload.availability_status)) { + await tryWriteStaticRegistryVersionObject( + env, + deps, + store, + requestId, + { ...record, direct_url: directUrl }, + snapshot, + staticOrigin, + evidence, + ); + } + return json(responseBody, 200, headers); + } catch (error) { + if (!commandCommitted) { + if (nonceKey) await store.releaseNonce({ nonce_key: nonceKey, request_id: requestId }); + await store.releaseProcessingIdempotencyKey({ key: idempotencyKey, request_hash: requestHash }); + } + throw error; + } +} + +interface LiveCellRpcResult { + status: string; + cell: Record; + block_hash: string; +} + +interface VerifiedDeployment { + block_hash?: string | null; + block_number?: string; + tip_block_number?: string; + confirmations?: number; + resolved_code_out_point?: { tx_hash: string; index: number }; + dep_group_size?: number; +} + +export async function verifyDeployment(env: Env, payload: DeploymentPayload): Promise { + const runtime = registryRuntimeConfig(env); + if (payload.network !== runtime.network) { + throw new ApiError(400, "unsupported_deployment_network", `deployment must use ${runtime.network}`); + } + const rpcUrl = runtime.rpc_url; + const rpcOptions = { + timeout_ms: boundedIntegerEnv(env.CKB_RPC_TIMEOUT_MS, 10_000, 1_000, 30_000), + maximum_bytes: boundedIntegerEnv(env.CKB_RPC_MAX_RESPONSE_BYTES, 2 * 1024 * 1024, 64 * 1024, 8 * 1024 * 1024), + }; + await requireRegistryRpc(rpcUrl, rpcOptions, runtime.network); + const declared = await getLiveCell(rpcUrl, payload.out_point, rpcOptions); + const observation = await requireMinimumConfirmations(env, rpcUrl, declared.block_hash, rpcOptions, "deployment"); + if (payload.dep_type === "code") { + verifyDeploymentCodeCell(declared.cell, payload); + return { ...(declared.block_hash !== undefined ? { block_hash: declared.block_hash } : {}), ...observation }; + } + + const depGroupData = assertPlainObject(declared.cell["data"], "invalid_ckb_rpc_response"); + const content = depGroupData["content"]; + if (typeof content !== "string") { + throw new ApiError(409, "invalid_dep_group", `${runtime.network} DepGroup Cell did not return output data`); + } + const members = parseDepGroupOutPoints(content); + const memberLimit = boundedIntegerEnv(env.CKB_DEP_GROUP_MAX_MEMBERS, 256, 1, 2048); + if (members.length > memberLimit) { + throw new ApiError(409, "dep_group_too_large", `DepGroup has ${members.length} members; Registry verification limit is ${memberLimit}`); + } + for (let offset = 0; offset < members.length; offset += 16) { + const candidates = await Promise.all(members.slice(offset, offset + 16).map(async (member) => { + try { + const candidate = await getLiveCell(rpcUrl, member, rpcOptions); + verifyDeploymentCodeCell(candidate.cell, payload); + await requireMinimumConfirmations(env, rpcUrl, candidate.block_hash, rpcOptions, "DepGroup code member"); + return member; + } catch (error) { + if (error instanceof ApiError && ["deployment_cell_not_live", "deployment_data_hash_mismatch", "deployment_code_hash_mismatch"].includes(error.code)) { + return null; + } + throw error; + } + })); + const member = candidates.find((candidate) => candidate !== null); + if (member) { + return { + ...(declared.block_hash !== undefined ? { block_hash: declared.block_hash } : {}), + ...observation, + resolved_code_out_point: member, + dep_group_size: members.length, + }; + } + } + throw new ApiError(409, "dep_group_artifact_not_found", "DepGroup does not resolve to a live code Cell matching the published executable"); +} + +/** Backward-compatible export for callers that predate the isolated testnet environment. */ +export async function verifyMainnetDeployment(env: Env, payload: DeploymentPayload): Promise { + return verifyDeployment(env, payload); +} + +async function getLiveCell( + rpcUrl: string, + outPoint: { tx_hash: string; index: number }, + options: { timeout_ms: number; maximum_bytes: number }, +): Promise { + const rpc = await ckbRpcRequest( + rpcUrl, + "get_live_cell", + [{ tx_hash: outPoint.tx_hash, index: `0x${outPoint.index.toString(16)}` }, true, false], + options, + ); + const result = assertPlainObject(rpc, "invalid_ckb_rpc_response"); + if (result["status"] !== "live") { + throw new ApiError(409, "deployment_cell_not_live", "deployment OutPoint is not a live Cell on the configured network"); + } + const cell = assertPlainObject(result["cell"], "invalid_ckb_rpc_response"); + const blockHash = await getCommittedTransactionBlockHash(rpcUrl, outPoint.tx_hash, options); + return { + status: "live", + cell, + block_hash: blockHash, + }; +} + +async function getCommittedTransactionBlockHash( + rpcUrl: string, + txHash: string, + options: { timeout_ms: number; maximum_bytes: number }, +): Promise { + const rawTransaction = await ckbRpcRequest(rpcUrl, "get_transaction", [txHash], options); + if (!rawTransaction || typeof rawTransaction !== "object" || Array.isArray(rawTransaction)) { + throw new ApiError(503, "invalid_ckb_rpc_response", "CKB RPC get_transaction returned no transaction status"); + } + const transaction = rawTransaction as Record; + const rawStatus = transaction["tx_status"]; + if (!rawStatus || typeof rawStatus !== "object" || Array.isArray(rawStatus)) { + throw new ApiError(503, "invalid_ckb_rpc_response", "CKB RPC get_transaction returned no tx_status object"); + } + const txStatus = rawStatus as Record; + if (typeof txStatus["status"] !== "string") { + throw new ApiError(503, "invalid_ckb_rpc_response", "CKB RPC get_transaction returned no transaction status value"); + } + if (txStatus["status"] !== "committed") { + throw new ApiError(409, "chain_observation_uncommitted", "Cell creation transaction is not committed"); + } + const blockHash = txStatus["block_hash"]; + if (typeof blockHash !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(blockHash)) { + throw new ApiError(503, "invalid_ckb_rpc_response", "committed CKB transaction has no valid block hash"); + } + return blockHash; +} + +async function requireRegistryRpc( + rpcUrl: string, + options: { timeout_ms: number; maximum_bytes: number }, + expectedNetwork: DeploymentPayload["network"] = "mainnet", +): Promise { + const info = assertPlainObject(await ckbRpcRequest(rpcUrl, "get_blockchain_info", [], options), "invalid_ckb_rpc_response"); + const chain = typeof info["chain"] === "string" + ? info["chain"] + : typeof info["chain_id"] === "string" ? info["chain_id"] : ""; + const normalized = chain.trim().toLowerCase().replaceAll("_", "-"); + const accepted = expectedNetwork === "mainnet" + ? ["ckb", "ckb-mainnet"] + : ["ckb-testnet", "pudge", "pudge-testnet"]; + if (!accepted.includes(normalized)) { + const code = expectedNetwork === "mainnet" ? "ckb_rpc_not_mainnet" : "ckb_rpc_not_testnet"; + throw new ApiError(503, code, `configured CKB RPC is not ${expectedNetwork} (reported chain '${chain || "unknown"}')`); + } +} + +interface ChainConfirmationObservation { + block_number: string; + tip_block_number: string; + confirmations: number; +} + +async function requireMinimumConfirmations( + env: Env, + rpcUrl: string, + blockHash: string | null | undefined, + options: { timeout_ms: number; maximum_bytes: number }, + label: string, +): Promise { + if (!blockHash || !/^0x[0-9a-fA-F]{64}$/.test(blockHash)) { + throw new ApiError(409, "chain_observation_uncommitted", `${label} Cell has no committed block hash`); + } + const [rawHeader, rawTip] = await Promise.all([ + ckbRpcRequest(rpcUrl, "get_header", [blockHash], options), + ckbRpcRequest(rpcUrl, "get_tip_header", [], options), + ]); + const header = assertPlainObject(rawHeader, "invalid_ckb_rpc_response"); + const tip = assertPlainObject(rawTip, "invalid_ckb_rpc_response"); + const blockNumber = parseRpcBlockNumber(header["number"], `${label} block number`); + const tipNumber = parseRpcBlockNumber(tip["number"], "CKB tip block number"); + if (tipNumber < blockNumber) { + throw new ApiError(503, "invalid_ckb_rpc_response", `${label} block is ahead of the reported CKB tip`); + } + const confirmationsBig = tipNumber - blockNumber + 1n; + const minimum = boundedIntegerEnv(env.CKB_MIN_CONFIRMATIONS, 24, 1, 10_000); + if (confirmationsBig < BigInt(minimum)) { + throw new ApiError( + 409, + "chain_confirmation_depth_insufficient", + `${label} Cell has ${confirmationsBig} confirmations; Registry requires ${minimum}`, + ); + } + return { + block_number: `0x${blockNumber.toString(16)}`, + tip_block_number: `0x${tipNumber.toString(16)}`, + confirmations: Number(confirmationsBig > BigInt(Number.MAX_SAFE_INTEGER) ? BigInt(Number.MAX_SAFE_INTEGER) : confirmationsBig), + }; +} + +function parseRpcBlockNumber(value: unknown, label: string): bigint { + try { + if (typeof value === "string" && /^0x[0-9a-fA-F]+$/.test(value)) return BigInt(value); + if (typeof value === "string" && /^[0-9]+$/.test(value)) return BigInt(value); + if (Number.isSafeInteger(value) && Number(value) >= 0) return BigInt(Number(value)); + } catch { + // Fall through to the stable API error below. + } + throw new ApiError(503, "invalid_ckb_rpc_response", `${label} is not a non-negative block number`); +} + +async function ckbRpcRequest( + rpcUrl: string, + method: string, + params: unknown[], + options: { timeout_ms: number; maximum_bytes: number }, +): Promise { + let response: Response; + try { + response = await fetch(rpcUrl, { + method: "POST", + headers: { "content-type": "application/json", accept: "application/json" }, + body: JSON.stringify({ + id: 1, + jsonrpc: "2.0", + method, + params, + }), + signal: AbortSignal.timeout(options.timeout_ms), + }); + } catch (error) { + throw new ApiError(503, "ckb_rpc_unavailable", `CKB RPC ${method} request failed: ${error instanceof Error ? error.message : String(error)}`); + } + if (!response.ok) { + throw new ApiError(503, "ckb_rpc_unavailable", `CKB RPC returned HTTP ${response.status}`); + } + let rpcBody: unknown; + try { + rpcBody = await readBoundedRpcJson(response, options.maximum_bytes); + } catch (error) { + if (error instanceof ApiError && error.code === "ckb_rpc_response_too_large") { + throw new ApiError( + error.status, + error.code, + `CKB RPC ${method} response exceeds the configured size limit`, + ); + } + throw error; + } + const rpc = assertPlainObject(rpcBody, "invalid_ckb_rpc_response"); + if (rpc["error"]) { + throw new ApiError(503, "ckb_rpc_error", `CKB RPC rejected ${method}`); + } + if (!("result" in rpc)) { + throw new ApiError(503, "invalid_ckb_rpc_response", `CKB RPC ${method} returned no result`); + } + return rpc["result"]; +} + +async function readBoundedRpcJson(response: Response, maximumBytes: number): Promise { + const declaredLength = response.headers.get("content-length"); + if (declaredLength && Number(declaredLength) > maximumBytes) { + throw new ApiError(503, "ckb_rpc_response_too_large", "CKB RPC response exceeds the configured size limit"); + } + if (!response.body) { + throw new ApiError(503, "invalid_ckb_rpc_response", "CKB RPC returned an empty response"); + } + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + let size = 0; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + size += value.byteLength; + if (size > maximumBytes) { + await reader.cancel(); + throw new ApiError(503, "ckb_rpc_response_too_large", "CKB RPC response exceeds the configured size limit"); + } + chunks.push(value); + } + const body = new Uint8Array(size); + let offset = 0; + for (const chunk of chunks) { + body.set(chunk, offset); + offset += chunk.byteLength; + } + try { + return JSON.parse(new TextDecoder().decode(body)); + } catch { + throw new ApiError(503, "invalid_ckb_rpc_response", "CKB RPC returned invalid JSON"); + } +} + +function boundedIntegerEnv(raw: string | undefined, fallback: number, minimum: number, maximum: number): number { + const parsed = raw === undefined ? fallback : Number(raw); + return Number.isSafeInteger(parsed) && parsed >= minimum && parsed <= maximum ? parsed : fallback; +} + +function verifyDeploymentCodeCell(cell: Record, payload: DeploymentPayload): void { + const data = assertPlainObject(cell["data"], "invalid_ckb_rpc_response"); + if (typeof data["hash"] !== "string" || !sameCkbHash(data["hash"], payload.data_hash)) { + throw new ApiError(409, "deployment_data_hash_mismatch", "live Cell data hash does not match the published executable"); + } + if (payload.hash_type === "type") { + const output = assertPlainObject(cell["output"], "invalid_ckb_rpc_response"); + if (!output["type"] || !sameCkbHash(ckbScriptHash(output["type"]), payload.code_hash)) { + throw new ApiError(409, "deployment_code_hash_mismatch", "live Cell type script hash does not match code_hash"); + } + } else if (!sameCkbHash(payload.code_hash, payload.data_hash)) { + throw new ApiError(400, "deployment_code_hash_mismatch", "data hash deployments must use the executable data hash as code_hash"); + } +} + +export function parseDepGroupOutPoints(content: string): Array<{ tx_hash: string; index: number }> { + if (!/^0x(?:[0-9a-fA-F]{2})+$/.test(content)) { + throw new ApiError(409, "invalid_dep_group", "DepGroup Cell data must be non-empty hexadecimal Molecule OutPointVec bytes"); + } + const bytes = Uint8Array.from(content.slice(2).match(/.{2}/g) ?? [], (value) => Number.parseInt(value, 16)); + if (bytes.length < 4) { + throw new ApiError(409, "invalid_dep_group", "DepGroup Cell data is shorter than an OutPointVec header"); + } + const count = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0, true); + if (count === 0 || count > 2048 || bytes.length !== 4 + count * 36) { + throw new ApiError(409, "invalid_dep_group", "DepGroup Cell data is not a canonical non-empty Molecule OutPointVec"); + } + const outPoints = []; + for (let item = 0; item < count; item += 1) { + const offset = 4 + item * 36; + const txHash = `0x${[...bytes.slice(offset, offset + 32)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`; + const index = new DataView(bytes.buffer, bytes.byteOffset + offset + 32, 4).getUint32(0, true); + outPoints.push({ tx_hash: txHash, index }); + } + return outPoints; +} + +export function registryCommitmentPayload( + version: PackageVersionRecord, + deployedEvidenceHash: string, +): Record { + const signedRelease = version.registry_entry.versions.find((entry) => entry.version === version.version); + if (!signedRelease) { + throw new ApiError(500, "registry_release_identity_missing", "signed Registry release identity is missing"); + } + return { + schema: "cellscript-registry-commitment-v1", + namespace: version.namespace, + name: version.name, + release: version.version, + source_hash: version.source_hash, + manifest_hash: version.manifest_hash, + artifact_hash: signedRelease.artifact_hash ?? null, + deployed_evidence_hash: deployedEvidenceHash, + }; +} + +export function registryCommitmentHash(version: PackageVersionRecord, deployedEvidenceHash: string): string { + return ckbBlake2bHex(canonicalJson(registryCommitmentPayload(version, deployedEvidenceHash))); +} + +export function registryCommitmentCellData(commitmentHash: string): string { + if (!/^(?:0x)?[0-9a-fA-F]{64}$/.test(commitmentHash)) { + throw new ApiError(400, "invalid_commitment_hash", "Registry commitment hash must be 32-byte hexadecimal data"); + } + const magic = [...new TextEncoder().encode("CSREGv1")].map((byte) => byte.toString(16).padStart(2, "0")).join(""); + return `0x${magic}${commitmentHash.replace(/^0x/, "").toLowerCase()}`; +} + +export function registryCommitmentConfiguration(env: Env, required: boolean): RegistryCommitmentConfiguration | null { + const values = [ + env.REGISTRY_TYPE_SCRIPT_JSON, + env.REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON, + env.REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON, + env.REGISTRY_COMMITMENT_LOCK_CELL_DEP_JSON, + ] + .map((value) => value?.trim() || undefined); + if (values.every((value) => value === undefined)) { + if (required) { + throw new ApiError(503, "registry_commitment_unconfigured", "Registry Type Script, commitment lock, and both CellDeps are required"); + } + return null; + } + if (values.some((value) => value === undefined)) { + throw new ApiError(503, "registry_commitment_misconfigured", "Registry commitment Script configuration must be complete"); + } + const typeScript = parseConfiguredJson(values[0]!, "REGISTRY_TYPE_SCRIPT_JSON"); + const typeScriptCellDep = parseConfiguredJson(values[1]!, "REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON"); + const commitmentLockScript = parseConfiguredJson(values[2]!, "REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON"); + const commitmentLockCellDep = parseConfiguredJson(values[3]!, "REGISTRY_COMMITMENT_LOCK_CELL_DEP_JSON"); + validateConfiguredScript(typeScript, "Registry Type Script"); + validateConfiguredScript(commitmentLockScript, "Registry commitment lock"); + validateConfiguredCellDep(typeScriptCellDep, "Registry Type Script CellDep"); + validateConfiguredCellDep(commitmentLockCellDep, "Registry commitment Lock CellDep"); + validateCanonicalMainnetRegistryConfiguration( + env, + typeScript, + typeScriptCellDep, + commitmentLockScript, + commitmentLockCellDep, + ); + return { + type_script: typeScript, + type_script_hash: ckbScriptHash(typeScript), + type_script_cell_dep: typeScriptCellDep, + commitment_lock_script: commitmentLockScript, + commitment_lock_hash: ckbScriptHash(commitmentLockScript), + commitment_lock_cell_dep: commitmentLockCellDep, + }; +} + +function validateCanonicalMainnetRegistryConfiguration( + env: Env, + typeScript: Record, + typeScriptCellDep: Record, + commitmentLockScript: Record, + commitmentLockCellDep: Record, +): void { + if (env.REGISTRY_ENVIRONMENT?.trim().toLowerCase() === "testnet-sandbox") { + validateCanonicalRegistryScripts( + typeScript, + typeScriptCellDep, + commitmentLockScript, + commitmentLockCellDep, + CKB_TESTNET_SIGHASH_DEP_GROUP, + "testnet-sandbox", + ); + return; + } + if (env.ENVIRONMENT?.trim().toLowerCase() !== "production") return; + + validateCanonicalRegistryScripts( + typeScript, + typeScriptCellDep, + commitmentLockScript, + commitmentLockCellDep, + CKB_MAINNET_SIGHASH_DEP_GROUP, + "production", + ); +} + +function validateCanonicalRegistryScripts( + typeScript: Record, + typeScriptCellDep: Record, + commitmentLockScript: Record, + commitmentLockCellDep: Record, + sighashDepGroup: { out_point: { tx_hash: string; index: string }; dep_type: string }, + environment: RegistryEnvironment, +): void { + + const typeScriptIsCanonical = sameCkbHash( + String(typeScript["code_hash"]), + CANONICAL_REGISTRY_TYPE_SCRIPT.code_hash, + ) + && typeScript["hash_type"] === CANONICAL_REGISTRY_TYPE_SCRIPT.hash_type + && typeof typeScript["args"] === "string" + && /^0x[0-9a-fA-F]{64}$/.test(typeScript["args"]) + && sameCkbHash(String(typeScript["args"]), ckbScriptHash(commitmentLockScript)); + if (!typeScriptIsCanonical || typeScriptCellDep["dep_type"] !== "code") { + throw new ApiError( + 503, + "registry_commitment_misconfigured", + `${environment} Registry Type Script must use the tracked immutable data1 release and a direct code CellDep`, + ); + } + + const lockArgs = commitmentLockScript["args"]; + const lockIsCanonical = sameCkbHash( + String(commitmentLockScript["code_hash"]), + CKB_MAINNET_SIGHASH_LOCK.code_hash, + ) + && commitmentLockScript["hash_type"] === CKB_MAINNET_SIGHASH_LOCK.hash_type + && typeof lockArgs === "string" + && /^0x[0-9a-fA-F]{40}$/.test(lockArgs); + if (!lockIsCanonical || !sameConfiguredCellDep(commitmentLockCellDep, sighashDepGroup)) { + throw new ApiError( + 503, + "registry_commitment_misconfigured", + `${environment} commitment custody must use a 20-byte secp256k1-blake160 lock and the matching network genesis DepGroup`, + ); + } +} + +function sameConfiguredCellDep( + actual: Record, + expected: { out_point: { tx_hash: string; index: string }; dep_type: string }, +): boolean { + const actualOutPoint = actual["out_point"] as Record; + const actualIndex = actualOutPoint["index"]; + const normalizedIndex = typeof actualIndex === "number" ? `0x${actualIndex.toString(16)}` : String(actualIndex).toLowerCase(); + return actual["dep_type"] === expected.dep_type + && sameCkbHash(String(actualOutPoint["tx_hash"]), expected.out_point.tx_hash) + && normalizedIndex === expected.out_point.index; +} + +async function verifyRegistryCommitmentConfigurationOnChain( + env: Env, + configuration: RegistryCommitmentConfiguration, +): Promise { + const runtime = registryRuntimeConfig(env); + const rpcUrl = runtime.rpc_url; + const rpcOptions = { + timeout_ms: boundedIntegerEnv(env.CKB_RPC_TIMEOUT_MS, 10_000, 1_000, 30_000), + maximum_bytes: boundedIntegerEnv(env.CKB_RPC_MAX_RESPONSE_BYTES, 2 * 1024 * 1024, 64 * 1024, 8 * 1024 * 1024), + }; + await requireRegistryRpc(rpcUrl, rpcOptions, runtime.network); + await verifyConfiguredScriptCellDepOnChain( + env, + rpcUrl, + rpcOptions, + configuration.type_script, + configuration.type_script_cell_dep, + "Registry Type Script", + ); + await verifyConfiguredScriptCellDepOnChain( + env, + rpcUrl, + rpcOptions, + configuration.commitment_lock_script, + configuration.commitment_lock_cell_dep, + "Registry commitment Lock Script", + ); +} + +async function requireLiveRegistryCommitmentConfiguration( + env: Env, + deps: AppDeps, + configuration: RegistryCommitmentConfiguration, +): Promise { + if (deps.verifyRegistryCommitmentConfiguration) { + await deps.verifyRegistryCommitmentConfiguration(configuration); + return; + } + await verifyRegistryCommitmentConfigurationOnChain(env, configuration); +} + +async function verifyConfiguredScriptCellDepOnChain( + env: Env, + rpcUrl: string, + rpcOptions: { timeout_ms: number; maximum_bytes: number }, + script: Record, + cellDep: Record, + label: string, +): Promise { + const rawOutPoint = assertPlainObject(cellDep["out_point"], "registry_commitment_misconfigured"); + const outPoint = { + tx_hash: String(rawOutPoint["tx_hash"]), + index: parseRpcUint32(rawOutPoint["index"], `${label} CellDep out_point.index`), + }; + const declared = await getLiveCell(rpcUrl, outPoint, rpcOptions); + await requireMinimumConfirmations(env, rpcUrl, declared.block_hash, rpcOptions, `${label} CellDep`); + const candidates: Record[] = []; + if (cellDep["dep_type"] === "code") { + candidates.push(declared.cell); + } else { + const data = assertPlainObject(declared.cell["data"], "invalid_ckb_rpc_response"); + if (typeof data["content"] !== "string") { + throw new ApiError(503, "registry_commitment_cell_dep_invalid", `${label} DepGroup has no output data`); + } + const members = parseDepGroupOutPoints(data["content"]); + const memberLimit = boundedIntegerEnv(env.CKB_DEP_GROUP_MAX_MEMBERS, 256, 1, 2048); + if (members.length > memberLimit) { + throw new ApiError(503, "registry_commitment_cell_dep_invalid", `${label} DepGroup exceeds the member limit`); + } + for (let offset = 0; offset < members.length; offset += 16) { + const page = await Promise.all(members.slice(offset, offset + 16).map(async (member) => { + try { + const live = await getLiveCell(rpcUrl, member, rpcOptions); + await requireMinimumConfirmations(env, rpcUrl, live.block_hash, rpcOptions, `${label} code Cell`); + return live.cell; + } catch (error) { + if (error instanceof ApiError && error.code === "deployment_cell_not_live") return null; + throw error; + } + })); + candidates.push(...page.filter((cell): cell is Record => cell !== null)); + } + } + if (!candidates.some((cell) => configuredScriptCodeHashMatches(cell, script))) { + throw new ApiError( + 503, + "registry_commitment_code_hash_unresolved", + `${label} CellDep does not resolve the configured code_hash`, + ); + } +} + +function configuredScriptCodeHashMatches(cell: Record, script: Record): boolean { + const codeHash = String(script["code_hash"]); + if (script["hash_type"] === "type") { + const output = assertPlainObject(cell["output"], "invalid_ckb_rpc_response"); + return Boolean(output["type"] && sameCkbHash(ckbScriptHash(output["type"]), codeHash)); + } + const data = assertPlainObject(cell["data"], "invalid_ckb_rpc_response"); + const content = data["content"]; + if (typeof content !== "string" || !/^0x(?:[0-9a-fA-F]{2})*$/.test(content)) return false; + const dataHash = typeof data["hash"] === "string" ? data["hash"] : ckbBlake2bHex(hexToBytes(content)); + return sameCkbHash(dataHash, codeHash); +} + +function parseConfiguredJson(raw: string, name: string): Record { + try { + return assertPlainObject(JSON.parse(raw), "registry_commitment_misconfigured"); + } catch { + throw new ApiError(503, "registry_commitment_misconfigured", `${name} must contain one JSON object`); + } +} + +function validateConfiguredScript(script: Record, label: string): void { + if (Object.keys(script).some((key) => !["code_hash", "hash_type", "args"].includes(key))) { + throw new ApiError(503, "registry_commitment_misconfigured", `${label} has an unknown field`); + } + if (typeof script["code_hash"] !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(script["code_hash"])) { + throw new ApiError(503, "registry_commitment_misconfigured", `${label}.code_hash must be a 32-byte hash`); + } + if (!(typeof script["hash_type"] === "string" && ["data", "data1", "data2", "type"].includes(script["hash_type"]))) { + throw new ApiError(503, "registry_commitment_misconfigured", `${label}.hash_type is invalid`); + } + if (typeof script["args"] !== "string" || !/^0x(?:[0-9a-fA-F]{2})*$/.test(script["args"])) { + throw new ApiError(503, "registry_commitment_misconfigured", `${label}.args must be hexadecimal bytes`); + } +} + +function validateConfiguredCellDep(cellDep: Record, label: string): void { + if (Object.keys(cellDep).some((key) => !["out_point", "dep_type"].includes(key))) { + throw new ApiError(503, "registry_commitment_misconfigured", `${label} has an unknown field`); + } + if (!(cellDep["dep_type"] === "code" || cellDep["dep_type"] === "dep_group")) { + throw new ApiError(503, "registry_commitment_misconfigured", `${label} dep_type is invalid`); + } + const rawOutPoint = cellDep["out_point"]; + if (typeof rawOutPoint !== "object" || rawOutPoint === null || Array.isArray(rawOutPoint)) { + throw new ApiError(503, "registry_commitment_misconfigured", `${label} out_point must be an object`); + } + const outPoint = rawOutPoint as Record; + if (Object.keys(outPoint).some((key) => !["tx_hash", "index"].includes(key))) { + throw new ApiError(503, "registry_commitment_misconfigured", `${label} out_point has an unknown field`); + } + if (typeof outPoint["tx_hash"] !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(outPoint["tx_hash"])) { + throw new ApiError(503, "registry_commitment_misconfigured", `${label} tx_hash is invalid`); + } + const index = outPoint["index"]; + if (!(typeof index === "string" && /^0x[0-9a-fA-F]+$/.test(index)) && !(Number.isSafeInteger(index) && Number(index) >= 0)) { + throw new ApiError(503, "registry_commitment_misconfigured", `${label} index is invalid`); + } +} + +async function listRegistryCommitmentCells( + env: Env, + configuration: RegistryCommitmentConfiguration, +): Promise { + const runtime = registryRuntimeConfig(env); + const rpcUrl = runtime.rpc_url; + const rpcOptions = { + timeout_ms: boundedIntegerEnv(env.CKB_RPC_TIMEOUT_MS, 10_000, 1_000, 30_000), + maximum_bytes: boundedIntegerEnv(env.CKB_RPC_MAX_RESPONSE_BYTES, 2 * 1024 * 1024, 64 * 1024, 8 * 1024 * 1024), + }; + await requireRegistryRpc(rpcUrl, rpcOptions, runtime.network); + const tip = assertPlainObject(await ckbRpcRequest(rpcUrl, "get_tip_header", [], rpcOptions), "invalid_ckb_rpc_response"); + const tipNumber = parseRpcBlockNumber(tip["number"], "CKB tip block number"); + const minimumConfirmations = boundedIntegerEnv(env.CKB_MIN_CONFIRMATIONS, 24, 1, 10_000); + const maximumCells = boundedIntegerEnv(env.CKB_REGISTRY_SCAN_MAX_CELLS, 1_000, 100, 10_000); + const cells: RegistryCommitmentCell[] = []; + let after: string | undefined; + while (cells.length < maximumCells) { + const searchKey = { + script: configuration.type_script, + script_type: "type", + script_search_mode: "exact", + filter: { + output_data: "0x43535245477631", + output_data_filter_mode: "prefix", + output_data_len_range: ["0x27", "0x28"], + }, + with_data: true, + }; + const params: unknown[] = [searchKey, "asc", "0x64"]; + if (after) params.push(after); + const page = assertPlainObject(await ckbRpcRequest(rpcUrl, "get_cells", params, rpcOptions), "invalid_ckb_rpc_response"); + const objects = page["objects"]; + if (!Array.isArray(objects)) { + throw new ApiError(503, "invalid_ckb_rpc_response", "CKB Indexer get_cells returned no objects array"); + } + for (const raw of objects) { + const cell = assertPlainObject(raw, "invalid_ckb_rpc_response"); + const output = assertPlainObject(cell["output"], "invalid_ckb_rpc_response"); + const content = cell["output_data"]; + if (typeof content !== "string" || !/^0x43535245477631[0-9a-fA-F]{64}$/.test(content)) continue; + if (!output["type"] || !sameCkbHash(ckbScriptHash(output["type"]), configuration.type_script_hash)) continue; + if (!output["lock"] || !sameCkbHash(ckbScriptHash(output["lock"]), configuration.commitment_lock_hash)) continue; + const outPoint = assertPlainObject(cell["out_point"], "invalid_ckb_rpc_response"); + const txHash = String(outPoint["tx_hash"] ?? ""); + const index = parseRpcUint32(outPoint["index"], "Registry commitment out_point.index"); + if (!/^0x[0-9a-fA-F]{64}$/.test(txHash)) { + throw new ApiError(503, "invalid_ckb_rpc_response", "Registry commitment out_point.tx_hash is invalid"); + } + const blockNumber = parseRpcBlockNumber(cell["block_number"], "Registry commitment block number"); + if (tipNumber < blockNumber || tipNumber - blockNumber + 1n < BigInt(minimumConfirmations)) continue; + cells.push({ + commitment_hash: `0x${content.slice(-64).toLowerCase()}`, + out_point: { tx_hash: txHash, index }, + block_number: `0x${blockNumber.toString(16)}`, + tip_block_number: `0x${tipNumber.toString(16)}`, + confirmations: Number(tipNumber - blockNumber + 1n), + output, + }); + if (cells.length >= maximumCells) break; + } + if (objects.length < 100) return cells; + const cursor = page["last_cursor"]; + if (typeof cursor !== "string" || cursor === after) { + throw new ApiError(503, "invalid_ckb_rpc_response", "CKB Indexer pagination cursor is invalid"); + } + after = cursor; + } + throw new ApiError(503, "registry_commitment_scan_limit", `Registry commitment scan exceeded ${maximumCells} live Cells`); +} + +function parseRpcUint32(value: unknown, label: string): number { + const parsed = typeof value === "string" && /^0x[0-9a-fA-F]+$/.test(value) ? Number.parseInt(value.slice(2), 16) : Number(value); + if (!Number.isSafeInteger(parsed) || parsed < 0 || parsed > 0xffff_ffff) { + throw new ApiError(503, "invalid_ckb_rpc_response", `${label} is not a u32`); + } + return parsed; +} + +async function reconcileRegistryChainState( + env: Env, + deps: AppDeps, + store: RegistryStore, + now: Date, + requestId: string, +): Promise { + const configuration = registryCommitmentConfiguration(env, true)!; + const listCommitmentCells = deps.listRegistryCommitmentCells ?? deps.listMainnetCommitmentCells; + const cells = listCommitmentCells + ? await listCommitmentCells(configuration) + : await listRegistryCommitmentCells(env, configuration); + const cellsByHash = new Map(cells.map((cell) => [cell.commitment_hash.toLowerCase(), cell])); + const staticOrigin = env.STATIC_REGISTRY_ORIGIN ?? DEFAULT_STATIC_REGISTRY_ORIGIN; + let checked = 0; + let committed = 0; + let demotedCommitments = 0; + let staleDeployments = 0; + const versionsToCheck: PackageVersionRecord[] = []; + for (let offset = 0; offset < 10_000; offset += 200) { + const versions = await store.listPackageVersions({ deployment_status: "chain_verified", limit: 200, offset }); + versionsToCheck.push(...versions); + if (versions.length < 200) break; + } + for (const version of versionsToCheck) { + checked += 1; + const previous = await store.listPackageEvidence(version.namespace, version.name, version.version); + const deployed = previous.filter((item) => item.kind === "deployed").at(-1); + if (!deployed) continue; + try { + const payload = deploymentPayloadFromEvidence(version, deployed.evidence, registryRuntimeConfig(env).network); + const deploymentVerifier = deps.verifyDeployment ?? deps.verifyMainnetDeployment; + if (deploymentVerifier) await deploymentVerifier(payload); + else await verifyDeployment(env, payload); + } catch (error) { + if (error instanceof ApiError && [ + "deployment_cell_not_live", + "dep_group_artifact_not_found", + "deployment_data_hash_mismatch", + "deployment_code_hash_mismatch", + "chain_observation_uncommitted", + "chain_confirmation_depth_insufficient", + ].includes(error.code)) { + const reconciled = await store.reconcilePackageVersionLifecycle({ + namespace: version.namespace, + name: version.name, + version: version.version, + status: "verified_build", + deployment_status: "undeployed", + request_id: requestId, + reason: error.code, + }); + staleDeployments += 1; + await syncLifecycleStatic(env, deps, store, reconciled, staticOrigin, requestId); + continue; + } + await store.appendAuditEvent({ + request_id: requestId, + event_type: "maintenance.lifecycle_check_failed", + namespace: version.namespace, + name: version.name, + version: version.version, + data: { error: error instanceof Error ? error.message : String(error) }, + }); + continue; + } + + const commitmentHash = registryCommitmentHash(version, deployed.evidence_hash); + const cell = cellsByHash.get(commitmentHash.toLowerCase()); + const priorCommitment = version.current_commitment_evidence_hash + ? previous.find((item) => item.kind === "on_chain_committed" + && item.evidence_hash === version.current_commitment_evidence_hash + && item.evidence["deployed_evidence_hash"] === deployed.evidence_hash) + : undefined; + if (!cell) { + if (version.current_commitment_evidence_hash) { + const reconciled = await store.reconcilePackageVersionLifecycle({ + namespace: version.namespace, + name: version.name, + version: version.version, + status: "deployed", + deployment_status: "chain_verified", + request_id: requestId, + reason: "registry_commitment_cell_not_live", + }); + demotedCommitments += 1; + await syncLifecycleStatic(env, deps, store, reconciled, staticOrigin, requestId); + } + continue; + } + const sameLiveCell = priorCommitment + && version.current_commitment_evidence_hash === priorCommitment.evidence_hash + && priorCommitment.evidence["commitment_tx_hash"] === cell.out_point.tx_hash + && assertPlainObject(priorCommitment.evidence["commitment_out_point"], "invalid_commitment_out_point")["index"] === cell.out_point.index; + if (sameLiveCell || version.availability_status !== "active") continue; + let evidence: Record = { + schema: "cellscript-registry-evidence", + kind: "on_chain_committed", + producer: `cellscript-registry-${registryRuntimeConfig(env).network}-indexer`, + generated_at: now.toISOString(), + verification_status: "passed", + source_hash: version.source_hash, + manifest_hash: version.manifest_hash, + deployed_evidence_hash: deployed.evidence_hash, + network: registryRuntimeConfig(env).network, + commitment_tx_hash: cell.out_point.tx_hash, + commitment_hash: commitmentHash, + commitment_lock_hash: configuration.commitment_lock_hash, + registry_type_hash: configuration.type_script_hash, + commitment_out_point: cell.out_point, + observed_at: now.toISOString(), + observed_block_number: cell.block_number, + ...(cell.tip_block_number ? { observed_tip_block_number: cell.tip_block_number } : {}), + ...(cell.confirmations !== undefined ? { confirmations: cell.confirmations } : {}), + commitment_status: "confirmed", + commitment_schema: "cellscript-registry-commitment-v1", + commitment_payload: registryCommitmentPayload(version, deployed.evidence_hash), + chain_verification: "get_cells+configured_type_index", + }; + if (version.compatibility_profile_hash) { + evidence = { ...evidence, compatibility_profile_hash: version.compatibility_profile_hash }; + } + evidence = validatePromotionEvidence( + evidence, + "on_chain_committed", + version, + previous, + registryRuntimeConfig(env).network, + ); + const evidenceHash = `sha256:${await sha256Hex(canonicalJson(evidence))}`; + const promoted = await store.promotePackageVersion({ + namespace: version.namespace, + name: version.name, + version: version.version, + kind: "on_chain_committed", + evidence_hash: evidenceHash, + evidence, + request_id: requestId, + admin_actor: `registry-${registryRuntimeConfig(env).network}-indexer`, + }); + committed += 1; + await syncLifecycleStatic(env, deps, store, promoted.version, staticOrigin, requestId); + } + await store.appendAuditEvent({ + request_id: requestId, + event_type: "maintenance.registry_commitments_reconciled", + data: { + checked, + live_commitment_cells: cells.length, + committed, + demoted_commitments: demotedCommitments, + stale_deployments: staleDeployments, + }, + }); +} + +async function demoteCurrentCommitments( + env: Env, + deps: AppDeps, + store: RegistryStore, + requestId: string, + reason: string, +): Promise { + const staticOrigin = env.STATIC_REGISTRY_ORIGIN ?? DEFAULT_STATIC_REGISTRY_ORIGIN; + let demoted = 0; + for (let offset = 0; offset < 10_000; offset += 200) { + const versions = await store.listPackageVersions({ deployment_status: "chain_verified", limit: 200, offset }); + for (const version of versions) { + if (!version.current_commitment_evidence_hash) continue; + const reconciled = await store.reconcilePackageVersionLifecycle({ + namespace: version.namespace, + name: version.name, + version: version.version, + status: "deployed", + deployment_status: "chain_verified", + request_id: requestId, + reason, + }); + demoted += 1; + await syncLifecycleStatic(env, deps, store, reconciled, staticOrigin, requestId); + } + if (versions.length < 200) break; + } + return demoted; +} + +function deploymentPayloadFromEvidence( + version: PackageVersionRecord, + evidence: Record, + network: DeploymentPayload["network"], +): DeploymentPayload { + const outPoint = assertPlainObject(evidence["out_point"], "invalid_deployment_out_point"); + return { + protocol: DEPLOYMENT_PROTOCOL, + action: DEPLOYMENT_ACTION, + registry_origin: DEFAULT_REGISTRY_ORIGIN, + namespace: version.namespace, + name: version.name, + release: version.version, + network, + artifact_hash: String(evidence["artifact_hash"]), + data_hash: String(evidence["data_hash"]), + code_hash: String(evidence["code_hash"]), + hash_type: evidence["hash_type"] as DeploymentPayload["hash_type"], + dep_type: evidence["dep_type"] as DeploymentPayload["dep_type"], + out_point: { tx_hash: String(outPoint["tx_hash"]), index: Number(outPoint["index"]) }, + capability_key_id: "registry-lifecycle-reconciliation", + nonce: `0x${"00".repeat(32)}`, + issued_at: String(evidence["generated_at"]), + expires_at: String(evidence["generated_at"]), + cli_version: "registry-lifecycle-reconciliation", + }; +} + +async function syncLifecycleStatic( + env: Env, + deps: AppDeps, + store: RegistryStore, + version: PackageVersionRecord, + staticOrigin: string, + requestId: string, +): Promise { + const snapshot = await store.getSnapshot(version.snapshot_hash); + if (!snapshot) return; + const evidence = await store.listPackageEvidence(version.namespace, version.name, version.version); + await tryWriteStaticRegistryVersionObject( + env, + deps, + store, + requestId, + { ...version, direct_url: staticPackageVersionUrl(staticOrigin, version.namespace, version.name, version.version) }, + snapshot, + staticOrigin, + evidence, + ); +} + +async function verifyRegistryCommitment( + env: Env, + evidence: Record, + version: PackageVersionRecord, + deployed: PackageEvidenceRecord, +): Promise> { + const configuration = registryCommitmentConfiguration(env, true)!; + const expectedHash = registryCommitmentHash(version, deployed.evidence_hash); + if (!sameCkbHash(String(evidence["commitment_hash"]), expectedHash)) { + throw new ApiError(409, "registry_commitment_mismatch", "commitment_hash does not commit to the accepted Registry release and deployment evidence"); + } + const rawOutPoint = assertPlainObject(evidence["commitment_out_point"], "invalid_commitment_out_point"); + const outPoint = { tx_hash: String(rawOutPoint["tx_hash"]), index: Number(rawOutPoint["index"]) }; + const runtime = registryRuntimeConfig(env); + const rpcUrl = runtime.rpc_url; + const rpcOptions = { + timeout_ms: boundedIntegerEnv(env.CKB_RPC_TIMEOUT_MS, 10_000, 1_000, 30_000), + maximum_bytes: boundedIntegerEnv(env.CKB_RPC_MAX_RESPONSE_BYTES, 2 * 1024 * 1024, 64 * 1024, 8 * 1024 * 1024), + }; + await requireRegistryRpc(rpcUrl, rpcOptions, runtime.network); + const live = await getLiveCell(rpcUrl, outPoint, rpcOptions); + const observation = await requireMinimumConfirmations(env, rpcUrl, live.block_hash, rpcOptions, "Registry commitment"); + const data = assertPlainObject(live.cell["data"], "invalid_ckb_rpc_response"); + if (typeof data["content"] !== "string" || data["content"].toLowerCase() !== registryCommitmentCellData(expectedHash)) { + throw new ApiError(409, "registry_commitment_data_mismatch", "live Registry commitment Cell data does not contain the expected compact commitment"); + } + const output = assertPlainObject(live.cell["output"], "invalid_ckb_rpc_response"); + const typeScript = output["type"]; + if (!typeScript) { + throw new ApiError(409, "registry_commitment_type_missing", "Registry commitment Cell must have a Type Script for chain indexing"); + } + const actualTypeHash = ckbScriptHash(typeScript); + if (!sameCkbHash(actualTypeHash, configuration.type_script_hash) + || !sameCkbHash(actualTypeHash, String(evidence["registry_type_hash"]))) { + throw new ApiError(409, "registry_commitment_type_mismatch", "Registry commitment Cell does not use the configured Registry Type Script"); + } + const actualLockHash = ckbScriptHash(output["lock"]); + if (!sameCkbHash(actualLockHash, configuration.commitment_lock_hash) + || !sameCkbHash(actualLockHash, String(evidence["commitment_lock_hash"]))) { + throw new ApiError(409, "commitment_lock_mismatch", "Registry commitment Cell does not use the configured commitment lock"); + } + return { + commitment_schema: "cellscript-registry-commitment-v1", + commitment_payload: registryCommitmentPayload(version, deployed.evidence_hash), + chain_verification: "get_transaction+get_live_cell+configured_type_index", + observed_block_hash: live.block_hash ?? null, + observed_block_number: observation.block_number, + observed_tip_block_number: observation.tip_block_number, + confirmations: observation.confirmations, + }; +} + +async function handleReadiness(env: Env, deps: AppDeps, requestId: string, headers: Headers): Promise { + let runtime: RegistryRuntimeConfig | null = null; + const storeConfigured = !!deps.store || !!env.HYPERDRIVE; + const objectStoreConfigured = + (!!deps.snapshotWriter && !!deps.registryObjectReader) + || !!env.REGISTRY_OBJECTS + || !!env.SOURCE_SNAPSHOTS; + const adminConfigured = typeof env.REGISTRY_ADMIN_TOKEN === "string" && env.REGISTRY_ADMIN_TOKEN.trim() !== ""; + const checks: Record = { + store: storeConfigured ? "configured" : "missing_hyperdrive", + object_store: objectStoreConfigured ? "configured" : "missing_r2", + admin_token: adminConfigured ? "configured" : "missing_secret", + }; + let dependenciesHealthy = true; + try { + runtime = registryRuntimeConfig(env); + checks["registry_environment"] = runtime.environment; + checks["ckb_network"] = runtime.network; + if (runtime.environment === "testnet-sandbox" || env.CKB_RPC_URL || env.CKB_MAINNET_RPC_URL) { + await requireRegistryRpc(runtime.rpc_url, { + timeout_ms: boundedIntegerEnv(env.CKB_RPC_TIMEOUT_MS, 10_000, 1_000, 30_000), + maximum_bytes: boundedIntegerEnv(env.CKB_RPC_MAX_RESPONSE_BYTES, 2 * 1024 * 1024, 64 * 1024, 8 * 1024 * 1024), + }, runtime.network); + checks["ckb_rpc"] = "configured_network_confirmed"; + } else { + checks["ckb_rpc"] = "default_mainnet"; + } + } catch { + checks["registry_environment"] = "misconfigured"; + checks["ckb_rpc"] = "wrong_network_or_unreachable"; + dependenciesHealthy = false; + } + try { + const commitmentConfiguration = registryCommitmentConfiguration(env, false); + if (commitmentConfiguration) { + await requireLiveRegistryCommitmentConfiguration(env, deps, commitmentConfiguration); + checks["registry_commitment"] = "configured_and_live"; + } else { + checks["registry_commitment"] = "disabled"; + } + } catch { + checks["registry_commitment"] = "misconfigured"; + dependenciesHealthy = false; + } + try { + const policy = registryReproducerPolicy(env, false); + if (policy) { + const keysAreImportable = await Promise.all( + [...policy.builders.values()].map((builder) => isImportableP256SpkiPublicKey(builder.public_key)), + ); + if (keysAreImportable.some((valid) => !valid)) { + throw new ApiError(503, "reproducer_policy_misconfigured", "trusted builder policy contains an invalid P-256 public key"); + } + checks["reproducer_policy"] = "configured"; + } else { + checks["reproducer_policy"] = "disabled"; + } + } catch { + checks["reproducer_policy"] = "misconfigured"; + dependenciesHealthy = false; + } + const store = optionalStore(env, deps); + if (store) { + try { + await store.healthCheck(); + checks["store"] = "ready"; + } catch { + checks["store"] = "unreachable"; + dependenciesHealthy = false; + } + } + if (deps.readinessCheck) { + try { + Object.assign(checks, await deps.readinessCheck()); + } catch { + checks["runtime"] = "unready"; + dependenciesHealthy = false; + } + } + const ready = storeConfigured && objectStoreConfigured && adminConfigured && dependenciesHealthy; + return json( + { + status: ready ? "ready" : "not_ready", + request_id: requestId, + checks, + }, + ready ? 200 : 503, + headers, + ); +} + +async function handleAdminReservedNamespace( + request: Request, + env: Env, + store: RegistryStore, + requestId: string, + headers: Headers, +): Promise { + const adminActor = await requireAdminActor(request, env); + const body = await readJson(request, maxJsonBytes(env)); + const namespace = validatePackageIdent(String(body["namespace"] ?? ""), "namespace"); + const matchType = requireOneOf(String(body["match_type"] ?? "exact"), ["exact", "prefix", "typosquat"], "invalid_reserved_match_type"); + const reason = requireNonEmptyAdminString(body["reason"], "reason"); + const record = await store.upsertReservedNamespace({ + namespace, + match_type: matchType, + reason, + request_id: requestId, + admin_actor: adminActor, + }); return json({ request_id: requestId, ...record }, 200, headers); } @@ -289,7 +2562,7 @@ async function handleAdminAuditEvents( requestId: string, headers: Headers, ): Promise { - requireAdminActor(request, env); + await requireAdminActor(request, env); const params = new URL(request.url).searchParams; const eventType = optionalAuditParam(params, "event_type"); const principalType = optionalAuditParam(params, "principal_type"); @@ -299,8 +2572,8 @@ async function handleAdminAuditEvents( const versionRaw = optionalAuditParam(params, "version"); const beforeRaw = optionalAuditParam(params, "before"); const limit = auditLimit(params); - if (principalType && principalType !== ACCEPTED_PRINCIPAL_TYPE) { - throw new ApiError(400, "invalid_audit_filter", "principal_type filter must be joyid_ckb"); + if (principalType && !isPrincipalType(principalType)) { + throw new ApiError(400, "invalid_audit_filter", "principal_type filter is unsupported"); } const before = beforeRaw ? parseAuditBefore(beforeRaw) : undefined; const namespace = namespaceRaw ? validatePackageIdent(namespaceRaw, "namespace") : undefined; @@ -328,6 +2601,40 @@ async function handleAdminAuditEvents( ); } +async function handleAdminVerificationQueue( + request: Request, + env: Env, + store: RegistryStore, + requestId: string, + headers: Headers, +): Promise { + await requireAdminActor(request, env); + const metrics = await store.getVerificationQueueMetrics(); + return json( + { + schema: "cellscript-registry-verification-queue-v1", + request_id: requestId, + ...metrics, + }, + 200, + headers, + ); +} + +async function handleAdminVerificationRetry( + request: Request, + env: Env, + store: RegistryStore, + requestId: string, + headers: Headers, + jobIdFromPath: string, +): Promise { + const adminActor = await requireAdminActor(request, env); + const jobId = requireUuid(jobIdFromPath, "verification_job_id"); + const job = await store.retryVerificationJob({ job_id: jobId, request_id: requestId, admin_actor: adminActor }); + return json({ request_id: requestId, job }, 200, headers); +} + async function handleAdminNamespaceStatus( request: Request, env: Env, @@ -336,7 +2643,7 @@ async function handleAdminNamespaceStatus( headers: Headers, namespaceFromPath: string, ): Promise { - const adminActor = requireAdminActor(request, env); + const adminActor = await requireAdminActor(request, env); const body = await readJson(request, maxJsonBytes(env)); const namespace = validatePackageIdent(namespaceFromPath, "namespace"); const status = requireOneOf( @@ -367,24 +2674,33 @@ async function handleAdminPackageVersionStatus( nameFromPath: string, versionFromPath: string, ): Promise { - const adminActor = requireAdminActor(request, env); + const adminActor = await requireAdminActor(request, env); const body = await readJson(request, maxJsonBytes(env)); const namespace = validatePackageIdent(namespaceFromPath, "namespace"); const name = validatePackageIdent(nameFromPath, "name"); const version = validateVersion(versionFromPath); const status = requireOneOf( - String(body["status"] ?? ""), - ["source_published", "indexed_pending", "verified_build", "deployed", "deprecated", "yanked", "quarantined"], - "invalid_package_version_status", + String(body["availability_status"] ?? ""), + ["active", "deprecated", "yanked", "quarantined"], + "invalid_availability_status", ); const reason = typeof body["reason"] === "string" && body["reason"].trim() !== "" ? body["reason"].trim() : undefined; const directUrl = staticPackageVersionUrl(staticOrigin, namespace, name, version); + const existing = await store.getPackageVersion(namespace, name, version); + if (!existing) { + throw new ApiError(404, "artifact_release_not_found", "artifact release is not known to the registry"); + } + const snapshot = await requireSnapshot(store, existing); + const evidence = await store.listPackageEvidence(namespace, name, version); if (isSuppressivePackageVersionStatus(status)) { - const existing = await store.getPackageVersion(namespace, name, version); - if (!existing) { - throw new ApiError(404, "package_version_not_found", "package version is not known to the registry"); - } - await writeStaticRegistryVersionObject(env, deps, { ...existing, status, direct_url: directUrl }); + await writeStaticRegistryVersionObject( + env, + deps, + { ...existing, status: status === "active" ? existing.status : status, availability_status: status, direct_url: directUrl }, + snapshot, + staticOrigin, + evidence, + ); } const record = await store.updatePackageVersionStatus({ namespace, @@ -396,11 +2712,149 @@ async function handleAdminPackageVersionStatus( admin_actor: adminActor, }); if (!isSuppressivePackageVersionStatus(status)) { - await writeStaticRegistryVersionObject(env, deps, { ...record, direct_url: directUrl }); + await tryWriteStaticRegistryVersionObject( + env, + deps, + store, + requestId, + { ...record, direct_url: directUrl }, + snapshot, + staticOrigin, + evidence, + ); } return json({ request_id: requestId, ...record }, 200, headers); } +async function handleAdminPackageVersionPromotion( + request: Request, + env: Env, + store: RegistryStore, + requestId: string, + staticOrigin: string, + deps: AppDeps, + headers: Headers, + namespaceFromPath: string, + nameFromPath: string, + versionFromPath: string, +): Promise { + const adminActor = await requireAdminActor(request, env); + const namespace = validatePackageIdent(namespaceFromPath, "namespace"); + const name = validatePackageIdent(nameFromPath, "name"); + const version = validateVersion(versionFromPath); + const body = await readJson(request, Math.min(maxJsonBytes(env), 512 * 1024)); + const kind = requireOneOf( + String(body["kind"] ?? ""), + ["verified_build", "reproduced_build", "deployed", "on_chain_committed"], + "invalid_evidence_kind", + ) as PackageEvidenceKind; + const existing = await store.getPackageVersion(namespace, name, version); + if (!existing) { + throw new ApiError(404, "artifact_release_not_found", "artifact release is not known to the registry"); + } + const previousEvidence = await store.listPackageEvidence(namespace, name, version); + if (kind === "deployed" && packageVersionRequiresReproduction(existing) && existing.verification_status !== "verified") { + throw new ApiError(409, "reproduction_evidence_missing", "reproducible artifacts require accepted independent reproduction evidence before deployment"); + } + const runtime = registryRuntimeConfig(env); + let evidence = validatePromotionEvidence(body["evidence"], kind, existing, previousEvidence, runtime.network); + if (kind === "reproduced_build") { + evidence = { + ...evidence, + ...(await verifyAuthenticatedReproductionReports(env, deps, evidence)), + }; + } else if (kind === "deployed") { + if (existing.artifact.profile !== "ckb_executable") { + throw new ApiError(409, "deployment_not_applicable", "only ckb_executable artifacts can record deployment evidence"); + } + const rawOutPoint = assertPlainObject(evidence["out_point"], "invalid_deployment_out_point"); + const deploymentPayload: DeploymentPayload = { + protocol: DEPLOYMENT_PROTOCOL, + action: DEPLOYMENT_ACTION, + registry_origin: env.REGISTRY_ORIGIN ?? DEFAULT_REGISTRY_ORIGIN, + namespace, + name, + release: version, + network: runtime.network, + artifact_hash: String(evidence["artifact_hash"]), + data_hash: String(evidence["data_hash"]), + code_hash: String(evidence["code_hash"]), + hash_type: evidence["hash_type"] as DeploymentPayload["hash_type"], + dep_type: evidence["dep_type"] as DeploymentPayload["dep_type"], + out_point: { tx_hash: String(rawOutPoint["tx_hash"]), index: Number(rawOutPoint["index"]) }, + capability_key_id: "admin-evidence-recovery", + nonce: `0x${"00".repeat(32)}`, + issued_at: String(evidence["generated_at"]), + expires_at: String(evidence["generated_at"]), + cli_version: "admin-evidence-recovery", + }; + const deploymentVerifier = deps.verifyDeployment ?? deps.verifyMainnetDeployment; + const chain = deploymentVerifier + ? await deploymentVerifier(deploymentPayload) + : await verifyDeployment(env, deploymentPayload); + evidence = { + ...evidence, + chain_verification: "get_transaction+get_live_cell", + ...(chain.block_hash ? { block_hash: chain.block_hash } : {}), + ...(chain.block_number ? { block_number: chain.block_number } : {}), + ...(chain.tip_block_number ? { observed_tip_block_number: chain.tip_block_number } : {}), + ...(chain.confirmations !== undefined ? { confirmations: chain.confirmations } : {}), + ...(chain.resolved_code_out_point ? { resolved_code_out_point: chain.resolved_code_out_point } : {}), + ...(chain.dep_group_size !== undefined ? { dep_group_size: chain.dep_group_size } : {}), + }; + } else if (kind === "on_chain_committed") { + const deployed = latestEvidence(previousEvidence, "deployed"); + if (!deployed.evidence["chain_verification"]) { + throw new ApiError(409, "deployment_chain_evidence_missing", "on-chain commitment requires RPC-verified deployment evidence"); + } + const configuration = registryCommitmentConfiguration(env, true)!; + await requireLiveRegistryCommitmentConfiguration(env, deps, configuration); + const verifyCommitment = deps.verifyRegistryCommitment ?? deps.verifyMainnetCommitment; + const chainEvidence = verifyCommitment + ? await verifyCommitment(evidence, existing, deployed) + : await verifyRegistryCommitment(env, evidence, existing, deployed); + evidence = { ...evidence, ...chainEvidence }; + } + const evidenceHash = `sha256:${await sha256Hex(canonicalJson(evidence))}`; + const promotion = { + namespace, + name, + version, + kind, + evidence_hash: evidenceHash, + evidence, + request_id: requestId, + admin_actor: adminActor, + }; + const promoted = kind === "deployed" + ? await store.recordChainVerifiedDeployment(promotion) + : await store.promotePackageVersion(promotion); + const allEvidence = await store.listPackageEvidence(namespace, name, version); + const snapshot = await requireSnapshot(store, promoted.version); + await tryWriteStaticRegistryVersionObject( + env, + deps, + store, + requestId, + { ...promoted.version, direct_url: staticPackageVersionUrl(staticOrigin, namespace, name, version) }, + snapshot, + staticOrigin, + allEvidence, + ); + return json( + { + request_id: requestId, + namespace, + name, + version, + status: promoted.version.status, + evidence: promoted.evidence, + }, + 200, + headers, + ); +} + function isSuppressivePackageVersionStatus(status: string): boolean { return status === "deprecated" || status === "yanked" || status === "quarantined"; } @@ -416,7 +2870,300 @@ function optionalStore(env: Env, deps: AppDeps): RegistryStore | undefined { if (deps.store) { return deps.store; } - return env.HYPERDRIVE ? new SqlRegistryStore(env.HYPERDRIVE) : undefined; + return env.HYPERDRIVE ? new SqlRegistryStore(env.HYPERDRIVE) : undefined; +} + +async function handleCreateAuthorisationSession( + request: Request, + env: Env, + store: RegistryStore, + requestId: string, + registryOrigin: string, + now: Date, + headers: Headers, +): Promise { + await throttleRequestSource(store, request, requestId, "authorisation_session_create", 30, 60, now); + const body = await readJson(request, Math.min(maxJsonBytes(env), 64 * 1024)); + const capabilityPubkey = String(body["capability_pubkey"] ?? "").trim(); + if (!isCanonicalP256SpkiPublicKey(capabilityPubkey) || !await isImportableP256SpkiPublicKey(capabilityPubkey)) { + throw new ApiError(400, "invalid_capability_pubkey", "capability_pubkey must be an importable canonical P-256 SPKI key"); + } + const scopesValue = body["requested_scopes"]; + if (!Array.isArray(scopesValue) || scopesValue.length !== 1 || typeof scopesValue[0] !== "string") { + throw new ApiError(400, "invalid_authorisation_session_scope", "browser authorisation requires one exact publish scope"); + } + const scope = scopesValue[0].trim(); + const scopeMatch = scope.match(/^publish:([^/]+)\/([^/]+)$/); + if (!scopeMatch) { + throw new ApiError(400, "invalid_authorisation_session_scope", "browser authorisation requires publish:namespace/name"); + } + const namespace = validatePackageIdent(scopeMatch[1] ?? "", "namespace"); + const name = validatePackageIdent(scopeMatch[2] ?? "", "name"); + const artifactKind = String(body["artifact_kind"] ?? "").trim() as ArtifactKind; + if (!ARTIFACT_KINDS.includes(artifactKind)) { + throw new ApiError(400, "invalid_artifact_kind", `artifact_kind must be one of ${ARTIFACT_KINDS.join(", ")}`); + } + const capabilityExpiresAt = String(body["capability_expires_at"] ?? "").trim(); + const capabilityExpiry = new Date(capabilityExpiresAt); + if (!Number.isFinite(capabilityExpiry.getTime()) || capabilityExpiry.getTime() <= now.getTime()) { + throw new ApiError(400, "invalid_capability_expiry", "capability_expires_at must be a future ISO timestamp"); + } + if (capabilityExpiry.getTime() > now.getTime() + 366 * 24 * 60 * 60 * 1_000) { + throw new ApiError(400, "capability_expiry_too_long", "browser-authorised capabilities may last no longer than 366 days"); + } + const cliVersion = String(body["cli_version"] ?? "").trim(); + if (!cliVersion || cliVersion.length > 64) throw new ApiError(400, "invalid_cli_version", "cli_version is required"); + + const sessionId = `auth_${crypto.randomUUID().replaceAll("-", "")}`; + const pollToken = `poll_${crypto.randomUUID().replaceAll("-", "")}`; + const browserToken = `browser_${crypto.randomUUID().replaceAll("-", "")}`; + const expiresAt = new Date(now.getTime() + AUTHORISATION_SESSION_TTL_MINUTES * 60 * 1_000).toISOString(); + const websiteOrigin = registryWebsiteOrigin(env); + const record = await store.createAuthorisationSession({ + session_id: sessionId, + poll_token_hash: `sha256:${await sha256Hex(pollToken)}`, + browser_token_hash: `sha256:${await sha256Hex(browserToken)}`, + registry_origin: registryOrigin, + website_origin: websiteOrigin, + capability_pubkey: capabilityPubkey, + requested_scopes: [scope], + capability_expires_at: capabilityExpiry.toISOString(), + cli_version: cliVersion, + namespace, + name, + artifact_kind: artifactKind, + status: "pending", + created_at: now.toISOString(), + updated_at: now.toISOString(), + expires_at: expiresAt, + request_id: requestId, + }); + await store.appendAuditEvent({ + request_id: requestId, + event_type: "authorisation_session.created", + namespace, + name, + data: { session_id: record.session_id, capability_key_id: await capabilityKeyId(capabilityPubkey), expires_at: expiresAt }, + }); + return json({ + schema: "cellscript-registry-authorisation-session-v1", + request_id: requestId, + session_id: record.session_id, + poll_token: pollToken, + browser_url: `${websiteOrigin}/registry/submit#authorisation_session=${encodeURIComponent(record.session_id)}&browser_token=${encodeURIComponent(browserToken)}`, + artifact: { namespace, name, kind: artifactKind }, + requested_scopes: record.requested_scopes, + expires_at: record.expires_at, + }, 201, headers); +} + +async function handleGetAuthorisationSession( + request: Request, + store: RegistryStore, + requestId: string, + now: Date, + headers: Headers, + sessionIdFromPath: string, +): Promise { + const sessionId = validateAuthorisationSessionId(sessionIdFromPath); + const session = await requireReadableAuthorisationSession(store, sessionId, now); + const authorization = request.headers.get("authorization"); + const token = authorization?.startsWith("Bearer ") ? authorization.slice("Bearer ".length).trim() : ""; + if (!token) throw new ApiError(401, "authorisation_session_token_required", "authorisation session bearer token is required"); + const tokenHash = `sha256:${await sha256Hex(token)}`; + const isCliPoll = await constantTimeSecretEqual(tokenHash, session.poll_token_hash); + const isBrowser = await constantTimeSecretEqual(tokenHash, session.browser_token_hash); + if (!isCliPoll && !isBrowser) { + throw new ApiError(401, "invalid_authorisation_session_token", "authorisation session bearer token is invalid"); + } + return json({ + schema: "cellscript-registry-authorisation-session-v1", + request_id: requestId, + session_id: session.session_id, + status: session.status, + artifact: { namespace: session.namespace, name: session.name, kind: session.artifact_kind }, + requested_scopes: session.requested_scopes, + capability_expires_at: session.capability_expires_at, + expires_at: session.expires_at, + ...(isCliPoll && session.capability_key_id ? { capability_key_id: session.capability_key_id } : {}), + ...(isCliPoll && session.namespace_status ? { namespace_status: session.namespace_status } : {}), + }, 200, headers); +} + +async function handlePrepareAuthorisationSession( + request: Request, + env: Env, + store: RegistryStore, + requestId: string, + registryOrigin: string, + now: Date, + headers: Headers, + sessionIdFromPath: string, +): Promise { + await throttleRequestSource(store, request, requestId, "authorisation_session_challenge", 60, 60, now); + const sessionId = validateAuthorisationSessionId(sessionIdFromPath); + const session = await requireReadableAuthorisationSession(store, sessionId, now); + await requireAuthorisationBrowserToken(request, session.browser_token_hash); + if (session.status !== "pending") { + throw new ApiError(409, "authorisation_session_complete", "authorisation session has already completed"); + } + if (session.registry_origin !== registryOrigin) { + throw new ApiError(409, "authorisation_session_origin_mismatch", "authorisation session belongs to another Registry origin"); + } + const body = await readJson(request, Math.min(maxJsonBytes(env), 16 * 1024)); + const issuedAt = now.toISOString(); + const challengeExpiresAt = new Date(Math.min(Date.parse(session.expires_at), now.getTime() + 10 * 60 * 1_000)).toISOString(); + const payload = validateCapabilityPayload({ + protocol: "cellscript-registry-auth-v1", + action: "authorize_capability", + registry_origin: registryOrigin, + principal_type: body["principal_type"], + principal_id: body["principal_id"], + capability_pubkey: session.capability_pubkey, + requested_scopes: session.requested_scopes, + capability_expires_at: session.capability_expires_at, + nonce: `0x${crypto.randomUUID().replaceAll("-", "")}`, + issued_at: issuedAt, + expires_at: challengeExpiresAt, + cli_version: session.cli_version, + }, registryOrigin, now); + const challengeToken = `challenge_${crypto.randomUUID().replaceAll("-", "")}`; + await store.prepareAuthorisationSession({ + session_id: sessionId, + principal_type: payload.principal_type, + principal_id: payload.principal_id, + payload, + challenge_token_hash: `sha256:${await sha256Hex(challengeToken)}`, + request_id: requestId, + }); + return json({ + schema: "cellscript-registry-authorisation-challenge-v1", + request_id: requestId, + session_id: sessionId, + challenge_token: challengeToken, + payload, + }, 200, headers); +} + +async function handleCompleteAuthorisationSession( + request: Request, + env: Env, + store: RegistryStore, + requestId: string, + registryOrigin: string, + now: Date, + deps: AppDeps, + headers: Headers, + sessionIdFromPath: string, +): Promise { + await throttleRequestSource(store, request, requestId, "authorisation_session_complete", 40, 60, now); + const sessionId = validateAuthorisationSessionId(sessionIdFromPath); + const session = await requireReadableAuthorisationSession(store, sessionId, now); + await requireAuthorisationBrowserToken(request, session.browser_token_hash); + if (session.status !== "pending") { + return json({ + schema: "cellscript-registry-authorisation-session-v1", + request_id: requestId, + session_id: session.session_id, + status: session.status, + ...(session.namespace_status ? { namespace_status: session.namespace_status } : {}), + }, 200, headers); + } + if (!session.payload || !session.challenge_token_hash) { + throw new ApiError(409, "authorisation_challenge_missing", "request a wallet challenge before completing this session"); + } + const body = await readJson(request, Math.min(maxJsonBytes(env), 128 * 1024)); + const challengeToken = String(body["challenge_token"] ?? "").trim(); + const challengeTokenHash = `sha256:${await sha256Hex(challengeToken)}`; + if (!challengeToken || !await constantTimeSecretEqual(challengeTokenHash, session.challenge_token_hash)) { + throw new ApiError(401, "invalid_authorisation_challenge_token", "authorisation challenge token is invalid or stale"); + } + const payload = validateCapabilityPayload(session.payload, registryOrigin, now); + const signature = requirePrincipalSignature(body, payload.principal_type); + await verifyPrincipalAuthorisationPayload(payload, signature, deps.joyidVerifier ?? productionJoyidVerifier()); + await throttle(store, requestId, `principal:${payload.principal_type}:${payload.principal_id}`, "capability", 8, 60 * 60, now); + await throttle(store, requestId, `principal:${payload.principal_type}:${payload.principal_id}`, "namespace_claim", 12, 24 * 60 * 60, now); + const existing = await store.getNamespace(session.namespace); + if (existing && (existing.owner_principal_type !== payload.principal_type || existing.owner_principal_id !== payload.principal_id)) { + throw new ApiError(409, "namespace_already_claimed", "namespace is already claimed by another principal"); + } + const nonce = await signedNonceUse(requestId, { + protocol: payload.protocol, + action: `${payload.action}:capability_create`, + nonce: payload.nonce, + expires_at: payload.expires_at, + principal_type: payload.principal_type, + principal_id: payload.principal_id, + }); + const completion = await store.finaliseAuthorisationSession({ + session_id: sessionId, + expected_challenge_token_hash: challengeTokenHash, + payload, + principal_signature: signature, + nonce: { + ...nonce, + principal_type: payload.principal_type, + principal_id: payload.principal_id, + }, + request_id: requestId, + now_iso: now.toISOString(), + namespace_claim_cooldown_seconds: namespaceClaimCooldownSeconds(env), + }); + const completed = completion.session; + const namespaceStatus = completed.namespace_status; + if (!namespaceStatus) throw new Error("completed authorisation session did not record namespace status"); + return json({ + schema: "cellscript-registry-authorisation-session-v1", + request_id: requestId, + session_id: completed.session_id, + status: completed.status, + namespace_status: namespaceStatus, + }, completion.replayed ? 200 : namespaceStatus === "active" ? 201 : 202, headers); +} + +function validateAuthorisationSessionId(value: string): string { + const sessionId = value.trim().toLowerCase(); + if (!/^auth_[0-9a-f]{32}$/.test(sessionId)) { + throw new ApiError(400, "invalid_authorisation_session_id", "authorisation session ID is malformed"); + } + return sessionId; +} + +async function requireReadableAuthorisationSession(store: RegistryStore, sessionId: string, now: Date) { + const session = await store.getAuthorisationSession(sessionId); + if (!session) throw new ApiError(404, "authorisation_session_not_found", "authorisation session was not found"); + if (session.status === "pending" && Date.parse(session.expires_at) <= now.getTime()) { + throw new ApiError(410, "authorisation_session_expired", "authorisation session has expired; start again from cellc"); + } + return session; +} + +async function requireAuthorisationBrowserToken(request: Request, expectedHash: string): Promise { + const authorization = request.headers.get("authorization"); + const token = authorization?.startsWith("Bearer ") ? authorization.slice("Bearer ".length).trim() : ""; + if (!token || !token.startsWith("browser_")) { + throw new ApiError(401, "authorisation_browser_token_required", "browser authorisation token is required"); + } + if (!await constantTimeSecretEqual(`sha256:${await sha256Hex(token)}`, expectedHash)) { + throw new ApiError(401, "invalid_authorisation_browser_token", "browser authorisation token is invalid"); + } +} + +function registryWebsiteOrigin(env: Env): string { + const configured = (env.REGISTRY_WEBSITE_ORIGIN ?? ( + registryRuntimeConfig(env).environment === "testnet-sandbox" + ? "https://testnet.registry.cellscript.dev" + : "https://cellscript.dev" + )).trim().replace(/\/$/, ""); + let url: URL; + try { url = new URL(configured); } + catch { throw new ApiError(503, "invalid_registry_website_origin", "REGISTRY_WEBSITE_ORIGIN must be an absolute URL"); } + const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]"; + if ((url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) + || !url.hostname || url.username || url.password || url.pathname !== "/" || url.search || url.hash) { + throw new ApiError(503, "invalid_registry_website_origin", "REGISTRY_WEBSITE_ORIGIN must be a credential-free HTTPS origin (HTTP is allowed only on loopback)"); + } + return url.origin; } async function handleCreateCapability( @@ -432,10 +3179,10 @@ async function handleCreateCapability( await throttleRequestSource(store, request, requestId, "capability_create", 120, 60, now); const body = await readJson(request, maxJsonBytes(env)); const payload = validateCapabilityPayload(body["payload"], registryOrigin, now); - const signature = requireJoyidSignature(body["joyid_signature"]); - await verifyJoyidAuthorisationPayload(payload, signature, deps.joyidVerifier ?? productionJoyidVerifier(),); + const signature = requirePrincipalSignature(body, payload.principal_type); + await verifyPrincipalAuthorisationPayload(payload, signature, deps.joyidVerifier ?? productionJoyidVerifier()); await throttle(store, requestId, `principal:${payload.principal_type}:${payload.principal_id}`, "capability", 8, 60 * 60, now); - await consumeSignedNonce(store, requestId, { + const nonceKey = await consumeSignedNonce(store, requestId, { protocol: payload.protocol, action: `${payload.action}:capability_create`, nonce: payload.nonce, @@ -443,7 +3190,13 @@ async function handleCreateCapability( principal_type: payload.principal_type, principal_id: payload.principal_id, }); - const capability = await store.recordCapability({ payload, joyid_signature: signature, request_id: requestId }); + let capability; + try { + capability = await store.recordCapability({ payload, principal_signature: signature, request_id: requestId }); + } catch (error) { + await store.releaseNonce({ nonce_key: nonceKey, request_id: requestId }); + throw error; + } return json( { request_id: requestId, @@ -473,11 +3226,11 @@ async function handleClaimNamespace( const body = await readJson(request, maxJsonBytes(env)); const namespace = validatePackageIdent(String(body["namespace"] ?? ""), "namespace"); const payload = validateCapabilityPayload(body["payload"], registryOrigin, now); - const signature = requireJoyidSignature(body["joyid_signature"]); + const signature = requirePrincipalSignature(body, payload.principal_type); if (!payload.requested_scopes.some((scope) => scope.startsWith(`publish:${namespace}/`))) { throw new ApiError(403, "namespace_scope_missing", "namespace claim requires a publish scope for that namespace"); } - await verifyJoyidAuthorisationPayload(payload, signature, deps.joyidVerifier ?? productionJoyidVerifier()); + await verifyPrincipalAuthorisationPayload(payload, signature, deps.joyidVerifier ?? productionJoyidVerifier()); await throttle(store, requestId, `principal:${payload.principal_type}:${payload.principal_id}`, "namespace_claim", 12, 24 * 60 * 60, now); const existing = await store.getNamespace(namespace); if ( @@ -501,13 +3254,86 @@ async function handleClaimNamespace( await enforceNamespaceClaimCooldown(store, requestId, payload.principal_type, payload.principal_id, now, namespaceClaimCooldownSeconds(env)); const claim = await store.claimNamespace({ namespace, - principal_type: ACCEPTED_PRINCIPAL_TYPE, + principal_type: payload.principal_type, principal_id: payload.principal_id, request_id: requestId, }); return json({ request_id: requestId, ...claim }, claim.status === "active" ? 201 : 202, headers); } +async function handleCapabilityCheck( + request: Request, + store: RegistryStore, + requestId: string, + now: Date, + headers: Headers, + keyIdFromPath: string, +): Promise { + await throttleRequestSource(store, request, requestId, "capability_check", 240, 60, now); + const keyId = keyIdFromPath.trim().toLowerCase(); + if (!/^cap_[0-9a-f]{32}$/.test(keyId)) { + throw new ApiError(400, "invalid_capability_key_id", "capability key ID must use the canonical cap_<32 lowercase hex> form"); + } + const url = new URL(request.url); + const namespace = validatePackageIdent(url.searchParams.get("namespace") ?? "", "namespace"); + const name = validatePackageIdent(url.searchParams.get("name") ?? "", "name"); + const capability = await store.getCapability(keyId); + if (!capability) { + throw new ApiError(404, "capability_not_found", "capability key is not known to the registry"); + } + + const namespaceRecord = await store.getNamespace(namespace); + const revoked = Boolean(capability.revoked_at); + const expiry = new Date(capability.expires_at).getTime(); + const invalidExpiry = !Number.isFinite(expiry); + const expired = invalidExpiry || expiry <= now.getTime(); + const active = !revoked && !expired; + const ownsNamespace = Boolean( + namespaceRecord + && namespaceRecord.owner_principal_type === capability.principal_type + && namespaceRecord.owner_principal_id === capability.principal_id, + ); + const namespaceActive = namespaceRecord?.status === "active"; + const allows = { + publish: scopeAllows(capability.scopes, "publish", namespace, name), + deployment: scopeAllows(capability.scopes, "deployment", namespace, name), + availability: scopeAllows(capability.scopes, "availability", namespace, name), + }; + const reasons = []; + if (revoked) reasons.push("capability_revoked"); + else if (invalidExpiry) reasons.push("capability_expiry_invalid"); + else if (expired) reasons.push("capability_expired"); + if (!allows.publish) reasons.push("publish_scope_missing"); + if (!namespaceRecord) reasons.push("namespace_not_claimed"); + else { + if (!namespaceActive) reasons.push("namespace_not_active"); + if (!ownsNamespace) reasons.push("namespace_owner_mismatch"); + } + + return json( + { + schema: "cellscript-registry-capability-check-v1", + request_id: requestId, + key_id: capability.key_id, + principal_type: capability.principal_type, + scopes: capability.scopes, + expires_at: capability.expires_at, + status: revoked ? "revoked" : expired ? "expired" : "active", + namespace: { + name: namespace, + status: namespaceRecord?.status ?? "unclaimed", + owned_by_capability_principal: ownsNamespace, + }, + artifact: { namespace, name }, + allows, + usable_for_publish: active && allows.publish && namespaceActive && ownsNamespace, + reasons, + }, + 200, + headers, + ); +} + async function handleRevokeCapability( request: Request, env: Env, @@ -530,12 +3356,12 @@ async function handleRevokeCapability( throw new ApiError(404, "capability_not_found", "capability key is not known to the registry"); } if (capability.principal_type !== payload.principal_type || capability.principal_id !== payload.principal_id) { - throw new ApiError(403, "capability_owner_mismatch", "JoyID principal does not own this capability"); + throw new ApiError(403, "capability_owner_mismatch", "wallet principal does not own this capability"); } - const signature = requireJoyidSignature(body["joyid_signature"]); - await verifyJoyidPayloadSignature(payload, signature, deps.joyidVerifier ?? productionJoyidVerifier()); + const signature = requirePrincipalSignature(body, payload.principal_type); + await verifyPrincipalPayloadSignature(payload, signature, deps.joyidVerifier ?? productionJoyidVerifier()); await throttle(store, requestId, `principal:${payload.principal_type}:${payload.principal_id}`, "capability_revoke", 8, 60 * 60, now); - await consumeSignedNonce(store, requestId, { + const nonceKey = await consumeSignedNonce(store, requestId, { protocol: payload.protocol, action: payload.action, nonce: payload.nonce, @@ -545,13 +3371,19 @@ async function handleRevokeCapability( capability_key_id: capability.key_id, }); const reason = typeof body["reason"] === "string" ? body["reason"] : undefined; - const revoked = await store.revokeCapability({ - key_id: capability.key_id, - principal_type: payload.principal_type, - principal_id: payload.principal_id, - request_id: requestId, - ...(reason ? { reason } : {}), - }); + let revoked; + try { + revoked = await store.revokeCapability({ + key_id: capability.key_id, + principal_type: payload.principal_type, + principal_id: payload.principal_id, + request_id: requestId, + ...(reason ? { reason } : {}), + }); + } catch (error) { + await store.releaseNonce({ nonce_key: nonceKey, request_id: requestId }); + throw error; + } return json( { request_id: requestId, @@ -579,6 +3411,7 @@ async function handlePublishVersion( namespaceFromPath: string, nameFromPath: string, ): Promise { + const runtime = registryRuntimeConfig(env); await throttleRequestSource(store, request, requestId, "publish", 80, 60 * 60, now); const body = await readJson(request, maxJsonBytes(env)); const payload = validatePublishPayload(body["payload"], registryOrigin, now); @@ -605,8 +3438,8 @@ async function handlePublishVersion( if (new Date(capability.expires_at).getTime() <= now.getTime()) { throw new ApiError(401, "capability_expired", "capability key has expired"); } - if (!scopeAllowsPublish(capability.scopes, payload.namespace, payload.name)) { - throw new ApiError(403, "capability_scope_denied", "capability scope does not allow this package publish"); + if (!scopeAllows(capability.scopes, "publish", payload.namespace, payload.name)) { + throw new ApiError(403, "capability_scope_denied", "capability scope does not allow this artifact publish"); } const namespace = await store.getNamespace(payload.namespace); if (!namespace) { @@ -625,9 +3458,13 @@ async function handlePublishVersion( throw new ApiError(401, "capability_signature_invalid", "capability signature verification failed"); } await throttle(store, requestId, `capability:${capability.key_id}`, "publish", 60, 60 * 60, now); - await throttle(store, requestId, `package:${payload.namespace}/${payload.name}`, "publish", 12, 60 * 60, now); + await throttle(store, requestId, `artifact:${payload.namespace}/${payload.name}`, "publish", 12, 60 * 60, now); + if (runtime.environment === "testnet-sandbox") { + await throttle(store, requestId, `sandbox-principal:${capability.principal_type}:${capability.principal_id}`, "sandbox_publish", 20, 24 * 60 * 60, now); + await throttle(store, requestId, `sandbox-artifact:${payload.namespace}/${payload.name}`, "sandbox_publish", 5, 24 * 60 * 60, now); + } if (await store.getPackageVersion(payload.namespace, payload.name, payload.version)) { - throw new ApiError(409, "package_version_exists", "package version already exists and cannot be overwritten"); + throw new ApiError(409, "artifact_release_exists", "artifact release already exists and cannot be overwritten"); } let idempotencyReserved = false; if (idempotencyKey) { @@ -649,8 +3486,9 @@ async function handlePublishVersion( idempotencyReserved = true; } + let consumedNonceKey: string | undefined; try { - await consumeSignedNonce(store, requestId, { + consumedNonceKey = await consumeSignedNonce(store, requestId, { protocol: payload.protocol, action: payload.action, nonce: payload.nonce, @@ -662,21 +3500,36 @@ async function handlePublishVersion( const snapshotRecord = await writeSnapshot(env, deps, payload.namespace, payload.name, payload.version, snapshot); const sourceRepo = typeof payload.registry_entry["repository"] === "string" ? payload.registry_entry["repository"] : undefined; - await store.ensurePackage({ + const packageInput = { namespace: payload.namespace, name: payload.name, principal_type: capability.principal_type, principal_id: capability.principal_id, ...(sourceRepo ? { source_repo: sourceRepo } : {}), request_id: requestId, - }); + }; const directUrl = staticPackageVersionUrl(staticOrigin, payload.namespace, payload.name, payload.version); + const publishedRegistryVersion = payload.registry_entry.versions[0]; + const states = initialArtifactStates(payload.artifact); + const expiresAt = runtime.record_ttl_hours === null + ? null + : new Date(now.getTime() + runtime.record_ttl_hours * 60 * 60 * 1000).toISOString(); + const purgeAfter = expiresAt === null || runtime.object_purge_grace_hours === null + ? null + : new Date(Date.parse(expiresAt) + runtime.object_purge_grace_hours * 60 * 60 * 1000).toISOString(); const versionInput = { namespace: payload.namespace, name: payload.name, version: payload.version, status: "source_published", + artifact: payload.artifact, + ...states, source_hash: payload.source_hash, + manifest_hash: payload.manifest_hash, + ...(publishedRegistryVersion.edition ? { edition: publishedRegistryVersion.edition } : {}), + ...(publishedRegistryVersion.compatibility_profile_hash + ? { compatibility_profile_hash: publishedRegistryVersion.compatibility_profile_hash } + : {}), capability_key_id: capability.key_id, principal_type: capability.principal_type, principal_id: capability.principal_id, @@ -684,12 +3537,12 @@ async function handlePublishVersion( snapshot_hash: snapshotRecord.snapshot_hash, direct_url: directUrl, created_at: now.toISOString(), + registry_environment: runtime.environment, + network: runtime.network, + expires_at: expiresAt, + purge_after: purgeAfter, } as const; - const version = payload.manifest_hash ? { ...versionInput, manifest_hash: payload.manifest_hash } : versionInput; - await writeStaticRegistryVersionObject(env, deps, version); - await store.recordSnapshot(snapshotRecord); - const recordedVersion = await store.recordPackageVersion(version); - await store.recordCapabilityUsage({ + const capabilityUsage = { key_id: capability.key_id, principal_type: capability.principal_type, principal_id: capability.principal_id, @@ -698,10 +3551,10 @@ async function handlePublishVersion( namespace: payload.namespace, name: payload.name, version: payload.version, - }); + }; const ipHash = await requestIpHash(request); const userAgent = request.headers.get("user-agent") ?? undefined; - await store.appendAuditEvent({ + const auditEvent = { request_id: requestId, event_type: "publish.accepted", principal_type: capability.principal_type, @@ -712,25 +3565,51 @@ async function handlePublishVersion( version: payload.version, ...(ipHash ? { ip_hash: ipHash } : {}), ...(userAgent ? { user_agent: userAgent } : {}), - data: { status: recordedVersion.status, snapshot_hash: snapshotRecord.snapshot_hash, direct_url: directUrl }, - }); + data: { artifact: payload.artifact, ...states, snapshot_hash: snapshotRecord.snapshot_hash, direct_url: directUrl }, + }; const responseBody = { request_id: requestId, - status: recordedVersion.status, + artifact: payload.artifact, + ...states, direct_url: directUrl, snapshot_hash: snapshotRecord.snapshot_hash, verification: "queued", + registry_environment: runtime.environment, + network: runtime.network, + expires_at: expiresAt, + purge_after: purgeAfter, }; - if (idempotencyKey) { - await store.completeIdempotencyKey({ - key: idempotencyKey, - request_hash: requestHash, - response_status: 202, - response_body: responseBody, - }); - } + await store.admitPackageVersion({ + package: packageInput, + snapshot: snapshotRecord, + version: versionInput, + capability_usage: capabilityUsage, + audit_event: auditEvent, + ...(idempotencyKey + ? { + idempotency: { + key: idempotencyKey, + request_hash: requestHash, + response_status: 202, + response_body: responseBody, + }, + } + : {}), + }); + await tryWriteStaticRegistryVersionObject( + env, + deps, + store, + requestId, + versionInput, + snapshotRecord, + staticOrigin, + ); return json(responseBody, 202, headers); } catch (error) { + if (consumedNonceKey) { + await store.releaseNonce({ nonce_key: consumedNonceKey, request_id: requestId }); + } if (idempotencyKey && idempotencyReserved) { await store.releaseProcessingIdempotencyKey({ key: idempotencyKey, request_hash: requestHash }); } @@ -740,7 +3619,7 @@ async function handlePublishVersion( async function publishRequestHash(payload: unknown, signature: CapabilitySignature, snapshot: SourceSnapshotInput): Promise { return sha256Hex(canonicalJson({ - route: "publish_package_version", + route: "publish_artifact_release", payload, capability_signature: signature, source_snapshot: snapshot, @@ -786,19 +3665,17 @@ function idempotencyResponse(record: IdempotencyRecord, headers: Headers): Respo return json(record.response_body, record.response_status, replayHeaders); } -async function consumeSignedNonce( - store: RegistryStore, - requestId: string, - input: { - protocol: string; - action: string; - nonce: string; - expires_at: string; - principal_type?: string; - principal_id?: string; - capability_key_id?: string; - }, -): Promise { +type SignedNonceUseSource = { + protocol: string; + action: string; + nonce: string; + expires_at: string; + principal_type?: PrincipalType; + principal_id?: string; + capability_key_id?: string; +}; + +async function signedNonceUse(requestId: string, input: SignedNonceUseSource) { const nonceKey = `nonce_${await sha256Hex(canonicalJson({ protocol: input.protocol, action: input.action, @@ -807,7 +3684,7 @@ async function consumeSignedNonce( principal_id: input.principal_id ?? null, capability_key_id: input.capability_key_id ?? null, }))}`; - const accepted = await store.consumeNonce({ + return { nonce_key: nonceKey, protocol: input.protocol, action: input.action, @@ -817,7 +3694,16 @@ async function consumeSignedNonce( ...(input.principal_type ? { principal_type: input.principal_type } : {}), ...(input.principal_id ? { principal_id: input.principal_id } : {}), ...(input.capability_key_id ? { capability_key_id: input.capability_key_id } : {}), - }); + }; +} + +async function consumeSignedNonce( + store: RegistryStore, + requestId: string, + input: SignedNonceUseSource, +): Promise { + const nonceUse = await signedNonceUse(requestId, input); + const accepted = await store.consumeNonce(nonceUse); if (!accepted) { await store.appendAuditEvent({ request_id: requestId, @@ -828,20 +3714,24 @@ async function consumeSignedNonce( data: { protocol: input.protocol, action: input.action, - nonce_key: nonceKey, + nonce_key: nonceUse.nonce_key, }, }); throw new ApiError(409, "nonce_replay", "signed nonce has already been used"); } + return nonceUse.nonce_key; } async function writeStaticRegistryVersionObject( env: Env, deps: AppDeps, version: SnapshotPackageVersionRecord, + snapshot: SnapshotRecord, + staticOrigin: string, + evidence: PackageEvidenceRecord[] = [], ): Promise { const key = staticPackageVersionKey(version.namespace, version.name, version.version); - const body = new TextEncoder().encode(`${JSON.stringify(staticRegistryVersionPayload(version), null, 2)}\n`); + const body = new TextEncoder().encode(`${JSON.stringify(staticRegistryVersionPayload(version, snapshot, staticOrigin, evidence), null, 2)}\n`); const writer = deps.snapshotWriter ?? r2SnapshotWriter(env); await writer.put(key, body, { contentType: "application/json; charset=utf-8", @@ -856,35 +3746,168 @@ async function writeStaticRegistryVersionObject( }); } +async function tryWriteStaticRegistryVersionObject( + env: Env, + deps: AppDeps, + store: RegistryStore, + requestId: string, + version: SnapshotPackageVersionRecord, + snapshot: SnapshotRecord, + staticOrigin: string, + evidence: PackageEvidenceRecord[] = [], +): Promise { + try { + await writeStaticRegistryVersionObject(env, deps, version, snapshot, staticOrigin, evidence); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + await store.requestStaticSync({ + namespace: version.namespace, + name: version.name, + version: version.version, + error_message: errorMessage, + }).catch(() => undefined); + await store.appendAuditEvent({ + request_id: requestId, + event_type: "static_registry.sync_deferred", + principal_type: version.principal_type, + principal_id: version.principal_id, + capability_key_id: version.capability_key_id, + namespace: version.namespace, + name: version.name, + version: version.version, + data: { error: errorMessage }, + }).catch(() => undefined); + } +} + +export async function syncStaticRegistryVersionObject( + env: Env, + deps: Pick, + store: RegistryStore, + version: PackageVersionRecord, + staticOrigin: string, +): Promise { + const snapshot = await requireSnapshot(store, version); + const evidence = await store.listPackageEvidence(version.namespace, version.name, version.version); + await writeStaticRegistryVersionObject( + env, + deps, + { ...version, direct_url: staticPackageVersionUrl(staticOrigin, version.namespace, version.name, version.version) }, + snapshot, + staticOrigin, + evidence, + ); +} + type SnapshotPackageVersionRecord = Awaited>; -function staticRegistryVersionPayload(version: SnapshotPackageVersionRecord): Record { +function staticRegistryVersionPayload( + version: SnapshotPackageVersionRecord, + snapshot: SnapshotRecord, + staticOrigin: string, + evidence: PackageEvidenceRecord[] = [], +): Record { + const signedRelease = version.registry_entry.versions.find((entry) => entry.version === version.version); + if (!signedRelease) { + throw new ApiError(500, "registry_release_identity_missing", "signed Registry release identity is missing"); + } return { - schema_version: 1, - kind: "cellscript.registry.package_version", + schema_version: REGISTRY_SCHEMA_VERSION, + kind: "cellscript.registry.artifact_release", coordinate: `${version.namespace}/${version.name}@${version.version}`, namespace: version.namespace, name: version.name, - version: version.version, - status: version.status, + release: version.version, + artifact: version.artifact, + verification_status: version.verification_status, + deployment_status: version.deployment_status, + availability_status: version.availability_status, source_hash: version.source_hash, - ...(version.manifest_hash ? { manifest_hash: version.manifest_hash } : {}), + manifest_hash: version.manifest_hash, + ...(signedRelease.artifact_hash ? { artifact_hash: signedRelease.artifact_hash } : {}), + ...(signedRelease.abi_hash ? { abi_hash: signedRelease.abi_hash } : {}), + ...(signedRelease.build_recipe_hash ? { build_recipe_hash: signedRelease.build_recipe_hash } : {}), + ...(signedRelease.profile_contract ? { profile_contract: signedRelease.profile_contract } : {}), + ...(releaseLsIdlInterface(version) ? { interface: releaseLsIdlInterface(version) } : {}), + ...(version.edition ? { edition: version.edition } : {}), + ...(version.compatibility_profile_hash ? { compatibility_profile_hash: version.compatibility_profile_hash } : {}), capability_key_id: version.capability_key_id, principal_type: version.principal_type, principal_id: version.principal_id, registry_entry: version.registry_entry, snapshot_hash: version.snapshot_hash, + immutable_bundle: sourceSnapshotPayload(snapshot, staticOrigin), direct_url: version.direct_url, created_at: version.created_at, + registry_environment: version.registry_environment ?? "production", + network: version.network ?? "mainnet", + ...(version.expires_at ? { expires_at: version.expires_at } : {}), + ...(version.purge_after ? { purge_after: version.purge_after } : {}), + evidence, + }; +} + +function releaseLsIdlInterface(version: PackageVersionRecord): Record | null { + const signedRelease = version.registry_entry.versions.find((entry) => entry.version === version.version); + const interfaceContract = signedRelease?.profile_contract?.["interface"]; + if (!interfaceContract || typeof interfaceContract !== "object" || Array.isArray(interfaceContract)) return null; + const value = interfaceContract as Record; + if (value["format"] !== "ls-idl") return null; + return { + schema: value["schema"], + format: "ls-idl", + format_version: value["format_version"], + content_type: value["content_type"], + encoding: value["encoding"], + commitment: value["commitment"], + }; +} + +async function requireSnapshot(store: RegistryStore, version: SnapshotPackageVersionRecord): Promise { + const snapshot = await store.getSnapshot(version.snapshot_hash); + if (!snapshot || snapshot.source_hash !== version.source_hash) { + throw new ApiError(503, "source_snapshot_unavailable", "package source snapshot metadata is unavailable or inconsistent"); + } + return snapshot; +} + +async function requireSnapshots( + store: RegistryStore, + versions: SnapshotPackageVersionRecord[], +): Promise> { + const snapshots = await store.getSnapshots(versions.map((version) => version.snapshot_hash)); + for (const version of versions) snapshotForVersion(snapshots, version); + return snapshots; +} + +function snapshotForVersion( + snapshots: Map, + version: SnapshotPackageVersionRecord, +): SnapshotRecord { + const snapshot = snapshots.get(version.snapshot_hash); + if (!snapshot || snapshot.source_hash !== version.source_hash) { + throw new ApiError(503, "source_snapshot_unavailable", "package source snapshot metadata is unavailable or inconsistent"); + } + return snapshot; +} + +function sourceSnapshotPayload(snapshot: SnapshotRecord, staticOrigin: string): Record { + return { + schema: "cellscript-registry-immutable-bundle", + url: `${staticOrigin.replace(/\/+$/, "")}/${snapshot.r2_key}`, + snapshot_hash: snapshot.snapshot_hash, + source_hash: snapshot.source_hash, + size_bytes: snapshot.size_bytes, + content_type: snapshot.content_type, }; } function staticPackageVersionKey(namespace: string, name: string, version: string): string { - return `packages/${namespace}/${name}/versions/${version}.json`; + return `artifacts/${namespace}/${name}/releases/${version}.json`; } function staticPackageVersionUrl(staticOrigin: string, namespace: string, name: string, version: string): string { - return `${staticOrigin.replace(/\/+$/, "")}/packages/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/versions/${encodeURIComponent(version)}.json`; + return `${staticOrigin.replace(/\/+$/, "")}/artifacts/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/releases/${encodeURIComponent(version)}.json`; } async function writeSnapshot( @@ -941,6 +3964,9 @@ function r2SnapshotWriter(env: Env): SnapshotWriter { customMetadata: options.metadata, }); }, + async delete(key) { + await bucket.delete(key); + }, }; } @@ -1102,11 +4128,31 @@ async function readJson(request: Request, maxBytes: number): Promise, principalType: PrincipalType): PrincipalSignature { + const value = body["wallet_signature"] ?? body["joyid_signature"]; + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new ApiError(400, "missing_wallet_signature", "wallet_signature is required"); + } + if (principalType === JOYID_PRINCIPAL_TYPE) { + return value as SignChallengeResponseData; + } + if (principalType !== CKB_SECP256K1_PRINCIPAL_TYPE) { + throw new ApiError(400, "unsupported_principal_type", "wallet principal type is unsupported"); + } + const signature = value as Record; + if ( + signature["scheme"] !== CKB_SECP256K1_PRINCIPAL_TYPE + || typeof signature["challenge"] !== "string" + || typeof signature["signature"] !== "string" + || typeof signature["public_key"] !== "string" + ) { + throw new ApiError( + 400, + "invalid_wallet_signature", + "ckb_secp256k1 wallet_signature must include scheme, challenge, signature, and public_key", + ); } - return value as SignChallengeResponseData; + return signature as unknown as CkbSecp256k1Signature; } function requireCapabilitySignature(value: unknown): CapabilitySignature { @@ -1121,21 +4167,36 @@ function requireCapabilitySignature(value: unknown): CapabilitySignature { return { algorithm, signature }; } -function requireAdminActor(request: Request, env: Env): string { - const expected = env.REGISTRY_ADMIN_TOKEN; - if (!expected || expected.trim() === "") { +async function requireAdminActor(request: Request, env: Env): Promise { + const expected = env.REGISTRY_ADMIN_TOKEN?.trim(); + if (!expected) { throw new ApiError(503, "admin_unconfigured", "REGISTRY_ADMIN_TOKEN must be configured for admin operations"); } const auth = request.headers.get("authorization") ?? ""; const bearer = auth.match(/^Bearer\s+(.+)$/i)?.[1]?.trim(); - const supplied = bearer || request.headers.get("x-registry-admin-token")?.trim(); - if (supplied !== expected) { + const supplied = bearer || request.headers.get("x-registry-admin-token")?.trim() || ""; + if (!(await constantTimeSecretEqual(supplied, expected))) { throw new ApiError(401, "admin_unauthorized", "admin token is missing or invalid"); } const actor = request.headers.get("x-registry-admin-actor")?.trim(); return actor && actor.length <= 128 ? actor : "registry-admin"; } +async function constantTimeSecretEqual(left: string, right: string): Promise { + const encoder = new TextEncoder(); + const [leftDigest, rightDigest] = await Promise.all([ + crypto.subtle.digest("SHA-256", encoder.encode(left)), + crypto.subtle.digest("SHA-256", encoder.encode(right)), + ]); + const leftBytes = new Uint8Array(leftDigest); + const rightBytes = new Uint8Array(rightDigest); + let mismatch = 0; + for (let index = 0; index < leftBytes.length; index += 1) { + mismatch |= leftBytes[index]! ^ rightBytes[index]!; + } + return mismatch === 0; +} + function requireNonEmptyAdminString(value: unknown, field: string): string { if (typeof value !== "string" || value.trim() === "") { throw new ApiError(400, "invalid_admin_field", `${field} is required`); @@ -1143,6 +4204,13 @@ function requireNonEmptyAdminString(value: unknown, field: string): string { return value.trim(); } +function requireUuid(value: string, field: string): string { + if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) { + throw new ApiError(400, `invalid_${field}`, `${field} must be a UUID`); + } + return value.toLowerCase(); +} + function requireOneOf(value: string, allowed: T, code: string): T[number] { if (!allowed.includes(value)) { throw new ApiError(400, code, `value must be one of: ${allowed.join(", ")}`); @@ -1181,6 +4249,400 @@ function parseAuditBefore(value: string): string { return date.toISOString(); } +function optionalPublicQuery(params: URLSearchParams, name: string): string | undefined { + const value = params.get(name)?.trim(); + if (!value) return undefined; + if (value.length > 160 || /[\u0000-\u001f\u007f]/.test(value)) { + throw new ApiError(400, "invalid_public_query", `${name} query parameter is invalid`); + } + return value; +} + +function publicListInteger(params: URLSearchParams, name: string, fallback: number, minimum: number, maximum: number): number { + const value = params.get(name); + if (value === null) return fallback; + const parsed = Number(value); + if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) { + throw new ApiError(400, "invalid_public_query", `${name} must be an integer between ${minimum} and ${maximum}`); + } + return parsed; +} + +export function validatePromotionEvidence( + value: unknown, + kind: PackageEvidenceKind, + version: PackageVersionRecord, + previous: PackageEvidenceRecord[], + expectedNetwork: DeploymentPayload["network"] = "mainnet", +): Record { + const evidence = assertPlainObject(value, "invalid_promotion_evidence"); + if (evidence["schema"] !== "cellscript-registry-evidence") { + throw new ApiError(400, "invalid_evidence_schema", "evidence.schema must be cellscript-registry-evidence"); + } + if (evidence["kind"] !== kind) { + throw new ApiError(400, "evidence_kind_mismatch", "evidence.kind must match the requested promotion kind"); + } + requireEvidenceString(evidence, "producer", 1, 200); + requireEvidenceTimestamp(evidence, "generated_at"); + if (evidence["verification_status"] !== "passed") { + throw new ApiError(400, "evidence_not_passed", "evidence.verification_status must be passed"); + } + requireMatchingEvidenceHash(evidence, "source_hash", version.source_hash); + requireMatchingEvidenceHash(evidence, "manifest_hash", version.manifest_hash); + if (version.compatibility_profile_hash) { + requireMatchingEvidenceHash(evidence, "compatibility_profile_hash", version.compatibility_profile_hash); + } + + if (kind === "verified_build") { + const level = requireEvidenceString(evidence, "verification_level", 1, 80); + if (!(["compiled", "hash_bound", "evidence_required", "structurally_verified"] as const).includes(level as any)) { + throw new ApiError(400, "invalid_verification_level", "verification_level is not recognised"); + } + if (version.artifact.profile !== "copy_material") { + const artifactHash = requireEvidenceHash(evidence, "artifact_hash"); + const signedRelease = version.registry_entry.versions.find((entry) => entry.version === version.version); + if (signedRelease?.artifact_hash && !sameHash(artifactHash, signedRelease.artifact_hash)) { + throw new ApiError(400, "verified_artifact_mismatch", "verified-build artifact_hash must match the signed Registry release"); + } + } + requireEvidenceHash(evidence, "metadata_hash"); + if (version.artifact.profile === "cellscript_source") requireEvidenceString(evidence, "compiler_version", 1, 80); + if (level === "structurally_verified") { + requireEvidenceString(evidence, "checker_version", 1, 80); + requireEvidenceString(evidence, "checker_policy_schema", 1, 120); + requireEvidenceHash(evidence, "checker_report_hash"); + } + } else if (kind === "reproduced_build") { + const verified = latestEvidence(previous, "verified_build"); + requireEvidenceReference(evidence, "verified_build_evidence_hash", verified); + if (!packageVersionRequiresReproduction(version)) { + throw new ApiError(409, "reproduction_not_applicable", "this artifact does not declare a reproducible build contract"); + } + if (requireEvidenceString(evidence, "verification_level", 1, 80) !== "reproduced") { + throw new ApiError(400, "invalid_verification_level", "reproduced-build evidence must use verification_level reproduced"); + } + const signedRelease = version.registry_entry.versions.find((entry) => entry.version === version.version); + const expectedArtifactHash = signedRelease?.artifact_hash; + const expectedRecipeHash = signedRelease?.build_recipe_hash; + if (!expectedArtifactHash || !expectedRecipeHash) { + throw new ApiError(500, "reproduction_contract_incomplete", "signed reproducible release is missing artifact or build-recipe identity"); + } + requireMatchingEvidenceHash(evidence, "artifact_hash", expectedArtifactHash); + requireMatchingEvidenceHash(evidence, "build_recipe_hash", expectedRecipeHash); + const verifiedArtifactHash = requireEvidenceHash(verified.evidence, "artifact_hash"); + if (!sameHash(verifiedArtifactHash, expectedArtifactHash)) { + throw new ApiError(409, "verified_artifact_mismatch", "accepted build evidence does not match the signed reproducible artifact"); + } + validateReproductionReports(evidence, version, expectedArtifactHash, expectedRecipeHash); + } else if (kind === "deployed") { + const verified = latestBuildEvidence(previous, version); + requireEvidenceReference(evidence, "verified_build_evidence_hash", verified); + const artifactHash = requireEvidenceHash(evidence, "artifact_hash"); + const verifiedArtifact = requireEvidenceHash(verified.evidence, "artifact_hash"); + if (!sameHash(artifactHash, verifiedArtifact)) { + throw new ApiError(400, "deployment_artifact_mismatch", "deployed artifact_hash must match verified-build evidence"); + } + if (requireEvidenceString(evidence, "network", 1, 80) !== expectedNetwork) { + throw new ApiError(400, "unsupported_deployment_network", `Registry deployment evidence must use ${expectedNetwork}`); + } + const codeHash = requireEvidenceHash(evidence, "code_hash"); + const dataHash = requireEvidenceHash(evidence, "data_hash"); + if (!sameHash(dataHash, artifactHash)) { + throw new ApiError(400, "deployment_data_hash_mismatch", "deployed data_hash must match the verified executable artifact_hash"); + } + const hashType = requireEvidenceString(evidence, "hash_type", 1, 16); + if (!("data data1 data2 type".split(" ").includes(hashType))) { + throw new ApiError(400, "invalid_deployment_hash_type", "evidence.hash_type is not recognised"); + } + if (hashType !== "type" && !sameHash(codeHash, dataHash)) { + throw new ApiError(400, "deployment_code_hash_mismatch", "data hash deployments must use the executable data hash as code_hash"); + } + const depType = requireEvidenceString(evidence, "dep_type", 1, 16); + if (!("code dep_group".split(" ").includes(depType))) { + throw new ApiError(400, "invalid_deployment_dep_type", "evidence.dep_type is not recognised"); + } + requireDeploymentProfileContract(version, hashType, depType); + const outPoint = assertPlainObject(evidence["out_point"], "invalid_deployment_out_point"); + requireEvidenceHash(outPoint, "tx_hash"); + const index = outPoint["index"]; + if (!Number.isSafeInteger(index) || Number(index) < 0 || Number(index) > 0xffff_ffff) { + throw new ApiError(400, "invalid_deployment_out_point", "evidence.out_point.index must be a non-negative u32 integer"); + } + if (evidence["deployment_status"] !== "live") { + throw new ApiError(400, "deployment_not_live", "evidence.deployment_status must be live"); + } + } else { + const deployed = latestEvidence(previous, "deployed"); + requireEvidenceReference(evidence, "deployed_evidence_hash", deployed); + if (requireEvidenceString(evidence, "network", 1, 80) !== expectedNetwork) { + throw new ApiError(400, "unsupported_commitment_network", `Registry commitments must use ${expectedNetwork}`); + } + requireEvidenceHash(evidence, "commitment_tx_hash"); + requireEvidenceHash(evidence, "commitment_hash"); + requireEvidenceHash(evidence, "commitment_lock_hash"); + requireEvidenceHash(evidence, "registry_type_hash"); + const outPoint = assertPlainObject(evidence["commitment_out_point"], "invalid_commitment_out_point"); + const txHash = requireEvidenceHash(outPoint, "tx_hash"); + if (!sameHash(txHash, requireEvidenceHash(evidence, "commitment_tx_hash"))) { + throw new ApiError(400, "commitment_out_point_mismatch", "commitment_out_point.tx_hash must match commitment_tx_hash"); + } + const outputIndex = outPoint["index"]; + if (!Number.isSafeInteger(outputIndex) || Number(outputIndex) < 0 || Number(outputIndex) > 0xffff_ffff) { + throw new ApiError(400, "invalid_commitment_out_point", "commitment_out_point.index must be a non-negative u32 integer"); + } + requireEvidenceTimestamp(evidence, "observed_at"); + if (evidence["commitment_status"] !== "confirmed") { + throw new ApiError(400, "commitment_not_confirmed", "evidence.commitment_status must be confirmed"); + } + } + return evidence; +} + +function requireDeploymentProfileContract( + version: PackageVersionRecord, + hashType: string, + depType: string, +): void { + const signedRelease = version.registry_entry.versions.find((entry) => entry.version === version.version); + const profileContract = signedRelease?.profile_contract; + const ckb = profileContract && typeof profileContract === "object" && !Array.isArray(profileContract) + ? (profileContract as Record)["ckb"] + : undefined; + if (!ckb || typeof ckb !== "object" || Array.isArray(ckb)) { + throw new ApiError(500, "deployment_profile_contract_missing", "signed executable release has no CKB deployment contract"); + } + const contract = ckb as Record; + if (contract["hash_type"] !== hashType) { + throw new ApiError(400, "deployment_hash_type_contract_mismatch", "deployment hash_type does not match the signed profile contract"); + } + if (contract["dep_type"] !== depType) { + throw new ApiError(400, "deployment_dep_type_contract_mismatch", "deployment dep_type does not match the signed profile contract"); + } +} + +function latestEvidence(records: PackageEvidenceRecord[], kind: PackageEvidenceKind): PackageEvidenceRecord { + const record = records.filter((item) => item.kind === kind).at(-1); + if (!record) { + throw new ApiError(409, "evidence_dependency_missing", `${kind} evidence must exist before this promotion`); + } + return record; +} + +function latestBuildEvidence(records: PackageEvidenceRecord[], version: PackageVersionRecord): PackageEvidenceRecord { + if (packageVersionRequiresReproduction(version)) return latestEvidence(records, "reproduced_build"); + return latestEvidence(records, "verified_build"); +} + +function validateReproductionReports( + evidence: Record, + version: PackageVersionRecord, + expectedArtifactHash: string, + expectedRecipeHash: string, +): void { + const minimum = evidence["minimum_reproducers"]; + if (!Number.isSafeInteger(minimum) || Number(minimum) < 2 || Number(minimum) > 16) { + throw new ApiError(400, "invalid_reproducer_threshold", "minimum_reproducers must be an integer between 2 and 16"); + } + const reports = evidence["reproducers"]; + if (!Array.isArray(reports) || reports.length < Number(minimum) || reports.length > 16) { + throw new ApiError(400, "insufficient_reproduction_evidence", "reproducers must contain the declared number of independent reports (maximum 16)"); + } + const signedRelease = version.registry_entry.versions.find((entry) => entry.version === version.version); + const reproduction = signedRelease?.profile_contract?.["reproduction"]; + const expectedEnvironment = reproduction && typeof reproduction === "object" && !Array.isArray(reproduction) + ? (reproduction as Record)["environment"] + : undefined; + const builderIds = new Set(); + for (const rawReport of reports) { + const report = assertPlainObject(rawReport, "invalid_reproduction_report"); + if (report["schema"] !== "cellscript-reproduction-report-v2") { + throw new ApiError(400, "invalid_reproduction_report", "each reproducer report must use schema cellscript-reproduction-report-v2"); + } + const builderId = requireEvidenceString(report, "builder_id", 1, 200); + if (builderIds.has(builderId)) { + throw new ApiError(400, "duplicate_reproducer", "reproducer reports must use distinct builder_id values"); + } + builderIds.add(builderId); + requireEvidenceString(report, "trust_domain", 1, 200); + const builderPublicKey = requireEvidenceString(report, "builder_public_key", 32, 2_000); + if (!builderPublicKey.startsWith("p256-spki:")) { + throw new ApiError(400, "invalid_reproducer_public_key", "reproducer builder_public_key must use p256-spki"); + } + const environment = requireEvidenceString(report, "environment", 1, 500); + if (typeof expectedEnvironment !== "string" || environment !== expectedEnvironment) { + throw new ApiError(400, "reproduction_environment_mismatch", "reproducer environment must match the signed reproduction contract"); + } + requireMatchingEvidenceHash(report, "source_hash", version.source_hash); + requireMatchingEvidenceHash(report, "build_recipe_hash", expectedRecipeHash); + requireMatchingEvidenceHash(report, "artifact_hash", expectedArtifactHash); + requireEvidenceHash(report, "build_log_hash"); + requireEvidenceTimestamp(report, "generated_at"); + const signature = assertPlainObject(report["signature"], "invalid_reproduction_signature"); + if (signature["algorithm"] !== "p256-sha256") { + throw new ApiError(400, "invalid_reproduction_signature", "reproducer signature.algorithm must be p256-sha256"); + } + requireEvidenceString(signature, "signature", 32, 2_000); + } +} + +interface ReproducerPolicyBuilder { + builder_id: string; + trust_domain: string; + public_key: string; +} + +interface ReproducerPolicy { + minimum_trust_domains: number; + builders: Map; +} + +function registryReproducerPolicy(env: Env, required: boolean): ReproducerPolicy | null { + const raw = env.REGISTRY_REPRODUCER_POLICY_JSON?.trim(); + if (!raw) { + if (required) { + throw new ApiError(503, "reproducer_policy_unconfigured", "signed reproduction evidence is disabled until a trusted builder policy is configured"); + } + return null; + } + const value = parseConfiguredJson(raw, "REGISTRY_REPRODUCER_POLICY_JSON"); + if (value["schema"] !== "cellscript-reproducer-policy-v1") { + throw new ApiError(503, "reproducer_policy_misconfigured", "reproducer policy schema must be cellscript-reproducer-policy-v1"); + } + const minimum = value["minimum_trust_domains"]; + if (!Number.isSafeInteger(minimum) || Number(minimum) < 2 || Number(minimum) > 16) { + throw new ApiError(503, "reproducer_policy_misconfigured", "minimum_trust_domains must be an integer between 2 and 16"); + } + if (!Array.isArray(value["builders"]) || value["builders"].length < Number(minimum) || value["builders"].length > 64) { + throw new ApiError(503, "reproducer_policy_misconfigured", "reproducer policy must contain enough trusted builders (maximum 64)"); + } + const builders = new Map(); + const publicKeys = new Set(); + const trustDomains = new Set(); + for (const rawBuilder of value["builders"]) { + const builder = assertPlainObject(rawBuilder, "reproducer_policy_misconfigured"); + const builderId = requireEvidenceString(builder, "builder_id", 1, 200); + const trustDomain = requireEvidenceString(builder, "trust_domain", 1, 200); + const publicKey = requireEvidenceString(builder, "public_key", 32, 2_000); + if (!isCanonicalP256SpkiPublicKey(publicKey) || builders.has(builderId) || publicKeys.has(publicKey)) { + throw new ApiError(503, "reproducer_policy_misconfigured", "trusted builders require unique ids and p256-spki public keys"); + } + builders.set(builderId, { builder_id: builderId, trust_domain: trustDomain, public_key: publicKey }); + publicKeys.add(publicKey); + trustDomains.add(trustDomain); + } + if (trustDomains.size < Number(minimum)) { + throw new ApiError(503, "reproducer_policy_misconfigured", "trusted builder policy does not span the required number of trust domains"); + } + return { minimum_trust_domains: Number(minimum), builders }; +} + +async function verifyAuthenticatedReproductionReports( + env: Env, + deps: AppDeps, + evidence: Record, +): Promise> { + const policy = registryReproducerPolicy(env, true)!; + const reports = evidence["reproducers"]; + if (!Array.isArray(reports)) { + throw new ApiError(400, "invalid_reproduction_report", "reproducers must be an array"); + } + const verifier = deps.capabilityVerifier ?? new WebCryptoP256Verifier(); + const trustDomains = new Set(); + const publicKeys = new Set(); + for (const rawReport of reports) { + const report = assertPlainObject(rawReport, "invalid_reproduction_report"); + const builderId = String(report["builder_id"]); + const trusted = policy.builders.get(builderId); + if (!trusted + || report["trust_domain"] !== trusted.trust_domain + || report["builder_public_key"] !== trusted.public_key) { + throw new ApiError(403, "untrusted_reproducer", `reproducer '${builderId}' is not an active trusted builder`); + } + if (publicKeys.has(trusted.public_key)) { + throw new ApiError(400, "duplicate_reproducer", "reproduction evidence repeats one trusted builder key"); + } + const signatureObject = assertPlainObject(report["signature"], "invalid_reproduction_signature"); + const signature = { + algorithm: signatureObject["algorithm"] as "p256-sha256", + signature: String(signatureObject["signature"]), + }; + const signedPayload = { ...report }; + delete signedPayload["signature"]; + if (!(await verifier.verify(canonicalJson(signedPayload), trusted.public_key, signature))) { + throw new ApiError(401, "reproduction_signature_invalid", `reproducer '${builderId}' signature verification failed`); + } + publicKeys.add(trusted.public_key); + trustDomains.add(trusted.trust_domain); + } + if (trustDomains.size < policy.minimum_trust_domains) { + throw new ApiError( + 409, + "insufficient_reproducer_trust_domains", + `reproduction evidence requires ${policy.minimum_trust_domains} independent trust domains`, + ); + } + const policyIdentity = { + schema: "cellscript-reproducer-policy-v1", + minimum_trust_domains: policy.minimum_trust_domains, + builders: [...policy.builders.values()].sort((left, right) => left.builder_id.localeCompare(right.builder_id)), + }; + return { + reproducer_policy: { + schema: "cellscript-reproducer-policy-acceptance-v1", + policy_hash: `sha256:${await sha256Hex(canonicalJson(policyIdentity))}`, + minimum_trust_domains: policy.minimum_trust_domains, + }, + }; +} + +function requireEvidenceReference(evidence: Record, key: string, expected: PackageEvidenceRecord): void { + const value = requireEvidenceString(evidence, key, 71, 71); + if (value !== expected.evidence_hash) { + throw new ApiError(400, "evidence_reference_mismatch", `${key} does not reference the accepted ${expected.kind} evidence`); + } +} + +function requireMatchingEvidenceHash(evidence: Record, key: string, expected: string): void { + const value = requireEvidenceHash(evidence, key); + if (!sameHash(value, expected)) { + throw new ApiError(400, "evidence_identity_mismatch", `evidence.${key} does not match the published package identity`); + } +} + +function requireEvidenceHash(evidence: Record, key: string): string { + const value = requireEvidenceString(evidence, key, 64, 66); + if (!/^(?:0x)?[0-9a-fA-F]{64}$/.test(value)) { + throw new ApiError(400, "invalid_evidence_hash", `evidence.${key} must be a 32-byte hex hash`); + } + return value; +} + +function sameHash(left: string, right: string): boolean { + return left.replace(/^0x/i, "").toLowerCase() === right.replace(/^0x/i, "").toLowerCase(); +} + +function requireEvidenceString( + evidence: Record, + key: string, + minimumLength: number, + maximumLength: number, +): string { + const value = evidence[key]; + if (typeof value !== "string" || value.trim() !== value || value.length < minimumLength || value.length > maximumLength) { + throw new ApiError(400, "invalid_evidence_field", `evidence.${key} is invalid`); + } + return value; +} + +function requireEvidenceTimestamp(evidence: Record, key: string): string { + const value = requireEvidenceString(evidence, key, 20, 40); + const timestamp = Date.parse(value); + if (!Number.isFinite(timestamp) || timestamp > Date.now() + 5 * 60 * 1000) { + throw new ApiError(400, "invalid_evidence_timestamp", `evidence.${key} must be a non-future ISO timestamp`); + } + return new Date(timestamp).toISOString(); +} + function maxJsonBytes(env: Env): number { return Number(env.MAX_JSON_BODY_BYTES ?? DEFAULT_MAX_JSON_BODY_BYTES); } @@ -1214,8 +4676,16 @@ function corsHeaders(requestId: string): Headers { return new Headers({ "access-control-allow-origin": "*", "access-control-allow-methods": "GET,POST,OPTIONS", - "access-control-allow-headers": "content-type,authorization,idempotency-key", - "access-control-expose-headers": "x-request-id,x-idempotency-status", + "access-control-allow-headers": "content-type,authorization,idempotency-key,x-registry-admin-token,x-registry-admin-actor", + "access-control-expose-headers": "x-request-id,x-idempotency-status,etag,x-ls-idl-format-version,x-ls-idl-sha256,x-ls-idl-coordinate,x-ls-idl-commitment,x-ls-idl-verification", + "cache-control": "no-store", + "content-security-policy": "default-src 'none'; base-uri 'none'; frame-ancestors 'none'", + "permissions-policy": "camera=(), geolocation=(), microphone=()", + "referrer-policy": "no-referrer", + "strict-transport-security": "max-age=31536000", + "x-content-type-options": "nosniff", + "x-frame-options": "DENY", + "x-permitted-cross-domain-policies": "none", "x-request-id": requestId, }); } diff --git a/services/registry-api/src/node-runtime-env.ts b/services/registry-api/src/node-runtime-env.ts new file mode 100644 index 00000000..e9119d30 --- /dev/null +++ b/services/registry-api/src/node-runtime-env.ts @@ -0,0 +1,26 @@ +import type { Env } from "./index"; + +type NodeCkbRpcEnv = Pick< + Env, + | "CKB_MAINNET_RPC_URL" + | "CKB_RPC_URL" + | "CKB_RPC_TIMEOUT_MS" + | "CKB_RPC_MAX_RESPONSE_BYTES" + | "CKB_DEP_GROUP_MAX_MEMBERS" +>; + +export function nodeCkbRpcEnv( + processEnv: Readonly>, +): Partial { + return { + ...(processEnv["CKB_MAINNET_RPC_URL"] ? { CKB_MAINNET_RPC_URL: processEnv["CKB_MAINNET_RPC_URL"] } : {}), + ...(processEnv["CKB_RPC_URL"] ? { CKB_RPC_URL: processEnv["CKB_RPC_URL"] } : {}), + ...(processEnv["CKB_RPC_TIMEOUT_MS"] ? { CKB_RPC_TIMEOUT_MS: processEnv["CKB_RPC_TIMEOUT_MS"] } : {}), + ...(processEnv["CKB_RPC_MAX_RESPONSE_BYTES"] + ? { CKB_RPC_MAX_RESPONSE_BYTES: processEnv["CKB_RPC_MAX_RESPONSE_BYTES"] } + : {}), + ...(processEnv["CKB_DEP_GROUP_MAX_MEMBERS"] + ? { CKB_DEP_GROUP_MAX_MEMBERS: processEnv["CKB_DEP_GROUP_MAX_MEMBERS"] } + : {}), + }; +} diff --git a/services/registry-api/src/node-server.ts b/services/registry-api/src/node-server.ts new file mode 100644 index 00000000..d6671005 --- /dev/null +++ b/services/registry-api/src/node-server.ts @@ -0,0 +1,221 @@ +import { constants as fsConstants } from "node:fs"; +import { access, mkdir, stat } from "node:fs/promises"; +import { createServer } from "node:http"; +import { resolve } from "node:path"; +import { randomUUID } from "node:crypto"; + +import { createApp, type Env } from "./index"; +import { FilesystemObjectStore } from "./filesystem-object-store"; +import { nodeCkbRpcEnv } from "./node-runtime-env"; +import { SqlRegistryStore } from "./sql-store"; + +const port = integerEnv("PORT", 8787, 1, 65_535); +const databaseUrl = requiredEnv("DATABASE_URL"); +const objectRoot = resolve(requiredEnv("REGISTRY_OBJECTS_DIR")); +const adminToken = requiredEnv("REGISTRY_ADMIN_TOKEN"); +const maxIncomingBodyBytes = integerEnv("MAX_INCOMING_BODY_BYTES", 7 * 1024 * 1024, 1_024, 64 * 1024 * 1024); +const requireVerifierReady = process.env["REQUIRE_REGISTRY_VERIFIER_READY"] === "true"; +const verifierHeartbeatPath = resolve(process.env["REGISTRY_VERIFIER_SHARED_HEARTBEAT"] ?? `${objectRoot}/.health/verifier-ready`); +const verifierHeartbeatMaxAgeSeconds = integerEnv("REGISTRY_VERIFIER_HEARTBEAT_MAX_AGE_SECONDS", 120, 30, 600); + +await mkdir(objectRoot, { recursive: true, mode: 0o750 }); +const managedObjectPrefixes = ["source-snapshots", "artifacts"].map((prefix) => resolve(objectRoot, prefix)); +for (const prefix of managedObjectPrefixes) { + await mkdir(prefix, { recursive: true, mode: 0o750 }); + await access(prefix, fsConstants.R_OK | fsConstants.W_OK); +} + +const store = new SqlRegistryStore({ connectionString: databaseUrl }); +const objectStore = new FilesystemObjectStore(objectRoot); +const env: Env = { + REGISTRY_ADMIN_TOKEN: adminToken, + REGISTRY_ORIGIN: process.env["REGISTRY_ORIGIN"] ?? "https://api.registry.cellscript.dev", + STATIC_REGISTRY_ORIGIN: process.env["STATIC_REGISTRY_ORIGIN"] ?? "https://registry.cellscript.dev", + REGISTRY_WEBSITE_ORIGIN: process.env["REGISTRY_WEBSITE_ORIGIN"] ?? "https://cellscript.dev", + ENVIRONMENT: process.env["ENVIRONMENT"] ?? "production", + REGISTRY_ENVIRONMENT: process.env["REGISTRY_ENVIRONMENT"] ?? "production", + ...(process.env["MAX_JSON_BODY_BYTES"] ? { MAX_JSON_BODY_BYTES: process.env["MAX_JSON_BODY_BYTES"] } : {}), + ...(process.env["MAX_SNAPSHOT_BYTES"] ? { MAX_SNAPSHOT_BYTES: process.env["MAX_SNAPSHOT_BYTES"] } : {}), + ...(process.env["CLEANUP_QUOTA_EVENT_RETENTION_HOURS"] + ? { CLEANUP_QUOTA_EVENT_RETENTION_HOURS: process.env["CLEANUP_QUOTA_EVENT_RETENTION_HOURS"] } + : {}), + ...(process.env["NAMESPACE_CLAIM_COOLDOWN_SECONDS"] + ? { NAMESPACE_CLAIM_COOLDOWN_SECONDS: process.env["NAMESPACE_CLAIM_COOLDOWN_SECONDS"] } + : {}), + ...nodeCkbRpcEnv(process.env), + ...(process.env["REGISTRY_TYPE_SCRIPT_JSON"] + ? { REGISTRY_TYPE_SCRIPT_JSON: process.env["REGISTRY_TYPE_SCRIPT_JSON"] } + : {}), + ...(process.env["REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON"] + ? { REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON: process.env["REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON"] } + : {}), + ...(process.env["REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON"] + ? { REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON: process.env["REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON"] } + : {}), + ...(process.env["REGISTRY_COMMITMENT_LOCK_CELL_DEP_JSON"] + ? { REGISTRY_COMMITMENT_LOCK_CELL_DEP_JSON: process.env["REGISTRY_COMMITMENT_LOCK_CELL_DEP_JSON"] } + : {}), + ...(process.env["REGISTRY_REPRODUCER_POLICY_JSON"] + ? { REGISTRY_REPRODUCER_POLICY_JSON: process.env["REGISTRY_REPRODUCER_POLICY_JSON"] } + : {}), + ...(process.env["CKB_REGISTRY_SCAN_MAX_CELLS"] + ? { CKB_REGISTRY_SCAN_MAX_CELLS: process.env["CKB_REGISTRY_SCAN_MAX_CELLS"] } + : {}), + ...(process.env["CKB_MIN_CONFIRMATIONS"] + ? { CKB_MIN_CONFIRMATIONS: process.env["CKB_MIN_CONFIRMATIONS"] } + : {}), +}; + +const app = createApp({ + store, + snapshotWriter: objectStore, + registryObjectReader: objectStore, + readinessCheck: async () => { + await access(objectRoot, fsConstants.R_OK | fsConstants.W_OK); + for (const prefix of managedObjectPrefixes) { + await access(prefix, fsConstants.R_OK | fsConstants.W_OK); + } + const checks: Record = { object_store: "ready", runtime: "ready" }; + if (requireVerifierReady) { + const heartbeat = await stat(verifierHeartbeatPath); + if (!heartbeat.isFile() || Date.now() - heartbeat.mtimeMs > verifierHeartbeatMaxAgeSeconds * 1_000) { + throw new Error("registry verifier heartbeat is stale"); + } + checks["verifier"] = "ready"; + } + return checks; + }, +}); + +const server = createServer(async (request, response) => { + const startedAt = Date.now(); + const requestId = request.headers["x-request-id"]?.toString() ?? randomUUID(); + try { + const protocol = firstHeader(request.headers["x-forwarded-proto"]) ?? "http"; + const host = firstHeader(request.headers.host) ?? `127.0.0.1:${port}`; + const url = new URL(request.url ?? "/", `${protocol}://${host}`); + const headers = new Headers(); + for (const [name, value] of Object.entries(request.headers)) { + if (value === undefined) continue; + if (Array.isArray(value)) { + for (const item of value) headers.append(name, item); + } else { + headers.set(name, value); + } + } + headers.set("x-request-id", requestId); + const method = request.method ?? "GET"; + const body = method === "GET" || method === "HEAD" ? undefined : await readIncomingBody(request, maxIncomingBodyBytes); + const requestInit: RequestInit = { method, headers }; + if (body) requestInit.body = body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength) as ArrayBuffer; + const registryResponse = await app.fetch(new Request(url, requestInit), env); + response.statusCode = registryResponse.status; + registryResponse.headers.forEach((value, name) => response.setHeader(name, value)); + if (method === "HEAD" || !registryResponse.body) { + response.end(); + } else { + response.end(Buffer.from(await registryResponse.arrayBuffer())); + } + log("request.completed", { + request_id: requestId, + method, + path: url.pathname, + status: registryResponse.status, + duration_ms: Date.now() - startedAt, + }); + } catch (error) { + const tooLarge = error instanceof IncomingBodyTooLargeError; + log("request.failed", { + request_id: requestId, + error: error instanceof Error ? error.message : "unknown error", + duration_ms: Date.now() - startedAt, + }); + if (!response.headersSent) { + response.statusCode = tooLarge ? 413 : 500; + response.setHeader("content-type", "application/json; charset=utf-8"); + response.setHeader("x-content-type-options", "nosniff"); + } + response.end(JSON.stringify({ + request_id: requestId, + error: { + code: tooLarge ? "request_body_too_large" : "node_adapter_error", + message: tooLarge ? "request body exceeds the configured limit" : "internal error", + }, + })); + } +}); + +server.requestTimeout = 30_000; +server.headersTimeout = 15_000; +server.keepAliveTimeout = 5_000; +server.listen(port, "0.0.0.0", () => log("server.started", { port, object_root: objectRoot })); + +let maintenanceRunning = false; +const runMaintenance = () => { + if (maintenanceRunning) { + log("maintenance.skipped", { reason: "previous_run_active" }); + return; + } + maintenanceRunning = true; + app.scheduled({} as ScheduledController, env) + .catch((error) => { + log("maintenance.failed", { error: error instanceof Error ? error.message : "unknown error" }); + }) + .finally(() => { + maintenanceRunning = false; + }); +}; +void runMaintenance(); +const maintenanceInterval = setInterval(runMaintenance, 15 * 60 * 1000); +maintenanceInterval.unref(); + +for (const signal of ["SIGTERM", "SIGINT"] as const) { + process.on(signal, () => { + clearInterval(maintenanceInterval); + log("server.stopping", { signal }); + server.close((error) => { + if (error) { + log("server.stop_failed", { error: error.message }); + process.exitCode = 1; + } + }); + }); +} + +class IncomingBodyTooLargeError extends Error {} + +async function readIncomingBody(request: import("node:http").IncomingMessage, maximumBytes: number): Promise { + const chunks: Buffer[] = []; + let receivedBytes = 0; + for await (const chunk of request) { + const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + receivedBytes += buffer.byteLength; + if (receivedBytes > maximumBytes) throw new IncomingBodyTooLargeError(); + chunks.push(buffer); + } + return Buffer.concat(chunks); +} + +function firstHeader(value: string | string[] | undefined): string | undefined { + return Array.isArray(value) ? value[0] : value; +} + +function requiredEnv(name: string): string { + const value = process.env[name]?.trim(); + if (!value) throw new Error(`${name} is required`); + return value; +} + +function integerEnv(name: string, fallback: number, minimum: number, maximum: number): number { + const raw = process.env[name]; + if (!raw) return fallback; + const value = Number(raw); + if (!Number.isSafeInteger(value) || value < minimum || value > maximum) { + throw new Error(`${name} must be an integer between ${minimum} and ${maximum}`); + } + return value; +} + +function log(event: string, data: Record): void { + process.stdout.write(`${JSON.stringify({ timestamp: new Date().toISOString(), event, ...data })}\n`); +} diff --git a/services/registry-api/src/sql-store.ts b/services/registry-api/src/sql-store.ts index eeb44708..cdb8b950 100644 --- a/services/registry-api/src/sql-store.ts +++ b/services/registry-api/src/sql-store.ts @@ -1,21 +1,46 @@ import { Client } from "pg"; -import type { - AuditEventInput, - AuditEventRecord, - CapabilityRecord, - IdempotencyRecord, - IdempotencyReservation, - ListAuditEventsInput, - MaintenanceResult, - NamespaceClaimResult, - NamespaceRecord, - NamespaceStatus, - PackageVersionRecord, - ReservedNamespaceRecord, - RegistryStore, - SnapshotRecord, +import { + assertPromotionTransition, + AUTHORISATION_SESSION_TERMINAL_RETENTION_HOURS, + deriveRegistryEntryStatus, + packageVersionRequiresReproduction, + type AuditEventInput, + type AuditEventRecord, + type AuthorisationSessionCompletionInput, + type AuthorisationSessionCompletionResult, + type AuthorisationSessionRecord, + type CapabilityRecord, + type IdempotencyRecord, + type IdempotencyReservation, + type ListAuditEventsInput, + type MaintenanceResult, + type NamespaceClaimResult, + type NamespaceRecord, + type NamespaceStatus, + type PackageEvidenceRecord, + type PackageVersionRecord, + type PackageVersionQuery, + type PromotePackageVersionInput, + type PublishAdmissionInput, + type ReservedNamespaceRecord, + type RegistryStore, + type SnapshotRecord, + type ScriptInterfaceCandidate, + type ScriptInterfaceLookup, + type VerificationJobRecord, + type VerificationJobStatus, + type VerificationQueueMetrics, } from "./store"; -import { ApiError, capabilityKeyId, canonicalJson, sha256Hex, type CapabilityAuthorisationPayload, type RegistryEntryStatus } from "./domain"; +import { + ApiError, + capabilityKeyId, + canonicalJson, + sha256Hex, + type AvailabilityStatus, + type CapabilityAuthorisationPayload, + type PrincipalType, + type RegistryEntryStatus, +} from "./domain"; export interface HyperdriveLike { connectionString: string; @@ -24,6 +49,24 @@ export interface HyperdriveLike { export class SqlRegistryStore implements RegistryStore { constructor(private readonly hyperdrive: HyperdriveLike) {} + async healthCheck(): Promise { + await this.withClient(async (client) => { + await client.query("select 1"); + }); + } + + async withMaintenanceLease(name: string, task: () => Promise): Promise { + return this.withClient(async (client) => { + const acquired = await client.query("select pg_try_advisory_lock(hashtext($1)) as acquired", [name]); + if (acquired.rows[0]?.acquired !== true) return null; + try { + return await task(); + } finally { + await client.query("select pg_advisory_unlock(hashtext($1))", [name]); + } + }); + } + private async withClient(fn: (client: Client) => Promise): Promise { const client = new Client({ connectionString: this.hyperdrive.connectionString }); await client.connect(); @@ -36,7 +79,7 @@ export class SqlRegistryStore implements RegistryStore { async recordCapability(input: { payload: CapabilityAuthorisationPayload; - joyid_signature: unknown; + principal_signature: unknown; request_id: string; }): Promise { const keyId = await capabilityKeyId(input.payload.capability_pubkey); @@ -72,7 +115,10 @@ export class SqlRegistryStore implements RegistryStore { input.payload.requested_scopes, input.payload.capability_expires_at, JSON.stringify(input.payload), - JSON.stringify(input.joyid_signature), + // The production schema keeps the original column name for a + // non-destructive migration; it stores either supported wallet + // signature envelope. + JSON.stringify(input.principal_signature), ], ); if (capabilityInsert.rowCount !== 1) { @@ -131,9 +177,338 @@ export class SqlRegistryStore implements RegistryStore { }); } + async createAuthorisationSession( + input: AuthorisationSessionRecord & { request_id: string }, + ): Promise { + await this.withClient(async (client) => { + const inserted = await client.query( + `insert into authorisation_sessions( + session_id, poll_token_hash, browser_token_hash, registry_origin, website_origin, + capability_pubkey, requested_scopes, capability_expires_at, cli_version, + namespace, name, artifact_kind, status, expires_at, audit_request_id + ) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 'pending', $13, $14) + on conflict (session_id) do nothing`, + [ + input.session_id, + input.poll_token_hash, + input.browser_token_hash, + input.registry_origin, + input.website_origin, + input.capability_pubkey, + input.requested_scopes, + input.capability_expires_at, + input.cli_version, + input.namespace, + input.name, + input.artifact_kind, + input.expires_at, + input.request_id, + ], + ); + if (inserted.rowCount !== 1) { + throw new ApiError(409, "authorisation_session_exists", "authorisation session already exists"); + } + }); + const record = await this.getAuthorisationSession(input.session_id); + if (!record) throw new Error("authorisation session insert did not return a readable record"); + return record; + } + + async getAuthorisationSession(sessionId: string): Promise { + return this.withClient(async (client) => { + const result = await client.query( + `select session_id, poll_token_hash, browser_token_hash, registry_origin, website_origin, + capability_pubkey, requested_scopes, capability_expires_at, cli_version, + namespace, name, artifact_kind, status, principal_type, principal_id, payload, + challenge_token_hash, capability_key_id, namespace_status, + created_at, updated_at, expires_at, completed_at + from authorisation_sessions where session_id = $1`, + [sessionId], + ); + return result.rows[0] ? authorisationSessionFromRow(result.rows[0]) : null; + }); + } + + async prepareAuthorisationSession(input: { + session_id: string; + principal_type: PrincipalType; + principal_id: string; + payload: CapabilityAuthorisationPayload; + challenge_token_hash: string; + request_id: string; + }): Promise { + await this.withClient(async (client) => { + const updated = await client.query( + `update authorisation_sessions + set principal_type = $2, + principal_id = $3, + payload = $4::jsonb, + challenge_token_hash = $5, + updated_at = now() + where session_id = $1 and status = 'pending' and expires_at > now()`, + [input.session_id, input.principal_type, input.principal_id, JSON.stringify(input.payload), input.challenge_token_hash], + ); + if (updated.rowCount !== 1) { + const existing = await client.query("select status, expires_at from authorisation_sessions where session_id = $1", [input.session_id]); + if (!existing.rows[0]) throw new ApiError(404, "authorisation_session_not_found", "authorisation session was not found"); + if (new Date(existing.rows[0].expires_at).getTime() <= Date.now()) { + throw new ApiError(410, "authorisation_session_expired", "authorisation session has expired"); + } + throw new ApiError(409, "authorisation_session_complete", "authorisation session has already completed"); + } + }); + const record = await this.getAuthorisationSession(input.session_id); + if (!record) throw new Error("prepared authorisation session was not readable"); + return record; + } + + async finaliseAuthorisationSession( + input: AuthorisationSessionCompletionInput, + ): Promise { + const keyId = await capabilityKeyId(input.payload.capability_pubkey); + const payloadHash = await sha256Hex(canonicalJson(input.payload)); + return this.withClient(async (client) => { + await client.query("begin"); + try { + const sessionResult = await client.query( + `select session_id, poll_token_hash, browser_token_hash, registry_origin, website_origin, + capability_pubkey, requested_scopes, capability_expires_at, cli_version, + namespace, name, artifact_kind, status, principal_type, principal_id, payload, + challenge_token_hash, capability_key_id, namespace_status, + created_at, updated_at, expires_at, completed_at + from authorisation_sessions + where session_id = $1 + for update`, + [input.session_id], + ); + const sessionRow = sessionResult.rows[0]; + if (!sessionRow) throw new ApiError(404, "authorisation_session_not_found", "authorisation session was not found"); + const session = authorisationSessionFromRow(sessionRow); + if (session.status !== "pending") { + await client.query("commit"); + return { session, replayed: true }; + } + if (Date.parse(session.expires_at) <= Date.parse(input.now_iso)) { + throw new ApiError(410, "authorisation_session_expired", "authorisation session has expired"); + } + if (session.challenge_token_hash !== input.expected_challenge_token_hash + || !session.payload + || canonicalJson(session.payload) !== canonicalJson(input.payload)) { + throw new ApiError(409, "authorisation_challenge_stale", "authorisation challenge was replaced; request a new wallet challenge"); + } + + const nonceInsert = await client.query( + `insert into used_nonces( + nonce_key, protocol, action, nonce, request_id, expires_at, + principal_type, principal_id, capability_key_id + ) values ($1, $2, $3, $4, $5, $6, $7, $8, null) + on conflict (nonce_key) do nothing`, + [ + input.nonce.nonce_key, + input.nonce.protocol, + input.nonce.action, + input.nonce.nonce, + input.request_id, + input.nonce.expires_at, + input.nonce.principal_type, + input.nonce.principal_id, + ], + ); + if (nonceInsert.rowCount !== 1) { + throw new ApiError(409, "nonce_replay", "signed nonce has already been used"); + } + + await client.query( + `insert into principals(principal_type, principal_id) + values ($1, $2) + on conflict (principal_type, principal_id) + do update set updated_at = now()`, + [input.payload.principal_type, input.payload.principal_id], + ); + + let namespaceResult = await client.query( + `select namespace, owner_principal_type, owner_principal_id, status, review_reason + from namespaces where namespace = $1 for update`, + [session.namespace], + ); + let namespaceInserted = false; + if (!namespaceResult.rows[0]) { + if (input.namespace_claim_cooldown_seconds > 0) { + const cooldownSince = new Date( + Date.parse(input.now_iso) - input.namespace_claim_cooldown_seconds * 1000, + ).toISOString(); + const recentClaims = await client.query( + `select count(*)::bigint as count + from quota_events + where quota_key = $1 and bucket = 'namespace_claim_cooldown' and created_at >= $2`, + [`principal:${input.payload.principal_type}:${input.payload.principal_id}`, cooldownSince], + ); + if (Number(recentClaims.rows[0]?.count ?? 0) >= 1) { + throw new ApiError(429, "namespace_claim_cooldown", "namespace claim cooldown is active"); + } + await client.query( + `insert into quota_events(quota_key, bucket) + values ($1, 'namespace_claim_cooldown')`, + [`principal:${input.payload.principal_type}:${input.payload.principal_id}`], + ); + } + const reserved = await client.query( + `select reason from reserved_namespaces + where (match_type in ('exact', 'typosquat') and namespace = $1) + or (match_type = 'prefix' and $1 like namespace || '%') + limit 1`, + [session.namespace], + ); + const reviewReason = reserved.rows[0]?.reason as string | undefined + ?? (session.namespace.length <= 3 ? "short_namespace_review" : undefined); + const inserted = await client.query( + `insert into namespaces( + namespace, owner_principal_type, owner_principal_id, status, review_reason, audit_request_id + ) values ($1, $2, $3, $4, $5, $6) + on conflict (namespace) do nothing`, + [ + session.namespace, + input.payload.principal_type, + input.payload.principal_id, + reviewReason ? "review_pending" : "active", + reviewReason ?? null, + input.request_id, + ], + ); + namespaceInserted = inserted.rowCount === 1; + namespaceResult = await client.query( + `select namespace, owner_principal_type, owner_principal_id, status, review_reason + from namespaces where namespace = $1 for update`, + [session.namespace], + ); + } + const namespace = namespaceResult.rows[0]; + if (!namespace) throw new Error("namespace claim did not return a readable record"); + if (namespace.owner_principal_type !== input.payload.principal_type + || namespace.owner_principal_id !== input.payload.principal_id) { + throw new ApiError(409, "namespace_already_claimed", "namespace is already claimed by another principal"); + } + const namespaceStatus: NamespaceClaimResult["status"] = namespace.status === "active" ? "active" : "review_pending"; + if (namespaceInserted) { + await client.query( + `insert into audit_events(request_id, event_type, principal_type, principal_id, namespace, data) + values ($1, 'namespace.claimed', $2, $3, $4, $5::jsonb)`, + [ + input.request_id, + input.payload.principal_type, + input.payload.principal_id, + session.namespace, + JSON.stringify({ review_reason: namespace.review_reason ?? null }), + ], + ); + } + + const capabilityInsert = await client.query( + `insert into capabilities( + key_id, principal_type, principal_id, capability_pubkey, scopes, + expires_at, authorisation_payload, joyid_signature + ) values ($1, $2, $3, $4, $5, $6, $7::jsonb, $8::jsonb) + on conflict (key_id) + do update set scopes = excluded.scopes, + expires_at = excluded.expires_at, + authorisation_payload = excluded.authorisation_payload, + joyid_signature = excluded.joyid_signature + where capabilities.revoked_at is null + returning key_id, principal_type, principal_id`, + [ + keyId, + input.payload.principal_type, + input.payload.principal_id, + input.payload.capability_pubkey, + input.payload.requested_scopes, + input.payload.capability_expires_at, + JSON.stringify(input.payload), + JSON.stringify(input.principal_signature), + ], + ); + const capabilityRow = capabilityInsert.rows[0]; + if (!capabilityRow) { + throw new ApiError(409, "capability_key_revoked", "revoked capability keys cannot be reactivated"); + } + if (capabilityRow.principal_type !== input.payload.principal_type + || capabilityRow.principal_id !== input.payload.principal_id) { + throw new ApiError(409, "capability_principal_mismatch", "publishing key is already bound to another principal"); + } + await client.query( + `insert into audit_events( + request_id, event_type, principal_type, principal_id, capability_key_id, data + ) values ($1, 'capability.created', $2, $3, $4, $5::jsonb)`, + [ + input.request_id, + input.payload.principal_type, + input.payload.principal_id, + keyId, + JSON.stringify({ scopes: input.payload.requested_scopes, payload_hash: payloadHash }), + ], + ); + + const completedResult = await client.query( + `update authorisation_sessions + set status = $2, + capability_key_id = $3, + namespace_status = $4, + challenge_token_hash = null, + completed_at = $5, + updated_at = $5 + where session_id = $1 + returning session_id, poll_token_hash, browser_token_hash, registry_origin, website_origin, + capability_pubkey, requested_scopes, capability_expires_at, cli_version, + namespace, name, artifact_kind, status, principal_type, principal_id, payload, + challenge_token_hash, capability_key_id, namespace_status, + created_at, updated_at, expires_at, completed_at`, + [ + input.session_id, + namespaceStatus === "active" ? "authorised" : "review_pending", + keyId, + namespaceStatus, + input.now_iso, + ], + ); + const completedRow = completedResult.rows[0]; + if (!completedRow) throw new Error("completed authorisation session was not readable"); + await client.query( + `insert into audit_events( + request_id, event_type, principal_type, principal_id, capability_key_id, namespace, name, data + ) values ($1, 'authorisation_session.completed', $2, $3, $4, $5, $6, $7::jsonb)`, + [ + input.request_id, + input.payload.principal_type, + input.payload.principal_id, + keyId, + session.namespace, + session.name, + JSON.stringify({ session_id: session.session_id, namespace_status: namespaceStatus }), + ], + ); + await client.query("commit"); + return { session: authorisationSessionFromRow(completedRow), replayed: false }; + } catch (error) { + await client.query("rollback"); + if (error instanceof ApiError && error.code === "nonce_replay") { + await client.query( + `insert into audit_events(request_id, event_type, principal_type, principal_id, data) + values ($1, 'nonce.replay_blocked', $2, $3, $4::jsonb)`, + [ + input.request_id, + input.payload.principal_type, + input.payload.principal_id, + JSON.stringify({ protocol: input.nonce.protocol, action: input.nonce.action, nonce_key: input.nonce.nonce_key }), + ], + ); + } + throw error; + } + }); + } + async revokeCapability(input: { key_id: string; - principal_type: "joyid_ckb"; + principal_type: PrincipalType; principal_id: string; request_id: string; reason?: string; @@ -201,7 +576,7 @@ export class SqlRegistryStore implements RegistryStore { async claimNamespace(input: { namespace: string; - principal_type: "joyid_ckb"; + principal_type: PrincipalType; principal_id: string; request_id: string; }): Promise { @@ -358,7 +733,7 @@ export class SqlRegistryStore implements RegistryStore { async ensurePackage(input: { namespace: string; name: string; - principal_type: string; + principal_type: PrincipalType; principal_id: string; source_repo?: string; request_id: string; @@ -386,14 +761,47 @@ export class SqlRegistryStore implements RegistryStore { }); } + async getSnapshot(snapshotHash: string): Promise { + return (await this.getSnapshots([snapshotHash])).get(snapshotHash) ?? null; + } + + async getSnapshots(snapshotHashes: string[]): Promise> { + const uniqueHashes = [...new Set(snapshotHashes)]; + if (uniqueHashes.length === 0) return new Map(); + return this.withClient(async (client) => { + const result = await client.query( + `select snapshot_hash, r2_key, source_hash, size_bytes, content_type + from source_snapshots + where snapshot_hash = any($1::text[]) and hidden_at is null`, + [uniqueHashes], + ); + return new Map(result.rows.map((row) => { + const snapshot: SnapshotRecord = { + snapshot_hash: String(row.snapshot_hash), + r2_key: String(row.r2_key), + source_hash: String(row.source_hash), + size_bytes: Number(row.size_bytes), + content_type: String(row.content_type), + }; + return [snapshot.snapshot_hash, snapshot]; + })); + }); + } + async getPackageVersion(namespace: string, name: string, version: string): Promise { return this.withClient(async (client) => { const result = await client.query( - `select namespace, name, version, status, source_hash, manifest_hash, + `select namespace, name, version, status, artifact, verification_status, deployment_status, availability_status, + current_commitment_evidence_hash, + source_hash, manifest_hash, + edition, compatibility_profile_hash, capability_key_id, principal_type, principal_id, registry_entry, - snapshot_hash, direct_url, created_at + snapshot_hash, direct_url, created_at, + registry_environment, chain_network, expires_at, expired_at, purge_after, + static_purged_at, source_purged_at from package_versions - where namespace = $1 and name = $2 and version = $3`, + where namespace = $1 and name = $2 and version = $3 + and (expires_at is null or expires_at > now())`, [namespace, name, version], ); const row = result.rows[0]; @@ -401,15 +809,139 @@ export class SqlRegistryStore implements RegistryStore { }); } + async listPackageVersions(input: PackageVersionQuery): Promise { + return this.withClient(async (client) => { + const result = await client.query( + `select pv.namespace, pv.name, pv.version, pv.status, pv.artifact, + pv.verification_status, pv.deployment_status, pv.availability_status, + pv.current_commitment_evidence_hash, + pv.source_hash, pv.manifest_hash, + pv.edition, pv.compatibility_profile_hash, + pv.capability_key_id, pv.principal_type, pv.principal_id, pv.registry_entry, + pv.snapshot_hash, pv.direct_url, pv.created_at, + pv.registry_environment, pv.chain_network, pv.expires_at, pv.expired_at, pv.purge_after, + pv.static_purged_at, pv.source_purged_at + from package_versions pv + join packages p on p.namespace = pv.namespace and p.name = pv.name + where ($1::text is null or pv.namespace = $1) + and ($2::text is null or pv.name = $2) + and ($3::text is null or pv.status = $3) + and ($7::text[] is null or pv.status = any($7::text[])) + and ($8::text is null or pv.artifact->>'kind' = $8) + and ($9::text is null or pv.verification_status = $9) + and ($12::text[] is null or pv.verification_status = any($12::text[])) + and ($10::text is null or pv.deployment_status = $10) + and ($11::text is null or pv.availability_status = $11) + and (pv.expires_at is null or pv.expires_at > now()) + and ( + $4::text is null + or pv.namespace ilike '%' || $4 || '%' + or pv.name ilike '%' || $4 || '%' + or pv.version ilike '%' || $4 || '%' + or coalesce(p.source_repo, '') ilike '%' || $4 || '%' + or pv.registry_entry::text ilike '%' || $4 || '%' + ) + order by pv.created_at desc, pv.namespace, pv.name, pv.version desc + limit $5 offset $6`, + [ + input.namespace ?? null, + input.name ?? null, + input.status ?? null, + input.query ?? null, + input.limit, + input.offset, + input.statuses ?? null, + input.artifact_kind ?? null, + input.verification_status ?? null, + input.deployment_status ?? null, + input.availability_status ?? null, + input.verification_statuses ?? null, + ], + ); + return result.rows.map(packageVersionFromRow); + }); + } + + async listArtifactPackagePage(input: PackageVersionQuery): Promise<{ records: PackageVersionRecord[]; has_more: boolean }> { + return this.withClient(async (client) => { + const result = await client.query( + `with matching as ( + select pv.namespace, pv.name, pv.version, pv.status, pv.artifact, + pv.verification_status, pv.deployment_status, pv.availability_status, + pv.current_commitment_evidence_hash, + pv.source_hash, pv.manifest_hash, pv.edition, pv.compatibility_profile_hash, + pv.capability_key_id, pv.principal_type, pv.principal_id, pv.registry_entry, + pv.snapshot_hash, pv.direct_url, pv.created_at, + pv.registry_environment, pv.chain_network, pv.expires_at, pv.expired_at, pv.purge_after, + pv.static_purged_at, pv.source_purged_at + from package_versions pv + join packages p on p.namespace = pv.namespace and p.name = pv.name + where ($1::text is null or pv.namespace = $1) + and ($2::text is null or pv.name = $2) + and ($3::text is null or pv.status = $3) + and ($7::text[] is null or pv.status = any($7::text[])) + and ($8::text is null or pv.artifact->>'kind' = $8) + and ($9::text is null or pv.verification_status = $9) + and ($12::text[] is null or pv.verification_status = any($12::text[])) + and ($10::text is null or pv.deployment_status = $10) + and ($11::text is null or pv.availability_status = $11) + and (pv.expires_at is null or pv.expires_at > now()) + and ( + $4::text is null + or pv.namespace ilike '%' || $4 || '%' + or pv.name ilike '%' || $4 || '%' + or pv.version ilike '%' || $4 || '%' + or coalesce(p.source_repo, '') ilike '%' || $4 || '%' + or pv.registry_entry::text ilike '%' || $4 || '%' + ) + ), package_page as ( + select namespace, name, max(created_at) as package_updated_at, + row_number() over (order by max(created_at) desc, namespace, name) as page_position + from matching + group by namespace, name + order by package_updated_at desc, namespace, name + limit $5 + 1 offset $6 + ) + select m.*, (select count(*) > $5 from package_page) as has_more + from matching m + join package_page pp on pp.namespace = m.namespace and pp.name = m.name + where pp.page_position <= $6 + $5 + order by pp.package_updated_at desc, m.namespace, m.name, m.created_at desc, m.version desc`, + [ + input.namespace ?? null, + input.name ?? null, + input.status ?? null, + input.query ?? null, + input.limit, + input.offset, + input.statuses ?? null, + input.artifact_kind ?? null, + input.verification_status ?? null, + input.deployment_status ?? null, + input.availability_status ?? null, + input.verification_statuses ?? null, + ], + ); + return { + records: result.rows.map(packageVersionFromRow), + has_more: result.rows[0]?.has_more === true, + }; + }); + } + async recordPackageVersion(input: PackageVersionRecord): Promise { await this.withClient(async (client) => { const result = await client.query( `insert into package_versions( - namespace, name, version, status, source_hash, manifest_hash, + namespace, name, version, status, artifact, verification_status, deployment_status, availability_status, + source_hash, manifest_hash, + edition, compatibility_profile_hash, capability_key_id, principal_type, principal_id, registry_entry, - snapshot_hash, direct_url + snapshot_hash, direct_url, + registry_environment, chain_network, expires_at, purge_after ) - values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11, $12) + values ($1, $2, $3, $4, $5::jsonb, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16::jsonb, $17, $18, + $19, $20, $21, $22) on conflict (namespace, name, version) do nothing returning namespace`, [ @@ -417,37 +949,105 @@ export class SqlRegistryStore implements RegistryStore { input.name, input.version, input.status, + JSON.stringify(input.artifact), + input.verification_status, + input.deployment_status, + input.availability_status, input.source_hash, - input.manifest_hash ?? null, + input.manifest_hash, + input.edition, + input.compatibility_profile_hash, input.capability_key_id, input.principal_type, input.principal_id, JSON.stringify(input.registry_entry), input.snapshot_hash, input.direct_url, + input.registry_environment ?? "production", + input.network ?? "mainnet", + input.expires_at ?? null, + input.purge_after ?? null, ], ); if (result.rowCount !== 1) { - throw new ApiError(409, "package_version_exists", "package version already exists and cannot be overwritten"); + throw new ApiError(409, "artifact_release_exists", "artifact release already exists and cannot be overwritten"); } }); return input; } - async recordCapabilityUsage(input: { - key_id: string; - principal_type: string; - principal_id: string; - request_id: string; - action: string; - namespace?: string; - name?: string; - version?: string; - }): Promise { + async admitPackageVersion(input: PublishAdmissionInput): Promise { await this.withClient(async (client) => { await client.query("begin"); try { - await client.query("update capabilities set last_used_at = now() where key_id = $1", [input.key_id]); + await client.query( + `insert into packages(namespace, name, source_repo) + values ($1, $2, $3) + on conflict (namespace, name) + do update set source_repo = coalesce(excluded.source_repo, packages.source_repo), + updated_at = now()`, + [input.package.namespace, input.package.name, input.package.source_repo ?? null], + ); + await client.query( + `insert into source_snapshots(snapshot_hash, r2_key, source_hash, size_bytes, content_type) + values ($1, $2, $3, $4, $5) + on conflict (snapshot_hash) do nothing`, + [ + input.snapshot.snapshot_hash, + input.snapshot.r2_key, + input.snapshot.source_hash, + input.snapshot.size_bytes, + input.snapshot.content_type, + ], + ); + const insertedVersion = await client.query( + `insert into package_versions( + namespace, name, version, status, artifact, verification_status, deployment_status, availability_status, + source_hash, manifest_hash, + edition, compatibility_profile_hash, + capability_key_id, principal_type, principal_id, registry_entry, + snapshot_hash, direct_url, + registry_environment, chain_network, expires_at, purge_after + ) + values ($1, $2, $3, $4, $5::jsonb, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16::jsonb, $17, $18, + $19, $20, $21, $22) + on conflict (namespace, name, version) do nothing + returning namespace`, + [ + input.version.namespace, + input.version.name, + input.version.version, + input.version.status, + JSON.stringify(input.version.artifact), + input.version.verification_status, + input.version.deployment_status, + input.version.availability_status, + input.version.source_hash, + input.version.manifest_hash, + input.version.edition, + input.version.compatibility_profile_hash, + input.version.capability_key_id, + input.version.principal_type, + input.version.principal_id, + JSON.stringify(input.version.registry_entry), + input.version.snapshot_hash, + input.version.direct_url, + input.version.registry_environment ?? "production", + input.version.network ?? "mainnet", + input.version.expires_at ?? null, + input.version.purge_after ?? null, + ], + ); + if (insertedVersion.rowCount !== 1) { + throw new ApiError(409, "artifact_release_exists", "artifact release already exists and cannot be overwritten"); + } + await client.query( + `insert into verification_jobs(namespace, name, version) + values ($1, $2, $3) + on conflict (namespace, name, version) do nothing`, + [input.version.namespace, input.version.name, input.version.version], + ); + await client.query("update capabilities set last_used_at = now() where key_id = $1", [input.capability_usage.key_id]); await client.query( `insert into audit_events( request_id, event_type, principal_type, principal_id, capability_key_id, @@ -455,74 +1055,589 @@ export class SqlRegistryStore implements RegistryStore { ) values ($1, 'capability.used', $2, $3, $4, $5, $6, $7, $8::jsonb)`, [ - input.request_id, - input.principal_type, - input.principal_id, - input.key_id, - input.namespace ?? null, - input.name ?? null, - input.version ?? null, - JSON.stringify({ action: input.action }), + input.capability_usage.request_id, + input.capability_usage.principal_type, + input.capability_usage.principal_id, + input.capability_usage.key_id, + input.capability_usage.namespace ?? null, + input.capability_usage.name ?? null, + input.capability_usage.version ?? null, + JSON.stringify({ action: input.capability_usage.action }), ], ); + await client.query( + `insert into audit_events( + request_id, event_type, principal_type, principal_id, capability_key_id, + namespace, name, version, ip_hash, user_agent, data + ) + values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb)`, + [ + input.audit_event.request_id, + input.audit_event.event_type, + input.audit_event.principal_type ?? null, + input.audit_event.principal_id ?? null, + input.audit_event.capability_key_id ?? null, + input.audit_event.namespace ?? null, + input.audit_event.name ?? null, + input.audit_event.version ?? null, + input.audit_event.ip_hash ?? null, + input.audit_event.user_agent ?? null, + JSON.stringify(input.audit_event.data ?? {}), + ], + ); + if (input.idempotency) { + const completed = await client.query( + `update idempotency_keys + set status = 'completed', + response_status = $3, + response = $4::jsonb, + completed_at = now() + where key = $1 and request_hash = $2 and status = 'processing'`, + [ + input.idempotency.key, + input.idempotency.request_hash, + input.idempotency.response_status, + JSON.stringify(input.idempotency.response_body), + ], + ); + if (completed.rowCount !== 1) { + throw new ApiError(409, "idempotency_key_conflict", "idempotency key is reserved for another request"); + } + } await client.query("commit"); } catch (error) { await client.query("rollback"); throw error; } }); + return input.version; } - async updatePackageVersionStatus(input: { - namespace: string; - name: string; - version: string; - status: RegistryEntryStatus; - reason?: string; - request_id: string; - admin_actor: string; - }): Promise { - const row = await this.withClient(async (client) => { + async listPackageEvidence(namespace: string, name: string, version: string): Promise { + return this.withClient(async (client) => { + const result = await client.query( + `select namespace, name, version, kind, evidence_hash, evidence, + request_id, admin_actor, created_at + from package_version_evidence + where namespace = $1 and name = $2 and version = $3 + order by created_at, kind`, + [namespace, name, version], + ); + return result.rows.map(packageEvidenceFromRow); + }); + } + + async listPackageEvidenceForPackage(namespace: string, name: string): Promise { + return this.withClient(async (client) => { + const result = await client.query( + `select namespace, name, version, kind, evidence_hash, evidence, + request_id, admin_actor, created_at + from package_version_evidence + where namespace = $1 and name = $2 + order by created_at, version, kind`, + [namespace, name], + ); + return result.rows.map(packageEvidenceFromRow); + }); + } + + async findScriptInterfaceCandidates(input: ScriptInterfaceLookup): Promise { + return this.withClient(async (client) => { + const result = await client.query( + `select pv.namespace, pv.name, pv.version, pv.status, pv.artifact, + pv.verification_status, pv.deployment_status, pv.availability_status, + pv.current_commitment_evidence_hash, + pv.source_hash, pv.manifest_hash, pv.edition, pv.compatibility_profile_hash, + pv.capability_key_id, pv.principal_type, pv.principal_id, pv.registry_entry, + pv.snapshot_hash, pv.direct_url, pv.created_at, + pv.registry_environment, pv.chain_network, pv.expires_at, pv.expired_at, pv.purge_after, + pv.static_purged_at, pv.source_purged_at, + e.kind as evidence_kind, e.evidence_hash, e.evidence, + e.request_id as evidence_request_id, e.admin_actor as evidence_admin_actor, + e.created_at as evidence_created_at + from package_versions pv + join lateral ( + select candidate.* + from package_version_evidence candidate + where candidate.namespace = pv.namespace + and candidate.name = pv.name + and candidate.version = pv.version + and candidate.kind = 'deployed' + order by candidate.created_at desc + limit 1 + ) e on true + where pv.artifact->>'kind' = 'deployable_contract' + and pv.artifact->>'profile' = 'ckb_executable' + and pv.registry_entry #>> '{versions,0,profile_contract,interface,format}' = 'ls-idl' + and pv.verification_status in ('hash_bound', 'verified', 'evidence_required') + and pv.deployment_status = 'chain_verified' + and pv.availability_status = 'active' + and (pv.expires_at is null or pv.expires_at > now()) + and lower(regexp_replace(e.evidence->>'code_hash', '^0x', '', 'i')) = lower(regexp_replace($1, '^0x', '', 'i')) + and e.evidence->>'network' = $2 + and ($3::text is null or e.evidence->>'hash_type' = $3) + and ($4::text is null or lower(regexp_replace(e.evidence->>'data_hash', '^0x', '', 'i')) = lower(regexp_replace($4, '^0x', '', 'i'))) + order by e.created_at desc + limit $5`, + [input.code_hash, input.network, input.hash_type ?? null, input.data_hash ?? null, input.limit], + ); + return result.rows.map((row) => ({ + version: packageVersionFromRow(row), + deployment: { + namespace: String(row.namespace), + name: String(row.name), + version: String(row.version), + kind: row.evidence_kind, + evidence_hash: String(row.evidence_hash), + evidence: row.evidence, + request_id: String(row.evidence_request_id), + admin_actor: String(row.evidence_admin_actor), + created_at: new Date(row.evidence_created_at).toISOString(), + }, + })); + }); + } + + async promotePackageVersion(input: PromotePackageVersionInput): Promise<{ + version: PackageVersionRecord; + evidence: PackageEvidenceRecord; + }> { + return this.withClient(async (client) => { await client.query("begin"); try { + const locked = await client.query( + `select namespace, name, version, status, artifact, verification_status, deployment_status, availability_status, + current_commitment_evidence_hash, + source_hash, manifest_hash, + edition, compatibility_profile_hash, + capability_key_id, principal_type, principal_id, registry_entry, + snapshot_hash, direct_url, created_at, + registry_environment, chain_network, expires_at, expired_at, purge_after, + static_purged_at, source_purged_at + from package_versions + where namespace = $1 and name = $2 and version = $3 + for update`, + [input.namespace, input.name, input.version], + ); + const currentRow = locked.rows[0]; + if (!currentRow) { + throw new ApiError(404, "artifact_release_not_found", "artifact release is not known to the registry"); + } + const current = packageVersionFromRow(currentRow); + assertPromotionTransition(current, input.kind); + await client.query( + `insert into package_version_evidence( + namespace, name, version, kind, evidence_hash, evidence, + request_id, admin_actor + ) values ($1, $2, $3, $4, $5, $6::jsonb, $7, $8) + on conflict (namespace, name, version, kind, evidence_hash) do nothing`, + [ + input.namespace, + input.name, + input.version, + input.kind, + input.evidence_hash, + JSON.stringify(input.evidence), + input.request_id, + input.admin_actor, + ], + ); const updated = await client.query( `update package_versions - set status = $4, - yanked_at = case when $4 = 'yanked' then coalesce(yanked_at, now()) else yanked_at end, - yanked_reason = case when $4 = 'yanked' then $5 else yanked_reason end, - quarantined_at = case when $4 = 'quarantined' then coalesce(quarantined_at, now()) else quarantined_at end, - quarantine_reason = case when $4 = 'quarantined' then $5 else quarantine_reason end, - indexed_at = case when $4 in ('indexed_pending', 'verified_build') then coalesce(indexed_at, now()) else indexed_at end, - verified_at = case when $4 = 'verified_build' then coalesce(verified_at, now()) else verified_at end + set status = case + when availability_status <> 'active' then availability_status + when $4 = 'on_chain_committed' then 'on_chain_committed' + when current_commitment_evidence_hash is not null then 'on_chain_committed' + when $4 = 'deployed' then 'deployed' + when deployment_status in ('deployed', 'chain_verified') then 'deployed' + else 'verified_build' + end, + verification_status = case + when $4 = 'reproduced_build' then 'verified' + when $4 = 'verified_build' and $5 = 'compiled' then 'verified' + when $4 = 'verified_build' and $5 = 'structurally_verified' then 'verified' + when $4 = 'verified_build' and $5 = 'hash_bound' then 'hash_bound' + when $4 = 'verified_build' and $5 = 'evidence_required' then 'evidence_required' + else verification_status + end, + deployment_status = case + when $4 = 'deployed' then 'deployed' + when $4 = 'on_chain_committed' then 'chain_verified' + else deployment_status + end, + current_commitment_evidence_hash = case + when $4 = 'on_chain_committed' then $6 + else current_commitment_evidence_hash + end, + indexed_at = coalesce(indexed_at, now()), + verified_at = case when $4 in ('verified_build', 'reproduced_build', 'deployed', 'on_chain_committed') then coalesce(verified_at, now()) else verified_at end where namespace = $1 and name = $2 and version = $3 - returning namespace, name, version, status, source_hash, manifest_hash, + returning namespace, name, version, status, artifact, verification_status, deployment_status, availability_status, + current_commitment_evidence_hash, + source_hash, manifest_hash, + edition, compatibility_profile_hash, capability_key_id, principal_type, principal_id, registry_entry, - snapshot_hash, direct_url, created_at`, - [input.namespace, input.name, input.version, input.status, input.reason ?? null], + snapshot_hash, direct_url, created_at, + registry_environment, chain_network, expires_at, expired_at, purge_after, + static_purged_at, source_purged_at`, + [input.namespace, input.name, input.version, input.kind, input.evidence["verification_level"] ?? null, input.evidence_hash], ); - const record = updated.rows[0]; - if (!record) { - throw new ApiError(404, "package_version_not_found", "package version is not known to the registry"); - } await client.query( `insert into audit_events( request_id, event_type, principal_type, principal_id, capability_key_id, namespace, name, version, data - ) - values ($1, 'admin.package_version.status_updated', $2, $3, $4, $5, $6, $7, $8::jsonb)`, + ) values ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb)`, [ input.request_id, - record.principal_type, - record.principal_id, - record.capability_key_id, + `evidence.${input.kind}.accepted`, + current.principal_type, + current.principal_id, + current.capability_key_id, input.namespace, input.name, input.version, - JSON.stringify({ admin_actor: input.admin_actor, status: input.status, reason: input.reason ?? null }), + JSON.stringify({ admin_actor: input.admin_actor, evidence_hash: input.evidence_hash }), ], ); + if (input.capability_usage) { + await client.query("update capabilities set last_used_at = now() where key_id = $1", [input.capability_usage.key_id]); + await client.query( + `insert into audit_events( + request_id, event_type, principal_type, principal_id, capability_key_id, + namespace, name, version, data + ) values ($1, 'capability.used', $2, $3, $4, $5, $6, $7, $8::jsonb)`, + [ + input.capability_usage.request_id, + input.capability_usage.principal_type, + input.capability_usage.principal_id, + input.capability_usage.key_id, + input.capability_usage.namespace ?? null, + input.capability_usage.name ?? null, + input.capability_usage.version ?? null, + JSON.stringify({ action: input.capability_usage.action }), + ], + ); + } + if (input.idempotency) { + await completeIdempotencyInTransaction(client, input.idempotency); + } + const evidenceResult = await client.query( + `select namespace, name, version, kind, evidence_hash, evidence, + request_id, admin_actor, created_at + from package_version_evidence + where namespace = $1 and name = $2 and version = $3 and kind = $4 and evidence_hash = $5`, + [input.namespace, input.name, input.version, input.kind, input.evidence_hash], + ); await client.query("commit"); - return record; + return { + version: packageVersionFromRow(updated.rows[0]), + evidence: packageEvidenceFromRow(evidenceResult.rows[0]), + }; + } catch (error) { + await client.query("rollback"); + throw error; + } + }); + } + + async recordChainVerifiedDeployment(input: PromotePackageVersionInput): Promise<{ + version: PackageVersionRecord; + evidence: PackageEvidenceRecord; + }> { + if (input.kind !== "deployed") { + throw new ApiError(500, "invalid_deployment_evidence_kind", "chain-verified deployment evidence must use kind deployed"); + } + return this.withClient(async (client) => { + await client.query("begin"); + try { + const locked = await client.query( + `select namespace, name, version, status, artifact, verification_status, deployment_status, availability_status, + current_commitment_evidence_hash, + source_hash, manifest_hash, edition, compatibility_profile_hash, + capability_key_id, principal_type, principal_id, registry_entry, + snapshot_hash, direct_url, created_at, + registry_environment, chain_network, expires_at, expired_at, purge_after, + static_purged_at, source_purged_at + from package_versions + where namespace = $1 and name = $2 and version = $3 + for update`, + [input.namespace, input.name, input.version], + ); + const currentRow = locked.rows[0]; + if (!currentRow) { + throw new ApiError(404, "artifact_release_not_found", "artifact release is not known to the registry"); + } + const current = packageVersionFromRow(currentRow); + if (current.deployment_status === "not_applicable") { + throw new ApiError(409, "deployment_not_applicable", "this artifact profile cannot have a CKB deployment"); + } + if (!(current.verification_status === "verified" || current.verification_status === "hash_bound" || current.verification_status === "evidence_required")) { + throw new ApiError(409, "artifact_not_verified", "artifact verification must finish before recording a deployment"); + } + if (packageVersionRequiresReproduction(current) && current.verification_status !== "verified") { + throw new ApiError(409, "reproduction_evidence_missing", "reproducible artifacts require accepted independent reproduction evidence before deployment"); + } + await client.query( + `insert into package_version_evidence( + namespace, name, version, kind, evidence_hash, evidence, request_id, admin_actor + ) values ($1, $2, $3, 'deployed', $4, $5::jsonb, $6, $7) + on conflict (namespace, name, version, kind, evidence_hash) do nothing`, + [input.namespace, input.name, input.version, input.evidence_hash, JSON.stringify(input.evidence), input.request_id, input.admin_actor], + ); + const updated = await client.query( + `update package_versions + set status = case when availability_status = 'active' then 'deployed' else status end, + deployment_status = 'chain_verified', + current_commitment_evidence_hash = null, + indexed_at = coalesce(indexed_at, now()) + where namespace = $1 and name = $2 and version = $3 + returning namespace, name, version, status, artifact, verification_status, deployment_status, availability_status, + current_commitment_evidence_hash, + source_hash, manifest_hash, edition, compatibility_profile_hash, + capability_key_id, principal_type, principal_id, registry_entry, + snapshot_hash, direct_url, created_at, + registry_environment, chain_network, expires_at, expired_at, purge_after, + static_purged_at, source_purged_at`, + [input.namespace, input.name, input.version], + ); + await client.query( + `insert into audit_events( + request_id, event_type, principal_type, principal_id, capability_key_id, + namespace, name, version, data + ) values ($1, 'deployment.chain_verified', $2, $3, $4, $5, $6, $7, $8::jsonb)`, + [ + input.request_id, + current.principal_type, + current.principal_id, + current.capability_key_id, + input.namespace, + input.name, + input.version, + JSON.stringify({ actor: input.admin_actor, evidence_hash: input.evidence_hash }), + ], + ); + if (input.capability_usage) { + await client.query("update capabilities set last_used_at = now() where key_id = $1", [input.capability_usage.key_id]); + await client.query( + `insert into audit_events( + request_id, event_type, principal_type, principal_id, capability_key_id, + namespace, name, version, data + ) values ($1, 'capability.used', $2, $3, $4, $5, $6, $7, $8::jsonb)`, + [ + input.capability_usage.request_id, + input.capability_usage.principal_type, + input.capability_usage.principal_id, + input.capability_usage.key_id, + input.capability_usage.namespace ?? null, + input.capability_usage.name ?? null, + input.capability_usage.version ?? null, + JSON.stringify({ action: input.capability_usage.action }), + ], + ); + } + if (input.idempotency) { + await completeIdempotencyInTransaction(client, input.idempotency); + } + const evidenceResult = await client.query( + `select namespace, name, version, kind, evidence_hash, evidence, + request_id, admin_actor, created_at + from package_version_evidence + where namespace = $1 and name = $2 and version = $3 and kind = 'deployed' and evidence_hash = $4`, + [input.namespace, input.name, input.version, input.evidence_hash], + ); + await client.query("commit"); + return { + version: packageVersionFromRow(updated.rows[0]), + evidence: packageEvidenceFromRow(evidenceResult.rows[0]), + }; + } catch (error) { + await client.query("rollback"); + throw error; + } + }); + } + + async reconcilePackageVersionLifecycle(input: { + namespace: string; + name: string; + version: string; + status: "verified_build" | "deployed"; + deployment_status: "undeployed" | "deployed" | "chain_verified"; + request_id: string; + reason: string; + }): Promise { + return this.withClient(async (client) => { + await client.query("begin"); + try { + const updated = await client.query( + `update package_versions + set status = case when availability_status = 'active' then $4 else status end, + deployment_status = $5, + current_commitment_evidence_hash = null + where namespace = $1 and name = $2 and version = $3 + returning namespace, name, version, status, artifact, verification_status, deployment_status, availability_status, + current_commitment_evidence_hash, + source_hash, manifest_hash, edition, compatibility_profile_hash, + capability_key_id, principal_type, principal_id, registry_entry, + snapshot_hash, direct_url, created_at, + registry_environment, chain_network, expires_at, expired_at, purge_after, + static_purged_at, source_purged_at`, + [input.namespace, input.name, input.version, input.status, input.deployment_status], + ); + const record = updated.rows[0]; + if (!record) { + throw new ApiError(404, "artifact_release_not_found", "artifact release is not known to the registry"); + } + await client.query( + `insert into audit_events( + request_id, event_type, principal_type, principal_id, capability_key_id, + namespace, name, version, data + ) values ($1, 'lifecycle.chain_state_reconciled', $2, $3, $4, $5, $6, $7, $8::jsonb)`, + [ + input.request_id, + record.principal_type, + record.principal_id, + record.capability_key_id, + input.namespace, + input.name, + input.version, + JSON.stringify({ status: input.status, deployment_status: input.deployment_status, reason: input.reason }), + ], + ); + await client.query("commit"); + return packageVersionFromRow(record); + } catch (error) { + await client.query("rollback"); + throw error; + } + }); + } + + async recordCapabilityUsage(input: { + key_id: string; + principal_type: string; + principal_id: string; + request_id: string; + action: string; + namespace?: string; + name?: string; + version?: string; + }): Promise { + await this.withClient(async (client) => { + await client.query("begin"); + try { + await client.query("update capabilities set last_used_at = now() where key_id = $1", [input.key_id]); + await client.query( + `insert into audit_events( + request_id, event_type, principal_type, principal_id, capability_key_id, + namespace, name, version, data + ) + values ($1, 'capability.used', $2, $3, $4, $5, $6, $7, $8::jsonb)`, + [ + input.request_id, + input.principal_type, + input.principal_id, + input.key_id, + input.namespace ?? null, + input.name ?? null, + input.version ?? null, + JSON.stringify({ action: input.action }), + ], + ); + await client.query("commit"); + } catch (error) { + await client.query("rollback"); + throw error; + } + }); + } + + async updatePackageVersionStatus(input: { + namespace: string; + name: string; + version: string; + status: AvailabilityStatus; + reason?: string; + request_id: string; + admin_actor: string; + audit_event_type?: string; + capability_usage?: PublishAdmissionInput["capability_usage"]; + idempotency?: PublishAdmissionInput["idempotency"]; + }): Promise { + const row = await this.withClient(async (client) => { + await client.query("begin"); + try { + const updated = await client.query( + `update package_versions + set status = case + when $4 <> 'active' then $4 + when current_commitment_evidence_hash is not null then 'on_chain_committed' + when deployment_status in ('chain_verified', 'deployed') then 'deployed' + when verification_status in ('verified', 'hash_bound', 'evidence_required') then 'verified_build' + else 'source_published' + end, + availability_status = $4, + yanked_at = case when $4 = 'yanked' then coalesce(yanked_at, now()) else yanked_at end, + yanked_reason = case when $4 = 'yanked' then $5 else yanked_reason end, + quarantined_at = case when $4 = 'quarantined' then coalesce(quarantined_at, now()) else quarantined_at end, + quarantine_reason = case when $4 = 'quarantined' then $5 else quarantine_reason end + where namespace = $1 and name = $2 and version = $3 + returning namespace, name, version, status, artifact, verification_status, deployment_status, availability_status, + current_commitment_evidence_hash, + source_hash, manifest_hash, + edition, compatibility_profile_hash, + capability_key_id, principal_type, principal_id, registry_entry, + snapshot_hash, direct_url, created_at, + registry_environment, chain_network, expires_at, expired_at, purge_after, + static_purged_at, source_purged_at`, + [input.namespace, input.name, input.version, input.status, input.reason ?? null], + ); + const record = updated.rows[0]; + if (!record) { + throw new ApiError(404, "artifact_release_not_found", "artifact release is not known to the registry"); + } + await client.query( + `insert into audit_events( + request_id, event_type, principal_type, principal_id, capability_key_id, + namespace, name, version, data + ) + values ($1, $9, $2, $3, $4, $5, $6, $7, $8::jsonb)`, + [ + input.request_id, + record.principal_type, + record.principal_id, + record.capability_key_id, + input.namespace, + input.name, + input.version, + JSON.stringify({ admin_actor: input.admin_actor, status: input.status, reason: input.reason ?? null }), + input.audit_event_type ?? "admin.package_version.status_updated", + ], + ); + if (input.capability_usage) { + await client.query("update capabilities set last_used_at = now() where key_id = $1", [input.capability_usage.key_id]); + await client.query( + `insert into audit_events( + request_id, event_type, principal_type, principal_id, capability_key_id, + namespace, name, version, data + ) values ($1, 'capability.used', $2, $3, $4, $5, $6, $7, $8::jsonb)`, + [ + input.capability_usage.request_id, + input.capability_usage.principal_type, + input.capability_usage.principal_id, + input.capability_usage.key_id, + input.capability_usage.namespace ?? null, + input.capability_usage.name ?? null, + input.capability_usage.version ?? null, + JSON.stringify({ action: input.capability_usage.action }), + ], + ); + } + if (input.idempotency) { + await completeIdempotencyInTransaction(client, input.idempotency); + } + await client.query("commit"); + return record; } catch (error) { await client.query("rollback"); throw error; @@ -640,6 +1755,16 @@ export class SqlRegistryStore implements RegistryStore { }); } + async releaseNonce(input: { nonce_key: string; request_id: string }): Promise { + await this.withClient(async (client) => { + await client.query( + `delete from used_nonces + where nonce_key = $1 and request_id = $2`, + [input.nonce_key, input.request_id], + ); + }); + } + async reserveIdempotencyKey(input: { key: string; request_hash: string; @@ -705,7 +1830,7 @@ export class SqlRegistryStore implements RegistryStore { response_status = $3, response = $4::jsonb, completed_at = now() - where key = $1 and request_hash = $2 + where key = $1 and request_hash = $2 and status = 'processing' returning key, request_hash, request_id, status, response_status, response, expires_at, created_at, completed_at`, [input.key, input.request_hash, input.response_status, JSON.stringify(input.response_body)], @@ -731,6 +1856,380 @@ export class SqlRegistryStore implements RegistryStore { }); } + async claimVerificationJob(input: { + worker_id: string; + lease_seconds: number; + now_iso: string; + }): Promise { + return this.withClient(async (client) => { + const result = await client.query( + `with candidate as ( + select id + from verification_jobs + where ( + status in ('queued', 'retry_wait') and available_at <= $3 + ) or ( + status in ('running', 'publishing') and lease_expires_at <= $3 + ) + order by available_at, created_at + for update skip locked + limit 1 + ), claimed as ( + update verification_jobs job + set status = case when job.evidence_hash is null then 'running' else 'publishing' end, + attempt_count = job.attempt_count + 1, + lease_owner = $1, + lease_expires_at = $3::timestamptz + make_interval(secs => $2), + started_at = coalesce(job.started_at, $3::timestamptz), + updated_at = $3::timestamptz + from candidate + where job.id = candidate.id + returning job.* + ) + select claimed.*, + pv.artifact, pv.source_hash, pv.manifest_hash, pv.compatibility_profile_hash, pv.snapshot_hash, + ss.r2_key as snapshot_object_key, ss.size_bytes as snapshot_size_bytes, + ss.content_type as snapshot_content_type + from claimed + join package_versions pv using (namespace, name, version) + join source_snapshots ss on ss.snapshot_hash = pv.snapshot_hash`, + [input.worker_id, input.lease_seconds, input.now_iso], + ); + return result.rows[0] ? verificationJobFromRow(result.rows[0]) : null; + }); + } + + async promoteVerifiedBuildForJob(input: { + job_id: string; + worker_id: string; + evidence_hash: string; + evidence: Record; + request_id: string; + admin_actor: string; + }): Promise<{ job: VerificationJobRecord; version: PackageVersionRecord; evidence: PackageEvidenceRecord }> { + return this.withClient(async (client) => { + await client.query("begin"); + try { + const locked = await client.query( + `select job.namespace, job.name, job.version, + pv.status, pv.artifact, pv.verification_status, pv.deployment_status, pv.availability_status, + pv.source_hash, pv.manifest_hash, pv.edition, + pv.compatibility_profile_hash, pv.capability_key_id, + pv.principal_type, pv.principal_id, pv.registry_entry, + pv.snapshot_hash, pv.direct_url, pv.created_at + from verification_jobs job + join package_versions pv using (namespace, name, version) + where job.id = $1 + and job.status = 'running' + and job.lease_owner = $2 + and job.lease_expires_at > now() + for update of job, pv`, + [input.job_id, input.worker_id], + ); + const currentRow = locked.rows[0]; + if (!currentRow) { + throw new ApiError(409, "verification_job_lease_lost", "verification job lease is no longer owned by this worker"); + } + const current = packageVersionFromRow(currentRow); + assertPromotionTransition(current, "verified_build"); + await client.query( + `insert into package_version_evidence( + namespace, name, version, kind, evidence_hash, evidence, + request_id, admin_actor + ) values ($1, $2, $3, 'verified_build', $4, $5::jsonb, $6, $7) + on conflict (namespace, name, version, kind, evidence_hash) do nothing`, + [ + current.namespace, + current.name, + current.version, + input.evidence_hash, + JSON.stringify(input.evidence), + input.request_id, + input.admin_actor, + ], + ); + const updatedVersion = await client.query( + `update package_versions + set status = case + when availability_status <> 'active' then availability_status + when current_commitment_evidence_hash is not null then 'on_chain_committed' + when deployment_status in ('deployed', 'chain_verified') then 'deployed' + else 'verified_build' + end, + verification_status = case + when $4 = 'compiled' then 'verified' + when $4 = 'structurally_verified' then 'verified' + when $4 = 'hash_bound' then 'hash_bound' + when $4 = 'evidence_required' then 'evidence_required' + else verification_status + end, + indexed_at = coalesce(indexed_at, now()), + verified_at = coalesce(verified_at, now()) + where namespace = $1 and name = $2 and version = $3 + returning namespace, name, version, status, artifact, verification_status, deployment_status, availability_status, + current_commitment_evidence_hash, + source_hash, manifest_hash, + edition, compatibility_profile_hash, capability_key_id, + principal_type, principal_id, registry_entry, snapshot_hash, + direct_url, created_at, + registry_environment, chain_network, expires_at, expired_at, purge_after, + static_purged_at, source_purged_at`, + [current.namespace, current.name, current.version, input.evidence["verification_level"] ?? null], + ); + await client.query( + `update verification_jobs + set status = 'publishing', evidence_hash = $3, evidence = $4::jsonb, + updated_at = now() + where id = $1 and lease_owner = $2`, + [input.job_id, input.worker_id, input.evidence_hash, JSON.stringify(input.evidence)], + ); + await client.query( + `insert into audit_events( + request_id, event_type, principal_type, principal_id, capability_key_id, + namespace, name, version, data + ) values ($1, 'evidence.verified_build.accepted', $2, $3, $4, $5, $6, $7, $8::jsonb)`, + [ + input.request_id, + current.principal_type, + current.principal_id, + current.capability_key_id, + current.namespace, + current.name, + current.version, + JSON.stringify({ admin_actor: input.admin_actor, evidence_hash: input.evidence_hash, job_id: input.job_id }), + ], + ); + const evidenceResult = await client.query( + `select namespace, name, version, kind, evidence_hash, evidence, + request_id, admin_actor, created_at + from package_version_evidence + where namespace = $1 and name = $2 and version = $3 + and kind = 'verified_build' and evidence_hash = $4`, + [current.namespace, current.name, current.version, input.evidence_hash], + ); + const job = await this.verificationJobById(client, input.job_id); + await client.query("commit"); + return { + job, + version: packageVersionFromRow(updatedVersion.rows[0]), + evidence: packageEvidenceFromRow(evidenceResult.rows[0]), + }; + } catch (error) { + await client.query("rollback"); + throw error; + } + }); + } + + async completeVerificationJob(input: { job_id: string; worker_id: string }): Promise { + return this.withClient(async (client) => { + await client.query("begin"); + try { + const updated = await client.query( + `update verification_jobs + set status = 'succeeded', lease_owner = null, lease_expires_at = null, + completed_at = now(), updated_at = now(), + last_error_code = null, last_error_message = null + where id = $1 and status = 'publishing' and lease_owner = $2 + and lease_expires_at > now() + returning namespace, name, version, attempt_count, evidence_hash`, + [input.job_id, input.worker_id], + ); + const row = updated.rows[0]; + if (!row) { + throw new ApiError(409, "verification_job_lease_lost", "verification job lease is no longer owned by this worker"); + } + await client.query( + `insert into audit_events(request_id, event_type, namespace, name, version, data) + values ($1, 'verification.succeeded', $2, $3, $4, $5::jsonb)`, + [ + `verification:${input.job_id}`, + row.namespace, + row.name, + row.version, + JSON.stringify({ job_id: input.job_id, attempt_count: row.attempt_count, evidence_hash: row.evidence_hash }), + ], + ); + const job = await this.verificationJobById(client, input.job_id); + await client.query("commit"); + return job; + } catch (error) { + await client.query("rollback"); + throw error; + } + }); + } + + async requestStaticSync(input: { namespace: string; name: string; version: string; error_message: string }): Promise { + await this.withClient(async (client) => { + await client.query( + `update verification_jobs + set status = 'retry_wait', lease_owner = null, lease_expires_at = null, + available_at = now(), completed_at = null, updated_at = now(), + last_error_code = 'static_registry_sync_deferred', last_error_message = $4 + where namespace = $1 and name = $2 and version = $3 + and status not in ('running', 'publishing')`, + [input.namespace, input.name, input.version, input.error_message], + ); + }); + } + + async failVerificationJob(input: { + job_id: string; + worker_id: string; + error_code: string; + error_message: string; + retryable: boolean; + retry_after_seconds: number; + request_id: string; + }): Promise { + return this.withClient(async (client) => { + await client.query("begin"); + try { + const updated = await client.query( + `update verification_jobs + set status = case + when $3::boolean and attempt_count < max_attempts + then 'retry_wait' + else 'dead_letter' + end, + available_at = now() + make_interval(secs => case + when $3::boolean and attempt_count < max_attempts then $4 + else 0 + end), + lease_owner = null, + lease_expires_at = null, + last_error_code = $5, + last_error_message = $6, + updated_at = now() + where id = $1 and lease_owner = $2 + and status in ('running', 'publishing') + and lease_expires_at > now() + returning namespace, name, version, status, attempt_count`, + [ + input.job_id, + input.worker_id, + input.retryable, + input.retry_after_seconds, + input.error_code, + input.error_message, + ], + ); + const row = updated.rows[0]; + if (!row) { + throw new ApiError(409, "verification_job_lease_lost", "verification job lease is no longer owned by this worker"); + } + const retry = row.status === "retry_wait"; + await client.query( + `insert into audit_events(request_id, event_type, namespace, name, version, data) + values ($1, $2, $3, $4, $5, $6::jsonb)`, + [ + input.request_id, + retry ? "verification.retry_scheduled" : "verification.dead_lettered", + row.namespace, + row.name, + row.version, + JSON.stringify({ + job_id: input.job_id, + attempt_count: row.attempt_count, + error_code: input.error_code, + retry_after_seconds: retry ? input.retry_after_seconds : null, + }), + ], + ); + const job = await this.verificationJobById(client, input.job_id); + await client.query("commit"); + return job; + } catch (error) { + await client.query("rollback"); + throw error; + } + }); + } + + async retryVerificationJob(input: { + job_id: string; + request_id: string; + admin_actor: string; + }): Promise { + return this.withClient(async (client) => { + await client.query("begin"); + try { + const updated = await client.query( + `update verification_jobs + set status = 'queued', attempt_count = 0, available_at = now(), + lease_owner = null, lease_expires_at = null, + last_error_code = null, last_error_message = null, + updated_at = now() + where id = $1 and status = 'dead_letter' + returning namespace, name, version`, + [input.job_id], + ); + const row = updated.rows[0]; + if (!row) { + const exists = await client.query("select status from verification_jobs where id = $1", [input.job_id]); + if (!exists.rows[0]) throw new ApiError(404, "verification_job_not_found", "verification job was not found"); + throw new ApiError(409, "verification_job_not_dead_letter", "only dead-letter verification jobs can be retried manually"); + } + await client.query( + `insert into audit_events(request_id, event_type, namespace, name, version, data) + values ($1, 'verification.requeued', $2, $3, $4, $5::jsonb)`, + [input.request_id, row.namespace, row.name, row.version, JSON.stringify({ job_id: input.job_id, admin_actor: input.admin_actor })], + ); + const job = await this.verificationJobById(client, input.job_id); + await client.query("commit"); + return job; + } catch (error) { + await client.query("rollback"); + throw error; + } + }); + } + + async getVerificationQueueMetrics(): Promise { + return this.withClient(async (client) => { + const result = await client.query( + `select status, count(*)::integer as count, + min(available_at) filter (where status in ('queued', 'retry_wait')) as oldest_available_at, + min(updated_at) filter (where status = 'dead_letter') as oldest_dead_letter_at + from verification_jobs + group by status`, + ); + const counts: Record = { + queued: 0, + running: 0, + publishing: 0, + retry_wait: 0, + succeeded: 0, + dead_letter: 0, + }; + let oldestAvailable: string | null = null; + let oldestDeadLetter: string | null = null; + for (const row of result.rows) { + counts[row.status as VerificationJobStatus] = Number(row.count); + if (row.oldest_available_at) oldestAvailable = new Date(row.oldest_available_at).toISOString(); + if (row.oldest_dead_letter_at) oldestDeadLetter = new Date(row.oldest_dead_letter_at).toISOString(); + } + return { counts, oldest_available_at: oldestAvailable, oldest_dead_letter_at: oldestDeadLetter }; + }); + } + + private async verificationJobById(client: Client, jobId: string): Promise { + const result = await client.query( + `select job.*, + pv.artifact, pv.source_hash, pv.manifest_hash, pv.compatibility_profile_hash, pv.snapshot_hash, + ss.r2_key as snapshot_object_key, ss.size_bytes as snapshot_size_bytes, + ss.content_type as snapshot_content_type + from verification_jobs job + join package_versions pv using (namespace, name, version) + join source_snapshots ss on ss.snapshot_hash = pv.snapshot_hash + where job.id = $1`, + [jobId], + ); + if (!result.rows[0]) throw new ApiError(404, "verification_job_not_found", "verification job was not found"); + return verificationJobFromRow(result.rows[0]); + } + async cleanupExpiredState(input: { now_iso: string; quota_events_before_iso: string; @@ -740,12 +2239,61 @@ export class SqlRegistryStore implements RegistryStore { try { const usedNonces = await client.query("delete from used_nonces where expires_at < $1", [input.now_iso]); const idempotencyKeys = await client.query("delete from idempotency_keys where expires_at < $1", [input.now_iso]); + const authorisationSessions = await client.query( + `delete from authorisation_sessions + where (status = 'pending' and expires_at < $1) + or (status <> 'pending' + and coalesce(completed_at, updated_at) < $1::timestamptz - ($2 * interval '1 hour'))`, + [input.now_iso, AUTHORISATION_SESSION_TERMINAL_RETENTION_HOURS], + ); const quotaEvents = await client.query("delete from quota_events where created_at < $1", [input.quota_events_before_iso]); + const expiredVersions = await client.query( + `update package_versions + set expired_at = $1 + where registry_environment = 'testnet-sandbox' + and expires_at <= $1 + and expired_at is null`, + [input.now_iso], + ); + const staticObjects = await client.query( + `select namespace, name, version + from package_versions + where registry_environment = 'testnet-sandbox' + and expires_at <= $1 + and static_purged_at is null`, + [input.now_iso], + ); + const sourceObjects = await client.query( + `select distinct ss.r2_key, ss.snapshot_hash + from source_snapshots ss + join package_versions due on due.snapshot_hash = ss.snapshot_hash + where due.registry_environment = 'testnet-sandbox' + and due.purge_after <= $1 + and due.source_purged_at is null + and not exists ( + select 1 from package_versions active + where active.snapshot_hash = ss.snapshot_hash + and (active.purge_after is null or active.purge_after > $1) + )`, + [input.now_iso], + ); await client.query("commit"); return { used_nonces_deleted: usedNonces.rowCount ?? 0, idempotency_keys_deleted: idempotencyKeys.rowCount ?? 0, + authorisation_sessions_deleted: authorisationSessions.rowCount ?? 0, quota_events_deleted: quotaEvents.rowCount ?? 0, + package_versions_expired: expiredVersions.rowCount ?? 0, + static_objects: staticObjects.rows.map((row) => ({ + key: `artifacts/${row.namespace}/${row.name}/releases/${row.version}.json`, + namespace: String(row.namespace), + name: String(row.name), + version: String(row.version), + })), + source_objects: sourceObjects.rows.map((row) => ({ + key: String(row.r2_key), + snapshot_hash: String(row.snapshot_hash), + })), }; } catch (error) { await client.query("rollback"); @@ -753,16 +2301,82 @@ export class SqlRegistryStore implements RegistryStore { } }); } + + async markSandboxObjectsPurged(input: { + static_objects: import("./store").SandboxObjectCandidate[]; + source_objects: import("./store").SandboxObjectCandidate[]; + purged_at: string; + }): Promise { + await this.withClient(async (client) => { + await client.query("begin"); + try { + for (const candidate of input.static_objects) { + if (!candidate.namespace || !candidate.name || !candidate.version) continue; + await client.query( + `update package_versions set static_purged_at = $4 + where namespace = $1 and name = $2 and version = $3 + and registry_environment = 'testnet-sandbox'`, + [candidate.namespace, candidate.name, candidate.version, input.purged_at], + ); + } + const snapshotHashes = input.source_objects + .map((candidate) => candidate.snapshot_hash) + .filter((value): value is string => !!value); + if (snapshotHashes.length > 0) { + await client.query( + `update package_versions set source_purged_at = $2 + where snapshot_hash = any($1::text[]) + and registry_environment = 'testnet-sandbox'`, + [snapshotHashes, input.purged_at], + ); + await client.query( + `update source_snapshots set hidden_at = coalesce(hidden_at, $2), hidden_reason = 'testnet_sandbox_expired' + where snapshot_hash = any($1::text[])`, + [snapshotHashes, input.purged_at], + ); + } + await client.query("commit"); + } catch (error) { + await client.query("rollback"); + throw error; + } + }); + } +} + +async function completeIdempotencyInTransaction( + client: Client, + input: NonNullable, +): Promise { + const completed = await client.query( + `update idempotency_keys + set status = 'completed', + response_status = $3, + response = $4::jsonb, + completed_at = now() + where key = $1 and request_hash = $2 and status = 'processing'`, + [input.key, input.request_hash, input.response_status, JSON.stringify(input.response_body)], + ); + if (completed.rowCount !== 1) { + throw new ApiError(409, "idempotency_key_conflict", "idempotency key is not owned by this command"); + } } function packageVersionFromRow(row: any): PackageVersionRecord { - return { + const record: PackageVersionRecord = { namespace: row.namespace, name: row.name, version: row.version, status: row.status, + artifact: row.artifact, + verification_status: row.verification_status, + deployment_status: row.deployment_status, + availability_status: row.availability_status, + current_commitment_evidence_hash: row.current_commitment_evidence_hash ? String(row.current_commitment_evidence_hash) : null, source_hash: row.source_hash, - ...(row.manifest_hash ? { manifest_hash: row.manifest_hash } : {}), + manifest_hash: row.manifest_hash, + ...(row.edition ? { edition: row.edition } : {}), + ...(row.compatibility_profile_hash ? { compatibility_profile_hash: row.compatibility_profile_hash } : {}), capability_key_id: row.capability_key_id, principal_type: row.principal_type, principal_id: row.principal_id, @@ -770,6 +2384,94 @@ function packageVersionFromRow(row: any): PackageVersionRecord { snapshot_hash: row.snapshot_hash, direct_url: row.direct_url, created_at: new Date(row.created_at).toISOString(), + registry_environment: row.registry_environment ?? "production", + network: row.chain_network ?? "mainnet", + expires_at: row.expires_at ? new Date(row.expires_at).toISOString() : null, + expired_at: row.expired_at ? new Date(row.expired_at).toISOString() : null, + purge_after: row.purge_after ? new Date(row.purge_after).toISOString() : null, + static_purged_at: row.static_purged_at ? new Date(row.static_purged_at).toISOString() : null, + source_purged_at: row.source_purged_at ? new Date(row.source_purged_at).toISOString() : null, + }; + record.status = deriveRegistryEntryStatus(record, record.status); + return record; +} + +function authorisationSessionFromRow(row: any): AuthorisationSessionRecord { + return { + session_id: String(row.session_id), + poll_token_hash: String(row.poll_token_hash), + browser_token_hash: String(row.browser_token_hash), + registry_origin: String(row.registry_origin), + website_origin: String(row.website_origin), + capability_pubkey: String(row.capability_pubkey), + requested_scopes: Array.isArray(row.requested_scopes) ? row.requested_scopes.map(String) : [], + capability_expires_at: new Date(row.capability_expires_at).toISOString(), + cli_version: String(row.cli_version), + namespace: String(row.namespace), + name: String(row.name), + artifact_kind: row.artifact_kind, + status: row.status, + principal_type: row.principal_type ?? null, + principal_id: row.principal_id ? String(row.principal_id) : null, + payload: row.payload && typeof row.payload === "object" && !Array.isArray(row.payload) ? row.payload : null, + challenge_token_hash: row.challenge_token_hash ? String(row.challenge_token_hash) : null, + capability_key_id: row.capability_key_id ? String(row.capability_key_id) : null, + namespace_status: row.namespace_status ?? null, + created_at: new Date(row.created_at).toISOString(), + updated_at: new Date(row.updated_at).toISOString(), + expires_at: new Date(row.expires_at).toISOString(), + completed_at: row.completed_at ? new Date(row.completed_at).toISOString() : null, + }; +} + +function packageEvidenceFromRow(row: any): PackageEvidenceRecord { + if (!row) { + throw new ApiError(500, "evidence_record_missing", "package evidence write did not return a readable record"); + } + return { + namespace: row.namespace, + name: row.name, + version: row.version, + kind: row.kind, + evidence_hash: row.evidence_hash, + evidence: row.evidence && typeof row.evidence === "object" && !Array.isArray(row.evidence) ? row.evidence : {}, + request_id: row.request_id, + admin_actor: row.admin_actor, + created_at: new Date(row.created_at).toISOString(), + }; +} + +function verificationJobFromRow(row: any): VerificationJobRecord { + if (!row) { + throw new ApiError(500, "verification_job_record_missing", "verification job write did not return a readable record"); + } + return { + id: String(row.id), + namespace: String(row.namespace), + name: String(row.name), + version: String(row.version), + status: row.status as VerificationJobStatus, + attempt_count: Number(row.attempt_count), + max_attempts: Number(row.max_attempts), + available_at: new Date(row.available_at).toISOString(), + lease_owner: row.lease_owner ? String(row.lease_owner) : null, + lease_expires_at: row.lease_expires_at ? new Date(row.lease_expires_at).toISOString() : null, + evidence_hash: row.evidence_hash ? String(row.evidence_hash) : null, + evidence: row.evidence && typeof row.evidence === "object" && !Array.isArray(row.evidence) ? row.evidence : null, + last_error_code: row.last_error_code ? String(row.last_error_code) : null, + last_error_message: row.last_error_message ? String(row.last_error_message) : null, + created_at: new Date(row.created_at).toISOString(), + updated_at: new Date(row.updated_at).toISOString(), + started_at: row.started_at ? new Date(row.started_at).toISOString() : null, + completed_at: row.completed_at ? new Date(row.completed_at).toISOString() : null, + source_hash: String(row.source_hash), + manifest_hash: String(row.manifest_hash), + artifact: row.artifact, + ...(row.compatibility_profile_hash ? { compatibility_profile_hash: String(row.compatibility_profile_hash) } : {}), + snapshot_hash: String(row.snapshot_hash), + snapshot_object_key: String(row.snapshot_object_key), + snapshot_size_bytes: Number(row.snapshot_size_bytes), + snapshot_content_type: String(row.snapshot_content_type), }; } diff --git a/services/registry-api/src/store.ts b/services/registry-api/src/store.ts index 4049ef19..08864e25 100644 --- a/services/registry-api/src/store.ts +++ b/services/registry-api/src/store.ts @@ -2,9 +2,16 @@ import { ApiError, capabilityKeyId, canonicalJson, + type ArtifactDescriptor, + type ArtifactKind, + type AvailabilityStatus, type CapabilityAuthorisationPayload, + type DeploymentStatus, + type PrincipalType, type PublishPayload, type RegistryEntryStatus, + type RegistryIndexEntry, + type VerificationStatus, } from "./domain"; export type NamespaceStatus = "active" | "review_pending" | "reserved" | "rejected" | "quarantined"; @@ -17,7 +24,7 @@ export interface ReservedNamespaceRecord { export interface CapabilityRecord { key_id: string; - principal_type: "joyid_ckb"; + principal_type: PrincipalType; principal_id: string; capability_pubkey: string; scopes: string[]; @@ -27,6 +34,35 @@ export interface CapabilityRecord { last_used_at?: string | null; } +export type AuthorisationSessionStatus = "pending" | "authorised" | "review_pending"; +export const AUTHORISATION_SESSION_TERMINAL_RETENTION_HOURS = 24; + +export interface AuthorisationSessionRecord { + session_id: string; + poll_token_hash: string; + browser_token_hash: string; + registry_origin: string; + website_origin: string; + capability_pubkey: string; + requested_scopes: string[]; + capability_expires_at: string; + cli_version: string; + namespace: string; + name: string; + artifact_kind: ArtifactKind; + status: AuthorisationSessionStatus; + principal_type?: PrincipalType | null; + principal_id?: string | null; + payload?: CapabilityAuthorisationPayload | null; + challenge_token_hash?: string | null; + capability_key_id?: string | null; + namespace_status?: NamespaceClaimResult["status"] | null; + created_at: string; + updated_at: string; + expires_at: string; + completed_at?: string | null; +} + export interface SnapshotRecord { snapshot_hash: string; r2_key: string; @@ -40,15 +76,92 @@ export interface PackageVersionRecord { name: string; version: string; status: RegistryEntryStatus; + artifact: ArtifactDescriptor; + verification_status: VerificationStatus; + deployment_status: DeploymentStatus; + availability_status: AvailabilityStatus; + /** Accepted commitment evidence that was observed in a currently live mainnet Cell. */ + current_commitment_evidence_hash?: string | null; source_hash: string; - manifest_hash?: string; + manifest_hash: string; + /** Source-language semantics, not a compiler or wire-ABI version. */ + edition?: "2026"; + /** Complete resolved compatibility identity across independent axes. */ + compatibility_profile_hash?: string; capability_key_id: string; - principal_type: string; + principal_type: PrincipalType; principal_id: string; - registry_entry: Record; + registry_entry: RegistryIndexEntry; snapshot_hash: string; direct_url: string; created_at: string; + registry_environment?: "production" | "testnet-sandbox"; + network?: "mainnet" | "testnet"; + expires_at?: string | null; + expired_at?: string | null; + purge_after?: string | null; + static_purged_at?: string | null; + source_purged_at?: string | null; +} + +export interface PackageVersionQuery { + query?: string; + namespace?: string; + name?: string; + artifact_kind?: ArtifactKind; + verification_status?: VerificationStatus; + verification_statuses?: VerificationStatus[]; + deployment_status?: DeploymentStatus; + availability_status?: AvailabilityStatus; + status?: RegistryEntryStatus; + statuses?: RegistryEntryStatus[]; + limit: number; + offset: number; +} + +export interface ArtifactPackagePage { + records: PackageVersionRecord[]; + has_more: boolean; +} + +export type PackageEvidenceKind = "verified_build" | "reproduced_build" | "deployed" | "on_chain_committed"; + +export interface PackageEvidenceRecord { + namespace: string; + name: string; + version: string; + kind: PackageEvidenceKind; + evidence_hash: string; + evidence: Record; + request_id: string; + admin_actor: string; + created_at: string; +} + +export interface ScriptInterfaceLookup { + code_hash: string; + network: "mainnet" | "testnet"; + hash_type?: "data" | "data1" | "data2" | "type"; + data_hash?: string; + limit: number; +} + +export interface ScriptInterfaceCandidate { + version: PackageVersionRecord; + deployment: PackageEvidenceRecord; +} + +export interface PromotePackageVersionInput { + namespace: string; + name: string; + version: string; + kind: PackageEvidenceKind; + evidence_hash: string; + evidence: Record; + request_id: string; + admin_actor: string; + capability_usage?: PublishAdmissionInput["capability_usage"]; + idempotency?: PublishAdmissionInput["idempotency"]; } export interface IdempotencyRecord { @@ -67,6 +180,61 @@ export interface MaintenanceResult { used_nonces_deleted: number; idempotency_keys_deleted: number; quota_events_deleted: number; + package_versions_expired?: number; + authorisation_sessions_deleted?: number; + static_objects?: SandboxObjectCandidate[]; + source_objects?: SandboxObjectCandidate[]; +} + +export interface SandboxObjectCandidate { + key: string; + namespace?: string; + name?: string; + version?: string; + snapshot_hash?: string; +} + +export type VerificationJobStatus = + | "queued" + | "running" + | "publishing" + | "retry_wait" + | "succeeded" + | "dead_letter"; + +export interface VerificationJobRecord { + id: string; + namespace: string; + name: string; + version: string; + status: VerificationJobStatus; + attempt_count: number; + max_attempts: number; + available_at: string; + lease_owner?: string | null; + lease_expires_at?: string | null; + evidence_hash?: string | null; + evidence?: Record | null; + last_error_code?: string | null; + last_error_message?: string | null; + created_at: string; + updated_at: string; + started_at?: string | null; + completed_at?: string | null; + source_hash: string; + manifest_hash: string; + artifact: ArtifactDescriptor; + compatibility_profile_hash?: string; + snapshot_hash: string; + snapshot_object_key: string; + snapshot_size_bytes: number; + snapshot_content_type: string; +} + +export interface VerificationQueueMetrics { + counts: Record; + oldest_available_at?: string | null; + oldest_dead_letter_at?: string | null; } export type IdempotencyReservation = @@ -94,6 +262,36 @@ export interface AuditEventRecord extends AuditEventInput { created_at: string; } +export interface PublishAdmissionInput { + package: { + namespace: string; + name: string; + principal_type: PrincipalType; + principal_id: string; + source_repo?: string; + request_id: string; + }; + snapshot: SnapshotRecord; + version: PackageVersionRecord; + capability_usage: { + key_id: string; + principal_type: PrincipalType; + principal_id: string; + request_id: string; + action: string; + namespace?: string; + name?: string; + version?: string; + }; + audit_event: AuditEventInput; + idempotency?: { + key: string; + request_hash: string; + response_status: number; + response_body: Record; + }; +} + export interface ListAuditEventsInput { event_type?: string; principal_type?: string; @@ -115,20 +313,57 @@ export interface NamespaceRecord { namespace: string; status: NamespaceStatus; review_reason?: string; - owner_principal_type: "joyid_ckb"; + owner_principal_type: PrincipalType; owner_principal_id: string; } +export interface AuthorisationSessionCompletionInput { + session_id: string; + expected_challenge_token_hash: string; + payload: CapabilityAuthorisationPayload; + principal_signature: unknown; + nonce: { + nonce_key: string; + protocol: string; + action: string; + nonce: string; + expires_at: string; + principal_type: PrincipalType; + principal_id: string; + }; + request_id: string; + now_iso: string; + namespace_claim_cooldown_seconds: number; +} + +export interface AuthorisationSessionCompletionResult { + session: AuthorisationSessionRecord; + replayed: boolean; +} + export interface RegistryStore { + healthCheck(): Promise; + withMaintenanceLease(name: string, task: () => Promise): Promise; recordCapability(input: { payload: CapabilityAuthorisationPayload; - joyid_signature: unknown; + principal_signature: unknown; request_id: string; }): Promise; getCapability(keyId: string): Promise; + createAuthorisationSession(input: AuthorisationSessionRecord & { request_id: string }): Promise; + getAuthorisationSession(sessionId: string): Promise; + prepareAuthorisationSession(input: { + session_id: string; + principal_type: PrincipalType; + principal_id: string; + payload: CapabilityAuthorisationPayload; + challenge_token_hash: string; + request_id: string; + }): Promise; + finaliseAuthorisationSession(input: AuthorisationSessionCompletionInput): Promise; revokeCapability(input: { key_id: string; - principal_type: "joyid_ckb"; + principal_type: PrincipalType; principal_id: string; request_id: string; reason?: string; @@ -136,7 +371,7 @@ export interface RegistryStore { getNamespace(namespace: string): Promise; claimNamespace(input: { namespace: string; - principal_type: "joyid_ckb"; + principal_type: PrincipalType; principal_id: string; request_id: string; }): Promise; @@ -154,17 +389,42 @@ export interface RegistryStore { ensurePackage(input: { namespace: string; name: string; - principal_type: string; + principal_type: PrincipalType; principal_id: string; source_repo?: string; request_id: string; }): Promise; recordSnapshot(input: SnapshotRecord): Promise; + getSnapshot(snapshotHash: string): Promise; + getSnapshots(snapshotHashes: string[]): Promise>; getPackageVersion(namespace: string, name: string, version: string): Promise; + listPackageVersions(input: PackageVersionQuery): Promise; + listArtifactPackagePage(input: PackageVersionQuery): Promise; recordPackageVersion(input: PackageVersionRecord): Promise; + admitPackageVersion(input: PublishAdmissionInput): Promise; + listPackageEvidence(namespace: string, name: string, version: string): Promise; + listPackageEvidenceForPackage(namespace: string, name: string): Promise; + findScriptInterfaceCandidates(input: ScriptInterfaceLookup): Promise; + promotePackageVersion(input: PromotePackageVersionInput): Promise<{ + version: PackageVersionRecord; + evidence: PackageEvidenceRecord; + }>; + recordChainVerifiedDeployment(input: PromotePackageVersionInput): Promise<{ + version: PackageVersionRecord; + evidence: PackageEvidenceRecord; + }>; + reconcilePackageVersionLifecycle(input: { + namespace: string; + name: string; + version: string; + status: "verified_build" | "deployed"; + deployment_status: "undeployed" | "deployed" | "chain_verified"; + request_id: string; + reason: string; + }): Promise; recordCapabilityUsage(input: { key_id: string; - principal_type: string; + principal_type: PrincipalType; principal_id: string; request_id: string; action: string; @@ -176,10 +436,13 @@ export interface RegistryStore { namespace: string; name: string; version: string; - status: RegistryEntryStatus; + status: AvailabilityStatus; reason?: string; request_id: string; admin_actor: string; + audit_event_type?: string; + capability_usage?: PublishAdmissionInput["capability_usage"]; + idempotency?: PublishAdmissionInput["idempotency"]; }): Promise; appendAuditEvent(event: AuditEventInput): Promise; listAuditEvents(input: ListAuditEventsInput): Promise; @@ -196,6 +459,10 @@ export interface RegistryStore { principal_id?: string; capability_key_id?: string; }): Promise; + releaseNonce(input: { + nonce_key: string; + request_id: string; + }): Promise; reserveIdempotencyKey(input: { key: string; request_hash: string; @@ -217,6 +484,53 @@ export interface RegistryStore { now_iso: string; quota_events_before_iso: string; }): Promise; + markSandboxObjectsPurged(input: { + static_objects: SandboxObjectCandidate[]; + source_objects: SandboxObjectCandidate[]; + purged_at: string; + }): Promise; + claimVerificationJob(input: { + worker_id: string; + lease_seconds: number; + now_iso: string; + }): Promise; + promoteVerifiedBuildForJob(input: { + job_id: string; + worker_id: string; + evidence_hash: string; + evidence: Record; + request_id: string; + admin_actor: string; + }): Promise<{ + job: VerificationJobRecord; + version: PackageVersionRecord; + evidence: PackageEvidenceRecord; + }>; + completeVerificationJob(input: { + job_id: string; + worker_id: string; + }): Promise; + requestStaticSync(input: { + namespace: string; + name: string; + version: string; + error_message: string; + }): Promise; + failVerificationJob(input: { + job_id: string; + worker_id: string; + error_code: string; + error_message: string; + retryable: boolean; + retry_after_seconds: number; + request_id: string; + }): Promise; + retryVerificationJob(input: { + job_id: string; + request_id: string; + admin_actor: string; + }): Promise; + getVerificationQueueMetrics(): Promise; } const DEFAULT_RESERVED_NAMESPACES: ReservedNamespaceRecord[] = [ @@ -237,10 +551,20 @@ function nowIso(): string { return new Date().toISOString(); } +function packageVersionIsPublic(record: PackageVersionRecord, now = Date.now()): boolean { + return !record.expires_at || Date.parse(record.expires_at) > now; +} + +function sandboxStaticObjectKey(namespace: string, name: string, version: string): string { + return `artifacts/${namespace}/${name}/releases/${version}.json`; +} + export class MemoryRegistryStore implements RegistryStore { capabilities = new Map(); + authorisationSessions = new Map(); namespaces = new Map(); packageVersions = new Map(); + packageEvidence = new Map(); snapshots = new Map(); reservedNamespaces = new Map(DEFAULT_RESERVED_NAMESPACES.map((record) => [record.namespace, record])); auditEvents: AuditEventRecord[] = []; @@ -257,10 +581,25 @@ export class MemoryRegistryStore implements RegistryStore { created_at: string; }>(); idempotencyKeys = new Map(); + verificationJobs = new Map(); + maintenanceLeases = new Set(); + private authorisationSessionCompletionLocks = new Map>(); + + async healthCheck(): Promise {} + + async withMaintenanceLease(name: string, task: () => Promise): Promise { + if (this.maintenanceLeases.has(name)) return null; + this.maintenanceLeases.add(name); + try { + return await task(); + } finally { + this.maintenanceLeases.delete(name); + } + } async recordCapability(input: { payload: CapabilityAuthorisationPayload; - joyid_signature: unknown; + principal_signature: unknown; request_id: string; }): Promise { const key_id = await capabilityKeyId(input.payload.capability_pubkey); @@ -285,7 +624,7 @@ export class MemoryRegistryStore implements RegistryStore { principal_type: record.principal_type, principal_id: record.principal_id, capability_key_id: key_id, - data: { scopes: record.scopes, payload_hash: await hashForMemory(input.payload), joyid_signature_present: !!input.joyid_signature }, + data: { scopes: record.scopes, payload_hash: await hashForMemory(input.payload), principal_signature_present: !!input.principal_signature }, }); return record; } @@ -294,9 +633,170 @@ export class MemoryRegistryStore implements RegistryStore { return this.capabilities.get(keyId) ?? null; } + async createAuthorisationSession( + input: AuthorisationSessionRecord & { request_id: string }, + ): Promise { + if (this.authorisationSessions.has(input.session_id)) { + throw new ApiError(409, "authorisation_session_exists", "authorisation session already exists"); + } + const { request_id: _requestId, ...record } = input; + this.authorisationSessions.set(record.session_id, record); + return record; + } + + async getAuthorisationSession(sessionId: string): Promise { + return this.authorisationSessions.get(sessionId) ?? null; + } + + async prepareAuthorisationSession(input: { + session_id: string; + principal_type: PrincipalType; + principal_id: string; + payload: CapabilityAuthorisationPayload; + challenge_token_hash: string; + request_id: string; + }): Promise { + return this.withAuthorisationSessionCompletionLock("authorisation-store", async () => { + const existing = this.authorisationSessions.get(input.session_id); + if (!existing) throw new ApiError(404, "authorisation_session_not_found", "authorisation session was not found"); + if (existing.status !== "pending") { + throw new ApiError(409, "authorisation_session_complete", "authorisation session has already completed"); + } + const updated: AuthorisationSessionRecord = { + ...existing, + principal_type: input.principal_type, + principal_id: input.principal_id, + payload: input.payload, + challenge_token_hash: input.challenge_token_hash, + updated_at: nowIso(), + }; + this.authorisationSessions.set(input.session_id, updated); + return updated; + }); + } + + async finaliseAuthorisationSession( + input: AuthorisationSessionCompletionInput, + ): Promise { + return this.withAuthorisationSessionCompletionLock("authorisation-store", async () => { + const existing = this.authorisationSessions.get(input.session_id); + if (!existing) throw new ApiError(404, "authorisation_session_not_found", "authorisation session was not found"); + if (existing.status !== "pending") return { session: existing, replayed: true }; + if (Date.parse(existing.expires_at) <= Date.parse(input.now_iso)) { + throw new ApiError(410, "authorisation_session_expired", "authorisation session has expired"); + } + if (existing.challenge_token_hash !== input.expected_challenge_token_hash + || !existing.payload + || canonicalJson(existing.payload) !== canonicalJson(input.payload)) { + throw new ApiError(409, "authorisation_challenge_stale", "authorisation challenge was replaced; request a new wallet challenge"); + } + + const capabilities = new Map(this.capabilities); + const namespaces = new Map(this.namespaces); + const usedNonces = new Map(this.usedNonces); + const quotaEventCount = this.quotaEvents.length; + const sessionBefore = existing; + const auditEventCount = this.auditEvents.length; + try { + if (!await this.consumeNonce({ ...input.nonce, request_id: input.request_id })) { + throw new ApiError(409, "nonce_replay", "signed nonce has already been used"); + } + const namespace = this.namespaces.get(existing.namespace); + if (namespace + && (namespace.owner_principal_type !== input.payload.principal_type + || namespace.owner_principal_id !== input.payload.principal_id)) { + throw new ApiError(409, "namespace_already_claimed", "namespace is already claimed by another principal"); + } + const namespaceClaim = namespace + ? { + namespace: namespace.namespace, + status: namespace.status === "active" ? "active" as const : "review_pending" as const, + ...(namespace.review_reason ? { review_reason: namespace.review_reason } : {}), + } + : await (async () => { + if (input.namespace_claim_cooldown_seconds > 0) { + const quotaKey = `principal:${input.payload.principal_type}:${input.payload.principal_id}`; + const since = new Date( + Date.parse(input.now_iso) - input.namespace_claim_cooldown_seconds * 1000, + ).toISOString(); + if (await this.countRecentQuotaEvents(quotaKey, "namespace_claim_cooldown", since) >= 1) { + throw new ApiError(429, "namespace_claim_cooldown", "namespace claim cooldown is active"); + } + await this.recordQuotaEvent(quotaKey, "namespace_claim_cooldown"); + } + return this.claimNamespace({ + namespace: existing.namespace, + principal_type: input.payload.principal_type, + principal_id: input.payload.principal_id, + request_id: input.request_id, + }); + })(); + const capabilityKey = await capabilityKeyId(input.payload.capability_pubkey); + const existingCapability = this.capabilities.get(capabilityKey); + if (existingCapability + && (existingCapability.principal_type !== input.payload.principal_type + || existingCapability.principal_id !== input.payload.principal_id)) { + throw new ApiError(409, "capability_principal_mismatch", "publishing key is already bound to another principal"); + } + const capability = await this.recordCapability({ + payload: input.payload, + principal_signature: input.principal_signature, + request_id: input.request_id, + }); + const completedAt = input.now_iso; + const completed: AuthorisationSessionRecord = { + ...existing, + status: namespaceClaim.status === "active" ? "authorised" : "review_pending", + capability_key_id: capability.key_id, + namespace_status: namespaceClaim.status, + challenge_token_hash: null, + updated_at: completedAt, + completed_at: completedAt, + }; + this.authorisationSessions.set(input.session_id, completed); + await this.appendAuditEvent({ + request_id: input.request_id, + event_type: "authorisation_session.completed", + principal_type: input.payload.principal_type, + principal_id: input.payload.principal_id, + capability_key_id: capability.key_id, + namespace: existing.namespace, + name: existing.name, + data: { session_id: existing.session_id, namespace_status: namespaceClaim.status }, + }); + return { session: completed, replayed: false }; + } catch (error) { + this.capabilities = capabilities; + this.namespaces = namespaces; + this.usedNonces = usedNonces; + this.quotaEvents.splice(quotaEventCount); + this.authorisationSessions.set(input.session_id, sessionBefore); + this.auditEvents.splice(auditEventCount); + throw error; + } + }); + } + + private async withAuthorisationSessionCompletionLock(sessionId: string, task: () => Promise): Promise { + const previous = this.authorisationSessionCompletionLocks.get(sessionId) ?? Promise.resolve(); + let release = () => {}; + const current = new Promise((resolve) => { release = resolve; }); + const queued = previous.then(() => current); + this.authorisationSessionCompletionLocks.set(sessionId, queued); + await previous; + try { + return await task(); + } finally { + release(); + if (this.authorisationSessionCompletionLocks.get(sessionId) === queued) { + this.authorisationSessionCompletionLocks.delete(sessionId); + } + } + } + async revokeCapability(input: { key_id: string; - principal_type: "joyid_ckb"; + principal_type: PrincipalType; principal_id: string; request_id: string; reason?: string; @@ -325,7 +825,7 @@ export class MemoryRegistryStore implements RegistryStore { async claimNamespace(input: { namespace: string; - principal_type: "joyid_ckb"; + principal_type: PrincipalType; principal_id: string; request_id: string; }): Promise { @@ -412,7 +912,7 @@ export class MemoryRegistryStore implements RegistryStore { async ensurePackage(input: { namespace: string; name: string; - principal_type: string; + principal_type: PrincipalType; principal_id: string; source_repo?: string; request_id: string; @@ -421,7 +921,7 @@ export class MemoryRegistryStore implements RegistryStore { this.namespaces.set(input.namespace, { namespace: input.namespace, status: "active", - owner_principal_type: input.principal_type as "joyid_ckb", + owner_principal_type: input.principal_type, owner_principal_id: input.principal_id, }); } @@ -440,23 +940,291 @@ export class MemoryRegistryStore implements RegistryStore { this.snapshots.set(input.snapshot_hash, input); } + async getSnapshot(snapshotHash: string): Promise { + return this.snapshots.get(snapshotHash) ?? null; + } + + async getSnapshots(snapshotHashes: string[]): Promise> { + const records = new Map(); + for (const hash of new Set(snapshotHashes)) { + const snapshot = this.snapshots.get(hash); + if (snapshot) records.set(hash, snapshot); + } + return records; + } + async getPackageVersion(namespace: string, name: string, version: string): Promise { - return this.packageVersions.get(`${namespace}/${name}@${version}`) ?? null; + const record = this.packageVersions.get(`${namespace}/${name}@${version}`); + return record && packageVersionIsPublic(record) ? record : null; + } + + async listPackageVersions(input: PackageVersionQuery): Promise { + const query = input.query?.toLowerCase(); + return [...this.packageVersions.values()] + .filter(packageVersionIsPublic) + .filter((record) => !input.namespace || record.namespace === input.namespace) + .filter((record) => !input.name || record.name === input.name) + .filter((record) => !input.artifact_kind || record.artifact.kind === input.artifact_kind) + .filter((record) => !input.verification_status || record.verification_status === input.verification_status) + .filter((record) => !input.verification_statuses || input.verification_statuses.includes(record.verification_status)) + .filter((record) => !input.deployment_status || record.deployment_status === input.deployment_status) + .filter((record) => !input.availability_status || record.availability_status === input.availability_status) + .filter((record) => !input.status || record.status === input.status) + .filter((record) => !input.statuses || input.statuses.includes(record.status)) + .filter((record) => { + if (!query) return true; + return `${record.namespace}/${record.name}@${record.version} ${JSON.stringify(record.registry_entry)}` + .toLowerCase() + .includes(query); + }) + .sort((left, right) => right.created_at.localeCompare(left.created_at)) + .slice(input.offset, input.offset + input.limit); + } + + async listArtifactPackagePage(input: PackageVersionQuery): Promise { + const all = await this.listPackageVersions({ ...input, limit: Number.MAX_SAFE_INTEGER, offset: 0 }); + const coordinates = [...new Set(all.map((record) => `${record.namespace}/${record.name}`))]; + const pageCoordinates = coordinates.slice(input.offset, input.offset + input.limit); + const selected = new Set(pageCoordinates); + return { + records: all.filter((record) => selected.has(`${record.namespace}/${record.name}`)), + has_more: coordinates.length > input.offset + input.limit, + }; } async recordPackageVersion(input: PackageVersionRecord): Promise { const key = `${input.namespace}/${input.name}@${input.version}`; const existing = this.packageVersions.get(key); if (existing) { - throw new ApiError(409, "package_version_exists", "package version already exists and cannot be overwritten"); + throw new ApiError(409, "artifact_release_exists", "artifact release already exists and cannot be overwritten"); } this.packageVersions.set(key, input); return input; } + async admitPackageVersion(input: PublishAdmissionInput): Promise { + const versionKey = `${input.version.namespace}/${input.version.name}@${input.version.version}`; + if (this.packageVersions.has(versionKey)) { + throw new ApiError(409, "artifact_release_exists", "artifact release already exists and cannot be overwritten"); + } + this.assertProcessingIdempotency(input.idempotency); + + await this.ensurePackage(input.package); + await this.recordSnapshot(input.snapshot); + await this.recordPackageVersion(input.version); + this.enqueueVerificationJob(input.version, input.snapshot); + await this.recordCapabilityUsage(input.capability_usage); + await this.appendAuditEvent(input.audit_event); + if (input.idempotency) { + await this.completeIdempotencyKey(input.idempotency); + } + return input.version; + } + + async listPackageEvidence(namespace: string, name: string, version: string): Promise { + const prefix = `${namespace}/${name}@${version}:`; + return [...this.packageEvidence.entries()] + .filter(([key]) => key.startsWith(prefix)) + .map(([, record]) => record) + .sort((left, right) => left.created_at.localeCompare(right.created_at)); + } + + async listPackageEvidenceForPackage(namespace: string, name: string): Promise { + const prefix = `${namespace}/${name}@`; + return [...this.packageEvidence.entries()] + .filter(([key]) => key.startsWith(prefix)) + .map(([, record]) => record) + .sort((left, right) => left.created_at.localeCompare(right.created_at)); + } + + async findScriptInterfaceCandidates(input: ScriptInterfaceLookup): Promise { + const normalize = (value: unknown): string => typeof value === "string" ? value.replace(/^0x/i, "").toLowerCase() : ""; + const codeHash = normalize(input.code_hash); + const dataHash = input.data_hash ? normalize(input.data_hash) : undefined; + const candidates: ScriptInterfaceCandidate[] = []; + for (const version of this.packageVersions.values()) { + if (!packageVersionIsPublic(version) + || version.availability_status !== "active" + || version.deployment_status !== "chain_verified" + || version.artifact.kind !== "deployable_contract" + || version.artifact.profile !== "ckb_executable") continue; + const release = version.registry_entry.versions.find((entry) => entry.version === version.version) as Record | undefined; + const profileContract = release?.["profile_contract"] as Record | undefined; + const interfaceContract = profileContract?.["interface"] as Record | undefined; + if (interfaceContract?.["format"] !== "ls-idl") continue; + const evidence = await this.listPackageEvidence(version.namespace, version.name, version.version); + const deployment = evidence.filter((item) => item.kind === "deployed").at(-1); + if (!deployment) continue; + const value = deployment.evidence; + if (value["network"] !== input.network + || normalize(value["code_hash"]) !== codeHash + || (input.hash_type && value["hash_type"] !== input.hash_type) + || (dataHash && normalize(value["data_hash"]) !== dataHash)) continue; + candidates.push({ version, deployment }); + if (candidates.length >= input.limit) break; + } + return candidates.sort((left, right) => right.deployment.created_at.localeCompare(left.deployment.created_at)); + } + + async promotePackageVersion(input: PromotePackageVersionInput): Promise<{ + version: PackageVersionRecord; + evidence: PackageEvidenceRecord; + }> { + const versionKey = `${input.namespace}/${input.name}@${input.version}`; + const existing = this.packageVersions.get(versionKey); + if (!existing) { + throw new ApiError(404, "artifact_release_not_found", "artifact release is not known to the registry"); + } + assertPromotionTransition(existing, input.kind); + this.assertProcessingIdempotency(input.idempotency); + const evidenceKey = `${versionKey}:${input.kind}:${input.evidence_hash}`; + const prior = this.packageEvidence.get(evidenceKey); + const evidence: PackageEvidenceRecord = prior ?? { + namespace: input.namespace, + name: input.name, + version: input.version, + kind: input.kind, + evidence_hash: input.evidence_hash, + evidence: input.evidence, + request_id: input.request_id, + admin_actor: input.admin_actor, + created_at: nowIso(), + }; + this.packageEvidence.set(evidenceKey, evidence); + const versionRecord: PackageVersionRecord = { + ...existing, + verification_status: verificationStatusForAcceptedEvidence(existing.verification_status, input.kind, input.evidence), + deployment_status: input.kind === "on_chain_committed" + ? "chain_verified" + : input.kind === "deployed" + ? "deployed" + : existing.deployment_status, + current_commitment_evidence_hash: input.kind === "on_chain_committed" + ? input.evidence_hash + : existing.current_commitment_evidence_hash ?? null, + }; + versionRecord.status = deriveRegistryEntryStatus(versionRecord, existing.status); + this.packageVersions.set(versionKey, versionRecord); + await this.appendAuditEvent({ + request_id: input.request_id, + event_type: `evidence.${input.kind}.accepted`, + principal_type: existing.principal_type, + principal_id: existing.principal_id, + capability_key_id: existing.capability_key_id, + namespace: input.namespace, + name: input.name, + version: input.version, + data: { admin_actor: input.admin_actor, evidence_hash: input.evidence_hash }, + }); + if (input.capability_usage) { + await this.recordCapabilityUsage(input.capability_usage); + } + if (input.idempotency) { + await this.completeIdempotencyKey(input.idempotency); + } + return { version: versionRecord, evidence }; + } + + async recordChainVerifiedDeployment(input: PromotePackageVersionInput): Promise<{ + version: PackageVersionRecord; + evidence: PackageEvidenceRecord; + }> { + if (input.kind !== "deployed") { + throw new ApiError(500, "invalid_deployment_evidence_kind", "chain-verified deployment evidence must use kind deployed"); + } + const versionKey = `${input.namespace}/${input.name}@${input.version}`; + const existing = this.packageVersions.get(versionKey); + if (!existing) { + throw new ApiError(404, "artifact_release_not_found", "artifact release is not known to the registry"); + } + if (existing.deployment_status === "not_applicable") { + throw new ApiError(409, "deployment_not_applicable", "this artifact profile cannot have a CKB deployment"); + } + if (!(existing.verification_status === "verified" || existing.verification_status === "hash_bound" || existing.verification_status === "evidence_required")) { + throw new ApiError(409, "artifact_not_verified", "artifact verification must finish before recording a deployment"); + } + if (packageVersionRequiresReproduction(existing) && existing.verification_status !== "verified") { + throw new ApiError(409, "reproduction_evidence_missing", "reproducible artifacts require accepted independent reproduction evidence before deployment"); + } + this.assertProcessingIdempotency(input.idempotency); + const evidenceKey = `${versionKey}:${input.kind}:${input.evidence_hash}`; + const evidence: PackageEvidenceRecord = this.packageEvidence.get(evidenceKey) ?? { + namespace: input.namespace, + name: input.name, + version: input.version, + kind: input.kind, + evidence_hash: input.evidence_hash, + evidence: input.evidence, + request_id: input.request_id, + admin_actor: input.admin_actor, + created_at: nowIso(), + }; + this.packageEvidence.set(evidenceKey, evidence); + const versionRecord: PackageVersionRecord = { + ...existing, + deployment_status: "chain_verified", + current_commitment_evidence_hash: null, + }; + versionRecord.status = deriveRegistryEntryStatus(versionRecord, existing.status); + this.packageVersions.set(versionKey, versionRecord); + await this.appendAuditEvent({ + request_id: input.request_id, + event_type: "deployment.chain_verified", + principal_type: existing.principal_type, + principal_id: existing.principal_id, + capability_key_id: existing.capability_key_id, + namespace: input.namespace, + name: input.name, + version: input.version, + data: { actor: input.admin_actor, evidence_hash: input.evidence_hash }, + }); + if (input.capability_usage) { + await this.recordCapabilityUsage(input.capability_usage); + } + if (input.idempotency) { + await this.completeIdempotencyKey(input.idempotency); + } + return { version: versionRecord, evidence }; + } + + async reconcilePackageVersionLifecycle(input: { + namespace: string; + name: string; + version: string; + status: "verified_build" | "deployed"; + deployment_status: "undeployed" | "deployed" | "chain_verified"; + request_id: string; + reason: string; + }): Promise { + const key = `${input.namespace}/${input.name}@${input.version}`; + const existing = this.packageVersions.get(key); + if (!existing) { + throw new ApiError(404, "artifact_release_not_found", "artifact release is not known to the registry"); + } + const updated: PackageVersionRecord = { + ...existing, + deployment_status: input.deployment_status, + current_commitment_evidence_hash: null, + }; + updated.status = deriveRegistryEntryStatus(updated, input.status); + this.packageVersions.set(key, updated); + await this.appendAuditEvent({ + request_id: input.request_id, + event_type: "lifecycle.chain_state_reconciled", + principal_type: existing.principal_type, + principal_id: existing.principal_id, + capability_key_id: existing.capability_key_id, + namespace: input.namespace, + name: input.name, + version: input.version, + data: { status: input.status, deployment_status: input.deployment_status, reason: input.reason }, + }); + return updated; + } + async recordCapabilityUsage(input: { key_id: string; - principal_type: string; + principal_type: PrincipalType; principal_id: string; request_id: string; action: string; @@ -485,21 +1253,29 @@ export class MemoryRegistryStore implements RegistryStore { namespace: string; name: string; version: string; - status: RegistryEntryStatus; + status: AvailabilityStatus; reason?: string; request_id: string; admin_actor: string; + audit_event_type?: string; + capability_usage?: PublishAdmissionInput["capability_usage"]; + idempotency?: PublishAdmissionInput["idempotency"]; }): Promise { const key = `${input.namespace}/${input.name}@${input.version}`; const existing = this.packageVersions.get(key); if (!existing) { - throw new ApiError(404, "package_version_not_found", "package version is not known to the registry"); + throw new ApiError(404, "artifact_release_not_found", "artifact release is not known to the registry"); } - const updated = { ...existing, status: input.status }; + this.assertProcessingIdempotency(input.idempotency); + const updated: PackageVersionRecord = { + ...existing, + availability_status: input.status, + }; + updated.status = deriveRegistryEntryStatus(updated, existing.status); this.packageVersions.set(key, updated); await this.appendAuditEvent({ request_id: input.request_id, - event_type: "admin.package_version.status_updated", + event_type: input.audit_event_type ?? "admin.package_version.status_updated", principal_type: existing.principal_type, principal_id: existing.principal_id, capability_key_id: existing.capability_key_id, @@ -508,6 +1284,12 @@ export class MemoryRegistryStore implements RegistryStore { version: input.version, data: { admin_actor: input.admin_actor, status: input.status, reason: input.reason ?? null }, }); + if (input.capability_usage) { + await this.recordCapabilityUsage(input.capability_usage); + } + if (input.idempotency) { + await this.completeIdempotencyKey(input.idempotency); + } return updated; } @@ -572,6 +1354,13 @@ export class MemoryRegistryStore implements RegistryStore { return true; } + async releaseNonce(input: { nonce_key: string; request_id: string }): Promise { + const existing = this.usedNonces.get(input.nonce_key); + if (existing?.request_id === input.request_id) { + this.usedNonces.delete(input.nonce_key); + } + } + async reserveIdempotencyKey(input: { key: string; request_hash: string; @@ -612,7 +1401,7 @@ export class MemoryRegistryStore implements RegistryStore { response_body: Record; }): Promise { const existing = this.idempotencyKeys.get(input.key); - if (!existing || existing.request_hash !== input.request_hash) { + if (!existing || existing.status !== "processing" || existing.request_hash !== input.request_hash) { throw new ApiError(409, "idempotency_key_conflict", "idempotency key is reserved for another request"); } const completed: IdempotencyRecord = { @@ -644,6 +1433,8 @@ export class MemoryRegistryStore implements RegistryStore { const quotaCutoff = Date.parse(input.quota_events_before_iso); let usedNoncesDeleted = 0; let idempotencyKeysDeleted = 0; + let packageVersionsExpired = 0; + let authorisationSessionsDeleted = 0; for (const [key, record] of this.usedNonces.entries()) { if (Date.parse(record.expires_at) < now) { @@ -657,14 +1448,334 @@ export class MemoryRegistryStore implements RegistryStore { idempotencyKeysDeleted += 1; } } + for (const [key, record] of this.authorisationSessions.entries()) { + const terminalRetentionDeadline = Date.parse(record.completed_at ?? record.updated_at) + + AUTHORISATION_SESSION_TERMINAL_RETENTION_HOURS * 60 * 60 * 1000; + const shouldDelete = record.status === "pending" + ? Date.parse(record.expires_at) < now + : terminalRetentionDeadline < now; + if (shouldDelete) { + this.authorisationSessions.delete(key); + authorisationSessionsDeleted += 1; + } + } const quotaBefore = this.quotaEvents.length; this.quotaEvents = this.quotaEvents.filter((event) => Date.parse(event.at) >= quotaCutoff); + for (const [key, record] of this.packageVersions.entries()) { + if (record.expires_at && Date.parse(record.expires_at) <= now && !record.expired_at) { + this.packageVersions.set(key, { ...record, expired_at: input.now_iso }); + packageVersionsExpired += 1; + } + } + const staticObjects = [...this.packageVersions.values()] + .filter((record) => record.expires_at && Date.parse(record.expires_at) <= now && !record.static_purged_at) + .map((record) => ({ + key: sandboxStaticObjectKey(record.namespace, record.name, record.version), + namespace: record.namespace, + name: record.name, + version: record.version, + })); + const sourceObjects = [...new Set( + [...this.packageVersions.values()] + .filter((record) => record.purge_after && Date.parse(record.purge_after) <= now && !record.source_purged_at) + .map((record) => record.snapshot_hash), + )] + .filter((snapshotHash) => [...this.packageVersions.values()] + .filter((record) => record.snapshot_hash === snapshotHash) + .every((record) => !!record.purge_after && Date.parse(record.purge_after) <= now)) + .flatMap((snapshotHash) => { + const snapshot = this.snapshots.get(snapshotHash); + return snapshot ? [{ key: snapshot.r2_key, snapshot_hash: snapshotHash }] : []; + }); + return { used_nonces_deleted: usedNoncesDeleted, idempotency_keys_deleted: idempotencyKeysDeleted, quota_events_deleted: quotaBefore - this.quotaEvents.length, + package_versions_expired: packageVersionsExpired, + authorisation_sessions_deleted: authorisationSessionsDeleted, + static_objects: staticObjects, + source_objects: sourceObjects, + }; + } + + async markSandboxObjectsPurged(input: { + static_objects: SandboxObjectCandidate[]; + source_objects: SandboxObjectCandidate[]; + purged_at: string; + }): Promise { + for (const candidate of input.static_objects) { + if (!candidate.namespace || !candidate.name || !candidate.version) continue; + const key = `${candidate.namespace}/${candidate.name}@${candidate.version}`; + const record = this.packageVersions.get(key); + if (record) this.packageVersions.set(key, { ...record, static_purged_at: input.purged_at }); + } + const snapshots = new Set(input.source_objects.map((candidate) => candidate.snapshot_hash).filter(Boolean)); + for (const [key, record] of this.packageVersions.entries()) { + if (snapshots.has(record.snapshot_hash)) { + this.packageVersions.set(key, { ...record, source_purged_at: input.purged_at }); + } + } + } + + async claimVerificationJob(input: { + worker_id: string; + lease_seconds: number; + now_iso: string; + }): Promise { + const now = Date.parse(input.now_iso); + const candidate = [...this.verificationJobs.values()] + .filter((job) => { + if ((job.status === "queued" || job.status === "retry_wait") && Date.parse(job.available_at) <= now) return true; + if ((job.status === "running" || job.status === "publishing") && job.lease_expires_at) { + return Date.parse(job.lease_expires_at) <= now; + } + return false; + }) + .sort((left, right) => left.available_at.localeCompare(right.available_at) || left.created_at.localeCompare(right.created_at))[0]; + if (!candidate) return null; + + const hasEvidence = !!candidate.evidence_hash && !!candidate.evidence; + const claimed: VerificationJobRecord = { + ...candidate, + status: hasEvidence ? "publishing" : "running", + attempt_count: candidate.attempt_count + 1, + lease_owner: input.worker_id, + lease_expires_at: new Date(now + input.lease_seconds * 1_000).toISOString(), + started_at: candidate.started_at ?? input.now_iso, + updated_at: input.now_iso, + }; + this.verificationJobs.set(claimed.id, claimed); + return claimed; + } + + async promoteVerifiedBuildForJob(input: { + job_id: string; + worker_id: string; + evidence_hash: string; + evidence: Record; + request_id: string; + admin_actor: string; + }): Promise<{ job: VerificationJobRecord; version: PackageVersionRecord; evidence: PackageEvidenceRecord }> { + const job = this.requireOwnedVerificationJob(input.job_id, input.worker_id, "running"); + const promoted = await this.promotePackageVersion({ + namespace: job.namespace, + name: job.name, + version: job.version, + kind: "verified_build", + evidence_hash: input.evidence_hash, + evidence: input.evidence, + request_id: input.request_id, + admin_actor: input.admin_actor, + }); + const updated: VerificationJobRecord = { + ...job, + status: "publishing", + evidence_hash: input.evidence_hash, + evidence: input.evidence, + updated_at: nowIso(), + }; + this.verificationJobs.set(job.id, updated); + return { job: updated, ...promoted }; + } + + async completeVerificationJob(input: { job_id: string; worker_id: string }): Promise { + const job = this.requireOwnedVerificationJob(input.job_id, input.worker_id, "publishing"); + const completedAt = nowIso(); + const completed: VerificationJobRecord = { + ...job, + status: "succeeded", + lease_owner: null, + lease_expires_at: null, + completed_at: completedAt, + updated_at: completedAt, + last_error_code: null, + last_error_message: null, + }; + this.verificationJobs.set(job.id, completed); + await this.appendAuditEvent({ + request_id: `verification:${job.id}`, + event_type: "verification.succeeded", + namespace: job.namespace, + name: job.name, + version: job.version, + data: { job_id: job.id, attempt_count: job.attempt_count, evidence_hash: job.evidence_hash }, + }); + return completed; + } + + async requestStaticSync(input: { namespace: string; name: string; version: string; error_message: string }): Promise { + const job = [...this.verificationJobs.values()].find((candidate) => + candidate.namespace === input.namespace && candidate.name === input.name && candidate.version === input.version + ); + if (!job) return; + if (job.status === "running" || job.status === "publishing") return; + const requestedAt = nowIso(); + this.verificationJobs.set(job.id, { + ...job, + status: "retry_wait", + lease_owner: null, + lease_expires_at: null, + available_at: requestedAt, + completed_at: null, + last_error_code: "static_registry_sync_deferred", + last_error_message: input.error_message, + updated_at: requestedAt, + }); + } + + async failVerificationJob(input: { + job_id: string; + worker_id: string; + error_code: string; + error_message: string; + retryable: boolean; + retry_after_seconds: number; + request_id: string; + }): Promise { + const job = this.requireOwnedVerificationJob(input.job_id, input.worker_id); + const retry = input.retryable && job.attempt_count < job.max_attempts; + const now = new Date(); + const failed: VerificationJobRecord = { + ...job, + status: retry ? "retry_wait" : "dead_letter", + available_at: new Date(now.getTime() + (retry ? input.retry_after_seconds : 0) * 1_000).toISOString(), + lease_owner: null, + lease_expires_at: null, + last_error_code: input.error_code, + last_error_message: input.error_message, + updated_at: now.toISOString(), + }; + this.verificationJobs.set(job.id, failed); + await this.appendAuditEvent({ + request_id: input.request_id, + event_type: retry ? "verification.retry_scheduled" : "verification.dead_lettered", + namespace: job.namespace, + name: job.name, + version: job.version, + data: { + job_id: job.id, + attempt_count: job.attempt_count, + error_code: input.error_code, + retry_after_seconds: retry ? input.retry_after_seconds : null, + }, + }); + return failed; + } + + async retryVerificationJob(input: { + job_id: string; + request_id: string; + admin_actor: string; + }): Promise { + const job = this.verificationJobs.get(input.job_id); + if (!job) throw new ApiError(404, "verification_job_not_found", "verification job was not found"); + if (job.status !== "dead_letter") { + throw new ApiError(409, "verification_job_not_dead_letter", "only dead-letter verification jobs can be retried manually"); + } + const now = nowIso(); + const retried: VerificationJobRecord = { + ...job, + status: "queued", + attempt_count: 0, + available_at: now, + lease_owner: null, + lease_expires_at: null, + last_error_code: null, + last_error_message: null, + updated_at: now, + }; + this.verificationJobs.set(job.id, retried); + await this.appendAuditEvent({ + request_id: input.request_id, + event_type: "verification.requeued", + namespace: job.namespace, + name: job.name, + version: job.version, + data: { job_id: job.id, admin_actor: input.admin_actor }, + }); + return retried; + } + + async getVerificationQueueMetrics(): Promise { + const counts: Record = { + queued: 0, + running: 0, + publishing: 0, + retry_wait: 0, + succeeded: 0, + dead_letter: 0, + }; + let oldestAvailable: string | undefined; + let oldestDeadLetter: string | undefined; + for (const job of this.verificationJobs.values()) { + counts[job.status] += 1; + if ((job.status === "queued" || job.status === "retry_wait") && (!oldestAvailable || job.available_at < oldestAvailable)) { + oldestAvailable = job.available_at; + } + if (job.status === "dead_letter" && (!oldestDeadLetter || job.updated_at < oldestDeadLetter)) { + oldestDeadLetter = job.updated_at; + } + } + return { + counts, + oldest_available_at: oldestAvailable ?? null, + oldest_dead_letter_at: oldestDeadLetter ?? null, + }; + } + + private enqueueVerificationJob(version: PackageVersionRecord, snapshot: SnapshotRecord): void { + const existing = [...this.verificationJobs.values()].find( + (job) => job.namespace === version.namespace && job.name === version.name && job.version === version.version, + ); + if (existing) return; + const createdAt = nowIso(); + const job: VerificationJobRecord = { + id: crypto.randomUUID(), + namespace: version.namespace, + name: version.name, + version: version.version, + status: "queued", + attempt_count: 0, + max_attempts: 3, + available_at: createdAt, + created_at: createdAt, + updated_at: createdAt, + source_hash: version.source_hash, + manifest_hash: version.manifest_hash, + artifact: version.artifact, + ...(version.compatibility_profile_hash ? { compatibility_profile_hash: version.compatibility_profile_hash } : {}), + snapshot_hash: snapshot.snapshot_hash, + snapshot_object_key: snapshot.r2_key, + snapshot_size_bytes: snapshot.size_bytes, + snapshot_content_type: snapshot.content_type, }; + this.verificationJobs.set(job.id, job); + } + + private requireOwnedVerificationJob( + jobId: string, + workerId: string, + requiredStatus?: "running" | "publishing", + ): VerificationJobRecord { + const job = this.verificationJobs.get(jobId); + if (!job) throw new ApiError(404, "verification_job_not_found", "verification job was not found"); + if (job.lease_owner !== workerId || !job.lease_expires_at || Date.parse(job.lease_expires_at) <= Date.now()) { + throw new ApiError(409, "verification_job_lease_lost", "verification job lease is no longer owned by this worker"); + } + if (requiredStatus ? job.status !== requiredStatus : job.status !== "running" && job.status !== "publishing") { + throw new ApiError(409, "verification_job_state_conflict", "verification job is not in an active worker state"); + } + return job; + } + + private assertProcessingIdempotency(input: PublishAdmissionInput["idempotency"]): void { + if (!input) return; + const reservation = this.idempotencyKeys.get(input.key); + if (reservation?.status !== "processing" || reservation.request_hash !== input.request_hash) { + throw new ApiError(409, "idempotency_key_conflict", "idempotency key is reserved for another request"); + } } private reservedNamespaceFor(namespace: string): ReservedNamespaceRecord | undefined { @@ -680,6 +1791,68 @@ export class MemoryRegistryStore implements RegistryStore { } } +export function assertPromotionTransition(current: PackageVersionRecord, next: PackageEvidenceKind): void { + let allowed = false; + if (next === "verified_build") { + allowed = true; + } else if (next === "reproduced_build") { + allowed = current.verification_status !== "pending" && current.verification_status !== "rejected"; + } else if (next === "deployed") { + allowed = current.deployment_status !== "not_applicable" + && ["hash_bound", "verified", "evidence_required"].includes(current.verification_status) + && (!packageVersionRequiresReproduction(current) || current.verification_status === "verified"); + } else if (next === "on_chain_committed") { + allowed = current.deployment_status === "deployed" || current.deployment_status === "chain_verified"; + } + if (!allowed) { + throw new ApiError( + 409, + "invalid_evidence_transition", + `cannot accept '${next}' evidence for verification='${current.verification_status}', deployment='${current.deployment_status}', availability='${current.availability_status}'`, + ); + } +} + +export function deriveRegistryEntryStatus( + version: Pick, + pendingStatus: RegistryEntryStatus = "source_published", +): RegistryEntryStatus { + if (version.availability_status !== "active") return version.availability_status; + if (version.current_commitment_evidence_hash) return "on_chain_committed"; + if (version.deployment_status === "deployed" || version.deployment_status === "chain_verified") return "deployed"; + if (["hash_bound", "verified", "evidence_required"].includes(version.verification_status)) return "verified_build"; + return pendingStatus === "indexed_pending" ? "indexed_pending" : "source_published"; +} + +function verificationStatusForAcceptedEvidence( + current: VerificationStatus, + kind: PackageEvidenceKind, + evidence: Record, +): VerificationStatus { + if (kind === "reproduced_build") return "verified"; + if (kind !== "verified_build") return current; + switch (evidence["verification_level"]) { + case "compiled": + case "structurally_verified": + return "verified"; + case "hash_bound": + return "hash_bound"; + case "evidence_required": + return "evidence_required"; + default: + throw new ApiError(500, "invalid_verification_level", "accepted build evidence has no recognised verification level"); + } +} + +export function packageVersionRequiresReproduction(version: PackageVersionRecord): boolean { + if (version.artifact.profile === "reproducible_build") return true; + const release = version.registry_entry.versions.find((entry) => entry.version === version.version); + const contract = release?.profile_contract; + if (!contract || typeof contract !== "object" || Array.isArray(contract)) return false; + const build = (contract as Record)["build"]; + return Boolean(build && typeof build === "object" && !Array.isArray(build) && (build as Record)["reproducible"] === true); +} + async function hashForMemory(value: unknown): Promise { const { sha256Hex } = await import("./domain"); return sha256Hex(canonicalJson(value)); diff --git a/services/registry-api/src/verification-worker.ts b/services/registry-api/src/verification-worker.ts new file mode 100644 index 00000000..7a95daba --- /dev/null +++ b/services/registry-api/src/verification-worker.ts @@ -0,0 +1,420 @@ +import { createHash, randomUUID } from "node:crypto"; +import { access, lstat, mkdir, readFile, writeFile } from "node:fs/promises"; +import { constants as fsConstants } from "node:fs"; +import { hostname } from "node:os"; +import { dirname, resolve } from "node:path"; +import type { ChildProcess } from "node:child_process"; + +import { ApiError, canonicalJson, sha256Hex } from "./domain"; +import { FilesystemObjectStore } from "./filesystem-object-store"; +import { syncStaticRegistryVersionObject, validatePromotionEvidence, type Env } from "./index"; +import { SqlRegistryStore } from "./sql-store"; +import type { PackageVersionRecord, VerificationJobRecord } from "./store"; +import { executeVerifierSubprocess } from "./verifier-subprocess"; + +const databaseUrl = requiredEnv("DATABASE_URL"); +const objectRoot = resolve(requiredEnv("REGISTRY_OBJECTS_DIR")); +const verifierBinary = process.env["REGISTRY_VERIFIER_BINARY"]?.trim() || "/usr/local/bin/cellscript-registry-verify"; +const artifactVerifierBinary = process.env["REGISTRY_ARTIFACT_VERIFIER_BINARY"]?.trim() + || "/usr/local/bin/cellscript-registry-artifact-verify"; +const workerId = process.env["REGISTRY_VERIFIER_WORKER_ID"]?.trim() || `${hostname()}:${process.pid}:${randomUUID()}`; +const pollIntervalMs = integerEnv("REGISTRY_VERIFIER_POLL_INTERVAL_MS", 2_000, 100, 60_000); +const jobTimeoutSeconds = integerEnv("REGISTRY_VERIFIER_JOB_TIMEOUT_SECONDS", 180, 5, 1_800); +const leaseSeconds = integerEnv("REGISTRY_VERIFIER_LEASE_SECONDS", 300, jobTimeoutSeconds + 30, 3_600); +const healthFile = resolve(process.env["REGISTRY_VERIFIER_HEALTH_FILE"]?.trim() || "/tmp/registry-verifier-ready"); +const sharedHeartbeatFile = resolve( + process.env["REGISTRY_VERIFIER_SHARED_HEARTBEAT"]?.trim() || `${objectRoot}/.health/verifier-ready`, +); +const staticOrigin = process.env["STATIC_REGISTRY_ORIGIN"]?.trim() || "https://registry.cellscript.dev"; + +const store = new SqlRegistryStore({ connectionString: databaseUrl }); +const objectStore = new FilesystemObjectStore(objectRoot); +const env: Env = { STATIC_REGISTRY_ORIGIN: staticOrigin, ENVIRONMENT: process.env["ENVIRONMENT"] ?? "production" }; + +let stopping = false; +let activeChild: ChildProcess | undefined; + +for (const signal of ["SIGTERM", "SIGINT"] as const) { + process.on(signal, () => { + if (stopping) return; + stopping = true; + log("verifier.stopping", { signal }); + activeChild?.kill("SIGTERM"); + }); +} + +async function initialize(): Promise { + const snapshotRoot = resolve(objectRoot, "source-snapshots"); + const packageRoot = resolve(objectRoot, "packages"); + await mkdir(snapshotRoot, { recursive: true, mode: 0o750 }); + await mkdir(packageRoot, { recursive: true, mode: 0o750 }); + await access(snapshotRoot, fsConstants.R_OK); + await access(packageRoot, fsConstants.R_OK | fsConstants.W_OK); + await access(verifierBinary, fsConstants.X_OK); + await access(artifactVerifierBinary, fsConstants.X_OK); + await mkdir(dirname(sharedHeartbeatFile), { recursive: true, mode: 0o750 }); + await store.healthCheck(); + await store.getVerificationQueueMetrics(); + await markHealthy(); + log("verifier.started", { + worker_id: workerId, + lease_seconds: leaseSeconds, + job_timeout_seconds: jobTimeoutSeconds, + poll_interval_ms: pollIntervalMs, + }); +} + +async function runLoop(): Promise { + while (!stopping) { + try { + const job = await store.claimVerificationJob({ + worker_id: workerId, + lease_seconds: leaseSeconds, + now_iso: new Date().toISOString(), + }); + await markHealthy(); + if (!job) { + await delay(pollIntervalMs); + continue; + } + await processJob(job); + await markHealthy(); + } catch (error) { + log("verifier.poll_failed", { error: safeErrorMessage(error) }); + await delay(Math.max(pollIntervalMs, 5_000)); + } + } + log("verifier.stopped", { worker_id: workerId }); +} + +async function processJob(job: VerificationJobRecord): Promise { + const requestId = `verification:${job.id}:${job.attempt_count}`; + log("verification.claimed", { + request_id: requestId, + job_id: job.id, + coordinate: `${job.namespace}/${job.name}@${job.version}`, + attempt_count: job.attempt_count, + phase: job.evidence_hash ? "static_sync" : "build", + }); + try { + let version: PackageVersionRecord; + if (job.evidence_hash && job.evidence) { + const existing = await store.getPackageVersion(job.namespace, job.name, job.version); + if (!existing || !["verified", "hash_bound", "evidence_required"].includes(existing.verification_status)) { + throw new Error("verification job has promoted evidence but package version is not promoted"); + } + version = existing; + } else { + const existing = await store.getPackageVersion(job.namespace, job.name, job.version); + if (!existing) throw new Error("verification job package version disappeared"); + const result = await runBuildVerification(job, existing); + const previous = await store.listPackageEvidence(job.namespace, job.name, job.version); + const evidence = validatePromotionEvidence( + { + schema: "cellscript-registry-evidence", + kind: "verified_build", + producer: result.compiler_version + ? `cellscript-registry-verifier/${result.compiler_version}` + : result.checker_version + ? `cellscript-registry-artifact-verifier/${result.checker_version}` + : `cellscript-registry-verifier/${job.artifact.profile}`, + generated_at: new Date().toISOString(), + verification_status: "passed", + verification_level: result.verification_level, + source_hash: result.source_hash, + manifest_hash: result.manifest_hash, + ...(result.compatibility_profile_hash ? { compatibility_profile_hash: result.compatibility_profile_hash } : {}), + ...(result.artifact_hash ? { artifact_hash: result.artifact_hash } : {}), + metadata_hash: result.metadata_hash, + ...(result.compiler_version ? { compiler_version: result.compiler_version } : {}), + ...(result.checker_version ? { checker_version: result.checker_version } : {}), + ...(result.checker_policy_schema ? { checker_policy_schema: result.checker_policy_schema } : {}), + ...(result.checker_report_hash ? { checker_report_hash: result.checker_report_hash } : {}), + artifact_format: result.artifact_format, + snapshot_hash: job.snapshot_hash, + verification_job_id: job.id, + }, + "verified_build", + existing, + previous, + ); + const evidenceHash = `sha256:${await sha256Hex(canonicalJson(evidence))}`; + const promoted = await store.promoteVerifiedBuildForJob({ + job_id: job.id, + worker_id: workerId, + evidence_hash: evidenceHash, + evidence, + request_id: requestId, + admin_actor: `verification-worker:${workerId}`, + }); + version = promoted.version; + } + + await syncStaticRegistryVersionObject(env, { snapshotWriter: objectStore }, store, version, staticOrigin); + const completed = await store.completeVerificationJob({ job_id: job.id, worker_id: workerId }); + log("verification.succeeded", { + request_id: requestId, + job_id: job.id, + coordinate: `${job.namespace}/${job.name}@${job.version}`, + attempt_count: completed.attempt_count, + evidence_hash: completed.evidence_hash, + }); + } catch (error) { + const retryable = !(error instanceof VerificationRejected); + const errorCode = error instanceof VerificationRejected ? error.code : error instanceof ApiError ? error.code : "verification_infrastructure_error"; + const retryAfterSeconds = Math.min(300, 5 * 2 ** Math.max(0, job.attempt_count - 1)); + try { + const failed = await store.failVerificationJob({ + job_id: job.id, + worker_id: workerId, + error_code: errorCode, + error_message: safeErrorMessage(error), + retryable, + retry_after_seconds: retryAfterSeconds, + request_id: requestId, + }); + log(failed.status === "dead_letter" ? "verification.dead_lettered" : "verification.retry_scheduled", { + request_id: requestId, + job_id: job.id, + coordinate: `${job.namespace}/${job.name}@${job.version}`, + attempt_count: failed.attempt_count, + error_code: errorCode, + error: safeErrorMessage(error), + ...(failed.status === "retry_wait" ? { retry_after_seconds: retryAfterSeconds } : {}), + }); + } catch (leaseError) { + log("verification.failure_not_recorded", { + request_id: requestId, + job_id: job.id, + error: safeErrorMessage(error), + record_error: safeErrorMessage(leaseError), + }); + } + } +} + +interface BuildVerificationResult { + status: "passed"; + verification_level: "compiled" | "hash_bound" | "evidence_required" | "structurally_verified"; + artifact_hash?: string; + metadata_hash: string; + compiler_version?: string; + source_hash: string; + manifest_hash: string; + compatibility_profile_hash?: string; + artifact_format: string; + checker_version?: string; + checker_policy_schema?: string; + checker_report_hash?: string; +} + +async function runBuildVerification(job: VerificationJobRecord, version: PackageVersionRecord): Promise { + const expectedContentType = job.artifact.profile === "cellscript_source" + ? "application/vnd.cellscript.source-snapshot+json" + : "application/vnd.cellscript.artifact-bundle+json"; + if (job.snapshot_content_type !== expectedContentType) { + throw new VerificationRejected( + "unsupported_snapshot_content_type", + `${job.artifact.profile} verification requires ${expectedContentType}, got ${job.snapshot_content_type}`, + ); + } + const snapshotPath = objectStore.pathFor(job.snapshot_object_key); + const metadata = await lstat(snapshotPath); + if (!metadata.isFile() || metadata.isSymbolicLink()) { + throw new VerificationRejected("invalid_snapshot_object", "source snapshot object is not a regular file"); + } + if (metadata.size !== job.snapshot_size_bytes || metadata.size <= 0 || metadata.size > 5 * 1024 * 1024) { + throw new VerificationRejected("snapshot_size_mismatch", "source snapshot object size does not match the admitted descriptor"); + } + const snapshot = await readFile(snapshotPath); + const snapshotHash = `sha256:${createHash("sha256").update(snapshot).digest("hex")}`; + if (snapshotHash.toLowerCase() !== job.snapshot_hash.toLowerCase()) { + throw new VerificationRejected("snapshot_hash_mismatch", "source snapshot object does not match its admitted SHA-256 identity"); + } + + const published = version.registry_entry.versions[0]; + const verifierArgs = [ + "--snapshot", + snapshotPath, + "--namespace", + job.namespace, + "--name", + job.name, + "--version", + job.version, + "--source-hash", + job.source_hash, + "--manifest-hash", + job.manifest_hash, + "--artifact-kind", + job.artifact.kind, + "--profile", + job.artifact.profile, + ]; + if (job.compatibility_profile_hash) verifierArgs.push("--compatibility-profile-hash", job.compatibility_profile_hash); + if (published.artifact_hash) verifierArgs.push("--artifact-hash", published.artifact_hash); + if (published.abi_hash) verifierArgs.push("--abi-hash", published.abi_hash); + if (published.build_recipe_hash) verifierArgs.push("--build-recipe-hash", published.build_recipe_hash); + let result: Awaited>; + try { + result = await executeVerifierSubprocess( + job.artifact.profile === "ckb_executable" ? artifactVerifierBinary : verifierBinary, + verifierArgs, + { + cwd: "/tmp", + env: { + PATH: process.env["PATH"] ?? "/usr/local/bin:/usr/bin:/bin", + HOME: process.env["HOME"] ?? "/tmp/verifier-home", + XDG_CACHE_HOME: process.env["XDG_CACHE_HOME"] ?? "/tmp/verifier-cache", + CELLSCRIPT_REGISTRY_API_URL: process.env["CELLSCRIPT_REGISTRY_API_URL"] ?? "https://api.registry.cellscript.dev", + NO_COLOR: "1", + }, + timeoutMs: jobTimeoutSeconds * 1_000, + onSpawn: (child) => { activeChild = child; }, + }, + ); + } finally { + activeChild = undefined; + } + + let payload: unknown; + try { + payload = JSON.parse(result.stdout); + } catch { + if (result.timedOut) throw new Error("CellScript verifier timed out"); + throw new Error(`CellScript verifier returned invalid JSON (exit ${result.exitCode ?? "signal"})`); + } + if (result.timedOut) throw new Error("CellScript verifier timed out"); + if (result.exitCode !== 0) { + const failure = plainObject(payload); + const code = safeToken(failure?.["error_code"]); + if (!code) throw new Error("CellScript verifier failure output omitted a stable error_code"); + const message = safeString(failure?.["message"]) ?? "CellScript package verification failed"; + throw new VerificationRejected(code, message); + } + const output = plainObject(payload); + if (!output || output["status"] !== "passed") { + throw new Error("CellScript verifier success output is malformed"); + } + const parsed: BuildVerificationResult = { + status: "passed", + verification_level: requiredVerificationLevel(output), + ...(optionalHash(output, "artifact_hash") ? { artifact_hash: optionalHash(output, "artifact_hash")! } : {}), + metadata_hash: requiredHash(output, "metadata_hash"), + ...(safeString(output["compiler_version"]) ? { compiler_version: requiredOutputString(output, "compiler_version", 80) } : {}), + source_hash: requiredHash(output, "source_hash"), + manifest_hash: requiredHash(output, "manifest_hash"), + ...(optionalHash(output, "compatibility_profile_hash") + ? { compatibility_profile_hash: optionalHash(output, "compatibility_profile_hash")! } + : {}), + artifact_format: requiredOutputString(output, "artifact_format", 80), + ...(safeString(output["checker_version"]) ? { checker_version: requiredOutputString(output, "checker_version", 80) } : {}), + ...(safeString(output["checker_policy_schema"]) + ? { checker_policy_schema: requiredOutputString(output, "checker_policy_schema", 120) } + : {}), + ...(optionalHash(output, "checker_report_hash") ? { checker_report_hash: optionalHash(output, "checker_report_hash")! } : {}), + }; + requireSameHash(parsed.source_hash, job.source_hash, "source_hash"); + requireSameHash(parsed.manifest_hash, job.manifest_hash, "manifest_hash"); + if (job.compatibility_profile_hash) { + if (!parsed.compatibility_profile_hash) throw new VerificationRejected("compatibility_profile_hash_missing", "verifier omitted compatibility_profile_hash"); + requireSameHash(parsed.compatibility_profile_hash, job.compatibility_profile_hash, "compatibility_profile_hash"); + } + if (parsed.verification_level === "structurally_verified" + && (!parsed.checker_version || !parsed.checker_policy_schema || !parsed.checker_report_hash)) { + throw new VerificationRejected("checker_identity_missing", "artifact verifier omitted checker version, policy, or report hash"); + } + return parsed; +} + +class VerificationRejected extends Error { + constructor(readonly code: string, message: string) { + super(message); + } +} + +function requiredHash(value: Record, key: string): string { + const hash = requiredOutputString(value, key, 66); + if (!/^(?:0x)?[0-9a-f]{64}$/i.test(hash)) throw new Error(`CellScript verifier ${key} is not a 32-byte hex hash`); + return hash; +} + +function optionalHash(value: Record, key: string): string | undefined { + if (value[key] == null) return undefined; + return requiredHash(value, key); +} + +function requiredVerificationLevel(value: Record): BuildVerificationResult["verification_level"] { + const level = requiredOutputString(value, "verification_level", 80); + if (level !== "compiled" && level !== "hash_bound" && level !== "evidence_required" && level !== "structurally_verified") { + throw new Error("CellScript verifier verification_level is not recognised"); + } + return level; +} + +function requiredOutputString(value: Record, key: string, maximum: number): string { + const item = value[key]; + if (typeof item !== "string" || item.length === 0 || item.length > maximum || item.trim() !== item) { + throw new Error(`CellScript verifier ${key} is invalid`); + } + return item; +} + +function requireSameHash(actual: string, expected: string, field: string): void { + const normalize = (value: string) => value.replace(/^0x/i, "").toLowerCase(); + if (normalize(actual) !== normalize(expected)) throw new VerificationRejected(`${field}_mismatch`, `${field} does not match the signed package identity`); +} + +function plainObject(value: unknown): Record | undefined { + return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record : undefined; +} + +function safeString(value: unknown): string | undefined { + if (typeof value !== "string") return undefined; + const normalized = value.replace(/[\u0000-\u001f\u007f]+/g, " ").trim(); + return normalized ? normalized.slice(0, 2_000) : undefined; +} + +function safeToken(value: unknown): string | undefined { + return typeof value === "string" && /^[a-z][a-z0-9_]{0,79}$/.test(value) ? value : undefined; +} + +function safeErrorMessage(error: unknown): string { + return safeString(error instanceof Error ? error.message : String(error)) ?? "unknown error"; +} + +async function markHealthy(): Promise { + const heartbeat = `${new Date().toISOString()}\n`; + await writeFile(healthFile, heartbeat, { mode: 0o600 }); + await writeFile(sharedHeartbeatFile, heartbeat, { mode: 0o640 }); +} + +async function delay(milliseconds: number): Promise { + const deadline = Date.now() + milliseconds; + while (!stopping && Date.now() < deadline) { + await new Promise((resolveDelay) => setTimeout(resolveDelay, Math.min(250, deadline - Date.now()))); + } +} + +function requiredEnv(name: string): string { + const value = process.env[name]?.trim(); + if (!value) throw new Error(`${name} is required`); + return value; +} + +function integerEnv(name: string, fallback: number, minimum: number, maximum: number): number { + const raw = process.env[name]; + if (!raw) return fallback; + const value = Number(raw); + if (!Number.isSafeInteger(value) || value < minimum || value > maximum) { + throw new Error(`${name} must be an integer between ${minimum} and ${maximum}`); + } + return value; +} + +function log(event: string, data: Record): void { + process.stdout.write(`${JSON.stringify({ timestamp: new Date().toISOString(), event, ...data })}\n`); +} + +await initialize(); +await runLoop(); diff --git a/services/registry-api/src/verifier-subprocess.ts b/services/registry-api/src/verifier-subprocess.ts new file mode 100644 index 00000000..3f2db343 --- /dev/null +++ b/services/registry-api/src/verifier-subprocess.ts @@ -0,0 +1,71 @@ +import { spawn, type ChildProcess } from "node:child_process"; + +export interface VerifierSubprocessResult { + exitCode: number | null; + timedOut: boolean; + stdout: string; + stderr: string; +} + +export interface VerifierSubprocessOptions { + cwd: string; + env: NodeJS.ProcessEnv; + timeoutMs: number; + maximumOutputBytes?: number; + onSpawn?: (child: ChildProcess) => void; +} + +export async function executeVerifierSubprocess( + binary: string, + args: string[], + options: VerifierSubprocessOptions, +): Promise { + const child = spawn(binary, args, { + cwd: options.cwd, + env: options.env, + stdio: ["ignore", "pipe", "pipe"], + }); + options.onSpawn?.(child); + return collectVerifierSubprocess(child, options.timeoutMs, options.maximumOutputBytes ?? 1024 * 1024); +} + +async function collectVerifierSubprocess( + child: ChildProcess, + timeoutMs: number, + maximumOutputBytes: number, +): Promise { + let stdout = ""; + let stderr = ""; + let overflow = false; + child.stdout?.setEncoding("utf8"); + child.stderr?.setEncoding("utf8"); + child.stdout?.on("data", (chunk: string) => { + if (overflow) return; + if (Buffer.byteLength(stdout) + Buffer.byteLength(chunk) > maximumOutputBytes) { + overflow = true; + child.kill("SIGKILL"); + return; + } + stdout += chunk; + }); + child.stderr?.on("data", (chunk: string) => { + if (overflow) return; + if (Buffer.byteLength(stderr) + Buffer.byteLength(chunk) > maximumOutputBytes) { + overflow = true; + child.kill("SIGKILL"); + return; + } + stderr += chunk; + }); + let timedOut = false; + const timer = setTimeout(() => { + timedOut = true; + child.kill("SIGKILL"); + }, timeoutMs); + const exitCode = await new Promise((resolveExit, reject) => { + child.once("error", reject); + child.once("close", (code) => resolveExit(code)); + }).finally(() => clearTimeout(timer)); + if (overflow) throw new Error("CellScript verifier output exceeded the configured limit"); + return { exitCode, timedOut, stdout, stderr }; +} diff --git a/services/registry-api/test/registry-api.test.ts b/services/registry-api/test/registry-api.test.ts index 1934e695..ee7b8c74 100644 --- a/services/registry-api/test/registry-api.test.ts +++ b/services/registry-api/test/registry-api.test.ts @@ -1,22 +1,280 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import type { SignChallengeResponseData } from "@joyid/ckb"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { blake2b } from "@noble/hashes/blake2.js"; +import { execFile } from "node:child_process"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { createServer } from "node:http"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; import { AUTH_ACTION, AUTH_PROTOCOL, AUTH_REVOKE_CAPABILITY_ACTION, + ARTIFACT_PROFILE_CATALOG, + ARTIFACT_PROFILE_CATALOG_SCHEMA, + AVAILABILITY_ACTION, + AVAILABILITY_PROTOCOL, + DEPLOYMENT_ACTION, + DEPLOYMENT_PROTOCOL, DEFAULT_REGISTRY_ORIGIN, PUBLISH_ACTION, PUBLISH_PROTOCOL, + ApiError, canonicalJson, capabilityKeyId, + ckbBlake2bHex, + ckbScriptHash, + ckbSecp256k1PrincipalIdFromPublicKey, + artifactProfileSupportsDependencyResolution, joyidPrincipalIdFromBinding, + scopeAllows, + sha256Hex, + validatePublishPayload, + validateArtifactDescriptor, + validateVersion, type CapabilityAuthorisationPayload, type CapabilityRevocationPayload, + type AvailabilityPayload, + type CkbSecp256k1Signature, + type DeploymentPayload, type PublishPayload, } from "../src/domain"; -import { MemoryRegistryStore, createApp, type SnapshotWriter } from "../src/index"; +import { + CANONICAL_REGISTRY_TYPE_SCRIPT, + CKB_MAINNET_SIGHASH_DEP_GROUP, + CKB_MAINNET_SIGHASH_LOCK, + MemoryRegistryStore, + createApp, + parseDepGroupOutPoints, + registryCommitmentHash, + registryRuntimeConfig, + verifyDeployment, + verifyMainnetDeployment, + type AppDeps, + type SnapshotWriter, +} from "../src/index"; +import type { PackageVersionRecord } from "../src/store"; +import { nodeCkbRpcEnv } from "../src/node-runtime-env"; const now = new Date("2026-06-23T12:00:00Z"); +const execFileAsync = promisify(execFile); +const ckbPrivateKey = Uint8Array.from({ length: 32 }, (_, index) => index === 31 ? 7 : 0); +const reproducerPublicKeys = { + "builder-a": "p256-spki:MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2GpMwoWK1SO7Vrd_Rn3kxf_VllpSMGMu1Mo40vH2IotxFkJwZwO7acw8A-lZB7z4l5QAYDKTP4ua7YilwZQfBw", + "builder-b": "p256-spki:MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcZljLFjOhAdes8hm88phoxoMmsya3kKGRbmwjtH1eW4tWV_sn81NRL5EwkrqhjPuYxXfEbYBfuSVPMVD3at7hQ", +} as const; + +describe("Node CKB RPC environment", () => { + it("forwards every bounded RPC control used by the shared API", () => { + expect(nodeCkbRpcEnv({ + CKB_MAINNET_RPC_URL: "https://mainnet.ckb.dev/rpc", + CKB_RPC_URL: "https://testnet.ckb.dev/rpc", + CKB_RPC_TIMEOUT_MS: "15000", + CKB_RPC_MAX_RESPONSE_BYTES: "8388608", + CKB_DEP_GROUP_MAX_MEMBERS: "256", + UNRELATED_SECRET: "must-not-pass-through", + })).toEqual({ + CKB_MAINNET_RPC_URL: "https://mainnet.ckb.dev/rpc", + CKB_RPC_URL: "https://testnet.ckb.dev/rpc", + CKB_RPC_TIMEOUT_MS: "15000", + CKB_RPC_MAX_RESPONSE_BYTES: "8388608", + CKB_DEP_GROUP_MAX_MEMBERS: "256", + }); + }); +}); + +describe("capability scopes", () => { + it("keeps publishing, deployment evidence, and availability changes independent", () => { + const scopes = ["publish:cellscript/demo", "deployment:cellscript/*"]; + + expect(scopeAllows(scopes, "publish", "cellscript", "demo")).toBe(true); + expect(scopeAllows(scopes, "deployment", "cellscript", "other")).toBe(true); + expect(scopeAllows(scopes, "availability", "cellscript", "demo")).toBe(false); + expect(scopeAllows(scopes, "publish", "cellscript", "other")).toBe(false); + }); +}); + +describe("SemVer admission", () => { + it("accepts canonical release, prerelease, and build metadata forms", () => { + expect(validateVersion("0.24.0")).toBe("0.24.0"); + expect(validateVersion("1.2.3-rc.1+build.7")).toBe("1.2.3-rc.1+build.7"); + }); + + it.each(["01.2.3", "1.02.3", "1.2.03", "1.2.3-01", "1.2.3-rc..1", "1.2.3+"])( + "rejects non-canonical version %s", + (version) => { + expect(() => validateVersion(version)).toThrow(ApiError); + }, + ); +}); + +function bytesHex(value: Uint8Array): string { + return `0x${[...value].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`; +} + +function depGroupData(outPoints: Array<{ tx_hash_byte: number; index: number }>): string { + const bytes = new Uint8Array(4 + outPoints.length * 36); + const view = new DataView(bytes.buffer); + view.setUint32(0, outPoints.length, true); + outPoints.forEach((outPoint, item) => { + const offset = 4 + item * 36; + bytes.fill(outPoint.tx_hash_byte, offset, offset + 32); + view.setUint32(offset + 32, outPoint.index, true); + }); + return bytesHex(bytes); +} + +describe("DepGroup decoding", () => { + it("decodes canonical Molecule OutPointVec data", () => { + expect(parseDepGroupOutPoints(depGroupData([ + { tx_hash_byte: 0x11, index: 3 }, + { tx_hash_byte: 0xab, index: 0xffff_fffe }, + ]))).toEqual([ + { tx_hash: `0x${"11".repeat(32)}`, index: 3 }, + { tx_hash: `0x${"ab".repeat(32)}`, index: 0xffff_fffe }, + ]); + }); + + it("rejects empty and non-canonical DepGroup data", () => { + expect(() => parseDepGroupOutPoints("0x00000000")).toThrow(/canonical non-empty/); + expect(() => parseDepGroupOutPoints("0x01000000aa")).toThrow(/canonical non-empty/); + }); +}); + +describe("CKB mainnet observations", () => { + it("requires the configured confirmation depth for a live deployment Cell", async () => { + const blockHash = `0x${"aa".repeat(32)}`; + const artifactHash = `0x${"bb".repeat(32)}`; + const deploymentTxHash = `0x${"dd".repeat(32)}`; + let reportedChain = "ckb"; + let transactionStatus = "committed"; + let transactionBlockHash: string | null = blockHash; + const transactionRequests: unknown[][] = []; + vi.stubGlobal("fetch", async (_input: RequestInfo | URL, init?: RequestInit) => { + const request = JSON.parse(String(init?.body)) as { method: string; params: unknown[] }; + if (request.method === "get_transaction") transactionRequests.push(request.params); + const results: Record = { + get_blockchain_info: { chain: reportedChain }, + get_live_cell: { + status: "live", + cell: { + data: { hash: artifactHash, content: "0x00" }, + output: { + capacity: "0x0", + lock: { code_hash: `0x${"cc".repeat(32)}`, hash_type: "type", args: "0x" }, + type: null, + }, + }, + }, + get_transaction: { + transaction: null, + cycles: null, + fee: null, + min_replace_fee: null, + time_added_to_pool: null, + tx_status: { + status: transactionStatus, + block_hash: transactionStatus === "committed" ? transactionBlockHash : null, + block_number: transactionStatus === "committed" ? "0x64" : null, + tx_index: transactionStatus === "committed" ? "0x0" : null, + reason: null, + }, + }, + get_header: { number: "0x64" }, + get_tip_header: { number: "0x6a" }, + }; + return Response.json({ jsonrpc: "2.0", id: 1, result: results[request.method] }); + }); + const payload: DeploymentPayload = { + protocol: DEPLOYMENT_PROTOCOL, + action: DEPLOYMENT_ACTION, + registry_origin: DEFAULT_REGISTRY_ORIGIN, + namespace: "fixture", + name: "contract", + release: "1.0.0", + network: "mainnet", + artifact_hash: artifactHash, + data_hash: artifactHash, + code_hash: artifactHash, + hash_type: "data1", + dep_type: "code", + out_point: { tx_hash: deploymentTxHash, index: 0 }, + capability_key_id: "cap_11111111111111111111111111111111", + nonce: "0x1111111111111111", + issued_at: "2026-06-23T12:00:00Z", + expires_at: "2026-06-23T12:10:00Z", + cli_version: "cellc 0.23.0", + }; + try { + await expect(verifyMainnetDeployment({ CKB_MIN_CONFIRMATIONS: "8" }, payload)) + .rejects.toMatchObject({ code: "chain_confirmation_depth_insufficient" }); + await expect(verifyMainnetDeployment({ CKB_MIN_CONFIRMATIONS: "7" }, payload)) + .resolves.toMatchObject({ block_hash: blockHash, block_number: "0x64", tip_block_number: "0x6a", confirmations: 7 }); + transactionStatus = "pending"; + await expect(verifyMainnetDeployment({ CKB_MIN_CONFIRMATIONS: "7" }, payload)) + .rejects.toMatchObject({ code: "chain_observation_uncommitted" }); + transactionStatus = "committed"; + transactionBlockHash = null; + await expect(verifyMainnetDeployment({ CKB_MIN_CONFIRMATIONS: "7" }, payload)) + .rejects.toMatchObject({ code: "invalid_ckb_rpc_response", status: 503 }); + transactionBlockHash = blockHash; + reportedChain = "ckb_testnet"; + await expect(verifyDeployment({ + REGISTRY_ENVIRONMENT: "testnet-sandbox", + REGISTRY_ORIGIN: "https://api.testnet.registry.cellscript.dev", + STATIC_REGISTRY_ORIGIN: "https://objects.testnet.registry.cellscript.dev", + CKB_MIN_CONFIRMATIONS: "7", + }, { ...payload, network: "testnet" })) + .resolves.toMatchObject({ block_number: "0x64", tip_block_number: "0x6a", confirmations: 7 }); + await expect(verifyDeployment({ + REGISTRY_ENVIRONMENT: "testnet-sandbox", + REGISTRY_ORIGIN: "https://api.testnet.registry.cellscript.dev", + STATIC_REGISTRY_ORIGIN: "https://objects.testnet.registry.cellscript.dev", + }, payload)).rejects.toMatchObject({ code: "unsupported_deployment_network" }); + expect(transactionRequests).toEqual([ + [deploymentTxHash], + [deploymentTxHash], + [deploymentTxHash], + [deploymentTxHash], + [deploymentTxHash], + ]); + } finally { + vi.unstubAllGlobals(); + } + }); +}); + +async function ckbAuthPayload(): Promise { + const publicKey = bytesHex(secp256k1.getPublicKey(ckbPrivateKey, true)); + return { + ...authPayload(), + principal_type: "ckb_secp256k1", + principal_id: await ckbSecp256k1PrincipalIdFromPublicKey(publicKey), + }; +} + +function ckbWalletSignature( + payload: CapabilityAuthorisationPayload | CapabilityRevocationPayload, +): CkbSecp256k1Signature { + const challenge = canonicalJson(payload); + const message = new TextEncoder().encode(`Nervos Message:${challenge}`); + const messageHash = blake2b(message, { + dkLen: 32, + personalization: new TextEncoder().encode("ckb-default-hash"), + }); + const recovered = secp256k1.sign(messageHash, ckbPrivateKey, { format: "recovered", prehash: false }); + const ckbSignature = new Uint8Array(65); + ckbSignature.set(recovered.subarray(1), 0); + ckbSignature[64] = recovered[0] ?? 0; + return { + scheme: "ckb_secp256k1", + challenge, + signature: bytesHex(ckbSignature), + public_key: bytesHex(secp256k1.getPublicKey(ckbPrivateKey, true)), + }; +} function authPayload(principalId = "0x1111111111111111111111111111111111111111"): CapabilityAuthorisationPayload { return { @@ -26,12 +284,16 @@ function authPayload(principalId = "0x1111111111111111111111111111111111111111") principal_type: "joyid_ckb", principal_id: principalId, capability_pubkey: `p256-spki:${principalId.slice(2)}`, - requested_scopes: ["publish:cellscript/demo"], + requested_scopes: [ + "publish:cellscript/demo", + "deployment:cellscript/demo", + "availability:cellscript/demo", + ], capability_expires_at: "2026-09-21T12:00:00Z", nonce: "0x1111111111111111", issued_at: "2026-06-23T12:00:00Z", expires_at: "2026-06-23T12:10:00Z", - cli_version: "cellc 0.20.0", + cli_version: "cellc 0.23.0", }; } @@ -61,7 +323,29 @@ function revokePayload(keyId: string, principalId = "0x1111111111111111111111111 nonce: "0x3333333333333333", issued_at: "2026-06-23T12:00:00Z", expires_at: "2026-06-23T12:10:00Z", - cli_version: "cellc 0.20.0", + cli_version: "cellc 0.23.0", + }; +} + +function availabilityPayload( + keyId: string, + status: AvailabilityPayload["availability_status"] = "yanked", + nonce = "0x7777777777777777", +): AvailabilityPayload { + return { + protocol: AVAILABILITY_PROTOCOL, + action: AVAILABILITY_ACTION, + registry_origin: DEFAULT_REGISTRY_ORIGIN, + namespace: "cellscript", + name: "demo", + release: "1.2.3", + availability_status: status, + ...(status === "yanked" ? { reason: "security review" } : {}), + capability_key_id: keyId, + nonce, + issued_at: "2026-06-23T12:00:00Z", + expires_at: "2026-06-23T12:10:00Z", + cli_version: "cellc 0.23.0", }; } @@ -94,13 +378,201 @@ async function publishPayload(keyId: string): Promise { nonce: "0x2222222222222222", issued_at: "2026-06-23T12:00:00Z", expires_at: "2026-06-23T12:10:00Z", - cli_version: "cellc 0.20.0", + cli_version: "cellc 0.23.0", + artifact: { + kind: "source_library", + profile: "cellscript_source", + consumption_mode: "dependency", + language: "cellscript", + }, registry_entry: { + schema_version: 1, namespace: "cellscript", name: "demo", - version: "1.2.3", + artifact: { + kind: "source_library", + profile: "cellscript_source", + consumption_mode: "dependency", + language: "cellscript", + }, repository: "https://github.com/cellscript/demo", + versions: [{ + version: "1.2.3", + tag: "v1.2.3", + source_hash: `0x${"ab".repeat(32)}`, + cellscript_version: "0.23.0", + edition: "2026", + compatibility_profile_hash: "ef".repeat(32), + dependencies: {}, + verification_status: "pending", + deployment_status: "not_applicable", + availability_status: "active", + }], + }, + }; +} + +async function ckbExecutablePublishPayload(keyId: string): Promise { + const payload = await publishPayload(keyId); + payload.artifact = { + kind: "deployable_contract", + profile: "ckb_executable", + consumption_mode: "deployment", + language: "rust", + }; + payload.registry_entry.artifact = payload.artifact; + const release = payload.registry_entry.versions[0]; + delete release.cellscript_version; + delete release.edition; + delete release.compatibility_profile_hash; + delete release.dependencies; + release.artifact_hash = `0x${"31".repeat(32)}`; + release.abi_hash = `0x${"32".repeat(32)}`; + release.profile_contract = { + schema: "cellscript-registry-profile-contract-v1", + artifact_kind: "deployable_contract", + profile: "ckb_executable", + build: { + target: "riscv64imac-unknown-none-elf", + toolchain: "rustc 1.97.1", + profile: "release", + source_revision: "0123456789abcdef", + reproducible: false, }, + security: { status: "review_required" }, + ckb: { + vm_version: "2", + script_role: "type", + hash_type: "data1", + dep_type: "code", + abi_hash: release.abi_hash, + }, + }; + payload.manifest_hash = ckbBlake2bHex(canonicalJson(release.profile_contract)); + release.deployment_status = "undeployed"; + return payload; +} + +function declareReproducibleBuild(payload: PublishPayload): void { + const release = payload.registry_entry.versions[0]; + const contract = release.profile_contract!; + const recipeHash = `0x${"34".repeat(32)}`; + (contract["build"] as Record)["reproducible"] = true; + contract["reproduction"] = { + environment: "docker.io/library/rust:1.97.1@sha256:0123456789abcdef", + command: "cargo build --locked --release", + recipe_hash: recipeHash, + expected_artifact_hash: release.artifact_hash, + }; + release.build_recipe_hash = recipeHash; + payload.manifest_hash = ckbBlake2bHex(canonicalJson(contract)); +} + +describe("generic artifact profile contracts", () => { + it("exposes one versioned, fail-closed definition for every artifact profile", () => { + expect(Object.keys(ARTIFACT_PROFILE_CATALOG).sort()).toEqual([ + "cellscript_source", + "ckb_executable", + "copy_material", + "reproducible_build", + ]); + expect(Object.values(ARTIFACT_PROFILE_CATALOG).every((definition) => definition.schema === ARTIFACT_PROFILE_CATALOG_SCHEMA)).toBe(true); + expect(artifactProfileSupportsDependencyResolution("cellscript_source")).toBe(true); + expect(artifactProfileSupportsDependencyResolution("ckb_executable")).toBe(false); + }); + + it("keeps unknown profiles and non-source dependency contracts out of the resolver surface", () => { + expect(() => validateArtifactDescriptor({ + kind: "source_library", + profile: "future_profile", + consumption_mode: "dependency", + language: "cellscript", + })).toThrow(/artifact.profile must be one of/); + expect(() => validateArtifactDescriptor({ + kind: "deployable_contract", + profile: "ckb_executable", + consumption_mode: "dependency", + language: "rust", + })).toThrow(/do not match its kind/); + }); + + it("requires a typed profile contract for non-CellScript releases", async () => { + const payload = await ckbExecutablePublishPayload("cap_test"); + delete payload.registry_entry.versions[0].profile_contract; + expect(() => validatePublishPayload(payload, DEFAULT_REGISTRY_ORIGIN, now)).toThrow(/profile_contract/); + }); + + it("rejects contract hashes that do not bind the immutable ABI identity", async () => { + const payload = await ckbExecutablePublishPayload("cap_test"); + const contract = payload.registry_entry.versions[0].profile_contract!; + (contract["ckb"] as Record)["abi_hash"] = `0x${"99".repeat(32)}`; + payload.manifest_hash = ckbBlake2bHex(canonicalJson(contract)); + expect(() => validatePublishPayload(payload, DEFAULT_REGISTRY_ORIGIN, now)).toThrow(/abi_hash.*does not match/); + }); + + it("rejects unknown profile contract fields", async () => { + const payload = await ckbExecutablePublishPayload("cap_test"); + const contract = payload.registry_entry.versions[0].profile_contract!; + contract["trust_me"] = true; + payload.manifest_hash = ckbBlake2bHex(canonicalJson(contract)); + expect(() => validatePublishPayload(payload, DEFAULT_REGISTRY_ORIGIN, now)).toThrow(/trust_me is not recognised/); + }); + + it("allows a deployed CKB executable to bind a reproducible build recipe", async () => { + const payload = await ckbExecutablePublishPayload("cap_test"); + declareReproducibleBuild(payload); + + expect(validatePublishPayload(payload, DEFAULT_REGISTRY_ORIGIN, now).artifact.profile).toBe("ckb_executable"); + }); + + it("admits only the exact LS-IDL 0.1 lock-script profile shape", async () => { + const payload = await ckbExecutablePublishPayload("cap_test"); + const release = payload.registry_entry.versions[0]; + const contract = release.profile_contract!; + (contract["ckb"] as Record)["script_role"] = "lock"; + contract["interface"] = { + schema: "cellscript-registry-ls-idl-interface-v1", + format: "ls-idl", + format_version: "0.1", + object_role: "abi", + content_type: "application/vnd.ckb.ls-idl+json", + encoding: "linear-le-v0", + commitment: { + algorithm: "sha256", + placement: "code-cell-data-suffix-32", + digest: `0x${"77".repeat(32)}`, + }, + }; + payload.manifest_hash = ckbBlake2bHex(canonicalJson(contract)); + expect(validatePublishPayload(payload, DEFAULT_REGISTRY_ORIGIN, now).registry_entry.versions[0].profile_contract) + .toMatchObject({ interface: { format: "ls-idl", format_version: "0.1" } }); + + (contract["ckb"] as Record)["script_role"] = "type"; + payload.manifest_hash = ckbBlake2bHex(canonicalJson(contract)); + expect(() => validatePublishPayload(payload, DEFAULT_REGISTRY_ORIGIN, now)).toThrow(/script_role must be 'lock'/); + }); +}); + +function deploymentPayload(keyId: string): DeploymentPayload { + return { + protocol: DEPLOYMENT_PROTOCOL, + action: DEPLOYMENT_ACTION, + registry_origin: DEFAULT_REGISTRY_ORIGIN, + namespace: "cellscript", + name: "demo", + release: "1.2.3", + network: "mainnet", + artifact_hash: `0x${"31".repeat(32)}`, + data_hash: `0x${"31".repeat(32)}`, + code_hash: `0x${"31".repeat(32)}`, + hash_type: "data1", + dep_type: "code", + out_point: { tx_hash: `0x${"41".repeat(32)}`, index: 0 }, + capability_key_id: keyId, + nonce: "0x4444444444444444", + issued_at: "2026-06-23T12:00:00Z", + expires_at: "2026-06-23T12:10:00Z", + cli_version: "cellc 0.23.0", }; } @@ -112,7 +584,7 @@ function utf8(bytes: Uint8Array): string { return new TextDecoder().decode(bytes); } -function testApp(store = new MemoryRegistryStore(), writer?: SnapshotWriter) { +function testApp(store = new MemoryRegistryStore(), writer?: SnapshotWriter, deps: Partial = {}) { const snapshots: Array<{ key: string; body: Uint8Array; contentType: string }> = []; const snapshotWriter = writer ?? @@ -127,6 +599,7 @@ function testApp(store = new MemoryRegistryStore(), writer?: SnapshotWriter) { joyidVerifier: { verifySignature: async () => true }, capabilityVerifier: { verify: async () => true }, snapshotWriter, + ...deps, }); return { app, store, snapshots }; } @@ -163,9 +636,255 @@ async function get( ); } +async function createBrowserAuthorisationSession( + app: ReturnType, + namespace = "walletdemo", + name = "demo", +) { + const response = await post(app, "/v1/authorisation-sessions", { + capability_pubkey: reproducerPublicKeys["builder-a"], + requested_scopes: [`publish:${namespace}/${name}`], + artifact_kind: "source_library", + capability_expires_at: "2026-09-21T12:00:00Z", + cli_version: "0.23.0", + }); + expect(response.status).toBe(201); + const created = await response.json() as any; + const browserParams = new URLSearchParams(new URL(created.browser_url).hash.slice(1)); + const browserToken = browserParams.get("browser_token"); + expect(browserToken).toMatch(/^browser_[0-9a-f]{32}$/); + return { created, browserToken: String(browserToken) }; +} + +async function prepareBrowserAuthorisationChallenge( + app: ReturnType, + sessionId: string, + browserToken: string, +) { + const wallet = await ckbAuthPayload(); + const response = await post(app, `/v1/authorisation-sessions/${sessionId}/challenge`, { + principal_type: wallet.principal_type, + principal_id: wallet.principal_id, + }, {}, { authorization: `Bearer ${browserToken}` }); + expect(response.status).toBe(200); + return await response.json() as any; +} + +async function completeBrowserAuthorisationSession( + app: ReturnType, + sessionId: string, + browserToken: string, + challenge: any, +) { + return post(app, `/v1/authorisation-sessions/${sessionId}/complete`, { + challenge_token: challenge.challenge_token, + wallet_signature: ckbWalletSignature(challenge.payload), + }, {}, { authorization: `Bearer ${browserToken}` }); +} + +async function lsIdlLookupApp(idlBytes: Uint8Array) { + const store = new MemoryRegistryStore(); + const idl = new TextDecoder().decode(idlBytes); + const digest = await sha256Hex(idlBytes); + const codeHash = `0x${"31".repeat(32)}`; + const payload = await ckbExecutablePublishPayload("cap_test"); + const release = payload.registry_entry.versions[0]; + const contract = release.profile_contract!; + (contract["ckb"] as Record)["script_role"] = "lock"; + contract["interface"] = { + schema: "cellscript-registry-ls-idl-interface-v1", + format: "ls-idl", + format_version: "0.1", + object_role: "abi", + content_type: "application/vnd.ckb.ls-idl+json", + encoding: "linear-le-v0", + commitment: { algorithm: "sha256", placement: "code-cell-data-suffix-32", digest: `0x${digest}` }, + }; + payload.manifest_hash = ckbBlake2bHex(canonicalJson(contract)); + const version: PackageVersionRecord = { + namespace: "cellscript", + name: "demo", + version: "1.2.3", + status: "deployed", + artifact: payload.artifact, + verification_status: "hash_bound", + deployment_status: "chain_verified", + availability_status: "active", + source_hash: payload.source_hash, + manifest_hash: payload.manifest_hash, + capability_key_id: "cap_test", + principal_type: "joyid_ckb", + principal_id: `0x${"11".repeat(20)}`, + registry_entry: payload.registry_entry, + snapshot_hash: `sha256:${"ab".repeat(32)}`, + direct_url: "https://registry.cellscript.dev/artifacts/cellscript/demo/releases/1.2.3.json", + created_at: now.toISOString(), + registry_environment: "production", + network: "mainnet", + }; + store.packageVersions.set("cellscript/demo@1.2.3", version); + store.packageEvidence.set("cellscript/demo@1.2.3:deployed:test", { + namespace: "cellscript", + name: "demo", + version: "1.2.3", + kind: "deployed", + evidence_hash: `sha256:${"cd".repeat(32)}`, + evidence: { + network: "mainnet", + code_hash: codeHash, + data_hash: codeHash, + hash_type: "data1", + dep_type: "code", + }, + request_id: "test", + admin_actor: "test", + created_at: now.toISOString(), + }); + store.snapshots.set(version.snapshot_hash, { + snapshot_hash: version.snapshot_hash, + r2_key: "source-snapshots/cellscript/demo/1.2.3/bundle.json", + source_hash: version.source_hash, + size_bytes: 1, + content_type: "application/vnd.cellscript.artifact-bundle+json", + }); + const bundle = JSON.stringify({ + schema: "cellscript-registry-bundle", + namespace: "cellscript", + name: "demo", + release: "1.2.3", + profile: "ckb_executable", + manifest_json: canonicalJson(contract), + objects: [ + { role: "source", content_base64: base64("source") }, + { role: "executable", content_base64: base64("binary") }, + { role: "abi", content_base64: Buffer.from(idlBytes).toString("base64") }, + ], + }); + const app = createApp({ + store, + registryObjectReader: { + async get(key) { + expect(key).toBe("source-snapshots/cellscript/demo/1.2.3/bundle.json"); + return { body: bundle, contentType: "application/json" }; + }, + }, + }); + return { app, codeHash, digest, idl }; +} + describe("registry api", () => { + it("serves exact LS-IDL bytes by chain-verified code hash without JSON reserialization", async () => { + const idl = "{\n \"witness\": [{\"name\":\"signature\",\"type\":\"secp256k1_sig\",\"required\":true}]\n}\n"; + const { app, codeHash, digest } = await lsIdlLookupApp(new TextEncoder().encode(idl)); + + const compatibility = await get(app, `/idl/${codeHash.slice(2)}`); + expect(compatibility.status).toBe(200); + expect(await compatibility.text()).toBe(idl); + expect(compatibility.headers.get("x-ls-idl-sha256")).toBe(digest); + expect(compatibility.headers.get("x-ls-idl-verification")).toBe("schema-and-suffix-bound"); + + const formal = await get(app, `/v1/ckb/scripts/${codeHash}/interfaces/ls-idl?hash_type=data1&data_hash=${codeHash}`); + expect(formal.status).toBe(200); + expect(await formal.text()).toBe(idl); + }); + + it.runIf(Boolean(process.env.CELLSCRIPT_CKB_IDL_CLIENT_REPO))( + "interoperates with the pinned upstream Rust client over the compatibility route", + async () => { + const upstreamClientRepo = String(process.env.CELLSCRIPT_CKB_IDL_CLIENT_REPO); + const encodedFixture = await readFile( + new URL("../../../tests/compat/ls_idl/scripts/simple-lock.idl.json.b64", import.meta.url), + "utf8", + ); + const idlBytes = Buffer.from(encodedFixture.replace(/\s/g, ""), "base64"); + const { app, codeHash } = await lsIdlLookupApp(idlBytes); + const server = createServer(async (request, response) => { + try { + const registryResponse = await app.fetch( + new Request(`http://127.0.0.1${request.url ?? "/"}`), + { REGISTRY_ORIGIN: DEFAULT_REGISTRY_ORIGIN }, + ); + const headers: Record = {}; + registryResponse.headers.forEach((value, name) => { headers[name] = value; }); + response.writeHead(registryResponse.status, headers); + response.end(Buffer.from(await registryResponse.arrayBuffer())); + } catch (error) { + response.writeHead(500, { "content-type": "text/plain" }); + response.end(error instanceof Error ? error.message : "Registry bridge failed"); + } + }); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Registry bridge did not bind a TCP port"); + + const temporaryProject = await mkdtemp(join(tmpdir(), "cellscript-ls-idl-upstream-")); + try { + await writeFile( + join(temporaryProject, "Cargo.toml"), + `[package]\nname = "cellscript-ls-idl-upstream-probe"\nversion = "0.0.0"\nedition = "2024"\n\n[[bin]]\nname = "cellscript-ls-idl-upstream-probe"\npath = "main.rs"\n\n[dependencies]\nckb-idl-client = { path = ${JSON.stringify(upstreamClientRepo)} }\nhex = "0.4"\nsha2 = "0.11.0"\ntokio = { version = "1", features = ["rt-multi-thread", "macros"] }\n`, + ); + await writeFile( + join(temporaryProject, "main.rs"), + `use ckb_idl_client::IdlClient;\nuse sha2::{Digest as _, Sha256};\n\n#[tokio::main]\nasync fn main() -> Result<(), Box> {\n let arguments: Vec = std::env::args().collect();\n let base_url = &arguments[1];\n let code_hash_bytes = hex::decode(&arguments[2])?;\n let code_hash: [u8; 32] = code_hash_bytes.try_into().map_err(|_| std::io::Error::other("code hash must be 32 bytes"))?;\n let idl_path = &arguments[3];\n\n let mut client = IdlClient::new();\n let document = client.fetch(base_url, code_hash).await?;\n assert_eq!(document.witness.len(), 1);\n assert_eq!(document.witness[0].name, "preimage");\n assert_eq!(document.witness[0].type_, "bytes");\n\n let expected_idl_bytes = std::fs::read(idl_path)?;\n let raw_url = format!("{}/idl/{}", base_url, hex::encode(code_hash));\n let fetched_idl_bytes = client.http.get(raw_url).send().await?.bytes().await?;\n assert_eq!(fetched_idl_bytes.as_ref(), expected_idl_bytes.as_slice());\n let mut code_cell_data = b"fixture executable".to_vec();\n code_cell_data.extend_from_slice(&Sha256::digest(&expected_idl_bytes));\n client.verify(code_hash, &fetched_idl_bytes, &code_cell_data)?;\n\n let cached = client.witness_requirements(base_url, code_hash).await?;\n assert_eq!(cached, document.witness);\n let decoded = client.validate_witness_bytes(&cached, &[5, 0, 0, 0, b'h', b'e', b'l', b'l', b'o'])?;\n assert_eq!(decoded.len(), 1);\n println!("upstream client fetch, SHA-256 verify, cache, and witness decode passed");\n Ok(())\n}\n`, + ); + const idlPath = join(temporaryProject, "simple-lock.idl.json"); + await writeFile(idlPath, idlBytes); + const targetDir = process.env.CELLSCRIPT_LS_IDL_CARGO_TARGET_DIR ?? join(temporaryProject, "target"); + const result = await execFileAsync( + "cargo", + [ + "run", + "--quiet", + "--manifest-path", + join(temporaryProject, "Cargo.toml"), + "--target-dir", + targetDir, + "--", + `http://127.0.0.1:${address.port}`, + codeHash.slice(2), + idlPath, + ], + { env: process.env, maxBuffer: 1024 * 1024 }, + ); + expect(result.stdout).toContain("upstream client fetch, SHA-256 verify, cache, and witness decode passed"); + } finally { + await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); + await rm(temporaryProject, { recursive: true, force: true }); + } + }, + 180_000, + ); + + it("matches the canonical CKB Molecule Script hash", () => { + expect(ckbScriptHash({ + code_hash: `0x${"11".repeat(32)}`, + hash_type: "type", + args: "0x1234", + })).toBe("0x6106e30cbb34d68302798abf8259e5a6e0adbbd73c7f3dfe1c96ada1f6c00cee"); + }); + + it("treats edition and compatibility profile as independent registry axes", async () => { + const first = await publishPayload("profile-axis-test"); + const second = structuredClone(first); + second.registry_entry.versions[0].compatibility_profile_hash = "12".repeat(32); + + const firstValidated = validatePublishPayload(first, DEFAULT_REGISTRY_ORIGIN, now); + const secondValidated = validatePublishPayload(second, DEFAULT_REGISTRY_ORIGIN, now); + + expect(firstValidated.registry_entry.versions[0].edition).toBe("2026"); + expect(secondValidated.registry_entry.versions[0].edition).toBe("2026"); + expect(secondValidated.registry_entry.versions[0].compatibility_profile_hash) + .not.toBe(firstValidated.registry_entry.versions[0].compatibility_profile_hash); + }); + it("reports readiness only when production bindings are configured", async () => { const app = createApp(); + const live = await get(app, "/health"); + expect(live.status).toBe(200); + expect(await live.json()).toMatchObject({ status: "ok" }); const missing = await get(app, "/ready"); expect(missing.status).toBe(503); expect(await missing.json()).toMatchObject({ @@ -177,20 +896,134 @@ describe("registry api", () => { }, }); - const ready = await get(app, "/ready", { - HYPERDRIVE: {}, - REGISTRY_OBJECTS: {}, - REGISTRY_ADMIN_TOKEN: "secret", + const readyApp = createApp({ + store: new MemoryRegistryStore(), + snapshotWriter: { async put() {} }, + registryObjectReader: { async get() { return null; } }, + readinessCheck: async () => ({ runtime: "ready" }), }); + const ready = await get(readyApp, "/ready", { REGISTRY_ADMIN_TOKEN: "secret" }); expect(ready.status).toBe(200); + expect(ready.headers.get("content-security-policy")) + .toBe("default-src 'none'; base-uri 'none'; frame-ancestors 'none'"); + expect(ready.headers.get("permissions-policy")).toBe("camera=(), geolocation=(), microphone=()"); + expect(ready.headers.get("strict-transport-security")).toBe("max-age=31536000"); + expect(ready.headers.get("x-frame-options")).toBe("DENY"); + expect(ready.headers.get("x-permitted-cross-domain-policies")).toBe("none"); expect(await ready.json()).toMatchObject({ status: "ready", checks: { - store: "configured", + store: "ready", object_store: "configured", admin_token: "configured", + runtime: "ready", + }, + }); + + const partiallyConfigured = await get(readyApp, "/ready", { + REGISTRY_ADMIN_TOKEN: "secret", + REGISTRY_TYPE_SCRIPT_JSON: JSON.stringify({ code_hash: `0x${"11".repeat(32)}`, hash_type: "type", args: "0x" }), + }); + expect(partiallyConfigured.status).toBe(503); + expect(await partiallyConfigured.json()).toMatchObject({ + status: "not_ready", + checks: { registry_commitment: "misconfigured" }, + }); + + const typeScript = { code_hash: `0x${"11".repeat(32)}`, hash_type: "data1", args: "0x01" }; + const commitmentLock = { code_hash: `0x${"22".repeat(32)}`, hash_type: "type", args: "0x02" }; + const typeCellDep = { + out_point: { tx_hash: `0x${"33".repeat(32)}`, index: "0x0" }, + dep_type: "code", + }; + const lockCellDep = { + out_point: { tx_hash: `0x${"44".repeat(32)}`, index: "0x0" }, + dep_type: "code", + }; + let configurationChecked = false; + const commitmentReadyApp = createApp({ + store: new MemoryRegistryStore(), + snapshotWriter: { async put() {} }, + registryObjectReader: { async get() { return null; } }, + verifyRegistryCommitmentConfiguration: async (configuration) => { + configurationChecked = true; + expect(configuration.type_script_hash).toBe(ckbScriptHash(typeScript)); + expect(configuration.commitment_lock_hash).toBe(ckbScriptHash(commitmentLock)); + }, + }); + const configured = await get(commitmentReadyApp, "/ready", { + REGISTRY_ADMIN_TOKEN: "secret", + REGISTRY_TYPE_SCRIPT_JSON: JSON.stringify(typeScript), + REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON: JSON.stringify(typeCellDep), + REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON: JSON.stringify(commitmentLock), + REGISTRY_COMMITMENT_LOCK_CELL_DEP_JSON: JSON.stringify(lockCellDep), + }); + expect(configured.status).toBe(200); + expect(configurationChecked).toBe(true); + expect(await configured.json()).toMatchObject({ + status: "ready", + checks: { registry_commitment: "configured_and_live" }, + }); + + const nonCanonicalProduction = await get(commitmentReadyApp, "/ready", { + ENVIRONMENT: "production", + REGISTRY_ADMIN_TOKEN: "secret", + REGISTRY_TYPE_SCRIPT_JSON: JSON.stringify(typeScript), + REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON: JSON.stringify(typeCellDep), + REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON: JSON.stringify(commitmentLock), + REGISTRY_COMMITMENT_LOCK_CELL_DEP_JSON: JSON.stringify(lockCellDep), + }); + expect(nonCanonicalProduction.status).toBe(503); + expect(await nonCanonicalProduction.json()).toMatchObject({ + status: "not_ready", + checks: { registry_commitment: "misconfigured" }, + }); + + const canonicalLock = { ...CKB_MAINNET_SIGHASH_LOCK, args: `0x${"55".repeat(20)}` }; + const canonicalTypeScript = { + ...CANONICAL_REGISTRY_TYPE_SCRIPT, + args: ckbScriptHash(canonicalLock), + }; + const canonicalTypeCellDep = { + out_point: { tx_hash: `0x${"66".repeat(32)}`, index: "0x0" }, + dep_type: "code", + }; + let canonicalConfigurationChecked = false; + const productionCommitmentApp = createApp({ + store: new MemoryRegistryStore(), + snapshotWriter: { async put() {} }, + registryObjectReader: { async get() { return null; } }, + verifyRegistryCommitmentConfiguration: async () => { + canonicalConfigurationChecked = true; }, }); + const canonicalProduction = await get(productionCommitmentApp, "/ready", { + ENVIRONMENT: "production", + REGISTRY_ADMIN_TOKEN: "secret", + REGISTRY_TYPE_SCRIPT_JSON: JSON.stringify(canonicalTypeScript), + REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON: JSON.stringify(canonicalTypeCellDep), + REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON: JSON.stringify(canonicalLock), + REGISTRY_COMMITMENT_LOCK_CELL_DEP_JSON: JSON.stringify(CKB_MAINNET_SIGHASH_DEP_GROUP), + }); + expect(canonicalProduction.status).toBe(200); + expect(canonicalConfigurationChecked).toBe(true); + + const invalidReproducerPolicy = await get(commitmentReadyApp, "/ready", { + REGISTRY_ADMIN_TOKEN: "secret", + REGISTRY_REPRODUCER_POLICY_JSON: JSON.stringify({ + schema: "cellscript-reproducer-policy-v1", + minimum_trust_domains: 2, + builders: [ + { builder_id: "builder-a", trust_domain: "same-operator", public_key: reproducerPublicKeys["builder-a"] }, + { builder_id: "builder-b", trust_domain: "same-operator", public_key: reproducerPublicKeys["builder-b"] }, + ], + }), + }); + expect(invalidReproducerPolicy.status).toBe(503); + expect(await invalidReproducerPolicy.json()).toMatchObject({ + status: "not_ready", + checks: { reproducer_policy: "misconfigured" }, + }); }); it("rejects JoyID signatures that do not bind the canonical capability payload", async () => { @@ -206,6 +1039,23 @@ describe("registry api", () => { expect(body.error.code).toBe("joyid_challenge_mismatch"); }); + it("rejects empty, duplicate, and unknown capability scopes", async () => { + for (const [requestedScopes, expectedCode] of [ + [[], "invalid_scope"], + [["publish:cellscript/demo", "publish:cellscript/demo"], "duplicate_scope"], + [["admin:cellscript/demo"], "invalid_scope"], + ] as const) { + const { app } = testApp(); + const payload = { ...authPayload(), requested_scopes: [...requestedScopes] }; + const response = await post(app, "/v1/capabilities", { + payload, + joyid_signature: joyidSignature(payload), + }); + expect(response.status).toBe(400); + expect((await response.json() as any).error.code).toBe(expectedCode); + } + }); + it("rejects JoyID signatures whose signer does not match principal_id", async () => { const { app } = testApp(); const payload = authPayload("0x1111111111111111111111111111111111111111"); @@ -256,93 +1106,674 @@ describe("registry api", () => { expect(body.principal_id).toBe(principalId); }); - it("creates a capability, claims namespace, stores snapshot, and admits source_published publish", async () => { - const { app, store, snapshots } = testApp(); - const payload = authPayload(); - const capabilityResponse = await post(app, "/v1/capabilities", { - payload, - joyid_signature: joyidSignature(payload), - }); - expect(capabilityResponse.status).toBe(201); - const capability = await capabilityResponse.json() as any; - expect(capability.key_id).toBe(await capabilityKeyId(payload.capability_pubkey)); - - const claimResponse = await post(app, "/v1/namespaces/claim", { - namespace: "cellscript", + it("accepts a capability authorised by a standard CKB secp256k1 wallet", async () => { + const { app } = testApp(); + const payload = await ckbAuthPayload(); + const response = await post(app, "/v1/capabilities", { payload, - joyid_signature: joyidSignature(payload), + wallet_signature: ckbWalletSignature(payload), }); - expect(claimResponse.status).toBe(202); - expect((await claimResponse.json() as any).status).toBe("review_pending"); - store.namespaces.set("cellscript", { - namespace: "cellscript", + expect(response.status).toBe(201); + expect(await response.json()).toMatchObject({ + principal_type: "ckb_secp256k1", + principal_id: payload.principal_id, status: "active", - owner_principal_type: "joyid_ckb", - owner_principal_id: payload.principal_id, }); + }); - const publish = await publishPayload(capability.key_id); - const publishResponse = await post(app, "/v1/packages/cellscript/demo/versions", { - payload: publish, - capability_signature: { algorithm: "p256-sha256", signature: "sig" }, - source_snapshot: { - content_base64: base64("source snapshot"), - content_type: "application/vnd.cellscript.source+tar", - size_bytes: "source snapshot".length, - source_hash: publish.source_hash, - }, + it("completes a short-lived CLI-to-browser authorisation session without exposing the poll result", async () => { + const { app, store } = testApp(); + const createdResponse = await post(app, "/v1/authorisation-sessions", { + capability_pubkey: reproducerPublicKeys["builder-a"], + requested_scopes: ["publish:walletdemo/demo"], + artifact_kind: "source_library", + capability_expires_at: "2026-09-21T12:00:00Z", + cli_version: "0.23.0", + }); + expect(createdResponse.status).toBe(201); + const created = await createdResponse.json() as any; + const browserUrl = new URL(created.browser_url); + const browserParams = new URLSearchParams(browserUrl.hash.slice(1)); + const browserToken = browserParams.get("browser_token"); + expect(browserUrl.origin + browserUrl.pathname).toBe("https://cellscript.dev/registry/submit"); + expect(browserParams.get("authorisation_session")).toBe(created.session_id); + expect(browserToken).toMatch(/^browser_[0-9a-f]{32}$/); + + const publicPending = await get(app, `/v1/authorisation-sessions/${created.session_id}`); + expect(publicPending.status).toBe(401); + const browserPending = await get(app, `/v1/authorisation-sessions/${created.session_id}`, {}, { + authorization: `Bearer ${browserToken}`, + }); + expect(await browserPending.json()).not.toHaveProperty("capability_key_id"); + + const wallet = await ckbAuthPayload(); + const challengeResponse = await post(app, `/v1/authorisation-sessions/${created.session_id}/challenge`, { + principal_type: wallet.principal_type, + principal_id: wallet.principal_id, + }, {}, { authorization: `Bearer ${browserToken}` }); + expect(challengeResponse.status).toBe(200); + const challenge = await challengeResponse.json() as any; + expect(challenge.payload).toMatchObject({ + principal_type: "ckb_secp256k1", + principal_id: wallet.principal_id, + requested_scopes: ["publish:walletdemo/demo"], + capability_pubkey: reproducerPublicKeys["builder-a"], }); - expect(publishResponse.status).toBe(202); - const body = await publishResponse.json() as any; - expect(body.status).toBe("source_published"); - expect(body.direct_url).toBe("https://registry.cellscript.dev/packages/cellscript/demo/versions/1.2.3.json"); - expect(snapshots).toHaveLength(2); - const sourceSnapshot = snapshots.find((snapshot) => snapshot.key.startsWith("source-snapshots/")); - const staticEntry = snapshots.find((snapshot) => snapshot.key === "packages/cellscript/demo/versions/1.2.3.json"); - expect(sourceSnapshot?.key).toContain("source-snapshots/cellscript/demo/1.2.3/"); - expect(staticEntry).toBeTruthy(); - const staticBody = JSON.parse(utf8(staticEntry!.body)) as any; - expect(staticBody.kind).toBe("cellscript.registry.package_version"); - expect(staticBody.coordinate).toBe("cellscript/demo@1.2.3"); - expect(staticBody.status).toBe("source_published"); - expect(store.packageVersions.get("cellscript/demo@1.2.3")?.status).toBe("source_published"); - expect(store.capabilities.get(capability.key_id)?.last_used_at).toBeTruthy(); - expect(store.auditEvents.some((event) => event.event_type === "capability.used" && event.capability_key_id === capability.key_id)).toBe(true); - expect(store.auditEvents.some((event) => event.event_type === "publish.accepted")).toBe(true); + const completeResponse = await post(app, `/v1/authorisation-sessions/${created.session_id}/complete`, { + challenge_token: challenge.challenge_token, + wallet_signature: ckbWalletSignature(challenge.payload), + }, {}, { authorization: `Bearer ${browserToken}` }); + expect(completeResponse.status).toBe(201); + expect(await completeResponse.json()).toMatchObject({ status: "authorised", namespace_status: "active" }); + + const browserComplete = await get(app, `/v1/authorisation-sessions/${created.session_id}`, {}, { + authorization: `Bearer ${browserToken}`, + }); + expect(await browserComplete.json()).not.toHaveProperty("capability_key_id"); + const cliPoll = await get(app, `/v1/authorisation-sessions/${created.session_id}`, {}, { + authorization: `Bearer ${created.poll_token}`, + }); + expect(await cliPoll.json()).toMatchObject({ + status: "authorised", + namespace_status: "active", + capability_key_id: await capabilityKeyId(reproducerPublicKeys["builder-a"]), + }); + expect(store.namespaces.get("walletdemo")).toMatchObject({ + owner_principal_type: "ckb_secp256k1", + owner_principal_id: wallet.principal_id, + }); }); - it("serves package-version JSON from the static registry read path without the write store", async () => { - const app = createApp({ - registryObjectReader: { - async get(key) { - expect(key).toBe("packages/cellscript/demo/versions/1.2.3.json"); - return { - body: JSON.stringify({ schema_version: 1, coordinate: "cellscript/demo@1.2.3", status: "source_published" }), - contentType: "application/json; charset=utf-8", - etag: "\"static-entry\"", - }; - }, - }, + it("expires browser authorisation sessions without creating Registry authority", async () => { + const store = new MemoryRegistryStore(); + const { app } = testApp(store); + const { created, browserToken } = await createBrowserAuthorisationSession(app); + const expiredApp = testApp(store, undefined, { + now: () => new Date("2026-06-23T12:16:00Z"), + }).app; + + const response = await get(expiredApp, `/v1/authorisation-sessions/${created.session_id}`, {}, { + authorization: `Bearer ${browserToken}`, }); - const response = await app.fetch(new Request("https://registry.cellscript.dev/packages/cellscript/demo/versions/1.2.3.json")); - expect(response.status).toBe(200); - expect(response.headers.get("cache-control")).toContain("max-age=60"); - expect(response.headers.get("etag")).toBe("\"static-entry\""); - expect((await response.json() as any).coordinate).toBe("cellscript/demo@1.2.3"); + expect(response.status).toBe(410); + expect(await response.json()).toMatchObject({ error: { code: "authorisation_session_expired" } }); + expect(store.capabilities.size).toBe(0); + expect(store.namespaces.size).toBe(0); + expect(store.usedNonces.size).toBe(0); }); - it("replays a successful publish response for the same Idempotency-Key without rewriting objects", async () => { - const { app, store, snapshots } = testApp(); - const payload = authPayload(); - const capabilityResponse = await post(app, "/v1/capabilities", { - payload, - joyid_signature: joyidSignature(payload), + it("keeps a completed authorisation session readable after its approval window closes", async () => { + const store = new MemoryRegistryStore(); + const { app } = testApp(store); + const { created, browserToken } = await createBrowserAuthorisationSession(app, "terminalread", "demo"); + const challenge = await prepareBrowserAuthorisationChallenge(app, created.session_id, browserToken); + const completed = await completeBrowserAuthorisationSession(app, created.session_id, browserToken, challenge); + expect(completed.status).toBe(201); + + const afterExpiry = testApp(store, undefined, { + now: () => new Date("2026-06-23T12:16:00Z"), + }).app; + const poll = await get(afterExpiry, `/v1/authorisation-sessions/${created.session_id}`, {}, { + authorization: `Bearer ${created.poll_token}`, }); - const capability = await capabilityResponse.json() as any; - store.namespaces.set("cellscript", { + + expect(poll.status).toBe(200); + expect(await poll.json()).toMatchObject({ + status: "authorised", + namespace_status: "active", + capability_key_id: await capabilityKeyId(reproducerPublicKeys["builder-a"]), + }); + + const retained = await store.cleanupExpiredState({ + now_iso: "2026-06-23T12:16:00.000Z", + quota_events_before_iso: "2026-06-22T12:16:00.000Z", + }); + expect(retained.authorisation_sessions_deleted).toBe(0); + expect(await store.getAuthorisationSession(created.session_id)).not.toBeNull(); + + const purged = await store.cleanupExpiredState({ + now_iso: "2026-06-24T12:01:00.000Z", + quota_events_before_iso: "2026-06-23T12:01:00.000Z", + }); + expect(purged.authorisation_sessions_deleted).toBe(1); + expect(await store.getAuthorisationSession(created.session_id)).toBeNull(); + }); + + it("rejects browser, poll, and challenge token substitution", async () => { + const { app, store } = testApp(); + const { created, browserToken } = await createBrowserAuthorisationSession(app); + const wrongSessionToken = await get(app, `/v1/authorisation-sessions/${created.session_id}`, {}, { + authorization: "Bearer browser_00000000000000000000000000000000", + }); + expect(wrongSessionToken.status).toBe(401); + + const pollAsBrowser = await post(app, `/v1/authorisation-sessions/${created.session_id}/challenge`, { + principal_type: "ckb_secp256k1", + principal_id: `0x${"11".repeat(20)}`, + }, {}, { authorization: `Bearer ${created.poll_token}` }); + expect(pollAsBrowser.status).toBe(401); + + const challenge = await prepareBrowserAuthorisationChallenge(app, created.session_id, browserToken); + const wrongChallenge = await post(app, `/v1/authorisation-sessions/${created.session_id}/complete`, { + challenge_token: "challenge_00000000000000000000000000000000", + wallet_signature: ckbWalletSignature(challenge.payload), + }, {}, { authorization: `Bearer ${browserToken}` }); + expect(wrongChallenge.status).toBe(401); + expect(await wrongChallenge.json()).toMatchObject({ error: { code: "invalid_authorisation_challenge_token" } }); + expect(store.capabilities.size).toBe(0); + expect(store.namespaces.size).toBe(0); + expect(store.usedNonces.size).toBe(0); + }); + + it("treats a completed challenge replay as an idempotent session read", async () => { + const { app, store } = testApp(); + const { created, browserToken } = await createBrowserAuthorisationSession(app); + const challenge = await prepareBrowserAuthorisationChallenge(app, created.session_id, browserToken); + + const first = await completeBrowserAuthorisationSession(app, created.session_id, browserToken, challenge); + const replay = await completeBrowserAuthorisationSession(app, created.session_id, browserToken, challenge); + + expect(first.status).toBe(201); + expect(replay.status).toBe(200); + expect(await replay.json()).toMatchObject({ status: "authorised", namespace_status: "active" }); + expect(store.capabilities.size).toBe(1); + expect(store.namespaces.size).toBe(1); + expect(store.usedNonces.size).toBe(1); + expect(store.auditEvents.filter((event) => event.event_type === "authorisation_session.completed")).toHaveLength(1); + }); + + it("serialises concurrent complete calls into one atomic authorisation", async () => { + const { app, store } = testApp(); + const { created, browserToken } = await createBrowserAuthorisationSession(app, "concurrent", "demo"); + const challenge = await prepareBrowserAuthorisationChallenge(app, created.session_id, browserToken); + + const responses = await Promise.all([ + completeBrowserAuthorisationSession(app, created.session_id, browserToken, challenge), + completeBrowserAuthorisationSession(app, created.session_id, browserToken, challenge), + ]); + const statuses = responses.map((response) => response.status).sort(); + const bodies = await Promise.all(responses.map((response) => response.json())); + + expect(statuses).toEqual([200, 201]); + expect(bodies).toEqual([ + expect.objectContaining({ status: "authorised", namespace_status: "active" }), + expect.objectContaining({ status: "authorised", namespace_status: "active" }), + ]); + expect(store.capabilities.size).toBe(1); + expect(store.namespaces.size).toBe(1); + expect(store.usedNonces.size).toBe(1); + expect(store.auditEvents.filter((event) => event.event_type === "capability.created")).toHaveLength(1); + expect(store.auditEvents.filter((event) => event.event_type === "authorisation_session.completed")).toHaveLength(1); + }); + + it("rolls back capability, namespace, nonce, and session changes when completion fails", async () => { + const { app, store } = testApp(); + const { created, browserToken } = await createBrowserAuthorisationSession(app, "rollback", "demo"); + const challenge = await prepareBrowserAuthorisationChallenge(app, created.session_id, browserToken); + const appendAuditEvent = store.appendAuditEvent.bind(store); + vi.spyOn(store, "appendAuditEvent").mockImplementation(async (event) => { + if (event.event_type === "authorisation_session.completed") throw new Error("injected completion failure"); + await appendAuditEvent(event); + }); + + const response = await completeBrowserAuthorisationSession(app, created.session_id, browserToken, challenge); + + expect(response.status).toBe(500); + expect(store.capabilities.size).toBe(0); + expect(store.namespaces.size).toBe(0); + expect(store.usedNonces.size).toBe(0); + expect(store.authorisationSessions.get(created.session_id)).toMatchObject({ status: "pending" }); + expect(store.authorisationSessions.get(created.session_id)?.capability_key_id).toBeFalsy(); + expect(store.auditEvents.some((event) => event.event_type === "capability.created")).toBe(false); + expect(store.auditEvents.some((event) => event.event_type === "namespace.claimed")).toBe(false); + }); + + it("keeps a session pending when another identity owns its namespace", async () => { + const { app, store } = testApp(); + const { created, browserToken } = await createBrowserAuthorisationSession(app, "occupied", "demo"); + const challenge = await prepareBrowserAuthorisationChallenge(app, created.session_id, browserToken); + store.namespaces.set("occupied", { + namespace: "occupied", + status: "active", + owner_principal_type: "joyid_ckb", + owner_principal_id: `0x${"44".repeat(20)}`, + }); + + const response = await completeBrowserAuthorisationSession(app, created.session_id, browserToken, challenge); + + expect(response.status).toBe(409); + expect(await response.json()).toMatchObject({ error: { code: "namespace_already_claimed" } }); + expect(store.capabilities.size).toBe(0); + expect(store.usedNonces.size).toBe(0); + expect(store.authorisationSessions.get(created.session_id)).toMatchObject({ status: "pending" }); + expect(store.authorisationSessions.get(created.session_id)?.capability_key_id).toBeFalsy(); + }); + + it("records review_pending atomically for a namespace that requires review", async () => { + const { app, store } = testApp(); + const { created, browserToken } = await createBrowserAuthorisationSession(app, "abc", "demo"); + const challenge = await prepareBrowserAuthorisationChallenge(app, created.session_id, browserToken); + + const response = await completeBrowserAuthorisationSession(app, created.session_id, browserToken, challenge); + + expect(response.status).toBe(202); + expect(await response.json()).toMatchObject({ status: "review_pending", namespace_status: "review_pending" }); + expect(store.capabilities.size).toBe(1); + expect(store.usedNonces.size).toBe(1); + expect(store.namespaces.get("abc")).toMatchObject({ status: "review_pending", review_reason: "short_namespace_review" }); + expect(store.authorisationSessions.get(created.session_id)).toMatchObject({ + status: "review_pending", + namespace_status: "review_pending", + }); + }); + + it("checks an existing capability against its exact artifact and namespace owner", async () => { + const { app, store } = testApp(); + const payload = authPayload(); + const capabilityResponse = await post(app, "/v1/capabilities", { + payload, + joyid_signature: joyidSignature(payload), + }); + expect(capabilityResponse.status).toBe(201); + const capability = await capabilityResponse.json() as any; + store.namespaces.set("cellscript", { + namespace: "cellscript", + status: "active", + owner_principal_type: payload.principal_type, + owner_principal_id: payload.principal_id, + }); + + const ready = await get(app, `/v1/capabilities/${capability.key_id}/check?namespace=cellscript&name=demo`); + expect(ready.status).toBe(200); + expect(await ready.json()).toMatchObject({ + schema: "cellscript-registry-capability-check-v1", + key_id: capability.key_id, + status: "active", + namespace: { + name: "cellscript", + status: "active", + owned_by_capability_principal: true, + }, + allows: { publish: true, deployment: true, availability: true }, + usable_for_publish: true, + reasons: [], + }); + + store.namespaces.set("cellscript", { + namespace: "cellscript", + status: "active", + owner_principal_type: payload.principal_type, + owner_principal_id: "0x2222222222222222222222222222222222222222", + }); + const wrongOwner = await get(app, `/v1/capabilities/${capability.key_id}/check?namespace=cellscript&name=demo`); + expect(await wrongOwner.json()).toMatchObject({ + namespace: { owned_by_capability_principal: false }, + usable_for_publish: false, + reasons: ["namespace_owner_mismatch"], + }); + + store.namespaces.set("cellscript", { + namespace: "cellscript", + status: "active", + owner_principal_type: payload.principal_type, + owner_principal_id: payload.principal_id, + }); + const storedCapability = store.capabilities.get(capability.key_id)!; + storedCapability.scopes = ["deployment:cellscript/demo"]; + const missingScope = await get(app, `/v1/capabilities/${capability.key_id}/check?namespace=cellscript&name=demo`); + expect(await missingScope.json()).toMatchObject({ + allows: { publish: false, deployment: true, availability: false }, + usable_for_publish: false, + reasons: ["publish_scope_missing"], + }); + + storedCapability.scopes = [...payload.requested_scopes]; + storedCapability.expires_at = "2026-06-23T11:59:59Z"; + const expired = await get(app, `/v1/capabilities/${capability.key_id}/check?namespace=cellscript&name=demo`); + expect(await expired.json()).toMatchObject({ + status: "expired", + usable_for_publish: false, + reasons: ["capability_expired"], + }); + + storedCapability.expires_at = "not-a-timestamp"; + const invalidExpiry = await get(app, `/v1/capabilities/${capability.key_id}/check?namespace=cellscript&name=demo`); + expect(await invalidExpiry.json()).toMatchObject({ + status: "expired", + usable_for_publish: false, + reasons: ["capability_expiry_invalid"], + }); + + storedCapability.expires_at = payload.capability_expires_at; + storedCapability.revoked_at = "2026-06-23T11:59:59Z"; + const revoked = await get(app, `/v1/capabilities/${capability.key_id}/check?namespace=cellscript&name=demo`); + expect(await revoked.json()).toMatchObject({ + status: "revoked", + usable_for_publish: false, + reasons: ["capability_revoked"], + }); + }); + + it("rejects malformed or unknown capability IDs from the check route", async () => { + const { app } = testApp(); + const malformed = await get(app, "/v1/capabilities/not-a-capability/check?namespace=cellscript&name=demo"); + expect(malformed.status).toBe(400); + expect((await malformed.json() as any).error.code).toBe("invalid_capability_key_id"); + + const missing = await get(app, "/v1/capabilities/cap_11111111111111111111111111111111/check?namespace=cellscript&name=demo"); + expect(missing.status).toBe(404); + expect((await missing.json() as any).error.code).toBe("capability_not_found"); + }); + + it("lets a standard CKB wallet claim a namespace and revoke its capability", async () => { + const { app, store } = testApp(); + const payload = await ckbAuthPayload(); + payload.requested_scopes = ["publish:walletdemo/demo"]; + const capabilityResponse = await post(app, "/v1/capabilities", { + payload, + wallet_signature: ckbWalletSignature(payload), + }); + expect(capabilityResponse.status).toBe(201); + const capability = await capabilityResponse.json() as any; + + const claimResponse = await post(app, "/v1/namespaces/claim", { + namespace: "walletdemo", + payload, + wallet_signature: ckbWalletSignature(payload), + }); + expect(claimResponse.status).toBe(201); + expect(await claimResponse.json()).toMatchObject({ + namespace: "walletdemo", + status: "active", + }); + expect(store.namespaces.get("walletdemo")).toMatchObject({ + owner_principal_type: "ckb_secp256k1", + owner_principal_id: payload.principal_id, + }); + + const revoke: CapabilityRevocationPayload = { + ...revokePayload(capability.key_id, payload.principal_id), + principal_type: "ckb_secp256k1", + }; + const revokeResponse = await post(app, `/v1/capabilities/${capability.key_id}/revoke`, { + payload: revoke, + wallet_signature: ckbWalletSignature(revoke), + reason: "rotated", + }); + expect(revokeResponse.status).toBe(200); + expect((await revokeResponse.json() as any).status).toBe("revoked"); + expect(store.capabilities.get(capability.key_id)?.revoked_at).toBeTruthy(); + }); + + it("rejects a CKB wallet signature whose public key is not the payload principal", async () => { + const { app } = testApp(); + const payload = await ckbAuthPayload(); + payload.principal_id = `0x${"44".repeat(32)}`; + const response = await post(app, "/v1/capabilities", { + payload, + wallet_signature: ckbWalletSignature(payload), + }); + + expect(response.status).toBe(401); + expect((await response.json() as any).error.code).toBe("ckb_principal_mismatch"); + }); + + it("creates a capability, claims namespace, stores snapshot, and admits source_published publish", async () => { + const { app, store, snapshots } = testApp(); + const payload = authPayload(); + const capabilityResponse = await post(app, "/v1/capabilities", { + payload, + joyid_signature: joyidSignature(payload), + }); + expect(capabilityResponse.status).toBe(201); + const capability = await capabilityResponse.json() as any; + expect(capability.key_id).toBe(await capabilityKeyId(payload.capability_pubkey)); + + const claimResponse = await post(app, "/v1/namespaces/claim", { + namespace: "cellscript", + payload, + joyid_signature: joyidSignature(payload), + }); + expect(claimResponse.status).toBe(202); + expect((await claimResponse.json() as any).status).toBe("review_pending"); + + store.namespaces.set("cellscript", { + namespace: "cellscript", + status: "active", + owner_principal_type: "joyid_ckb", + owner_principal_id: payload.principal_id, + }); + + const publish = await publishPayload(capability.key_id); + const publishResponse = await post(app, "/v1/artifacts/cellscript/demo/releases", { + payload: publish, + capability_signature: { algorithm: "p256-sha256", signature: "sig" }, + source_snapshot: { + content_base64: base64("source snapshot"), + content_type: "application/vnd.cellscript.source+tar", + size_bytes: "source snapshot".length, + source_hash: publish.source_hash, + }, + }); + + expect(publishResponse.status).toBe(202); + const body = await publishResponse.json() as any; + expect(body).toMatchObject({ + verification_status: "pending", + deployment_status: "not_applicable", + availability_status: "active", + }); + expect(body.direct_url).toBe("https://registry.cellscript.dev/artifacts/cellscript/demo/releases/1.2.3.json"); + expect(snapshots).toHaveLength(2); + const sourceSnapshot = snapshots.find((snapshot) => snapshot.key.startsWith("source-snapshots/")); + const staticEntry = snapshots.find((snapshot) => snapshot.key === "artifacts/cellscript/demo/releases/1.2.3.json"); + expect(sourceSnapshot?.key).toContain("source-snapshots/cellscript/demo/1.2.3/"); + expect(staticEntry).toBeTruthy(); + const staticBody = JSON.parse(utf8(staticEntry!.body)) as any; + expect(staticBody.kind).toBe("cellscript.registry.artifact_release"); + expect(staticBody.schema_version).toBe(1); + expect(staticBody.coordinate).toBe("cellscript/demo@1.2.3"); + expect(staticBody).toMatchObject({ + verification_status: "pending", + deployment_status: "not_applicable", + availability_status: "active", + }); + expect(staticBody.edition).toBe("2026"); + expect(staticBody.compatibility_profile_hash).toBe("ef".repeat(32)); + expect(store.packageVersions.get("cellscript/demo@1.2.3")?.status).toBe("source_published"); + expect(store.capabilities.get(capability.key_id)?.last_used_at).toBeTruthy(); + expect(store.auditEvents.some((event) => event.event_type === "capability.used" && event.capability_key_id === capability.key_id)).toBe(true); + expect(store.auditEvents.some((event) => event.event_type === "publish.accepted")).toBe(true); + expect([...store.verificationJobs.values()]).toHaveLength(1); + expect([...store.verificationJobs.values()][0]?.status).toBe("queued"); + }); + + it("leases verification jobs once, dead-letters terminal failures, and resumes static sync without rebuilding", async () => { + const { app, store } = testApp(); + const payload = authPayload(); + const capabilityResponse = await post(app, "/v1/capabilities", { + payload, + joyid_signature: joyidSignature(payload), + }); + const capability = await capabilityResponse.json() as any; + store.namespaces.set("cellscript", { + namespace: "cellscript", + status: "active", + owner_principal_type: "joyid_ckb", + owner_principal_id: payload.principal_id, + }); + const publish = await publishPayload(capability.key_id); + const publishResponse = await post(app, "/v1/artifacts/cellscript/demo/releases", { + payload: publish, + capability_signature: { algorithm: "p256-sha256", signature: "sig" }, + source_snapshot: { + content_base64: base64("source snapshot"), + content_type: "application/vnd.cellscript.source-snapshot+json", + size_bytes: "source snapshot".length, + source_hash: publish.source_hash, + }, + }); + expect(publishResponse.status).toBe(202); + + const claimTime = new Date().toISOString(); + const first = await store.claimVerificationJob({ worker_id: "worker-a", lease_seconds: 300, now_iso: claimTime }); + expect(first).toMatchObject({ status: "running", attempt_count: 1, lease_owner: "worker-a" }); + expect(await store.claimVerificationJob({ worker_id: "worker-b", lease_seconds: 300, now_iso: claimTime })).toBeNull(); + + const dead = await store.failVerificationJob({ + job_id: first!.id, + worker_id: "worker-a", + error_code: "compile_rejected", + error_message: "package does not compile", + retryable: false, + retry_after_seconds: 5, + request_id: "verification:test:1", + }); + expect(dead.status).toBe("dead_letter"); + + const adminEnv = { REGISTRY_ADMIN_TOKEN: "secret" }; + const adminHeaders = { authorization: "Bearer secret", "x-registry-admin-actor": "release-bot" }; + const queue = await get(app, "/v1/admin/verification-queue", adminEnv, adminHeaders); + expect(queue.status).toBe(200); + expect(await queue.json()).toMatchObject({ counts: { dead_letter: 1, running: 0 } }); + const retry = await post(app, `/v1/admin/verification-jobs/${first!.id}/retry`, {}, adminEnv, adminHeaders); + expect(retry.status).toBe(200); + expect((await retry.json() as any).job.status).toBe("queued"); + + const second = await store.claimVerificationJob({ + worker_id: "worker-b", + lease_seconds: 300, + now_iso: new Date(Date.now() + 1_000).toISOString(), + }); + expect(second).toMatchObject({ status: "running", attempt_count: 1, lease_owner: "worker-b" }); + const evidence = { + schema: "cellscript-registry-evidence-v1", + kind: "verified_build", + producer: "test-verifier", + generated_at: new Date().toISOString(), + verification_status: "passed", + verification_level: "compiled", + source_hash: publish.source_hash, + manifest_hash: publish.manifest_hash, + compatibility_profile_hash: publish.registry_entry.versions[0].compatibility_profile_hash, + artifact_hash: `0x${"31".repeat(32)}`, + metadata_hash: `0x${"32".repeat(32)}`, + compiler_version: "0.23.0", + }; + const promoted = await store.promoteVerifiedBuildForJob({ + job_id: second!.id, + worker_id: "worker-b", + evidence_hash: `sha256:${"11".repeat(32)}`, + evidence, + request_id: "verification:test:2", + admin_actor: "verification-worker:test", + }); + expect(promoted.job.status).toBe("publishing"); + expect(promoted.version.status).toBe("verified_build"); + + const staticRetry = await store.failVerificationJob({ + job_id: second!.id, + worker_id: "worker-b", + error_code: "static_sync_failed", + error_message: "object store unavailable", + retryable: true, + retry_after_seconds: 5, + request_id: "verification:test:2", + }); + expect(staticRetry).toMatchObject({ status: "retry_wait", attempt_count: 1, evidence_hash: `sha256:${"11".repeat(32)}` }); + const resumed = await store.claimVerificationJob({ + worker_id: "worker-c", + lease_seconds: 300, + now_iso: new Date(Date.now() + 10_000).toISOString(), + }); + expect(resumed).toMatchObject({ status: "publishing", attempt_count: 2, lease_owner: "worker-c" }); + const completed = await store.completeVerificationJob({ job_id: resumed!.id, worker_id: "worker-c" }); + expect(completed.status).toBe("succeeded"); + expect((await store.getVerificationQueueMetrics()).counts.succeeded).toBe(1); + }); + + it("serves package-version JSON from the static registry read path without the write store", async () => { + const app = createApp({ + registryObjectReader: { + async get(key) { + expect(key).toBe("artifacts/cellscript/demo/releases/1.2.3.json"); + return { + body: JSON.stringify({ schema_version: 1, coordinate: "cellscript/demo@1.2.3", status: "source_published" }), + contentType: "application/json; charset=utf-8", + etag: "\"static-entry\"", + }; + }, + }, + }); + + const response = await app.fetch(new Request("https://registry.cellscript.dev/artifacts/cellscript/demo/releases/1.2.3.json")); + expect(response.status).toBe(200); + expect(response.headers.get("cache-control")).toContain("max-age=60"); + expect(response.headers.get("etag")).toBe("\"static-entry\""); + expect((await response.json() as any).coordinate).toBe("cellscript/demo@1.2.3"); + }); + + it("rejects unknown schemas, incomplete entries, and mismatched nested identities", async () => { + const { app } = testApp(); + const publish = await publishPayload("cap_11111111111111111111111111111111"); + const sourceSnapshot = { + content_base64: base64("source snapshot"), + content_type: "application/vnd.cellscript.source+tar", + size_bytes: "source snapshot".length, + source_hash: publish.source_hash, + }; + const submit = (payload: unknown) => + post(app, "/v1/artifacts/cellscript/demo/releases", { + payload, + capability_signature: { algorithm: "p256-sha256", signature: "sig" }, + source_snapshot: sourceSnapshot, + }); + + const unknownSchema = await submit({ + ...publish, + registry_entry: { ...publish.registry_entry, schema_version: 2 }, + }); + expect(unknownSchema.status).toBe(400); + expect((await unknownSchema.json() as any).error.code).toBe("unsupported_registry_schema"); + + for (const [field, expectedCode] of [ + ["dependencies", "invalid_registry_dependencies"], + ["verification_status", "invalid_initial_artifact_state"], + ["availability_status", "invalid_initial_artifact_state"], + ] as const) { + const incompleteVersion = { ...publish.registry_entry.versions[0] } as Record; + delete incompleteVersion[field]; + const incomplete = await submit({ + ...publish, + registry_entry: { ...publish.registry_entry, versions: [incompleteVersion] }, + }); + expect(incomplete.status).toBe(400); + expect((await incomplete.json() as any).error.code).toBe(expectedCode); + } + + const wrongVersion = await submit({ + ...publish, + registry_entry: { + ...publish.registry_entry, + versions: [{ ...publish.registry_entry.versions[0], version: "1.2.4", tag: "v1.2.4" }], + }, + }); + expect(wrongVersion.status).toBe(400); + expect((await wrongVersion.json() as any).error.code).toBe("registry_identity_mismatch"); + }); + + it("replays a successful publish response for the same Idempotency-Key without rewriting objects", async () => { + const { app, store, snapshots } = testApp(); + const payload = authPayload(); + const capabilityResponse = await post(app, "/v1/capabilities", { + payload, + joyid_signature: joyidSignature(payload), + }); + const capability = await capabilityResponse.json() as any; + store.namespaces.set("cellscript", { namespace: "cellscript", status: "active", owner_principal_type: "joyid_ckb", @@ -360,11 +1791,11 @@ describe("registry api", () => { source_hash: publish.source_hash, }, }; - const first = await post(app, "/v1/packages/cellscript/demo/versions", body, {}, { "idempotency-key": "publish-key-0001" }); + const first = await post(app, "/v1/artifacts/cellscript/demo/releases", body, {}, { "idempotency-key": "publish-key-0001" }); expect(first.status).toBe(202); const firstBody = await first.json() as any; - const replay = await post(app, "/v1/packages/cellscript/demo/versions", body, {}, { "idempotency-key": "publish-key-0001" }); + const replay = await post(app, "/v1/artifacts/cellscript/demo/releases", body, {}, { "idempotency-key": "publish-key-0001" }); expect(replay.status).toBe(202); expect(replay.headers.get("x-idempotency-status")).toBe("replayed"); const replayBody = await replay.json() as any; @@ -389,7 +1820,7 @@ describe("registry api", () => { }); const publish = await publishPayload(capability.key_id); - const first = await post(app, "/v1/packages/cellscript/demo/versions", { + const first = await post(app, "/v1/artifacts/cellscript/demo/releases", { payload: publish, capability_signature: { algorithm: "p256-sha256", signature: "sig" }, source_snapshot: { @@ -405,9 +1836,17 @@ describe("registry api", () => { ...publish, version: "1.2.4", source_hash: `0x${"ef".repeat(32)}`, - registry_entry: { ...publish.registry_entry, version: "1.2.4" }, + registry_entry: { + ...publish.registry_entry, + versions: [{ + ...publish.registry_entry.versions[0], + version: "1.2.4", + tag: "v1.2.4", + source_hash: `0x${"ef".repeat(32)}`, + }], + }, }; - const conflict = await post(app, "/v1/packages/cellscript/demo/versions", { + const conflict = await post(app, "/v1/artifacts/cellscript/demo/releases", { payload: changed, capability_signature: { algorithm: "p256-sha256", signature: "sig" }, source_snapshot: { @@ -437,7 +1876,7 @@ describe("registry api", () => { }); const publish = await publishPayload(capability.key_id); - const first = await post(app, "/v1/packages/cellscript/demo/versions", { + const first = await post(app, "/v1/artifacts/cellscript/demo/releases", { payload: publish, capability_signature: { algorithm: "p256-sha256", signature: "sig" }, source_snapshot: { @@ -453,9 +1892,17 @@ describe("registry api", () => { ...publish, version: "1.2.4", source_hash: `0x${"ef".repeat(32)}`, - registry_entry: { ...publish.registry_entry, version: "1.2.4" }, + registry_entry: { + ...publish.registry_entry, + versions: [{ + ...publish.registry_entry.versions[0], + version: "1.2.4", + tag: "v1.2.4", + source_hash: `0x${"ef".repeat(32)}`, + }], + }, }; - const replay = await post(app, "/v1/packages/cellscript/demo/versions", { + const replay = await post(app, "/v1/artifacts/cellscript/demo/releases", { payload: replayedNonce, capability_signature: { algorithm: "p256-sha256", signature: "sig" }, source_snapshot: { @@ -465,116 +1912,782 @@ describe("registry api", () => { source_hash: replayedNonce.source_hash, }, }); - expect(replay.status).toBe(409); - expect((await replay.json() as any).error.code).toBe("nonce_replay"); - expect(snapshots).toHaveLength(2); - expect(store.auditEvents.some((event) => event.event_type === "nonce.replay_blocked")).toBe(true); + expect(replay.status).toBe(409); + expect((await replay.json() as any).error.code).toBe("nonce_replay"); + expect(snapshots).toHaveLength(2); + expect(store.auditEvents.some((event) => event.event_type === "nonce.replay_blocked")).toBe(true); + }); + + it("keeps the database authoritative when a static mirror write fails after admission", async () => { + const store = new MemoryRegistryStore(); + const writes: Array<{ key: string; body: Uint8Array; contentType: string }> = []; + let failStaticWrites = true; + const app = createApp({ + store, + now: () => now, + joyidVerifier: { verifySignature: async () => true }, + capabilityVerifier: { verify: async () => true }, + snapshotWriter: { + async put(key, body, options) { + if (failStaticWrites && key.startsWith("artifacts/")) { + throw new Error("static registry object write failed"); + } + writes.push({ key, body, contentType: options.contentType }); + }, + } satisfies SnapshotWriter, + }); + const payload = authPayload(); + const capabilityResponse = await post(app, "/v1/capabilities", { + payload, + joyid_signature: joyidSignature(payload), + }); + const capability = await capabilityResponse.json() as any; + store.namespaces.set("cellscript", { + namespace: "cellscript", + status: "active", + owner_principal_type: "joyid_ckb", + owner_principal_id: payload.principal_id, + }); + + const publish = await publishPayload(capability.key_id); + const sourceSnapshot = { + content_base64: base64("source snapshot"), + content_type: "application/vnd.cellscript.source+tar", + size_bytes: "source snapshot".length, + source_hash: publish.source_hash, + }; + const idempotencyKey = "publish-key-static-fail"; + const noncesBeforePublish = store.usedNonces.size; + const response = await post(app, "/v1/artifacts/cellscript/demo/releases", { + payload: publish, + capability_signature: { algorithm: "p256-sha256", signature: "sig" }, + source_snapshot: sourceSnapshot, + }, {}, { "idempotency-key": idempotencyKey }); + + expect(response.status).toBe(202); + expect((await response.json() as any).verification_status).toBe("pending"); + expect(writes).toHaveLength(1); + expect(writes[0]?.key).toContain("source-snapshots/cellscript/demo/1.2.3/"); + expect(store.snapshots.size).toBe(1); + expect(store.packageVersions.has("cellscript/demo@1.2.3")).toBe(true); + expect(store.idempotencyKeys.get(`publish:${idempotencyKey}`)?.status).toBe("completed"); + expect(store.usedNonces.size).toBe(noncesBeforePublish + 1); + expect(store.capabilities.get(capability.key_id)?.last_used_at).toBeTruthy(); + expect(store.auditEvents.some((event) => event.event_type === "capability.used")).toBe(true); + expect(store.auditEvents.some((event) => event.event_type === "publish.accepted")).toBe(true); + expect(store.auditEvents.some((event) => event.event_type === "static_registry.sync_deferred")).toBe(true); + expect([...store.verificationJobs.values()][0]?.status).toBe("retry_wait"); + + failStaticWrites = false; + const retry = await post(app, "/v1/artifacts/cellscript/demo/releases", { + payload: publish, + capability_signature: { algorithm: "p256-sha256", signature: "sig" }, + source_snapshot: sourceSnapshot, + }, {}, { "idempotency-key": idempotencyKey }); + + expect(retry.status).toBe(202); + expect(retry.headers.get("x-idempotency-status")).toBe("replayed"); + expect((await retry.json() as any).verification_status).toBe("pending"); + expect(store.packageVersions.has("cellscript/demo@1.2.3")).toBe(true); + expect(store.idempotencyKeys.get(`publish:${idempotencyKey}`)?.status).toBe("completed"); + expect(store.capabilities.get(capability.key_id)?.last_used_at).toBeTruthy(); + expect(store.auditEvents.some((event) => event.event_type === "capability.used")).toBe(true); + expect(store.auditEvents.some((event) => event.event_type === "publish.accepted")).toBe(true); + }); + + it("allows audited admin review and quarantine transitions with an admin token", async () => { + const { app, store, snapshots } = testApp(); + const payload = authPayload(); + const capabilityResponse = await post(app, "/v1/capabilities", { + payload, + joyid_signature: joyidSignature(payload), + }); + const capability = await capabilityResponse.json() as any; + + const claimResponse = await post(app, "/v1/namespaces/claim", { + namespace: "cellscript", + payload, + joyid_signature: joyidSignature(payload), + }); + expect(claimResponse.status).toBe(202); + expect((await claimResponse.json() as any).status).toBe("review_pending"); + + const adminEnv = { REGISTRY_ADMIN_TOKEN: "secret" }; + const adminHeaders = { authorization: "Bearer secret", "x-registry-admin-actor": "ops@example.com" }; + const approveResponse = await post( + app, + "/v1/admin/namespaces/cellscript/status", + { status: "active", review_reason: "approved core namespace" }, + adminEnv, + adminHeaders, + ); + expect(approveResponse.status).toBe(200); + expect((await approveResponse.json() as any).status).toBe("active"); + + const publish = await publishPayload(capability.key_id); + const publishResponse = await post(app, "/v1/artifacts/cellscript/demo/releases", { + payload: publish, + capability_signature: { algorithm: "p256-sha256", signature: "sig" }, + source_snapshot: { + content_base64: base64("source snapshot"), + content_type: "application/vnd.cellscript.source+tar", + size_bytes: "source snapshot".length, + source_hash: publish.source_hash, + }, + }); + expect(publishResponse.status).toBe(202); + + const unsupportedPromotion = await post( + app, + "/v1/admin/artifacts/cellscript/demo/releases/1.2.3/availability", + { availability_status: "verified_build", reason: "manual claim without evidence" }, + adminEnv, + adminHeaders, + ); + expect(unsupportedPromotion.status).toBe(400); + expect((await unsupportedPromotion.json() as any).error.code).toBe("invalid_availability_status"); + + const quarantineResponse = await post( + app, + "/v1/admin/artifacts/cellscript/demo/releases/1.2.3/availability", + { availability_status: "quarantined", reason: "manual review" }, + adminEnv, + adminHeaders, + ); + expect(quarantineResponse.status).toBe(200); + expect((await quarantineResponse.json() as any).availability_status).toBe("quarantined"); + expect(store.packageVersions.get("cellscript/demo@1.2.3")?.availability_status).toBe("quarantined"); + const staticEntryWrites = snapshots.filter((snapshot) => snapshot.key === "artifacts/cellscript/demo/releases/1.2.3.json"); + expect(staticEntryWrites).toHaveLength(2); + expect(JSON.parse(utf8(staticEntryWrites.at(-1)!.body)).availability_status).toBe("quarantined"); + expect(store.auditEvents.some((event) => event.event_type === "admin.namespace.status_updated")).toBe(true); + expect(store.auditEvents.some((event) => event.event_type === "admin.package_version.status_updated")).toBe(true); + }); + + it("lists public packages and requires chained evidence for production promotions", async () => { + const registryTypeScript = { code_hash: `0x${"71".repeat(32)}`, hash_type: "data1", args: "0x01" }; + const commitmentLockScript = { code_hash: `0x${"72".repeat(32)}`, hash_type: "type", args: "0x02" }; + const registryTypeCellDep = { out_point: { tx_hash: `0x${"73".repeat(32)}`, index: "0x0" }, dep_type: "code" }; + const commitmentLockCellDep = { out_point: { tx_hash: `0x${"74".repeat(32)}`, index: "0x0" }, dep_type: "code" }; + const { app, store, snapshots } = testApp(undefined, undefined, { + verifyMainnetDeployment: async () => ({ block_hash: `0x${"60".repeat(32)}` }), + verifyRegistryCommitmentConfiguration: async () => {}, + verifyMainnetCommitment: async () => ({ + commitment_schema: "cellscript-registry-commitment-v1", + chain_verification: "get_live_cell+type_index", + observed_block_hash: `0x${"61".repeat(32)}`, + }), + }); + const payload = authPayload(); + const capabilityResponse = await post(app, "/v1/capabilities", { + payload, + joyid_signature: joyidSignature(payload), + }); + const capability = await capabilityResponse.json() as any; + store.namespaces.set("cellscript", { + namespace: "cellscript", + status: "active", + owner_principal_type: "joyid_ckb", + owner_principal_id: payload.principal_id, + }); + const publish = await ckbExecutablePublishPayload(capability.key_id); + const publishResponse = await post(app, "/v1/artifacts/cellscript/demo/releases", { + payload: publish, + capability_signature: { algorithm: "p256-sha256", signature: "sig" }, + source_snapshot: { + content_base64: base64("artifact bundle"), + content_type: "application/vnd.cellscript.artifact-bundle+json", + size_bytes: "artifact bundle".length, + source_hash: publish.source_hash, + }, + }); + expect(publishResponse.status).toBe(202); + + const publicIndex = await get(app, "/v1/artifacts?q=demo&limit=10"); + expect(publicIndex.status).toBe(200); + expect(await publicIndex.json()).toMatchObject({ + schema: "cellscript-registry-artifact-index", + count: 0, + artifacts: [], + }); + const explicitlyUnverified = await get(app, "/v1/artifacts?q=demo&verification=pending&limit=10"); + expect(explicitlyUnverified.status).toBe(200); + expect(await explicitlyUnverified.json()).toMatchObject({ + schema: "cellscript-registry-artifact-index", + count: 1, + artifacts: [{ + coordinate: "cellscript/demo", + latest_release: "1.2.3", + verification_status: "pending", + releases: [{ + immutable_bundle: { + schema: "cellscript-registry-immutable-bundle", + url: expect.stringContaining("https://registry.cellscript.dev/source-snapshots/cellscript/demo/1.2.3/"), + content_type: "application/vnd.cellscript.artifact-bundle+json", + }, + }], + }], + }); + + const adminEnv = { + REGISTRY_ADMIN_TOKEN: "secret", + REGISTRY_TYPE_SCRIPT_JSON: JSON.stringify(registryTypeScript), + REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON: JSON.stringify(registryTypeCellDep), + REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON: JSON.stringify(commitmentLockScript), + REGISTRY_COMMITMENT_LOCK_CELL_DEP_JSON: JSON.stringify(commitmentLockCellDep), + }; + const adminHeaders = { authorization: "Bearer secret", "x-registry-admin-actor": "release-bot" }; + const commonEvidence = { + schema: "cellscript-registry-evidence", + producer: "cellscript-release-gate/0.23.0", + generated_at: "2026-06-23T12:00:00Z", + verification_status: "passed", + source_hash: publish.source_hash, + manifest_hash: publish.manifest_hash, + }; + + const missingDependency = await post( + app, + "/v1/admin/artifacts/cellscript/demo/releases/1.2.3/promote", + { + kind: "deployed", + evidence: { + ...commonEvidence, + kind: "deployed", + verified_build_evidence_hash: `sha256:${"11".repeat(32)}`, + artifact_hash: `0x${"31".repeat(32)}`, + network: "mainnet", + code_hash: `0x${"31".repeat(32)}`, + data_hash: `0x${"31".repeat(32)}`, + hash_type: "data1", + dep_type: "code", + out_point: { tx_hash: `0x${"43".repeat(32)}`, index: 0 }, + deployment_status: "live", + }, + }, + adminEnv, + adminHeaders, + ); + expect(missingDependency.status).toBe(409); + expect((await missingDependency.json() as any).error.code).toBe("evidence_dependency_missing"); + + const verified = await post( + app, + "/v1/admin/artifacts/cellscript/demo/releases/1.2.3/promote", + { + kind: "verified_build", + evidence: { + ...commonEvidence, + kind: "verified_build", + verification_level: "hash_bound", + artifact_hash: `0x${"31".repeat(32)}`, + metadata_hash: `0x${"32".repeat(32)}`, + }, + }, + adminEnv, + adminHeaders, + ); + expect(verified.status).toBe(200); + const verifiedBody = await verified.json() as any; + expect(verifiedBody.status).toBe("verified_build"); + const verifiedIndex = await (await get(app, "/v1/artifacts?q=demo&limit=10")).json() as any; + expect(verifiedIndex.count).toBe(1); + expect(verifiedIndex.artifacts[0].verification_status).toBe("hash_bound"); + + const mismatchedDeployment = await post( + app, + "/v1/admin/artifacts/cellscript/demo/releases/1.2.3/promote", + { + kind: "deployed", + evidence: { + ...commonEvidence, + kind: "deployed", + verified_build_evidence_hash: verifiedBody.evidence.evidence_hash, + artifact_hash: `0x${"31".repeat(32)}`, + network: "mainnet", + code_hash: `0x${"44".repeat(32)}`, + data_hash: `0x${"44".repeat(32)}`, + hash_type: "data1", + dep_type: "code", + out_point: { tx_hash: `0x${"43".repeat(32)}`, index: 0 }, + deployment_status: "live", + }, + }, + adminEnv, + adminHeaders, + ); + expect(mismatchedDeployment.status).toBe(400); + expect((await mismatchedDeployment.json() as any).error.code).toBe("deployment_data_hash_mismatch"); + + const deployed = await post( + app, + "/v1/admin/artifacts/cellscript/demo/releases/1.2.3/promote", + { + kind: "deployed", + evidence: { + ...commonEvidence, + kind: "deployed", + verified_build_evidence_hash: verifiedBody.evidence.evidence_hash, + artifact_hash: `0x${"31".repeat(32)}`, + network: "mainnet", + code_hash: `0x${"31".repeat(32)}`, + data_hash: `0x${"31".repeat(32)}`, + hash_type: "data1", + dep_type: "code", + out_point: { tx_hash: `0x${"43".repeat(32)}`, index: 0 }, + deployment_status: "live", + }, + }, + adminEnv, + adminHeaders, + ); + expect(deployed.status).toBe(200); + const deployedBody = await deployed.json() as any; + expect(deployedBody.status).toBe("deployed"); + expect(store.packageVersions.get("cellscript/demo@1.2.3")?.deployment_status).toBe("chain_verified"); + + const attested = await post( + app, + "/v1/admin/artifacts/cellscript/demo/releases/1.2.3/promote", + { + kind: "on_chain_committed", + evidence: { + ...commonEvidence, + kind: "on_chain_committed", + deployed_evidence_hash: deployedBody.evidence.evidence_hash, + network: "mainnet", + commitment_tx_hash: `0x${"51".repeat(32)}`, + commitment_hash: `0x${"52".repeat(32)}`, + commitment_lock_hash: `0x${"53".repeat(32)}`, + registry_type_hash: `0x${"54".repeat(32)}`, + commitment_out_point: { tx_hash: `0x${"51".repeat(32)}`, index: 0 }, + observed_at: "2026-06-23T12:00:00Z", + commitment_status: "confirmed", + }, + }, + adminEnv, + adminHeaders, + ); + expect(attested.status).toBe(200); + expect((await attested.json() as any).status).toBe("on_chain_committed"); + + const acceptedIndex = await get(app, "/v1/artifacts?q=demo&limit=10"); + expect(acceptedIndex.status).toBe(200); + expect(await acceptedIndex.json()).toMatchObject({ + count: 1, + artifacts: [{ coordinate: "cellscript/demo", verification_status: "hash_bound", deployment_status: "chain_verified" }], + }); + + const detail = await get(app, "/v1/artifacts/cellscript/demo"); + expect(detail.status).toBe(200); + expect(await detail.json()).toMatchObject({ + coordinate: "cellscript/demo", + verification_status: "hash_bound", + deployment_status: "chain_verified", + releases: [{ + release: "1.2.3", + verification_status: "hash_bound", + deployment_status: "chain_verified", + immutable_bundle: { schema: "cellscript-registry-immutable-bundle" }, + evidence: [{ kind: "verified_build" }, { kind: "deployed" }, { kind: "on_chain_committed" }], + }], + }); + const evidence = await get(app, "/v1/artifacts/cellscript/demo/releases/1.2.3/evidence"); + expect(evidence.status).toBe(200); + expect((await evidence.json() as any).evidence).toHaveLength(3); + const staticWrites = snapshots.filter((snapshot) => snapshot.key === "artifacts/cellscript/demo/releases/1.2.3.json"); + expect(staticWrites).toHaveLength(4); + expect(JSON.parse(utf8(staticWrites.at(-1)!.body)).evidence).toHaveLength(3); + expect(JSON.parse(utf8(staticWrites.at(-1)!.body)).immutable_bundle.url).toContain("/source-snapshots/cellscript/demo/1.2.3/"); + }); + + it("requires two independent reproduction reports before deploying a reproducible executable", async () => { + let acceptReproducerSignatures = true; + const { app, store } = testApp(undefined, undefined, { + verifyMainnetDeployment: async () => ({ block_hash: `0x${"60".repeat(32)}` }), + capabilityVerifier: { + verify: async (canonicalPayload) => !canonicalPayload.includes('"schema":"cellscript-reproduction-report-v2"') + || acceptReproducerSignatures, + }, + }); + const payload = authPayload(); + const capability = await (await post(app, "/v1/capabilities", { + payload, + joyid_signature: joyidSignature(payload), + })).json() as any; + store.namespaces.set("cellscript", { + namespace: "cellscript", + status: "active", + owner_principal_type: "joyid_ckb", + owner_principal_id: payload.principal_id, + }); + const publish = await ckbExecutablePublishPayload(capability.key_id); + declareReproducibleBuild(publish); + expect((await post(app, "/v1/artifacts/cellscript/demo/releases", { + payload: publish, + capability_signature: { algorithm: "p256-sha256", signature: "sig" }, + source_snapshot: { + content_base64: base64("reproducible artifact bundle"), + content_type: "application/vnd.cellscript.artifact-bundle+json", + size_bytes: "reproducible artifact bundle".length, + source_hash: publish.source_hash, + }, + })).status).toBe(202); + + const adminEnv = { + REGISTRY_ADMIN_TOKEN: "secret", + REGISTRY_REPRODUCER_POLICY_JSON: JSON.stringify({ + schema: "cellscript-reproducer-policy-v1", + minimum_trust_domains: 2, + builders: [ + { builder_id: "builder-a", trust_domain: "org-a", public_key: reproducerPublicKeys["builder-a"] }, + { builder_id: "builder-b", trust_domain: "org-b", public_key: reproducerPublicKeys["builder-b"] }, + ], + }), + }; + const adminHeaders = { authorization: "Bearer secret", "x-registry-admin-actor": "release-bot" }; + const commonEvidence = { + schema: "cellscript-registry-evidence", + producer: "cellscript-release-gate/0.23.0", + generated_at: "2026-06-23T12:00:00Z", + verification_status: "passed", + source_hash: publish.source_hash, + manifest_hash: publish.manifest_hash, + }; + const verifiedResponse = await post( + app, + "/v1/admin/artifacts/cellscript/demo/releases/1.2.3/promote", + { + kind: "verified_build", + evidence: { + ...commonEvidence, + kind: "verified_build", + verification_level: "evidence_required", + artifact_hash: `0x${"31".repeat(32)}`, + metadata_hash: `0x${"32".repeat(32)}`, + }, + }, + adminEnv, + adminHeaders, + ); + expect(verifiedResponse.status).toBe(200); + const verified = await verifiedResponse.json() as any; + expect(store.packageVersions.get("cellscript/demo@1.2.3")?.verification_status).toBe("evidence_required"); + + const deploymentEvidence = (buildEvidenceHash: string) => ({ + ...commonEvidence, + kind: "deployed", + verified_build_evidence_hash: buildEvidenceHash, + artifact_hash: `0x${"31".repeat(32)}`, + network: "mainnet", + code_hash: `0x${"31".repeat(32)}`, + data_hash: `0x${"31".repeat(32)}`, + hash_type: "data1", + dep_type: "code", + out_point: { tx_hash: `0x${"43".repeat(32)}`, index: 0 }, + deployment_status: "live", + }); + const prematureDeployment = await post( + app, + "/v1/admin/artifacts/cellscript/demo/releases/1.2.3/promote", + { kind: "deployed", evidence: deploymentEvidence(verified.evidence.evidence_hash) }, + adminEnv, + adminHeaders, + ); + expect(prematureDeployment.status).toBe(409); + expect((await prematureDeployment.json() as any).error.code).toBe("reproduction_evidence_missing"); + + const report = (builderId: "builder-a" | "builder-b") => ({ + schema: "cellscript-reproduction-report-v2", + builder_id: builderId, + trust_domain: builderId === "builder-a" ? "org-a" : "org-b", + builder_public_key: reproducerPublicKeys[builderId], + environment: "docker.io/library/rust:1.97.1@sha256:0123456789abcdef", + source_hash: publish.source_hash, + build_recipe_hash: `0x${"34".repeat(32)}`, + artifact_hash: `0x${"31".repeat(32)}`, + build_log_hash: `0x${"71".repeat(32)}`, + generated_at: "2026-06-23T12:00:00Z", + signature: { algorithm: "p256-sha256", signature: "signed-reproduction-report-value" }, + }); + const reproducedEvidence = { + ...commonEvidence, + kind: "reproduced_build", + verification_level: "reproduced", + verified_build_evidence_hash: verified.evidence.evidence_hash, + artifact_hash: `0x${"31".repeat(32)}`, + build_recipe_hash: `0x${"34".repeat(32)}`, + minimum_reproducers: 2, + reproducers: [report("builder-a"), report("builder-b")], + }; + const duplicate = await post( + app, + "/v1/admin/artifacts/cellscript/demo/releases/1.2.3/promote", + { kind: "reproduced_build", evidence: { ...reproducedEvidence, reproducers: [report("builder-a"), report("builder-a")] } }, + adminEnv, + adminHeaders, + ); + expect(duplicate.status).toBe(400); + expect((await duplicate.json() as any).error.code).toBe("duplicate_reproducer"); + + acceptReproducerSignatures = false; + const invalidSignature = await post( + app, + "/v1/admin/artifacts/cellscript/demo/releases/1.2.3/promote", + { kind: "reproduced_build", evidence: reproducedEvidence }, + adminEnv, + adminHeaders, + ); + expect(invalidSignature.status).toBe(401); + expect((await invalidSignature.json() as any).error.code).toBe("reproduction_signature_invalid"); + acceptReproducerSignatures = true; + + const reproducedResponse = await post( + app, + "/v1/admin/artifacts/cellscript/demo/releases/1.2.3/promote", + { kind: "reproduced_build", evidence: reproducedEvidence }, + adminEnv, + adminHeaders, + ); + expect(reproducedResponse.status).toBe(200); + const reproduced = await reproducedResponse.json() as any; + expect(reproduced.status).toBe("verified_build"); + expect(reproduced.evidence.evidence.reproducer_policy).toMatchObject({ + schema: "cellscript-reproducer-policy-acceptance-v1", + policy_hash: expect.stringMatching(/^sha256:[0-9a-f]{64}$/), + minimum_trust_domains: 2, + }); + expect(store.packageVersions.get("cellscript/demo@1.2.3")?.verification_status).toBe("verified"); + + const deployed = await post( + app, + "/v1/admin/artifacts/cellscript/demo/releases/1.2.3/promote", + { kind: "deployed", evidence: deploymentEvidence(reproduced.evidence.evidence_hash) }, + adminEnv, + adminHeaders, + ); + expect(deployed.status).toBe(200); + expect((await deployed.json() as any).status).toBe("deployed"); + }); + + it("paginates public discovery by package without splitting a package's releases", async () => { + const { app, store } = testApp(); + const snapshotHash = `sha256:${"90".repeat(32)}`; + const sourceHash = `0x${"91".repeat(32)}`; + store.snapshots.set(snapshotHash, { + snapshot_hash: snapshotHash, + r2_key: "source-snapshots/shared.tar", + source_hash: sourceHash, + size_bytes: 1, + content_type: "application/vnd.cellscript.source+tar", + }); + const record = (name: string, version: string, createdAt: string): PackageVersionRecord => ({ + namespace: "cellscript", + name, + version, + status: "verified_build", + artifact: { kind: "source_library", profile: "cellscript_source", consumption_mode: "dependency", language: "cellscript" }, + verification_status: "verified", + deployment_status: "not_applicable", + availability_status: "active", + source_hash: sourceHash, + manifest_hash: `0x${"92".repeat(32)}`, + edition: "2026", + compatibility_profile_hash: "93".repeat(32), + capability_key_id: "cap_11111111111111111111111111111111", + principal_type: "joyid_ckb", + principal_id: "0x1111111111111111111111111111111111111111", + registry_entry: { + schema_version: 1, + namespace: "cellscript", + name, + artifact: { kind: "source_library", profile: "cellscript_source", consumption_mode: "dependency", language: "cellscript" }, + versions: [{ + version, + tag: `v${version}`, + source_hash: sourceHash, + cellscript_version: "0.23.0", + edition: "2026", + compatibility_profile_hash: "93".repeat(32), + dependencies: {}, + verification_status: "pending", + deployment_status: "not_applicable", + availability_status: "active", + }], + }, + snapshot_hash: snapshotHash, + direct_url: `https://registry.cellscript.dev/artifacts/cellscript/${name}/releases/${version}.json`, + created_at: createdAt, + }); + for (const item of [ + record("alpha", "2.0.0", "2026-06-23T12:04:00Z"), + record("alpha", "1.0.0", "2026-06-23T12:01:00Z"), + record("beta", "1.0.0", "2026-06-23T12:03:00Z"), + record("gamma", "1.0.0", "2026-06-23T12:02:00Z"), + ]) { + store.packageVersions.set(`${item.namespace}/${item.name}@${item.version}`, item); + } + + const first = await (await get(app, "/v1/artifacts?limit=1&offset=0")).json() as any; + const second = await (await get(app, "/v1/artifacts?limit=1&offset=1")).json() as any; + const third = await (await get(app, "/v1/artifacts?limit=1&offset=2")).json() as any; + expect(first.artifacts[0].coordinate).toBe("cellscript/alpha"); + expect(first.artifacts[0].releases).toHaveLength(2); + expect(first.next_offset).toBe(1); + expect(second.artifacts[0].coordinate).toBe("cellscript/beta"); + expect(second.next_offset).toBe(2); + expect(third.artifacts[0].coordinate).toBe("cellscript/gamma"); + expect(third.next_offset).toBeUndefined(); }); - it("releases publish idempotency reservation when static registry object write fails before admission", async () => { + it("records only capability-signed, chain-verified mainnet deployments for executable artifacts", async () => { const store = new MemoryRegistryStore(); - const writes: Array<{ key: string; body: Uint8Array; contentType: string }> = []; - let failStaticWrites = true; + const snapshots: Array<{ key: string; body: Uint8Array }> = []; const app = createApp({ store, now: () => now, joyidVerifier: { verifySignature: async () => true }, capabilityVerifier: { verify: async () => true }, + verifyMainnetDeployment: async (payload) => { + expect(payload.network).toBe("mainnet"); + expect(payload.out_point).toEqual({ tx_hash: `0x${"41".repeat(32)}`, index: 0 }); + return { block_hash: `0x${"51".repeat(32)}` }; + }, snapshotWriter: { - async put(key, body, options) { - if (failStaticWrites && key.startsWith("packages/")) { - throw new Error("static registry object write failed"); - } - writes.push({ key, body, contentType: options.contentType }); - }, - } satisfies SnapshotWriter, - }); - const payload = authPayload(); - const capabilityResponse = await post(app, "/v1/capabilities", { - payload, - joyid_signature: joyidSignature(payload), + async put(key, body) { snapshots.push({ key, body }); }, + }, }); - const capability = await capabilityResponse.json() as any; + const root = authPayload(); + const capability = await (await post(app, "/v1/capabilities", { + payload: root, + joyid_signature: joyidSignature(root), + })).json() as any; store.namespaces.set("cellscript", { namespace: "cellscript", status: "active", owner_principal_type: "joyid_ckb", - owner_principal_id: payload.principal_id, + owner_principal_id: root.principal_id, }); - - const publish = await publishPayload(capability.key_id); - const sourceSnapshot = { - content_base64: base64("source snapshot"), - content_type: "application/vnd.cellscript.source+tar", - size_bytes: "source snapshot".length, - source_hash: publish.source_hash, - }; - const idempotencyKey = "publish-key-static-fail"; - const response = await post(app, "/v1/packages/cellscript/demo/versions", { + const publish = await ckbExecutablePublishPayload(capability.key_id); + const published = await post(app, "/v1/artifacts/cellscript/demo/releases", { payload: publish, capability_signature: { algorithm: "p256-sha256", signature: "sig" }, - source_snapshot: sourceSnapshot, - }, {}, { "idempotency-key": idempotencyKey }); - - expect(response.status).toBe(500); - expect((await response.json() as any).error.code).toBe("internal_error"); - expect(writes).toHaveLength(1); - expect(writes[0]?.key).toContain("source-snapshots/cellscript/demo/1.2.3/"); - expect(store.snapshots.size).toBe(0); - expect(store.packageVersions.has("cellscript/demo@1.2.3")).toBe(false); - expect(store.idempotencyKeys.has(`publish:${idempotencyKey}`)).toBe(false); - expect(store.capabilities.get(capability.key_id)?.last_used_at).toBeFalsy(); - expect(store.auditEvents.some((event) => event.event_type === "capability.used")).toBe(false); - expect(store.auditEvents.some((event) => event.event_type === "publish.accepted")).toBe(false); - - failStaticWrites = false; - const retryPublish = { ...publish, nonce: "0x4444444444444444" }; - const retry = await post(app, "/v1/packages/cellscript/demo/versions", { - payload: retryPublish, - capability_signature: { algorithm: "p256-sha256", signature: "sig" }, - source_snapshot: sourceSnapshot, - }, {}, { "idempotency-key": idempotencyKey }); - - expect(retry.status).toBe(202); - expect((await retry.json() as any).status).toBe("source_published"); - expect(store.packageVersions.has("cellscript/demo@1.2.3")).toBe(true); - expect(store.idempotencyKeys.get(`publish:${idempotencyKey}`)?.status).toBe("completed"); - expect(store.capabilities.get(capability.key_id)?.last_used_at).toBeTruthy(); - expect(store.auditEvents.some((event) => event.event_type === "capability.used")).toBe(true); - expect(store.auditEvents.some((event) => event.event_type === "publish.accepted")).toBe(true); - }); - - it("allows audited admin review and quarantine transitions with an admin token", async () => { - const { app, store, snapshots } = testApp(); - const payload = authPayload(); - const capabilityResponse = await post(app, "/v1/capabilities", { - payload, - joyid_signature: joyidSignature(payload), + source_snapshot: { + content_base64: base64("artifact bundle"), + content_type: "application/vnd.cellscript.artifact-bundle+json", + size_bytes: "artifact bundle".length, + source_hash: publish.source_hash, + }, }); - const capability = await capabilityResponse.json() as any; - - const claimResponse = await post(app, "/v1/namespaces/claim", { + expect(published.status).toBe(202); + await store.updatePackageVersionStatus({ namespace: "cellscript", - payload, - joyid_signature: joyidSignature(payload), + name: "demo", + version: "1.2.3", + status: "yanked", + request_id: "yank-during-verification", + admin_actor: "test", }); - expect(claimResponse.status).toBe(202); - expect((await claimResponse.json() as any).status).toBe("review_pending"); + const verifiedWhileYanked = await store.promotePackageVersion({ + namespace: "cellscript", + name: "demo", + version: "1.2.3", + kind: "verified_build", + evidence_hash: `sha256:${"61".repeat(32)}`, + evidence: { verification_level: "hash_bound", artifact_hash: `0x${"31".repeat(32)}` }, + request_id: "verification:test", + admin_actor: "verification-worker:test", + }); + expect(verifiedWhileYanked.version.status).toBe("yanked"); + expect(verifiedWhileYanked.version.verification_status).toBe("hash_bound"); + const restoredAfterVerification = await store.updatePackageVersionStatus({ + namespace: "cellscript", + name: "demo", + version: "1.2.3", + status: "active", + request_id: "restore-after-verification", + admin_actor: "test", + }); + expect(restoredAfterVerification.status).toBe("verified_build"); - const adminEnv = { REGISTRY_ADMIN_TOKEN: "secret" }; - const adminHeaders = { authorization: "Bearer secret", "x-registry-admin-actor": "ops@example.com" }; - const approveResponse = await post( + const deployment = deploymentPayload(capability.key_id); + const contractMismatch = await post(app, "/v1/artifacts/cellscript/demo/releases/1.2.3/deployments", { + payload: { ...deployment, hash_type: "data" }, + capability_signature: { algorithm: "p256-sha256", signature: "sig" }, + }); + expect(contractMismatch.status).toBe(400); + expect((await contractMismatch.json() as any).error.code).toBe("deployment_hash_type_contract_mismatch"); + const deploymentRequest = { + payload: deployment, + capability_signature: { algorithm: "p256-sha256", signature: "sig" }, + }; + store.capabilities.get(capability.key_id)!.scopes = ["publish:cellscript/demo"]; + const publishOnlyDeployment = await post(app, "/v1/artifacts/cellscript/demo/releases/1.2.3/deployments", deploymentRequest); + expect(publishOnlyDeployment.status).toBe(403); + expect((await publishOnlyDeployment.json() as any).error.code).toBe("capability_scope_denied"); + store.capabilities.get(capability.key_id)!.scopes = root.requested_scopes; + const response = await post(app, "/v1/artifacts/cellscript/demo/releases/1.2.3/deployments", deploymentRequest); + expect(response.status).toBe(201); + const deploymentResponse = await response.json(); + expect(deploymentResponse).toMatchObject({ + coordinate: "cellscript/demo@1.2.3", + deployment_status: "chain_verified", + evidence: { + kind: "deployed", + evidence: { + network: "mainnet", + deployment_status: "live", + chain_verification: "get_transaction+get_live_cell", + }, + }, + }); + const replayedDeployment = await post(app, "/v1/artifacts/cellscript/demo/releases/1.2.3/deployments", deploymentRequest); + expect(replayedDeployment.status).toBe(201); + expect(await replayedDeployment.json()).toEqual(deploymentResponse); + const deploymentNonceReplay = await post( app, - "/v1/admin/namespaces/cellscript/status", - { status: "active", review_reason: "approved core namespace" }, - adminEnv, - adminHeaders, + "/v1/artifacts/cellscript/demo/releases/1.2.3/deployments", + deploymentRequest, + {}, + { "idempotency-key": "deployment-replay-cleanup" }, ); - expect(approveResponse.status).toBe(200); - expect((await approveResponse.json() as any).status).toBe("active"); + expect(deploymentNonceReplay.status).toBe(409); + expect((await deploymentNonceReplay.json() as any).error.code).toBe("nonce_replay"); + expect(store.idempotencyKeys.has("deployment:deployment-replay-cleanup")).toBe(false); + expect((await store.listPackageEvidence("cellscript", "demo", "1.2.3")).filter((item) => item.kind === "deployed")).toHaveLength(1); + expect(store.packageVersions.get("cellscript/demo@1.2.3")?.deployment_status).toBe("chain_verified"); + expect(store.auditEvents.some((event) => event.event_type === "deployment.chain_verified")).toBe(true); + expect(snapshots.filter((item) => item.key === "artifacts/cellscript/demo/releases/1.2.3.json")).toHaveLength(2); + const commitment = await get(app, "/v1/artifacts/cellscript/demo/releases/1.2.3/commitment"); + expect(commitment.status).toBe(200); + expect(await commitment.json()).toMatchObject({ + schema: "cellscript-registry-commitment-proof-v1", + status: "commitment_ready", + payload: { + schema: "cellscript-registry-commitment-v1", + namespace: "cellscript", + name: "demo", + release: "1.2.3", + }, + cell_data: expect.stringMatching(/^0x43535245477631[0-9a-f]{64}$/), + }); + }); + it("lets the namespace owner yank and restore a release with a scoped capability", async () => { + const { app, store, snapshots } = testApp(); + const root = authPayload(); + const capability = await (await post(app, "/v1/capabilities", { + payload: root, + joyid_signature: joyidSignature(root), + })).json() as any; + store.namespaces.set("cellscript", { + namespace: "cellscript", + status: "active", + owner_principal_type: "joyid_ckb", + owner_principal_id: root.principal_id, + }); const publish = await publishPayload(capability.key_id); - const publishResponse = await post(app, "/v1/packages/cellscript/demo/versions", { + expect((await post(app, "/v1/artifacts/cellscript/demo/releases", { payload: publish, capability_signature: { algorithm: "p256-sha256", signature: "sig" }, source_snapshot: { @@ -583,24 +2696,73 @@ describe("registry api", () => { size_bytes: "source snapshot".length, source_hash: publish.source_hash, }, + })).status).toBe(202); + + const yank = availabilityPayload(capability.key_id); + store.capabilities.get(capability.key_id)!.scopes = ["publish:cellscript/demo"]; + const publishOnlyYank = await post(app, "/v1/artifacts/cellscript/demo/releases/1.2.3/availability", { + payload: yank, + capability_signature: { algorithm: "p256-sha256", signature: "sig" }, }); - expect(publishResponse.status).toBe(202); + expect(publishOnlyYank.status).toBe(403); + expect((await publishOnlyYank.json() as any).error.code).toBe("capability_scope_denied"); + store.capabilities.get(capability.key_id)!.scopes = root.requested_scopes; + const yanked = await post(app, "/v1/artifacts/cellscript/demo/releases/1.2.3/availability", { + payload: yank, + capability_signature: { algorithm: "p256-sha256", signature: "sig" }, + }); + expect(yanked.status).toBe(200); + const yankedBody = await yanked.json(); + expect(yankedBody).toMatchObject({ + coordinate: "cellscript/demo@1.2.3", + availability_status: "yanked", + }); + expect(store.packageVersions.get("cellscript/demo@1.2.3")?.availability_status).toBe("yanked"); + expect(store.auditEvents.some((event) => event.event_type === "publisher.package_version.availability_updated")).toBe(true); + expect(JSON.parse(utf8(snapshots.at(-1)!.body)).availability_status).toBe("yanked"); - const quarantineResponse = await post( + const replayedYank = await post(app, "/v1/artifacts/cellscript/demo/releases/1.2.3/availability", { + payload: yank, + capability_signature: { algorithm: "p256-sha256", signature: "sig" }, + }); + expect(replayedYank.status).toBe(200); + expect(await replayedYank.json()).toEqual(yankedBody); + expect(store.auditEvents.filter((event) => event.event_type === "publisher.package_version.availability_updated")).toHaveLength(1); + const availabilityNonceReplay = await post( app, - "/v1/admin/packages/cellscript/demo/versions/1.2.3/status", - { status: "quarantined", reason: "manual review" }, - adminEnv, - adminHeaders, + "/v1/artifacts/cellscript/demo/releases/1.2.3/availability", + { + payload: yank, + capability_signature: { algorithm: "p256-sha256", signature: "sig" }, + }, + {}, + { "idempotency-key": "availability-replay-cleanup" }, ); - expect(quarantineResponse.status).toBe(200); - expect((await quarantineResponse.json() as any).status).toBe("quarantined"); - expect(store.packageVersions.get("cellscript/demo@1.2.3")?.status).toBe("quarantined"); - const staticEntryWrites = snapshots.filter((snapshot) => snapshot.key === "packages/cellscript/demo/versions/1.2.3.json"); - expect(staticEntryWrites).toHaveLength(2); - expect(JSON.parse(utf8(staticEntryWrites.at(-1)!.body)).status).toBe("quarantined"); - expect(store.auditEvents.some((event) => event.event_type === "admin.namespace.status_updated")).toBe(true); - expect(store.auditEvents.some((event) => event.event_type === "admin.package_version.status_updated")).toBe(true); + expect(availabilityNonceReplay.status).toBe(409); + expect((await availabilityNonceReplay.json() as any).error.code).toBe("nonce_replay"); + expect(store.idempotencyKeys.has("availability:availability-replay-cleanup")).toBe(false); + + const active = availabilityPayload(capability.key_id, "active", "0x8888888888888888"); + const restored = await post(app, "/v1/artifacts/cellscript/demo/releases/1.2.3/availability", { + payload: active, + capability_signature: { algorithm: "p256-sha256", signature: "sig" }, + }); + expect(restored.status).toBe(200); + expect((await restored.json() as any).availability_status).toBe("active"); + expect(store.packageVersions.get("cellscript/demo@1.2.3")?.availability_status).toBe("active"); + }); + + it("rejects testnet deployment payloads and exposes no retired package routes", async () => { + const { app } = testApp(); + const deployment = { ...deploymentPayload("cap_11111111111111111111111111111111"), network: "testnet" }; + const rejected = await post(app, "/v1/artifacts/cellscript/demo/releases/1.2.3/deployments", { + payload: deployment, + capability_signature: { algorithm: "p256-sha256", signature: "sig" }, + }); + expect(rejected.status).toBe(400); + expect((await rejected.json() as any).error.code).toBe("unsupported_deployment_network"); + expect((await get(app, "/v1/packages")).status).toBe(404); + expect((await get(app, "/v1/packages/cellscript/demo")).status).toBe(404); }); it("does not change DB package status when a suppressive static update fails", async () => { @@ -614,7 +2776,7 @@ describe("registry api", () => { capabilityVerifier: { verify: async () => true }, snapshotWriter: { async put(key, body, options) { - if (failStaticWrites && key.startsWith("packages/")) { + if (failStaticWrites && key.startsWith("artifacts/")) { throw new Error("static registry object write failed"); } snapshots.push({ key, body, contentType: options.contentType }); @@ -634,7 +2796,7 @@ describe("registry api", () => { owner_principal_id: payload.principal_id, }); const publish = await publishPayload(capability.key_id); - const publishResponse = await post(app, "/v1/packages/cellscript/demo/versions", { + const publishResponse = await post(app, "/v1/artifacts/cellscript/demo/releases", { payload: publish, capability_signature: { algorithm: "p256-sha256", signature: "sig" }, source_snapshot: { @@ -649,19 +2811,19 @@ describe("registry api", () => { failStaticWrites = true; const response = await post( app, - "/v1/admin/packages/cellscript/demo/versions/1.2.3/status", - { status: "quarantined", reason: "manual review" }, + "/v1/admin/artifacts/cellscript/demo/releases/1.2.3/availability", + { availability_status: "quarantined", reason: "manual review" }, { REGISTRY_ADMIN_TOKEN: "secret" }, { authorization: "Bearer secret" }, ); expect(response.status).toBe(500); expect((await response.json() as any).error.code).toBe("internal_error"); - expect(store.packageVersions.get("cellscript/demo@1.2.3")?.status).toBe("source_published"); + expect(store.packageVersions.get("cellscript/demo@1.2.3")?.availability_status).toBe("active"); expect(store.auditEvents.some((event) => event.event_type === "admin.package_version.status_updated")).toBe(false); - const staticEntryWrites = snapshots.filter((snapshot) => snapshot.key === "packages/cellscript/demo/versions/1.2.3.json"); + const staticEntryWrites = snapshots.filter((snapshot) => snapshot.key === "artifacts/cellscript/demo/releases/1.2.3.json"); expect(staticEntryWrites).toHaveLength(1); - expect(JSON.parse(utf8(staticEntryWrites[0]!.body)).status).toBe("source_published"); + expect(JSON.parse(utf8(staticEntryWrites[0]!.body)).availability_status).toBe("active"); }); it("rejects publish when the capability principal does not own the namespace", async () => { @@ -681,7 +2843,7 @@ describe("registry api", () => { const keyId = await capabilityKeyId(otherPayload.capability_pubkey); const publish = await publishPayload(keyId); - const response = await post(app, "/v1/packages/cellscript/demo/versions", { + const response = await post(app, "/v1/artifacts/cellscript/demo/releases", { payload: publish, capability_signature: { algorithm: "p256-sha256", signature: "sig" }, source_snapshot: { @@ -724,7 +2886,7 @@ describe("registry api", () => { }); const publish = await publishPayload(capability.key_id); - const response = await post(app, "/v1/packages/cellscript/demo/versions", { + const response = await post(app, "/v1/artifacts/cellscript/demo/releases", { payload: publish, capability_signature: { algorithm: "p256-sha256", signature: "sig" }, source_snapshot: { @@ -740,7 +2902,7 @@ describe("registry api", () => { expect(snapshots).toHaveLength(0); const event = store.auditEvents.find((entry) => entry.event_type === "auth.failure"); expect(event?.data).toMatchObject({ - path: "/v1/packages/cellscript/demo/versions", + path: "/v1/artifacts/cellscript/demo/releases", status: 401, code: "capability_signature_invalid", }); @@ -821,6 +2983,15 @@ describe("registry api", () => { expect(unauthorized.status).toBe(401); expect((await unauthorized.json() as any).error.code).toBe("admin_unauthorized"); + const wrongToken = await get( + app, + "/v1/admin/audit-events", + { REGISTRY_ADMIN_TOKEN: "secret" }, + { authorization: "Bearer secres" }, + ); + expect(wrongToken.status).toBe(401); + expect((await wrongToken.json() as any).error.code).toBe("admin_unauthorized"); + const invalidLimit = await get( app, "/v1/admin/audit-events?limit=999", @@ -868,7 +3039,7 @@ describe("registry api", () => { it("runs scheduled cleanup for expired replay and quota state", async () => { const { app, store } = testApp(); store.usedNonces.set("old-nonce", { - protocol: "cellscript-registry-publish-v1", + protocol: PUBLISH_PROTOCOL, action: "publish", nonce: "0xaaaaaaaaaaaaaaaa", request_id: "old-request", @@ -876,7 +3047,7 @@ describe("registry api", () => { created_at: "2026-06-23T11:50:00Z", }); store.usedNonces.set("live-nonce", { - protocol: "cellscript-registry-publish-v1", + protocol: PUBLISH_PROTOCOL, action: "publish", nonce: "0xbbbbbbbbbbbbbbbb", request_id: "live-request", @@ -924,6 +3095,396 @@ describe("registry api", () => { }); }); + it("isolates the Pudge sandbox and purges records on the 72-hour lifecycle", async () => { + expect(() => registryRuntimeConfig({ REGISTRY_ENVIRONMENT: "testnet-sandbox" })) + .toThrow(/dedicated Registry API and object origins/); + const sandboxEnv = { + REGISTRY_ENVIRONMENT: "testnet-sandbox", + REGISTRY_ORIGIN: "https://api.testnet.registry.cellscript.dev", + STATIC_REGISTRY_ORIGIN: "https://objects.testnet.registry.cellscript.dev", + } as const; + expect(registryRuntimeConfig(sandboxEnv)).toMatchObject({ + environment: "testnet-sandbox", + network: "testnet", + record_ttl_hours: 72, + object_purge_grace_hours: 24, + }); + + const deleted: string[] = []; + const writer: SnapshotWriter = { + async put() {}, + async delete(key) { deleted.push(key); }, + }; + const { app, store } = testApp(undefined, writer); + const snapshotHash = `sha256:${"a1".repeat(32)}`; + store.snapshots.set(snapshotHash, { + snapshot_hash: snapshotHash, + r2_key: "source-snapshots/sandbox/demo/1.0.0/a1.tar", + source_hash: `0x${"a2".repeat(32)}`, + size_bytes: 1, + content_type: "application/x-tar", + }); + store.packageVersions.set("sandbox/demo@1.0.0", { + namespace: "sandbox", + name: "demo", + version: "1.0.0", + status: "source_published", + artifact: { kind: "source_library", profile: "cellscript_source", consumption_mode: "dependency", language: "cellscript" }, + verification_status: "pending", + deployment_status: "not_applicable", + availability_status: "active", + source_hash: `0x${"a2".repeat(32)}`, + manifest_hash: `0x${"a3".repeat(32)}`, + capability_key_id: "cap_sandbox", + principal_type: "joyid_ckb", + principal_id: "0x1111111111111111111111111111111111111111", + registry_entry: { + schema_version: 1, + namespace: "sandbox", + name: "demo", + artifact: { kind: "source_library", profile: "cellscript_source", consumption_mode: "dependency", language: "cellscript" }, + versions: [{ + version: "1.0.0", + tag: "v1.0.0", + source_hash: `0x${"a2".repeat(32)}`, + dependencies: {}, + verification_status: "pending", + deployment_status: "not_applicable", + availability_status: "active", + }], + }, + snapshot_hash: snapshotHash, + direct_url: "https://objects.testnet.registry.cellscript.dev/artifacts/sandbox/demo/releases/1.0.0.json", + created_at: "2026-06-20T11:00:00Z", + registry_environment: "testnet-sandbox", + network: "testnet", + expires_at: "2026-06-23T11:00:00Z", + purge_after: "2026-06-23T11:30:00Z", + }); + + await app.scheduled({ scheduledTime: now.getTime(), cron: "*/15 * * * *" } as ScheduledController, sandboxEnv); + + expect(await store.getPackageVersion("sandbox", "demo", "1.0.0")).toBeNull(); + expect(deleted).toEqual([ + "artifacts/sandbox/demo/releases/1.0.0.json", + "source-snapshots/sandbox/demo/1.0.0/a1.tar", + ]); + expect(store.packageVersions.get("sandbox/demo@1.0.0")).toMatchObject({ + expired_at: now.toISOString(), + static_purged_at: now.toISOString(), + source_purged_at: now.toISOString(), + }); + }); + + it("stamps sandbox publishes with the isolated network and fixed retention window", async () => { + const sandboxEnv = { + REGISTRY_ENVIRONMENT: "testnet-sandbox", + REGISTRY_ORIGIN: "https://api.testnet.registry.cellscript.dev", + STATIC_REGISTRY_ORIGIN: "https://objects.testnet.registry.cellscript.dev", + } as const; + const { app, store, snapshots } = testApp(); + const authorisation = authPayload(); + authorisation.registry_origin = sandboxEnv.REGISTRY_ORIGIN; + const capabilityResponse = await post(app, "/v1/capabilities", { + payload: authorisation, + joyid_signature: joyidSignature(authorisation), + }, sandboxEnv); + expect(capabilityResponse.status).toBe(201); + const capability = await capabilityResponse.json() as any; + store.namespaces.set("cellscript", { + namespace: "cellscript", + status: "active", + owner_principal_type: "joyid_ckb", + owner_principal_id: authorisation.principal_id, + }); + const publish = await publishPayload(capability.key_id); + publish.registry_origin = sandboxEnv.REGISTRY_ORIGIN; + const response = await post(app, "/v1/artifacts/cellscript/demo/releases", { + payload: publish, + capability_signature: { algorithm: "p256-sha256", signature: "sig" }, + source_snapshot: { + content_base64: base64("sandbox source"), + content_type: "application/vnd.cellscript.source+tar", + size_bytes: "sandbox source".length, + source_hash: publish.source_hash, + }, + }, sandboxEnv); + + expect(response.status).toBe(202); + expect(await response.json()).toMatchObject({ + registry_environment: "testnet-sandbox", + network: "testnet", + expires_at: "2026-06-26T12:00:00.000Z", + purge_after: "2026-06-27T12:00:00.000Z", + }); + expect(store.packageVersions.get("cellscript/demo@1.2.3")).toMatchObject({ + registry_environment: "testnet-sandbox", + network: "testnet", + expires_at: "2026-06-26T12:00:00.000Z", + purge_after: "2026-06-27T12:00:00.000Z", + }); + const staticEntry = snapshots.find((snapshot) => snapshot.key === "artifacts/cellscript/demo/releases/1.2.3.json"); + expect(JSON.parse(utf8(staticEntry!.body))).toMatchObject({ + registry_environment: "testnet-sandbox", + network: "testnet", + expires_at: "2026-06-26T12:00:00.000Z", + }); + }); + + it("serialises overlapping scheduled maintenance runs", async () => { + const { app, store } = testApp(); + const cleanup = store.cleanupExpiredState.bind(store); + let cleanupCalls = 0; + let announceStarted!: () => void; + let releaseCleanup!: () => void; + const started = new Promise((resolve) => { announceStarted = resolve; }); + const held = new Promise((resolve) => { releaseCleanup = resolve; }); + store.cleanupExpiredState = async (input) => { + cleanupCalls += 1; + announceStarted(); + await held; + return cleanup(input); + }; + + const first = app.scheduled( + { scheduledTime: now.getTime(), cron: "*/15 * * * *" } as ScheduledController, + {}, + ); + await started; + await app.scheduled( + { scheduledTime: now.getTime(), cron: "*/15 * * * *" } as ScheduledController, + {}, + ); + expect(cleanupCalls).toBe(1); + releaseCleanup(); + await first; + expect(cleanupCalls).toBe(1); + }); + + it("indexes configured Registry commitment Cells and never restores a spent commitment from history", async () => { + const typeScript = { code_hash: `0x${"71".repeat(32)}`, hash_type: "data1", args: "0x01" }; + const commitmentLock = { code_hash: `0x${"72".repeat(32)}`, hash_type: "type", args: "0x02" }; + const typeCellDep = { + out_point: { tx_hash: `0x${"73".repeat(32)}`, index: "0x0" }, + dep_type: "code", + }; + const lockCellDep = { + out_point: { tx_hash: `0x${"75".repeat(32)}`, index: "0x0" }, + dep_type: "code", + }; + let commitmentHash = `0x${"00".repeat(32)}`; + let commitmentLive = true; + let commitmentConfigurationLive = true; + let deploymentLive = true; + const { app, store } = testApp(undefined, undefined, { + verifyMainnetDeployment: async () => { + if (!deploymentLive) { + throw new ApiError(409, "deployment_cell_not_live", "deployment Cell is spent"); + } + return { block_hash: `0x${"60".repeat(32)}` }; + }, + verifyRegistryCommitmentConfiguration: async () => { + if (!commitmentConfigurationLive) { + throw new ApiError(409, "deployment_cell_not_live", "Registry commitment Lock CellDep is not live"); + } + }, + listMainnetCommitmentCells: async (configuration) => { + expect(configuration.type_script_hash).toBe(ckbScriptHash(typeScript)); + expect(configuration.commitment_lock_hash).toBe(ckbScriptHash(commitmentLock)); + return commitmentLive + ? [{ + commitment_hash: commitmentHash, + out_point: { tx_hash: `0x${"74".repeat(32)}`, index: 1 }, + block_number: "0x1234", + output: { lock: commitmentLock, type: typeScript }, + }] + : []; + }, + }); + const owner = authPayload(); + const capability = await (await post(app, "/v1/capabilities", { + payload: owner, + joyid_signature: joyidSignature(owner), + })).json() as any; + store.namespaces.set("cellscript", { + namespace: "cellscript", + status: "active", + owner_principal_type: "joyid_ckb", + owner_principal_id: owner.principal_id, + }); + const publish = await ckbExecutablePublishPayload(capability.key_id); + expect((await post(app, "/v1/artifacts/cellscript/demo/releases", { + payload: publish, + capability_signature: { algorithm: "p256-sha256", signature: "sig" }, + source_snapshot: { + content_base64: base64("commitment artifact bundle"), + content_type: "application/vnd.cellscript.artifact-bundle+json", + size_bytes: "commitment artifact bundle".length, + source_hash: publish.source_hash, + }, + })).status).toBe(202); + const adminEnv = { REGISTRY_ADMIN_TOKEN: "secret" }; + const adminHeaders = { authorization: "Bearer secret", "x-registry-admin-actor": "release-bot" }; + const commonEvidence = { + schema: "cellscript-registry-evidence", + producer: "cellscript-release-gate/0.23.0", + generated_at: "2026-06-23T12:00:00Z", + verification_status: "passed", + source_hash: publish.source_hash, + manifest_hash: publish.manifest_hash, + }; + const verified = await (await post( + app, + "/v1/admin/artifacts/cellscript/demo/releases/1.2.3/promote", + { + kind: "verified_build", + evidence: { + ...commonEvidence, + kind: "verified_build", + verification_level: "hash_bound", + artifact_hash: `0x${"31".repeat(32)}`, + metadata_hash: `0x${"32".repeat(32)}`, + }, + }, + adminEnv, + adminHeaders, + )).json() as any; + const deployed = await (await post( + app, + "/v1/admin/artifacts/cellscript/demo/releases/1.2.3/promote", + { + kind: "deployed", + evidence: { + ...commonEvidence, + kind: "deployed", + verified_build_evidence_hash: verified.evidence.evidence_hash, + artifact_hash: `0x${"31".repeat(32)}`, + network: "mainnet", + code_hash: `0x${"31".repeat(32)}`, + data_hash: `0x${"31".repeat(32)}`, + hash_type: "data1", + dep_type: "code", + out_point: { tx_hash: `0x${"43".repeat(32)}`, index: 0 }, + deployment_status: "live", + }, + }, + adminEnv, + adminHeaders, + )).json() as any; + const version = store.packageVersions.get("cellscript/demo@1.2.3")!; + commitmentHash = registryCommitmentHash(version, deployed.evidence.evidence_hash); + const scheduledEnv = { + REGISTRY_TYPE_SCRIPT_JSON: JSON.stringify(typeScript), + REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON: JSON.stringify(typeCellDep), + REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON: JSON.stringify(commitmentLock), + REGISTRY_COMMITMENT_LOCK_CELL_DEP_JSON: JSON.stringify(lockCellDep), + }; + + await app.scheduled( + { scheduledTime: now.getTime(), cron: "*/15 * * * *" } as ScheduledController, + scheduledEnv, + ); + expect(store.packageVersions.get("cellscript/demo@1.2.3")?.status).toBe("on_chain_committed"); + const commitmentProof = await (await get( + app, + "/v1/artifacts/cellscript/demo/releases/1.2.3/commitment", + scheduledEnv, + )).json() as any; + expect(commitmentProof.status).toBe("on_chain_committed"); + expect(commitmentProof.transaction_intent.output.type).toEqual(typeScript); + expect(commitmentProof.transaction_intent.required_cell_deps).toEqual([typeCellDep]); + expect(commitmentProof.transaction_intent.custody_cell_dep).toEqual(lockCellDep); + + commitmentConfigurationLive = false; + await app.scheduled( + { scheduledTime: now.getTime(), cron: "*/15 * * * *" } as ScheduledController, + scheduledEnv, + ); + expect(store.packageVersions.get("cellscript/demo@1.2.3")?.status).toBe("deployed"); + expect(store.auditEvents.some((event) => event.event_type === "maintenance.registry_commitment_configuration_failed" + && event.data?.["demoted_commitments"] === 1)).toBe(true); + + const unsafeIntent = await get( + app, + "/v1/artifacts/cellscript/demo/releases/1.2.3/commitment", + scheduledEnv, + ); + expect(unsafeIntent.status).toBe(409); + + commitmentConfigurationLive = true; + await app.scheduled( + { scheduledTime: now.getTime(), cron: "*/15 * * * *" } as ScheduledController, + scheduledEnv, + ); + expect(store.packageVersions.get("cellscript/demo@1.2.3")?.status).toBe("on_chain_committed"); + + const proofWithoutConfiguration = await (await get( + app, + "/v1/artifacts/cellscript/demo/releases/1.2.3/commitment", + )).json() as any; + expect(proofWithoutConfiguration.status).toBe("commitment_unconfigured"); + expect(proofWithoutConfiguration.commitment).toBeUndefined(); + expect(proofWithoutConfiguration.transaction_intent).toBeNull(); + + await app.scheduled( + { scheduledTime: now.getTime(), cron: "*/15 * * * *" } as ScheduledController, + {}, + ); + expect(store.packageVersions.get("cellscript/demo@1.2.3")?.status).toBe("deployed"); + expect(store.auditEvents.some((event) => event.event_type === "maintenance.registry_commitment_disabled" + && event.data?.["demoted_commitments"] === 1)).toBe(true); + + await app.scheduled( + { scheduledTime: now.getTime(), cron: "*/15 * * * *" } as ScheduledController, + scheduledEnv, + ); + expect(store.packageVersions.get("cellscript/demo@1.2.3")?.status).toBe("on_chain_committed"); + + commitmentLive = false; + await app.scheduled( + { scheduledTime: now.getTime(), cron: "*/15 * * * *" } as ScheduledController, + scheduledEnv, + ); + expect(store.packageVersions.get("cellscript/demo@1.2.3")?.status).toBe("deployed"); + const reconciledProof = await (await get( + app, + "/v1/artifacts/cellscript/demo/releases/1.2.3/commitment", + scheduledEnv, + )).json() as any; + expect(reconciledProof.status).toBe("commitment_ready"); + expect(store.auditEvents.some((event) => event.event_type === "lifecycle.chain_state_reconciled")).toBe(true); + expect(store.packageVersions.get("cellscript/demo@1.2.3")?.current_commitment_evidence_hash).toBeNull(); + + await store.updatePackageVersionStatus({ + namespace: "cellscript", + name: "demo", + version: "1.2.3", + status: "yanked", + request_id: "yank-after-spend", + admin_actor: "test", + }); + const restored = await store.updatePackageVersionStatus({ + namespace: "cellscript", + name: "demo", + version: "1.2.3", + status: "active", + request_id: "restore-after-spend", + admin_actor: "test", + }); + expect(restored.status).toBe("deployed"); + expect(restored.current_commitment_evidence_hash).toBeNull(); + + deploymentLive = false; + await app.scheduled( + { scheduledTime: now.getTime(), cron: "*/15 * * * *" } as ScheduledController, + scheduledEnv, + ); + const staleDeployment = store.packageVersions.get("cellscript/demo@1.2.3")!; + expect(staleDeployment.status).toBe("verified_build"); + expect(staleDeployment.deployment_status).toBe("undeployed"); + expect(staleDeployment.current_commitment_evidence_hash).toBeNull(); + }); + it("revokes a capability with JoyID and blocks later publish", async () => { const { app, store } = testApp(); const payload = authPayload(); @@ -951,7 +3512,7 @@ describe("registry api", () => { expect(store.capabilities.get(capability.key_id)?.revoked_at).toBeTruthy(); const publish = await publishPayload(capability.key_id); - const publishResponse = await post(app, "/v1/packages/cellscript/demo/versions", { + const publishResponse = await post(app, "/v1/artifacts/cellscript/demo/releases", { payload: publish, capability_signature: { algorithm: "p256-sha256", signature: "sig" }, source_snapshot: { diff --git a/services/registry-api/test/sql-registry-store.test.ts b/services/registry-api/test/sql-registry-store.test.ts new file mode 100644 index 00000000..4f43a184 --- /dev/null +++ b/services/registry-api/test/sql-registry-store.test.ts @@ -0,0 +1,307 @@ +import { randomUUID } from "node:crypto"; +import { readdir, readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { Client } from "pg"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +import { SqlRegistryStore } from "../src/sql-store"; + +const databaseUrl = process.env.REGISTRY_TEST_DATABASE_URL; +const describePostgres = databaseUrl ? describe : describe.skip; +const migrationsDirectory = fileURLToPath(new URL("../migrations/", import.meta.url)); + +function schemaConnectionString(connectionString: string, schema: string): string { + const url = new URL(connectionString); + url.searchParams.set("options", `-csearch_path=${schema}`); + return url.toString(); +} + +describePostgres("SqlRegistryStore PostgreSQL contract", () => { + const schema = `registry_test_${randomUUID().replaceAll("-", "")}`; + let admin: Client; + let scopedConnectionString: string; + + beforeAll(async () => { + admin = new Client({ connectionString: databaseUrl! }); + await admin.connect(); + await admin.query(`create schema ${schema}`); + scopedConnectionString = schemaConnectionString(databaseUrl!, schema); + }); + + afterAll(async () => { + if (!admin) return; + await admin.query(`drop schema if exists ${schema} cascade`); + await admin.end(); + }); + + it("migrates legacy commitments, enforces the current pointer, and serialises maintenance", async () => { + const client = new Client({ connectionString: scopedConnectionString }); + await client.connect(); + try { + const migrationFiles = (await readdir(migrationsDirectory)) + .filter((file) => /^[0-9]{4}_.+[.]sql$/.test(file)) + .sort(); + const currentCommitmentMigration = "0007_current_commitment_state.sql"; + const authorisationSessionsMigration = "0009_authorisation_sessions.sql"; + expect(migrationFiles.at(-1)).toBe(authorisationSessionsMigration); + + for (const file of migrationFiles.filter((item) => item < currentCommitmentMigration)) { + await client.query(await readFile(new URL(`../migrations/${file}`, import.meta.url), "utf8")); + } + + const evidenceHash = `sha256:${"a1".repeat(32)}`; + await client.query(` + insert into principals(principal_type, principal_id) + values ('joyid_ckb', '0x1111111111111111111111111111111111111111'); + insert into namespaces(namespace, owner_principal_type, owner_principal_id, audit_request_id) + values ('fixture', 'joyid_ckb', '0x1111111111111111111111111111111111111111', 'fixture'); + insert into packages(namespace, name) values ('fixture', 'contract'); + insert into capabilities( + key_id, principal_type, principal_id, capability_pubkey, scopes, expires_at, + authorisation_payload, joyid_signature + ) values ( + 'cap_fixturefixturefixturefixture12', 'joyid_ckb', + '0x1111111111111111111111111111111111111111', 'p256-spki:fixture', + array['publish:fixture/contract'], '2099-01-01T00:00:00Z', '{}'::jsonb, '{}'::jsonb + ); + insert into source_snapshots(snapshot_hash, r2_key, source_hash, size_bytes, content_type) + values ( + 'sha256:${"b2".repeat(32)}', 'fixture/source.tar', '${"c3".repeat(32)}', 1, + 'application/vnd.cellscript.source+tar' + ); + insert into package_versions( + namespace, name, version, status, artifact, verification_status, deployment_status, + availability_status, source_hash, manifest_hash, edition, compatibility_profile_hash, + capability_key_id, principal_type, principal_id, registry_entry, snapshot_hash, direct_url + ) values ( + 'fixture', 'contract', '1.0.0', 'on_chain_attested', + '{"kind":"deployable_contract","profile":"ckb_executable","consumption_mode":"deployment","language":"rust"}'::jsonb, + 'verified', 'chain_verified', 'active', '${"c3".repeat(32)}', '${"d4".repeat(32)}', + '2026', '${"e5".repeat(32)}', 'cap_fixturefixturefixturefixture12', 'joyid_ckb', + '0x1111111111111111111111111111111111111111', '{}'::jsonb, + 'sha256:${"b2".repeat(32)}', 'https://registry.cellscript.dev/fixture/contract/1.0.0' + ); + insert into package_version_evidence( + namespace, name, version, kind, evidence_hash, evidence, request_id, admin_actor + ) values ( + 'fixture', 'contract', '1.0.0', 'on_chain_attested', '${evidenceHash}', + '{"chain_verification":"get_live_cell+configured_type_index"}'::jsonb, + 'legacy-commitment', 'fixture' + ); + `); + + await client.query(await readFile(new URL(`../migrations/${currentCommitmentMigration}`, import.meta.url), "utf8")); + const migrated = await client.query( + `select status, current_commitment_evidence_hash from package_versions + where namespace = 'fixture' and name = 'contract' and version = '1.0.0'`, + ); + expect(migrated.rows[0]).toEqual({ + status: "deployed", + current_commitment_evidence_hash: null, + }); + expect((await client.query( + `select kind from package_version_evidence + where namespace = 'fixture' and name = 'contract' and version = '1.0.0'`, + )).rows[0]?.kind).toBe("on_chain_committed"); + + for (const file of migrationFiles.filter((item) => item > currentCommitmentMigration)) { + await client.query(await readFile(new URL(`../migrations/${file}`, import.meta.url), "utf8")); + } + + const store = new SqlRegistryStore({ connectionString: scopedConnectionString }); + await client.query(` + insert into source_snapshots(snapshot_hash, r2_key, source_hash, size_bytes, content_type) + values ( + 'sha256:${"b3".repeat(32)}', 'fixture/source-sandbox.tar', '${"c3".repeat(32)}', 1, + 'application/vnd.cellscript.source+tar' + ); + insert into package_versions( + namespace, name, version, status, artifact, verification_status, deployment_status, + availability_status, source_hash, manifest_hash, edition, compatibility_profile_hash, + capability_key_id, principal_type, principal_id, registry_entry, snapshot_hash, direct_url, + registry_environment, chain_network, expires_at, purge_after + ) values ( + 'fixture', 'contract', '2.0.0', 'source_published', + '{"kind":"deployable_contract","profile":"ckb_executable","consumption_mode":"deployment","language":"rust"}'::jsonb, + 'pending', 'undeployed', 'active', '${"c3".repeat(32)}', '${"d4".repeat(32)}', + '2026', '${"e5".repeat(32)}', 'cap_fixturefixturefixturefixture12', 'joyid_ckb', + '0x1111111111111111111111111111111111111111', '{}'::jsonb, + 'sha256:${"b3".repeat(32)}', 'https://objects.testnet.registry.cellscript.dev/artifacts/fixture/contract/releases/2.0.0.json', + 'testnet-sandbox', 'testnet', '2026-06-23T12:00:00Z', '2026-06-24T12:00:00Z' + ) + `); + const sandboxCleanup = await store.cleanupExpiredState({ + now_iso: "2026-06-25T12:00:00Z", + quota_events_before_iso: "2026-06-23T12:00:00Z", + }); + expect(sandboxCleanup).toMatchObject({ + package_versions_expired: 1, + static_objects: [{ key: "artifacts/fixture/contract/releases/2.0.0.json" }], + source_objects: [{ key: "fixture/source-sandbox.tar", snapshot_hash: `sha256:${"b3".repeat(32)}` }], + }); + expect(await store.getPackageVersion("fixture", "contract", "2.0.0")).toBeNull(); + await store.markSandboxObjectsPurged({ + static_objects: sandboxCleanup.static_objects ?? [], + source_objects: sandboxCleanup.source_objects ?? [], + purged_at: "2026-06-25T12:00:00Z", + }); + expect((await client.query( + `select static_purged_at is not null as static_purged, source_purged_at is not null as source_purged + from package_versions where namespace = 'fixture' and name = 'contract' and version = '2.0.0'`, + )).rows[0]).toEqual({ static_purged: true, source_purged: true }); + + await expect(client.query( + `update package_versions + set status = 'on_chain_committed', current_commitment_evidence_hash = null + where namespace = 'fixture' and name = 'contract' and version = '1.0.0'`, + )).rejects.toMatchObject({ code: "23514" }); + + const recommitted = await store.promotePackageVersion({ + namespace: "fixture", + name: "contract", + version: "1.0.0", + kind: "on_chain_committed", + evidence_hash: evidenceHash, + evidence: { + chain_verification: "get_live_cell+configured_type_index", + observed_live: true, + confirmations: 24, + }, + request_id: "commitment-reobserved", + admin_actor: "fixture-indexer", + }); + expect(recommitted.version.status).toBe("on_chain_committed"); + expect(recommitted.version.current_commitment_evidence_hash).toBe(evidenceHash); + expect((await store.listPackageVersions({ + deployment_status: "chain_verified", + limit: 10, + offset: 0, + }))[0]?.current_commitment_evidence_hash).toBe(evidenceHash); + expect((await store.listArtifactPackagePage({ + deployment_status: "chain_verified", + limit: 10, + offset: 0, + })).records[0]?.current_commitment_evidence_hash).toBe(evidenceHash); + + const reconciled = await store.reconcilePackageVersionLifecycle({ + namespace: "fixture", + name: "contract", + version: "1.0.0", + status: "deployed", + deployment_status: "chain_verified", + request_id: "commitment-spent", + reason: "registry_commitment_cell_not_live", + }); + expect(reconciled.status).toBe("deployed"); + expect(reconciled.current_commitment_evidence_hash).toBeNull(); + + await store.updatePackageVersionStatus({ + namespace: "fixture", + name: "contract", + version: "1.0.0", + status: "yanked", + request_id: "yank-after-spend", + admin_actor: "fixture", + }); + + await client.query( + `insert into idempotency_keys(key, request_hash, request_id, expires_at) + values ('restore-fixture', 'correct-hash', 'restore-after-spend', '2099-01-01T00:00:00Z')`, + ); + await expect(store.updatePackageVersionStatus({ + namespace: "fixture", + name: "contract", + version: "1.0.0", + status: "active", + request_id: "wrong-restore", + admin_actor: "fixture", + idempotency: { + key: "restore-fixture", + request_hash: "wrong-hash", + response_status: 200, + response_body: { restored: true }, + }, + })).rejects.toMatchObject({ code: "idempotency_key_conflict" }); + expect((await store.getPackageVersion("fixture", "contract", "1.0.0"))?.availability_status).toBe("yanked"); + + const restored = await store.updatePackageVersionStatus({ + namespace: "fixture", + name: "contract", + version: "1.0.0", + status: "active", + request_id: "restore-after-spend", + admin_actor: "fixture", + idempotency: { + key: "restore-fixture", + request_hash: "correct-hash", + response_status: 200, + response_body: { restored: true }, + }, + }); + expect(restored.status).toBe("deployed"); + expect(restored.current_commitment_evidence_hash).toBeNull(); + expect((await client.query( + "select status, response from idempotency_keys where key = 'restore-fixture'", + )).rows[0]).toEqual({ status: "completed", response: { restored: true } }); + + const staleDeployment = await store.reconcilePackageVersionLifecycle({ + namespace: "fixture", + name: "contract", + version: "1.0.0", + status: "verified_build", + deployment_status: "undeployed", + request_id: "deployment-spent", + reason: "deployment_cell_not_live", + }); + expect(staleDeployment.status).toBe("verified_build"); + expect(staleDeployment.deployment_status).toBe("undeployed"); + expect(staleDeployment.current_commitment_evidence_hash).toBeNull(); + + await store.updatePackageVersionStatus({ + namespace: "fixture", + name: "contract", + version: "1.0.0", + status: "yanked", + request_id: "yank-during-reverification", + admin_actor: "fixture", + }); + const verifiedWhileYanked = await store.promotePackageVersion({ + namespace: "fixture", + name: "contract", + version: "1.0.0", + kind: "verified_build", + evidence_hash: `sha256:${"55".repeat(32)}`, + evidence: { verification_level: "compiled" }, + request_id: "reverify-yanked", + admin_actor: "fixture-verifier", + }); + expect(verifiedWhileYanked.version.status).toBe("yanked"); + expect(verifiedWhileYanked.version.verification_status).toBe("verified"); + const restoredAfterVerification = await store.updatePackageVersionStatus({ + namespace: "fixture", + name: "contract", + version: "1.0.0", + status: "active", + request_id: "restore-after-reverification", + admin_actor: "fixture", + }); + expect(restoredAfterVerification.status).toBe("verified_build"); + + let releaseLease!: () => void; + let announceLease!: () => void; + const leaseAcquired = new Promise((resolve) => { announceLease = resolve; }); + const leaseHeld = new Promise((resolve) => { releaseLease = resolve; }); + const firstLease = store.withMaintenanceLease("registry-maintenance", async () => { + announceLease(); + await leaseHeld; + return "complete"; + }); + await leaseAcquired; + expect(await store.withMaintenanceLease("registry-maintenance", async () => "overlap")).toBeNull(); + releaseLease(); + expect(await firstLease).toBe("complete"); + } finally { + await client.end(); + } + }, 30_000); +}); diff --git a/services/registry-api/test/verifier-subprocess.test.ts b/services/registry-api/test/verifier-subprocess.test.ts new file mode 100644 index 00000000..9eb733d2 --- /dev/null +++ b/services/registry-api/test/verifier-subprocess.test.ts @@ -0,0 +1,89 @@ +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterEach, describe, expect, it } from "vitest"; + +import { canonicalJson, ckbBlake2bHex } from "../src/domain"; +import { executeVerifierSubprocess } from "../src/verifier-subprocess"; + +const verifierBinary = process.env["CELLSCRIPT_REGISTRY_VERIFIER_TEST_BINARY"]?.trim(); +const temporaryRoots: string[] = []; + +afterEach(async () => { + await Promise.all(temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); +}); + +describe.skipIf(!verifierBinary)("Rust verifier subprocess contract", () => { + it("passes a copy-material bundle through the same Node subprocess boundary used by the worker", async () => { + const root = await mkdtemp(join(tmpdir(), "cellscript-verifier-contract-")); + temporaryRoots.push(root); + const source = new TextEncoder().encode("starter artifact"); + const contract = { + schema: "cellscript-registry-profile-contract-v1", + artifact_kind: "template", + profile: "copy_material", + copy: { format: "file_map_v1", entrypoint: "template.cell" }, + }; + const manifestJson = canonicalJson(contract); + const bundle = { + schema: "cellscript-registry-bundle", + namespace: "cellscript", + name: "starter", + release: "1.0.0", + profile: "copy_material", + manifest_json: manifestJson, + objects: [{ role: "source", content_base64: Buffer.from(source).toString("base64") }], + }; + const snapshotPath = join(root, "artifact.bundle.json"); + await writeFile(snapshotPath, JSON.stringify(bundle)); + const args = verifierArgs(snapshotPath, ckbBlake2bHex(source), ckbBlake2bHex(manifestJson)); + + const result = await executeVerifierSubprocess(verifierBinary!, args, { + cwd: root, + env: { ...process.env, NO_COLOR: "1" }, + timeoutMs: 30_000, + }); + + expect(result.timedOut).toBe(false); + expect(result.exitCode).toBe(0); + expect(JSON.parse(result.stdout)).toMatchObject({ + status: "passed", + verification_level: "hash_bound", + artifact_format: "copy-material", + }); + }); + + it("preserves the Rust verifier's stable rejection code", async () => { + const root = await mkdtemp(join(tmpdir(), "cellscript-verifier-contract-")); + temporaryRoots.push(root); + const snapshotPath = join(root, "invalid.bundle.json"); + await writeFile(snapshotPath, "not-json"); + + const result = await executeVerifierSubprocess( + verifierBinary!, + verifierArgs(snapshotPath, "11".repeat(32), "22".repeat(32)), + { cwd: root, env: { ...process.env, NO_COLOR: "1" }, timeoutMs: 30_000 }, + ); + + expect(result.timedOut).toBe(false); + expect(result.exitCode).toBe(1); + expect(JSON.parse(result.stdout)).toMatchObject({ + status: "failed", + error_code: "artifact_bundle_invalid", + }); + }); +}); + +function verifierArgs(snapshotPath: string, sourceHash: string, manifestHash: string): string[] { + return [ + "--snapshot", snapshotPath, + "--namespace", "cellscript", + "--name", "starter", + "--version", "1.0.0", + "--source-hash", sourceHash, + "--manifest-hash", manifestHash, + "--artifact-kind", "template", + "--profile", "copy_material", + ]; +} diff --git a/services/registry-api/wrangler.example.toml b/services/registry-api/wrangler.example.toml index bc5ca9c8..9852a727 100644 --- a/services/registry-api/wrangler.example.toml +++ b/services/registry-api/wrangler.example.toml @@ -5,7 +5,7 @@ compatibility_flags = ["nodejs_compat"] routes = [ { pattern = "api.registry.cellscript.dev/*", zone_name = "cellscript.dev" }, - { pattern = "registry.cellscript.dev/packages/*", zone_name = "cellscript.dev" } + { pattern = "registry.cellscript.dev/artifacts/*", zone_name = "cellscript.dev" } ] [triggers] @@ -13,12 +13,23 @@ crons = ["*/15 * * * *"] [vars] ENVIRONMENT = "production" +REGISTRY_ENVIRONMENT = "production" REGISTRY_ORIGIN = "https://api.registry.cellscript.dev" STATIC_REGISTRY_ORIGIN = "https://registry.cellscript.dev" +REGISTRY_WEBSITE_ORIGIN = "https://cellscript.dev" +CKB_RPC_URL = "https://mainnet.ckb.dev/rpc" JOYID_SERVER_URL = "https://api.joy.id/api/v1" MAX_JSON_BODY_BYTES = "6291456" CLEANUP_QUOTA_EVENT_RETENTION_HOURS = "48" NAMESPACE_CLAIM_COOLDOWN_SECONDS = "3600" +CKB_REGISTRY_SCAN_MAX_CELLS = "1000" +# Enable only after the canonical mainnet Registry Type Script is deployed. +# REGISTRY_TYPE_SCRIPT_JSON = '{"code_hash":"0x0dd596ade29e06e5bcc00f56abf36ecbe9afaa09f1b26a64436aa37854da622b","hash_type":"data1","args":"0x"}' +# REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON = '{"out_point":{"tx_hash":"0x","index":"0x0"},"dep_type":"code"}' +# REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON = '{"code_hash":"0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8","hash_type":"type","args":"0x"}' +# REGISTRY_COMMITMENT_LOCK_CELL_DEP_JSON = '{"out_point":{"tx_hash":"0x71a7ba8fc96349fea0ed3a5c47992e3b4084b031a42264a018e0072e8172e46c","index":"0x0"},"dep_type":"dep_group"}' +# REGISTRY_REPRODUCER_POLICY_JSON = '{"schema":"cellscript-reproducer-policy-v1","minimum_trust_domains":2,"builders":[{"builder_id":"builder-a","trust_domain":"org-a","public_key":"p256-spki:..."},{"builder_id":"builder-b","trust_domain":"org-b","public_key":"p256-spki:..."}]}' +# CKB_MIN_CONFIRMATIONS = "24" [[r2_buckets]] binding = "REGISTRY_OBJECTS" diff --git a/services/registry-artifact-verifier/Cargo.lock b/services/registry-artifact-verifier/Cargo.lock new file mode 100644 index 00000000..82c49ff2 --- /dev/null +++ b/services/registry-artifact-verifier/Cargo.lock @@ -0,0 +1,2395 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse 0.2.7", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse 1.0.0", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "blake2b_simd" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" + +[[package]] +name = "cc" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cellscript" +version = "0.24.0" +dependencies = [ + "anyhow", + "base64", + "blake2b_simd", + "camino", + "cellscript-artifact-checker", + "ckb-vm", + "clap", + "colored", + "env_logger", + "hex", + "indexmap", + "keyring", + "log", + "reqwest", + "ring", + "semver", + "serde", + "serde_json", + "sha2", + "thiserror 1.0.69", + "tokio", + "toml", + "tower-lsp", + "unicode-width", +] + +[[package]] +name = "cellscript-artifact-checker" +version = "0.24.0" +dependencies = [ + "blake2b_simd", + "clap", + "serde", + "serde_json", +] + +[[package]] +name = "cellscript-registry-artifact-verifier" +version = "0.24.0" +dependencies = [ + "anyhow", + "base64", + "cellscript", + "cellscript-artifact-checker", + "serde", + "serde_json", + "sha2", + "tempfile", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "ckb-vm" +version = "0.24.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad137e2f1c9a363ce19a883a2113b1dfcc00a936945e34b62e3726c49e7171fb" +dependencies = [ + "byteorder", + "bytes", + "cc", + "ckb-vm-definitions", + "derive_more", + "goblin 0.2.3", + "goblin 0.4.0", + "rand 0.7.3", + "scroll", + "serde", +] + +[[package]] +name = "ckb-vm-definitions" +version = "0.24.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b436017fd6676bea413d54e07a5a9cc1d7c4b5c02e4ab07d3527225a5de6677" +dependencies = [ + "paste", +] + +[[package]] +name = "clap" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" +dependencies = [ + "anstream 0.6.21", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.20", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream 1.0.0", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "goblin" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d20fd25aa456527ce4f544271ae4fea65d2eda4a6561ea56f39fb3ee4f7e3884" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "goblin" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532a09cd3df2c6bbfc795fb0434bff8f22255d1d07328180e918a2e6ce122d4d" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "log", + "zeroize", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lsp-types" +version = "0.94.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" +dependencies = [ + "bitflags 1.3.2", + "serde", + "serde_json", + "serde_repr", + "url", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.20", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.20", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower 0.5.3", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scroll" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda28d4b4830b807a8b43f7b0e6b5df875311b3e7621d84577188c175b6ec1ec" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaaae8f38bb311444cfb7f1979af0bc9240d95795f75f9ceddf6a59b79ceffa0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower 0.5.3", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-lsp" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ba052b54a6627628d9b3c34c176e7eda8359b7da9acd497b9f20998d118508" +dependencies = [ + "async-trait", + "auto_impl", + "bytes", + "dashmap", + "futures", + "httparse", + "lsp-types", + "memchr", + "serde", + "serde_json", + "tokio", + "tokio-util", + "tower 0.4.13", + "tower-lsp-macros", + "tracing", +] + +[[package]] +name = "tower-lsp-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" +dependencies = [ + "memchr", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/services/registry-artifact-verifier/Cargo.toml b/services/registry-artifact-verifier/Cargo.toml new file mode 100644 index 00000000..a3035f6f --- /dev/null +++ b/services/registry-artifact-verifier/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "cellscript-registry-artifact-verifier" +version = "0.24.0" +edition = "2024" +rust-version = "1.97.1" +publish = false +description = "Least-privilege Registry admission worker for verified CellScript CKB artifacts" +license = "MIT" + +[[bin]] +name = "cellscript-registry-artifact-verify" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0" +base64 = "0.22" +cellscript-artifact-checker = { path = "../../crates/cellscript-artifact-checker" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sha2 = "0.10" + +[workspace] + +[dev-dependencies] +cellscript = { path = "../.." } +tempfile = "3.10" diff --git a/services/registry-artifact-verifier/README.md b/services/registry-artifact-verifier/README.md new file mode 100644 index 00000000..9c4cce6f --- /dev/null +++ b/services/registry-artifact-verifier/README.md @@ -0,0 +1,24 @@ +# Registry Artifact Verifier + +This is the least-privilege `ckb_executable` admission worker. Its normal +dependency graph contains `cellscript-artifact-checker` and does not contain +the CellScript compiler. + +The worker accepts one path-confined Registry bundle and verifies its +coordinate, canonical manifest, and declared hashes. Generic source/executable/ +ABI bundles remain `hash_bound`. If any CellScript verified sidecar is present, +the worker requires the complete metadata/lowering-record/source-map set and +runs the standalone checker. Successful structural JSON records +`structurally_verified`, checker version, checker policy schema, and a hash of +the canonical checker report. + +For `cellscript-registry-ls-idl-interface-v1`, the same worker independently +validates the bounded LS-IDL schema, hashes the exact ABI object bytes with +SHA-256, and requires that digest as the executable's final 32 bytes. Its +result records the interface format, digest, and +`schema-and-suffix-bound` status. That status is byte-identity evidence, not an +implementation-correctness or security-audit claim. + +The root gate proves the production dependency boundary with `cargo tree`. +The root compiler is present only as a dev-dependency so integration tests can +construct a real valid bundle; it is not linked into the production binary. diff --git a/services/registry-artifact-verifier/src/main.rs b/services/registry-artifact-verifier/src/main.rs new file mode 100644 index 00000000..7bee1de6 --- /dev/null +++ b/services/registry-artifact-verifier/src/main.rs @@ -0,0 +1,669 @@ +//! Least-privilege Registry worker for artifact-only CKB admission. +//! +//! This binary intentionally has no dependency on the CellScript compiler. + +use anyhow::{bail, Context, Result}; +use base64::Engine as _; +use serde::{Deserialize, Serialize}; +use sha2::{Digest as _, Sha256}; +use std::collections::{BTreeMap, BTreeSet}; +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::ExitCode; + +const MAX_SNAPSHOT_BYTES: u64 = 5 * 1024 * 1024; + +#[derive(Debug)] +struct Args { + snapshot: PathBuf, + namespace: String, + name: String, + version: String, + source_hash: String, + manifest_hash: String, + artifact_kind: String, + profile: String, + compatibility_profile_hash: Option, + artifact_hash: String, + abi_hash: String, + build_recipe_hash: Option, +} + +#[derive(Debug, Serialize)] +struct VerificationOutput { + status: &'static str, + verification_level: &'static str, + artifact_hash: String, + metadata_hash: String, + compiler_version: Option, + source_hash: String, + manifest_hash: String, + #[serde(skip_serializing_if = "Option::is_none")] + compatibility_profile_hash: Option, + artifact_format: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + checker_version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + checker_policy_schema: Option, + #[serde(skip_serializing_if = "Option::is_none")] + checker_report_hash: Option, + #[serde(skip_serializing_if = "Option::is_none")] + interface_format: Option<&'static str>, + #[serde(skip_serializing_if = "Option::is_none")] + interface_digest: Option, + #[serde(skip_serializing_if = "Option::is_none")] + interface_commitment_status: Option<&'static str>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ArtifactBundle { + schema: String, + namespace: String, + name: String, + release: String, + profile: String, + manifest_json: String, + objects: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ArtifactBundleObject { + role: String, + content_base64: String, +} + +#[derive(Serialize)] +struct FailureOutput<'a> { + status: &'static str, + error_code: &'static str, + message: &'a str, +} + +fn main() -> ExitCode { + match run() { + Ok(output) => { + if serde_json::to_writer(std::io::stdout(), &output).is_err() { + return ExitCode::from(70); + } + println!(); + ExitCode::SUCCESS + } + Err(error) => { + let message = error.to_string(); + let output = FailureOutput { status: "failed", error_code: error_code(&error), message: &message }; + let _ = serde_json::to_writer(std::io::stdout(), &output); + println!(); + ExitCode::from(1) + } + } +} + +fn run() -> Result { + let args = parse_args()?; + verify(args) +} + +fn verify(args: Args) -> Result { + if args.profile != "ckb_executable" || args.artifact_kind != "deployable_contract" { + bail!("artifact-only verifier requires ckb_executable/deployable_contract"); + } + let metadata = fs::symlink_metadata(&args.snapshot) + .with_context(|| format!("failed to inspect artifact snapshot '{}'", args.snapshot.display()))?; + if !metadata.is_file() || metadata.file_type().is_symlink() || metadata.len() == 0 || metadata.len() > MAX_SNAPSHOT_BYTES { + bail!("artifact snapshot must be a non-empty, non-symlink regular file no larger than {MAX_SNAPSHOT_BYTES} bytes"); + } + let snapshot = + fs::read(&args.snapshot).with_context(|| format!("failed to read artifact snapshot '{}'", args.snapshot.display()))?; + let bundle: ArtifactBundle = serde_json::from_slice(&snapshot).context("artifact bundle must be valid JSON")?; + if bundle.schema != "cellscript-registry-bundle" + || bundle.namespace != args.namespace + || bundle.name != args.name + || bundle.release != args.version + || bundle.profile != args.profile + { + bail!("artifact bundle identity does not match the verification job"); + } + let manifest: serde_json::Value = + serde_json::from_str(&bundle.manifest_json).context("artifact bundle manifest_json must be valid JSON")?; + if !manifest.is_object() || serde_json::to_string(&manifest)? != bundle.manifest_json { + bail!("artifact bundle manifest_json must be canonical compact JSON"); + } + validate_contract(&manifest, &args)?; + require_hash("manifest_hash", &hash(bundle.manifest_json.as_bytes()), &args.manifest_hash)?; + let has_verified_sidecars = validate_roles(&bundle, &manifest)?; + + let source = object(&bundle, "source")?; + require_hash("source_hash", &hash(&source), &args.source_hash)?; + let executable = object(&bundle, "executable")?; + let artifact_hash = hash(&executable); + require_hash("artifact_hash", &artifact_hash, &args.artifact_hash)?; + let abi = object(&bundle, "abi")?; + require_hash("abi_hash", &hash(&abi), &args.abi_hash)?; + let interface_digest = + if manifest.get("interface").is_some() { validate_ls_idl_interface(&manifest, &abi, &executable)? } else { None }; + if manifest.pointer("/build/reproducible").and_then(serde_json::Value::as_bool) == Some(true) { + let recipe = object(&bundle, "build_recipe")?; + let expected = args.build_recipe_hash.as_deref().context("reproducible ckb_executable requires --build-recipe-hash")?; + require_hash("build_recipe_hash", &hash(&recipe), expected)?; + } + if let Some(expected) = manifest.pointer("/security/audit_report_hash").and_then(serde_json::Value::as_str) { + require_hash("audit_report_hash", &hash(&object(&bundle, "audit_report")?), expected)?; + } + + let checker = if has_verified_sidecars { + let compile_metadata = object(&bundle, "metadata")?; + let lowering_record = object(&bundle, "lowering_record")?; + let source_map = object(&bundle, "source_map")?; + let budgets = cellscript_artifact_checker::CheckerBudgets::default(); + let report = + cellscript_artifact_checker::check_bundle(&executable, &compile_metadata, &lowering_record, &source_map, &budgets) + .map_err(anyhow::Error::msg) + .context("artifact bundle independent checker rejected the CKB executable")?; + let record = cellscript_artifact_checker::parse_lowering_record(&lowering_record, &budgets) + .map_err(anyhow::Error::msg) + .context("failed to read checker-approved lowering record")?; + if let Some(expected) = args.compatibility_profile_hash.as_deref() { + require_hash("compatibility_profile_hash", &record.compatibility_profile_hash, expected)?; + } + let report_bytes = cellscript_artifact_checker::canonical_bytes(&report).map_err(anyhow::Error::msg)?; + Some((record.compatibility_profile_hash, report.checker_version, report.checker_policy_schema, hash(&report_bytes))) + } else { + if args.compatibility_profile_hash.is_some() { + bail!("compatibility_profile_hash requires metadata, lowering_record, and source_map objects"); + } + None + }; + + Ok(VerificationOutput { + status: "passed", + verification_level: if checker.is_some() { "structurally_verified" } else { "hash_bound" }, + artifact_hash, + metadata_hash: hash(&snapshot), + compiler_version: None, + source_hash: args.source_hash, + manifest_hash: args.manifest_hash, + compatibility_profile_hash: checker.as_ref().map(|item| item.0.clone()), + artifact_format: "ckb-vm-executable", + checker_version: checker.as_ref().map(|item| item.1.clone()), + checker_policy_schema: checker.as_ref().map(|item| item.2.clone()), + checker_report_hash: checker.map(|item| item.3), + interface_format: interface_digest.as_ref().map(|_| "ls-idl"), + interface_digest, + interface_commitment_status: manifest.get("interface").map(|_| "schema-and-suffix-bound"), + }) +} + +fn validate_contract(contract: &serde_json::Value, args: &Args) -> Result<()> { + let string = |pointer: &str| contract.pointer(pointer).and_then(serde_json::Value::as_str); + if string("/schema") != Some("cellscript-registry-profile-contract-v1") + || string("/artifact_kind") != Some(args.artifact_kind.as_str()) + || string("/profile") != Some(args.profile.as_str()) + || string("/build/target") != Some("riscv64imac-unknown-none-elf") + || string("/ckb/abi_hash").is_none() + { + bail!("artifact profile contract is not a bounded CKB executable contract"); + } + require_hash("abi_hash", string("/ckb/abi_hash").unwrap(), &args.abi_hash)?; + if contract.pointer("/build/reproducible").and_then(serde_json::Value::as_bool) == Some(true) { + let recipe = string("/reproduction/recipe_hash").context("reproducible contract is missing recipe_hash")?; + let artifact = + string("/reproduction/expected_artifact_hash").context("reproducible contract is missing expected_artifact_hash")?; + require_hash( + "build_recipe_hash", + recipe, + args.build_recipe_hash.as_deref().context("reproducible contract requires --build-recipe-hash")?, + )?; + require_hash("artifact_hash", artifact, &args.artifact_hash)?; + } + Ok(()) +} + +fn validate_ls_idl_interface(contract: &serde_json::Value, abi: &[u8], executable: &[u8]) -> Result> { + let interface = contract + .get("interface") + .and_then(serde_json::Value::as_object) + .context("artifact profile contract interface must be an object")?; + exact_keys( + interface, + &["schema", "format", "format_version", "object_role", "content_type", "encoding", "commitment"], + "artifact profile contract interface", + )?; + require_literal(interface, "schema", "cellscript-registry-ls-idl-interface-v1")?; + require_literal(interface, "format", "ls-idl")?; + require_literal(interface, "format_version", "0.1")?; + require_literal(interface, "object_role", "abi")?; + require_literal(interface, "content_type", "application/vnd.ckb.ls-idl+json")?; + require_literal(interface, "encoding", "linear-le-v0")?; + if contract.pointer("/ckb/script_role").and_then(serde_json::Value::as_str) != Some("lock") { + bail!("artifact profile contract LS-IDL interface requires ckb.script_role='lock'"); + } + let commitment = interface + .get("commitment") + .and_then(serde_json::Value::as_object) + .context("artifact profile contract interface.commitment must be an object")?; + exact_keys(commitment, &["algorithm", "placement", "digest"], "artifact profile contract interface.commitment")?; + require_literal(commitment, "algorithm", "sha256")?; + require_literal(commitment, "placement", "code-cell-data-suffix-32")?; + validate_ls_idl_document(abi)?; + let digest: [u8; 32] = Sha256::digest(abi).into(); + let digest_hex = cellscript_artifact_checker::hex_encode(&digest); + let declared = commitment + .get("digest") + .and_then(serde_json::Value::as_str) + .context("artifact profile contract interface.commitment.digest must be a 32-byte hash")?; + require_hash("interface.commitment.digest", &digest_hex, declared)?; + if !executable.ends_with(&digest) { + bail!("artifact profile contract LS-IDL digest is not the exact 32-byte executable suffix"); + } + Ok(Some(digest_hex)) +} + +fn validate_ls_idl_document(bytes: &[u8]) -> Result<()> { + const MAX_LS_IDL_BYTES: usize = 256 * 1024; + if bytes.is_empty() || bytes.len() > MAX_LS_IDL_BYTES { + bail!("LS-IDL must be non-empty and no larger than {MAX_LS_IDL_BYTES} bytes"); + } + let value: serde_json::Value = serde_json::from_slice(bytes).context("LS-IDL must be valid JSON")?; + let document = value.as_object().context("LS-IDL must be a JSON object")?; + exact_keys(document, &["idl_version", "name", "witness", "description", "script_version", "signing"], "LS-IDL")?; + for key in ["idl_version", "name", "description", "script_version"] { + if let Some(value) = document.get(key) { + let text = value.as_str().with_context(|| format!("LS-IDL.{key} must be a string"))?; + if text.len() > 1024 { + bail!("LS-IDL.{key} exceeds the 1024-byte limit"); + } + } + } + let fields = document.get("witness").and_then(serde_json::Value::as_array).context("LS-IDL.witness must be an array")?; + if fields.len() > 256 { + bail!("LS-IDL.witness may contain at most 256 fields"); + } + let mut names = BTreeSet::new(); + for (index, value) in fields.iter().enumerate() { + let label = format!("LS-IDL.witness[{index}]"); + let field = value.as_object().with_context(|| format!("{label} must be an object"))?; + exact_keys(field, &["name", "type", "required", "description"], &label)?; + let name = nonempty_string(field, "name", &label)?; + if name.len() > 128 || !names.insert(name) { + bail!("{label}.name must be unique and no longer than 128 bytes"); + } + let type_name = nonempty_string(field, "type", &label)?; + if !matches!(type_name, "uint8" | "uint32" | "uint64" | "secp256k1_sig" | "secp256k1_pubkey" | "schnorr_sig" | "bytes") { + bail!("{label}.type is not supported by LS-IDL 0.1"); + } + if !matches!(field.get("required"), Some(serde_json::Value::Bool(_))) { + bail!("{label}.required must be a boolean"); + } + if let Some(description) = field.get("description") { + let description = description.as_str().with_context(|| format!("{label}.description must be a string"))?; + if description.len() > 1024 { + bail!("{label}.description exceeds the 1024-byte limit"); + } + } + } + if let Some(value) = document.get("signing") { + let signing = value.as_object().context("LS-IDL.signing must be an object")?; + exact_keys(signing, &["algorithm", "message", "hasher"], "LS-IDL.signing")?; + for key in ["algorithm", "message", "hasher"] { + if nonempty_string(signing, key, "LS-IDL.signing")?.len() > 1024 { + bail!("LS-IDL.signing.{key} exceeds the 1024-byte limit"); + } + } + } + Ok(()) +} + +fn exact_keys(object: &serde_json::Map, allowed: &[&str], label: &str) -> Result<()> { + if let Some(key) = object.keys().find(|key| !allowed.contains(&key.as_str())) { + bail!("{label}.{key} is not recognised"); + } + Ok(()) +} + +fn require_literal(object: &serde_json::Map, key: &str, expected: &str) -> Result<()> { + let value = nonempty_string(object, key, "artifact profile contract interface")?; + if value != expected { + bail!("artifact profile contract interface.{key} must be '{expected}'"); + } + Ok(()) +} + +fn nonempty_string<'a>(object: &'a serde_json::Map, key: &str, label: &str) -> Result<&'a str> { + object + .get(key) + .and_then(serde_json::Value::as_str) + .filter(|value| !value.is_empty()) + .with_context(|| format!("{label}.{key} must be a non-empty string")) +} + +fn validate_roles(bundle: &ArtifactBundle, contract: &serde_json::Value) -> Result { + let verified_roles = BTreeSet::from(["metadata", "lowering_record", "source_map"]); + let has_any_verified_role = bundle.objects.iter().any(|item| verified_roles.contains(item.role.as_str())); + let mut required = BTreeSet::from(["source", "executable", "abi"]); + if has_any_verified_role { + required.extend(verified_roles); + } + if contract.pointer("/build/reproducible").and_then(serde_json::Value::as_bool) == Some(true) { + required.insert("build_recipe"); + } + if contract.pointer("/security/audit_report_hash").is_some() { + required.insert("audit_report"); + } + let mut seen = BTreeSet::new(); + for item in &bundle.objects { + if !required.contains(item.role.as_str()) || !seen.insert(item.role.as_str()) { + bail!("artifact bundle contains an unexpected or duplicate '{}' object", item.role); + } + } + if seen != required { + bail!("artifact bundle is missing one or more required verified-artifact objects"); + } + Ok(has_any_verified_role) +} + +fn object(bundle: &ArtifactBundle, role: &str) -> Result> { + let item = bundle.objects.iter().find(|item| item.role == role).with_context(|| format!("artifact bundle is missing '{role}'"))?; + let bytes = base64::engine::general_purpose::STANDARD + .decode(&item.content_base64) + .with_context(|| format!("artifact bundle '{role}' object is not valid base64"))?; + if bytes.is_empty() { + bail!("artifact bundle '{role}' object is empty"); + } + Ok(bytes) +} + +fn parse_args() -> Result { + let mut values = BTreeMap::new(); + let mut arguments = env::args().skip(1); + while let Some(flag) = arguments.next() { + if !flag.starts_with("--") { + bail!("unexpected positional argument '{flag}'"); + } + let value = arguments.next().with_context(|| format!("missing value for '{flag}'"))?; + if values.insert(flag.clone(), value).is_some() { + bail!("duplicate argument '{flag}'"); + } + } + let snapshot = PathBuf::from(take_required_arg(&mut values, "--snapshot")?); + let namespace = take_required_arg(&mut values, "--namespace")?; + let name = take_required_arg(&mut values, "--name")?; + let version = take_required_arg(&mut values, "--version")?; + let source_hash = take_required_arg(&mut values, "--source-hash")?; + let manifest_hash = take_required_arg(&mut values, "--manifest-hash")?; + let artifact_kind = take_required_arg(&mut values, "--artifact-kind")?; + let profile = take_required_arg(&mut values, "--profile")?; + let artifact_hash = take_required_arg(&mut values, "--artifact-hash")?; + let abi_hash = take_required_arg(&mut values, "--abi-hash")?; + let args = Args { + snapshot, + namespace, + name, + version, + source_hash, + manifest_hash, + artifact_kind, + profile, + compatibility_profile_hash: values.remove("--compatibility-profile-hash"), + artifact_hash, + abi_hash, + build_recipe_hash: values.remove("--build-recipe-hash"), + }; + if let Some((unknown, _)) = values.into_iter().next() { + bail!("unknown argument '{unknown}'"); + } + Ok(args) +} + +fn take_required_arg(values: &mut BTreeMap, name: &str) -> Result { + values.remove(name).with_context(|| format!("missing required argument '{name}'")) +} + +fn require_hash(field: &str, actual: &str, expected: &str) -> Result<()> { + let normalize = |value: &str| value.strip_prefix("0x").unwrap_or(value).to_ascii_lowercase(); + let actual = normalize(actual); + let expected = normalize(expected); + if actual.len() != 64 || expected.len() != 64 || actual != expected { + bail!("{field} mismatch: artifact value does not match the signed Registry identity"); + } + Ok(()) +} + +fn hash(bytes: &[u8]) -> String { + cellscript_artifact_checker::hex_encode(&cellscript_artifact_checker::ckb_blake2b256(bytes)) +} + +fn error_code(error: &anyhow::Error) -> &'static str { + let messages = error.chain().map(ToString::to_string).collect::>(); + let contains = |needle: &str| messages.iter().any(|message| message.contains(needle)); + if contains("unexpected positional") || contains("missing required argument") || contains("unknown argument") { + "invalid_arguments" + } else if contains("snapshot") && (contains("failed to") || contains("regular file")) { + "snapshot_invalid" + } else if contains("identity does not match") { + "artifact_identity_mismatch" + } else if contains("_hash mismatch") { + "identity_hash_mismatch" + } else if contains("independent checker rejected") || messages.iter().any(|message| message.starts_with('V')) { + "artifact_checker_rejected" + } else if contains("artifact bundle") { + "artifact_bundle_invalid" + } else if contains("LS-IDL") { + "ls_idl_invalid" + } else if contains("artifact profile contract") { + "profile_contract_invalid" + } else { + "verifier_internal_error" + } +} + +#[cfg(test)] +mod tests { + use base64::Engine as _; + use serde_json::json; + + use super::*; + + #[test] + fn dependency_boundary_has_no_compiler_api() { + assert_eq!(cellscript_artifact_checker::CHECKER_POLICY_SCHEMA, "cellscript-artifact-checker-policy-v1"); + } + + #[test] + fn rejects_non_canonical_contract_json_before_checker_execution() { + let value: serde_json::Value = serde_json::from_str("{\n \"schema\": \"x\"\n}").unwrap(); + assert_ne!(serde_json::to_string(&value).unwrap(), "{\n \"schema\": \"x\"\n}"); + } + + #[test] + fn ls_idl_validation_preserves_exact_bytes_and_checks_the_executable_suffix() { + let idl = br#"{ + "witness": [{"name":"signature","type":"secp256k1_sig","required":true}] +} +"#; + let digest: [u8; 32] = Sha256::digest(idl).into(); + let mut executable = b"riscv-elf".to_vec(); + executable.extend_from_slice(&digest); + let manifest = json!({ + "ckb": { "script_role": "lock" }, + "interface": { + "schema": "cellscript-registry-ls-idl-interface-v1", + "format": "ls-idl", + "format_version": "0.1", + "object_role": "abi", + "content_type": "application/vnd.ckb.ls-idl+json", + "encoding": "linear-le-v0", + "commitment": { + "algorithm": "sha256", + "placement": "code-cell-data-suffix-32", + "digest": cellscript_artifact_checker::hex_encode(&digest) + } + } + }); + + assert_eq!( + validate_ls_idl_interface(&manifest, idl, &executable).unwrap(), + Some(cellscript_artifact_checker::hex_encode(&digest)) + ); + executable.pop(); + assert!(validate_ls_idl_interface(&manifest, idl, &executable).unwrap_err().to_string().contains("exact 32-byte")); + } + + #[test] + fn verifies_a_real_compiler_bundle_without_linking_the_compiler_into_the_worker() { + let source = br#"module artifact_worker_fixture + +action main(value: u64) -> u64 { + verification + return value +} +"#; + let result = cellscript::compile( + std::str::from_utf8(source).unwrap(), + cellscript::CompileOptions { target: Some("riscv64-elf".to_string()), ..Default::default() }, + ) + .unwrap(); + let abi = br#"{"actions":["main"]}"#; + let abi_hash = hash(abi); + let artifact_hash = hash(&result.artifact_bytes); + let metadata = serde_json::to_vec(&result.metadata).unwrap(); + let lowering_record = cellscript_artifact_checker::canonical_bytes(result.verified_lowering_record.as_ref().unwrap()).unwrap(); + let source_map = cellscript_artifact_checker::canonical_bytes(result.source_artifact_map.as_ref().unwrap()).unwrap(); + let compatibility_profile_hash = result.verified_lowering_record.as_ref().unwrap().compatibility_profile_hash.clone(); + let manifest = json!({ + "schema": "cellscript-registry-profile-contract-v1", + "artifact_kind": "deployable_contract", + "profile": "ckb_executable", + "build": { + "target": "riscv64imac-unknown-none-elf", + "toolchain": "rustc 1.97.1", + "profile": "release", + "source_revision": "test-fixture", + "reproducible": false + }, + "security": { "status": "review_required" }, + "ckb": { + "vm_version": "2", + "script_role": "type", + "hash_type": "data1", + "dep_type": "code", + "abi_hash": abi_hash.clone() + } + }); + let manifest_json = manifest.to_string(); + let encode = |role: &str, bytes: &[u8]| { + json!({ + "role": role, + "content_base64": base64::engine::general_purpose::STANDARD.encode(bytes) + }) + }; + let bundle = json!({ + "schema": "cellscript-registry-bundle", + "namespace": "cellscript", + "name": "artifact-worker-fixture", + "release": "0.24.0-test", + "profile": "ckb_executable", + "manifest_json": manifest_json.clone(), + "objects": [ + encode("source", source), + encode("executable", &result.artifact_bytes), + encode("abi", abi), + encode("metadata", &metadata), + encode("lowering_record", &lowering_record), + encode("source_map", &source_map) + ] + }); + let root = tempfile::tempdir().unwrap(); + let snapshot = root.path().join("bundle.json"); + fs::write(&snapshot, serde_json::to_vec(&bundle).unwrap()).unwrap(); + + let output = verify(Args { + snapshot, + namespace: "cellscript".to_string(), + name: "artifact-worker-fixture".to_string(), + version: "0.24.0-test".to_string(), + source_hash: hash(source), + manifest_hash: hash(manifest_json.as_bytes()), + artifact_kind: "deployable_contract".to_string(), + profile: "ckb_executable".to_string(), + compatibility_profile_hash: Some(compatibility_profile_hash), + artifact_hash, + abi_hash, + build_recipe_hash: None, + }) + .unwrap(); + assert_eq!(output.status, "passed"); + assert_eq!(output.verification_level, "structurally_verified"); + assert_eq!(output.checker_version.as_deref(), Some(cellscript_artifact_checker::CHECKER_VERSION)); + assert_eq!(output.checker_policy_schema.as_deref(), Some(cellscript_artifact_checker::CHECKER_POLICY_SCHEMA)); + } + + #[test] + fn generic_ckb_bundle_remains_hash_bound_without_cellscript_sidecars() { + let source = b"generic CKB source"; + let executable = b"generic executable bytes"; + let abi = br#"{"entry":"main"}"#; + let abi_hash = hash(abi); + let artifact_hash = hash(executable); + let manifest = json!({ + "schema": "cellscript-registry-profile-contract-v1", + "artifact_kind": "deployable_contract", + "profile": "ckb_executable", + "build": { + "target": "riscv64imac-unknown-none-elf", + "toolchain": "external", + "profile": "release", + "source_revision": "exact-external-revision", + "reproducible": false + }, + "security": { "status": "review_required" }, + "ckb": { + "vm_version": "2", + "script_role": "lock", + "hash_type": "data1", + "dep_type": "code", + "abi_hash": abi_hash.clone() + } + }); + let manifest_json = manifest.to_string(); + let encode = |role: &str, bytes: &[u8]| { + json!({ + "role": role, + "content_base64": base64::engine::general_purpose::STANDARD.encode(bytes) + }) + }; + let bundle = json!({ + "schema": "cellscript-registry-bundle", + "namespace": "external", + "name": "generic-ckb", + "release": "1.0.0", + "profile": "ckb_executable", + "manifest_json": manifest_json.clone(), + "objects": [encode("source", source), encode("executable", executable), encode("abi", abi)] + }); + let root = tempfile::tempdir().unwrap(); + let snapshot = root.path().join("bundle.json"); + fs::write(&snapshot, serde_json::to_vec(&bundle).unwrap()).unwrap(); + + let output = verify(Args { + snapshot, + namespace: "external".to_string(), + name: "generic-ckb".to_string(), + version: "1.0.0".to_string(), + source_hash: hash(source), + manifest_hash: hash(manifest_json.as_bytes()), + artifact_kind: "deployable_contract".to_string(), + profile: "ckb_executable".to_string(), + compatibility_profile_hash: None, + artifact_hash, + abi_hash, + build_recipe_hash: None, + }) + .unwrap(); + assert_eq!(output.verification_level, "hash_bound"); + assert!(output.checker_version.is_none()); + assert!(output.checker_report_hash.is_none()); + } +} diff --git a/services/registry-verifier/Cargo.lock b/services/registry-verifier/Cargo.lock new file mode 100644 index 00000000..fd7c8784 --- /dev/null +++ b/services/registry-verifier/Cargo.lock @@ -0,0 +1,2397 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse 0.2.7", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse 1.0.0", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "blake2b_simd" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" + +[[package]] +name = "cc" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cellscript" +version = "0.24.0" +dependencies = [ + "anyhow", + "base64", + "blake2b_simd", + "camino", + "cellscript-artifact-checker", + "ckb-vm", + "clap", + "colored", + "env_logger", + "hex", + "indexmap", + "keyring", + "log", + "reqwest", + "ring", + "semver", + "serde", + "serde_json", + "sha2", + "thiserror 1.0.69", + "tokio", + "toml", + "tower-lsp", + "unicode-width", +] + +[[package]] +name = "cellscript-artifact-checker" +version = "0.24.0" +dependencies = [ + "blake2b_simd", + "clap", + "serde", + "serde_json", +] + +[[package]] +name = "cellscript-registry-verifier" +version = "0.24.0" +dependencies = [ + "anyhow", + "base64", + "camino", + "cellscript", + "cellscript-artifact-checker", + "hex", + "serde", + "serde_json", + "sha2", + "tempfile", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "ckb-vm" +version = "0.24.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad137e2f1c9a363ce19a883a2113b1dfcc00a936945e34b62e3726c49e7171fb" +dependencies = [ + "byteorder", + "bytes", + "cc", + "ckb-vm-definitions", + "derive_more", + "goblin 0.2.3", + "goblin 0.4.0", + "rand 0.7.3", + "scroll", + "serde", +] + +[[package]] +name = "ckb-vm-definitions" +version = "0.24.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b436017fd6676bea413d54e07a5a9cc1d7c4b5c02e4ab07d3527225a5de6677" +dependencies = [ + "paste", +] + +[[package]] +name = "clap" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" +dependencies = [ + "anstream 0.6.21", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.20", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream 1.0.0", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "goblin" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d20fd25aa456527ce4f544271ae4fea65d2eda4a6561ea56f39fb3ee4f7e3884" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "goblin" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532a09cd3df2c6bbfc795fb0434bff8f22255d1d07328180e918a2e6ce122d4d" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "log", + "zeroize", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lsp-types" +version = "0.94.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" +dependencies = [ + "bitflags 1.3.2", + "serde", + "serde_json", + "serde_repr", + "url", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.20", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.20", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower 0.5.3", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scroll" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda28d4b4830b807a8b43f7b0e6b5df875311b3e7621d84577188c175b6ec1ec" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaaae8f38bb311444cfb7f1979af0bc9240d95795f75f9ceddf6a59b79ceffa0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower 0.5.3", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-lsp" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ba052b54a6627628d9b3c34c176e7eda8359b7da9acd497b9f20998d118508" +dependencies = [ + "async-trait", + "auto_impl", + "bytes", + "dashmap", + "futures", + "httparse", + "lsp-types", + "memchr", + "serde", + "serde_json", + "tokio", + "tokio-util", + "tower 0.4.13", + "tower-lsp-macros", + "tracing", +] + +[[package]] +name = "tower-lsp-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" +dependencies = [ + "memchr", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/services/registry-verifier/Cargo.toml b/services/registry-verifier/Cargo.toml new file mode 100644 index 00000000..1d964853 --- /dev/null +++ b/services/registry-verifier/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "cellscript-registry-verifier" +version = "0.24.0" +edition = "2024" +rust-version = "1.97.1" +publish = false + +[[bin]] +name = "cellscript-registry-verify" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0" +base64 = "0.22" +camino = "1.1" +cellscript = { path = "../.." } +cellscript-artifact-checker = { path = "../../crates/cellscript-artifact-checker" } +hex = "0.4" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sha2 = "0.10" + +[workspace] + +[dev-dependencies] +tempfile = "3.10" diff --git a/services/registry-verifier/src/main.rs b/services/registry-verifier/src/main.rs new file mode 100644 index 00000000..b3f2f46e --- /dev/null +++ b/services/registry-verifier/src/main.rs @@ -0,0 +1,830 @@ +//! Isolated source/build verifier used by the production Registry worker. + +use std::collections::BTreeMap; +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::ExitCode; +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::{bail, Context, Result}; +use base64::Engine as _; +use camino::Utf8PathBuf; +use serde::{Deserialize, Serialize}; + +const MAX_SNAPSHOT_BYTES: u64 = 5 * 1024 * 1024; + +#[derive(Debug)] +struct Args { + snapshot: PathBuf, + namespace: String, + name: String, + version: String, + source_hash: String, + manifest_hash: String, + artifact_kind: String, + profile: String, + compatibility_profile_hash: Option, + artifact_hash: Option, + abi_hash: Option, + build_recipe_hash: Option, +} + +#[derive(Debug, Serialize)] +struct VerificationOutput { + status: &'static str, + verification_level: &'static str, + artifact_hash: Option, + metadata_hash: String, + compiler_version: Option, + source_hash: String, + manifest_hash: String, + compatibility_profile_hash: Option, + artifact_format: String, + checker_version: Option, + checker_policy_schema: Option, + checker_report_hash: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ArtifactBundle { + schema: String, + namespace: String, + name: String, + release: String, + profile: String, + manifest_json: String, + objects: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ArtifactBundleObject { + role: String, + content_base64: String, +} + +#[derive(Serialize)] +struct FailureOutput<'a> { + status: &'static str, + error_code: &'static str, + message: &'a str, +} + +fn main() -> ExitCode { + match run() { + Ok(output) => { + if let Err(error) = serde_json::to_writer(std::io::stdout(), &output) { + eprintln!("failed to serialize verifier output: {error}"); + return ExitCode::from(70); + } + println!(); + ExitCode::SUCCESS + } + Err(error) => { + let message = error.to_string(); + let output = FailureOutput { status: "failed", error_code: verifier_error_code(&error), message: &message }; + let _ = serde_json::to_writer(std::io::stdout(), &output); + println!(); + ExitCode::from(1) + } + } +} + +fn verifier_error_code(error: &anyhow::Error) -> &'static str { + let messages = error.chain().map(ToString::to_string).collect::>(); + let contains = |needle: &str| messages.iter().any(|message| message.contains(needle)); + let starts_with = |prefix: &str| messages.iter().any(|message| message.starts_with(prefix)); + + if starts_with("unexpected positional argument") + || starts_with("missing value for") + || starts_with("duplicate argument") + || starts_with("missing required argument") + || starts_with("unknown argument") + || contains("requires --") + { + "invalid_arguments" + } else if contains("failed to inspect source snapshot") || contains("failed to read source snapshot") { + "snapshot_unavailable" + } else if contains("source snapshot must be a non-empty regular file") { + "snapshot_invalid" + } else if contains("source snapshot authentication failed") { + "snapshot_authentication_failed" + } else if contains("unsupported artifact profile") || contains("unsupported artifact bundle profile") { + "unsupported_profile" + } else if contains("package identity does not match") || contains("artifact bundle identity does not match") { + "artifact_identity_mismatch" + } else if contains("_hash mismatch") { + "identity_hash_mismatch" + } else if contains("CellScript package compilation failed") { + "cellscript_compilation_failed" + } else if contains("independent checker rejected") || messages.iter().any(|message| message.starts_with('V')) { + "artifact_checker_rejected" + } else if contains("artifact bundle") { + "artifact_bundle_invalid" + } else if contains("artifact profile contract") { + "profile_contract_invalid" + } else if contains("failed to read materialized Cell.toml") || contains("canonical package manifest") { + "manifest_invalid" + } else { + "verifier_internal_error" + } +} + +fn run() -> Result { + let args = parse_args()?; + verify(args) +} + +fn verify(args: Args) -> Result { + let metadata = + fs::metadata(&args.snapshot).with_context(|| format!("failed to inspect source snapshot '{}'", args.snapshot.display()))?; + if !metadata.is_file() || metadata.len() == 0 || metadata.len() > MAX_SNAPSHOT_BYTES { + bail!("source snapshot must be a non-empty regular file no larger than {MAX_SNAPSHOT_BYTES} bytes"); + } + let snapshot = + fs::read(&args.snapshot).with_context(|| format!("failed to read source snapshot '{}'", args.snapshot.display()))?; + + match args.profile.as_str() { + "cellscript_source" => verify_cellscript_source(args, &snapshot), + "ckb_executable" | "reproducible_build" | "copy_material" => verify_artifact_bundle(args, &snapshot), + profile => bail!("unsupported artifact profile '{profile}'"), + } +} + +fn verify_cellscript_source(args: Args, snapshot: &[u8]) -> Result { + let compatibility_profile_expected = + args.compatibility_profile_hash.as_deref().context("cellscript_source requires --compatibility-profile-hash")?; + + let work = unique_work_dir()?; + let _cleanup = Cleanup(work.clone()); + cellscript::package::registry::materialize_generated_source_snapshot_bytes( + snapshot, + &work, + &args.namespace, + &args.name, + &args.version, + &args.source_hash, + ) + .context("source snapshot authentication failed")?; + + let package_manager = cellscript::package::PackageManager::new(&work); + let manifest = package_manager.read_manifest().context("failed to read materialized Cell.toml")?; + if manifest.package.namespace.as_deref() != Some(args.namespace.as_str()) + || manifest.package.name != args.name + || manifest.package.version != args.version + { + bail!("materialized package identity does not match the verification job"); + } + let manifest_hash = cellscript::package::registry::compute_package_manifest_hash(&manifest) + .context("failed to compute canonical package manifest hash")?; + require_matching_hash("manifest_hash", &manifest_hash, &args.manifest_hash)?; + + let compile_root = Utf8PathBuf::from_path_buf(work.clone()) + .map_err(|path| anyhow::anyhow!("verification work path is not valid UTF-8: {}", path.display()))?; + let result = cellscript::compile_path(&compile_root, cellscript::CompileOptions::default()) + .context("CellScript package compilation failed")?; + let compatibility_profile_bytes = + serde_json::to_vec(&result.metadata.compatibility_profile).context("failed to serialize compatibility profile")?; + let compatibility_profile_hash = hex::encode(cellscript::ckb_blake2b256(&compatibility_profile_bytes)); + require_matching_hash("compatibility_profile_hash", &compatibility_profile_hash, compatibility_profile_expected)?; + + let artifact_hash = result.metadata.artifact_hash.clone().unwrap_or_else(|| hex::encode(result.artifact_hash)); + let metadata_bytes = serde_json::to_vec(&result.metadata).context("failed to serialize compile metadata")?; + let metadata_hash = hex::encode(cellscript::ckb_blake2b256(&metadata_bytes)); + + Ok(VerificationOutput { + status: "passed", + verification_level: "compiled", + artifact_hash: Some(artifact_hash), + metadata_hash, + compiler_version: Some(result.metadata.compiler_version), + source_hash: args.source_hash, + manifest_hash: args.manifest_hash, + compatibility_profile_hash: args.compatibility_profile_hash, + artifact_format: result.artifact_format.display_name().to_string(), + checker_version: None, + checker_policy_schema: None, + checker_report_hash: None, + }) +} + +fn verify_artifact_bundle(args: Args, snapshot: &[u8]) -> Result { + let bundle: ArtifactBundle = serde_json::from_slice(snapshot).context("artifact bundle must be valid JSON")?; + if bundle.schema != "cellscript-registry-bundle" { + bail!("artifact bundle schema must be 'cellscript-registry-bundle'"); + } + if bundle.namespace != args.namespace + || bundle.name != args.name + || bundle.release != args.version + || bundle.profile != args.profile + { + bail!("artifact bundle identity does not match the verification job"); + } + let manifest: serde_json::Value = + serde_json::from_str(&bundle.manifest_json).context("artifact bundle manifest_json must be valid JSON")?; + if !manifest.is_object() { + bail!("artifact bundle manifest_json must encode a JSON object"); + } + validate_bundle_roles(&bundle, &args.profile, &manifest)?; + let canonical_manifest = cellscript::package::registry::canonical_artifact_contract_json(&manifest) + .map_err(anyhow::Error::msg) + .context("artifact profile contract canonicalization failed")?; + let manifest_hash = hex::encode(cellscript::ckb_blake2b256(canonical_manifest.as_bytes())); + require_matching_hash("manifest_hash", &manifest_hash, &args.manifest_hash)?; + let source = bundle_object(&bundle, "source")?; + let source_hash = hex::encode(cellscript::ckb_blake2b256(&source)); + require_matching_hash("source_hash", &source_hash, &args.source_hash)?; + let audit_report_hash = if manifest.pointer("/security/audit_report_hash").is_some() { + Some(hex::encode(cellscript::ckb_blake2b256(&bundle_object(&bundle, "audit_report")?))) + } else { + None + }; + + let mut checker_version = None; + let mut checker_policy_schema = None; + let mut checker_report_hash = None; + let mut verified_compatibility_profile_hash = None; + let (artifact_hash, abi_hash, build_recipe_hash, artifact_format, verification_level) = match args.profile.as_str() { + "ckb_executable" => { + let executable = bundle_object(&bundle, "executable")?; + let actual_artifact_hash = hex::encode(cellscript::ckb_blake2b256(&executable)); + require_matching_hash( + "artifact_hash", + &actual_artifact_hash, + args.artifact_hash.as_deref().context("ckb_executable requires --artifact-hash")?, + )?; + let abi = bundle_object(&bundle, "abi")?; + let actual_abi_hash = hex::encode(cellscript::ckb_blake2b256(&abi)); + require_matching_hash( + "abi_hash", + &actual_abi_hash, + args.abi_hash.as_deref().context("ckb_executable requires --abi-hash")?, + )?; + let actual_recipe_hash = if manifest.pointer("/build/reproducible").and_then(serde_json::Value::as_bool) == Some(true) { + let recipe = bundle_object(&bundle, "build_recipe")?; + let hash = hex::encode(cellscript::ckb_blake2b256(&recipe)); + require_matching_hash( + "build_recipe_hash", + &hash, + args.build_recipe_hash.as_deref().context("reproducible ckb_executable requires --build-recipe-hash")?, + )?; + Some(hash) + } else { + None + }; + let metadata = bundle_object(&bundle, "metadata")?; + let lowering_record = bundle_object(&bundle, "lowering_record")?; + let source_map = bundle_object(&bundle, "source_map")?; + let budgets = cellscript_artifact_checker::CheckerBudgets::default(); + let checker_report = + cellscript_artifact_checker::check_bundle(&executable, &metadata, &lowering_record, &source_map, &budgets) + .map_err(anyhow::Error::msg) + .context("artifact bundle independent checker rejected the CKB executable")?; + let report_bytes = cellscript_artifact_checker::canonical_bytes(&checker_report) + .map_err(anyhow::Error::msg) + .context("failed to canonicalize artifact checker report")?; + let record = cellscript_artifact_checker::parse_lowering_record(&lowering_record, &budgets) + .map_err(anyhow::Error::msg) + .context("failed to read checker-approved lowering record")?; + checker_version = Some(checker_report.checker_version); + checker_policy_schema = Some(checker_report.checker_policy_schema); + checker_report_hash = Some(hex::encode(cellscript_artifact_checker::ckb_blake2b256(&report_bytes))); + verified_compatibility_profile_hash = Some(record.compatibility_profile_hash); + (Some(actual_artifact_hash), Some(actual_abi_hash), actual_recipe_hash, "ckb-vm-executable", "structurally_verified") + } + "reproducible_build" => { + let executable = bundle_object(&bundle, "executable")?; + let actual_artifact_hash = hex::encode(cellscript::ckb_blake2b256(&executable)); + require_matching_hash( + "artifact_hash", + &actual_artifact_hash, + args.artifact_hash.as_deref().context("reproducible_build requires --artifact-hash")?, + )?; + let recipe = bundle_object(&bundle, "build_recipe")?; + let actual_recipe_hash = hex::encode(cellscript::ckb_blake2b256(&recipe)); + require_matching_hash( + "build_recipe_hash", + &actual_recipe_hash, + args.build_recipe_hash.as_deref().context("reproducible_build requires --build-recipe-hash")?, + )?; + (Some(actual_artifact_hash), None, Some(actual_recipe_hash), "reproducible-binary", "evidence_required") + } + "copy_material" => (None, None, None, "copy-material", "hash_bound"), + _ => unreachable!("profile was checked before bundle verification"), + }; + let (abi_sha256, executable_ls_idl_bound) = if manifest.get("interface").is_some() { + use sha2::Digest as _; + let abi = bundle_object(&bundle, "abi")?; + cellscript::package::registry::validate_ls_idl_document(&abi) + .map_err(anyhow::Error::msg) + .context("LS-IDL schema validation failed")?; + let digest = sha2::Sha256::digest(&abi); + let executable = bundle_object(&bundle, "executable")?; + let digest: [u8; 32] = digest.into(); + (Some(hex::encode(digest)), Some(executable.ends_with(&digest))) + } else { + (None, None) + }; + cellscript::package::registry::validate_artifact_profile_contract( + &args.artifact_kind, + &args.profile, + &manifest, + cellscript::package::registry::ArtifactContractHashes { + artifact_hash: artifact_hash.as_deref(), + abi_hash: abi_hash.as_deref(), + abi_sha256: abi_sha256.as_deref(), + executable_ls_idl_bound, + build_recipe_hash: build_recipe_hash.as_deref(), + audit_report_hash: audit_report_hash.as_deref(), + }, + ) + .map_err(anyhow::Error::msg) + .context("artifact profile contract validation failed")?; + let metadata_hash = hex::encode(cellscript::ckb_blake2b256(snapshot)); + Ok(VerificationOutput { + status: "passed", + verification_level, + artifact_hash, + metadata_hash, + compiler_version: None, + source_hash: args.source_hash, + manifest_hash: args.manifest_hash, + compatibility_profile_hash: verified_compatibility_profile_hash, + artifact_format: artifact_format.to_string(), + checker_version, + checker_policy_schema, + checker_report_hash, + }) +} + +fn bundle_object(bundle: &ArtifactBundle, role: &str) -> Result> { + let mut matching = bundle.objects.iter().filter(|object| object.role == role); + let object = matching.next().with_context(|| format!("artifact bundle is missing required '{role}' object"))?; + if matching.next().is_some() { + bail!("artifact bundle contains more than one '{role}' object"); + } + let bytes = base64::engine::general_purpose::STANDARD + .decode(&object.content_base64) + .with_context(|| format!("artifact bundle '{role}' object is not valid base64"))?; + if bytes.is_empty() { + bail!("artifact bundle '{role}' object must not be empty"); + } + Ok(bytes) +} + +fn validate_bundle_roles(bundle: &ArtifactBundle, profile: &str, contract: &serde_json::Value) -> Result<()> { + let mut required = match profile { + "ckb_executable" => vec!["source", "executable", "abi", "metadata", "lowering_record", "source_map"], + "reproducible_build" => vec!["source", "executable", "build_recipe"], + "copy_material" => vec!["source"], + other => bail!("unsupported artifact bundle profile '{other}'"), + }; + if contract.pointer("/security/audit_report_hash").is_some() { + required.push("audit_report"); + } + if profile == "ckb_executable" && contract.pointer("/build/reproducible").and_then(serde_json::Value::as_bool) == Some(true) { + required.push("build_recipe"); + } + let mut seen = std::collections::BTreeSet::new(); + for object in &bundle.objects { + if !required.contains(&object.role.as_str()) { + bail!("artifact bundle role '{}' is not allowed for profile '{profile}'", object.role); + } + if !seen.insert(object.role.as_str()) { + bail!("artifact bundle contains more than one '{}' object", object.role); + } + } + for role in required { + if !seen.contains(role) { + bail!("artifact bundle is missing required '{role}' object"); + } + } + Ok(()) +} + +fn parse_args() -> Result { + let mut values = BTreeMap::new(); + let mut arguments = env::args().skip(1); + while let Some(flag) = arguments.next() { + if !flag.starts_with("--") { + bail!("unexpected positional argument '{flag}'"); + } + let value = arguments.next().with_context(|| format!("missing value for '{flag}'"))?; + if values.insert(flag.clone(), value).is_some() { + bail!("duplicate argument '{flag}'"); + } + } + let mut take = |name: &str| values.remove(name).with_context(|| format!("missing required argument '{name}'")); + let args = Args { + snapshot: PathBuf::from(take("--snapshot")?), + namespace: take("--namespace")?, + name: take("--name")?, + version: take("--version")?, + source_hash: take("--source-hash")?, + manifest_hash: take("--manifest-hash")?, + artifact_kind: take("--artifact-kind")?, + profile: take("--profile")?, + compatibility_profile_hash: values.remove("--compatibility-profile-hash"), + artifact_hash: values.remove("--artifact-hash"), + abi_hash: values.remove("--abi-hash"), + build_recipe_hash: values.remove("--build-recipe-hash"), + }; + if let Some((unknown, _)) = values.into_iter().next() { + bail!("unknown argument '{unknown}'"); + } + Ok(args) +} + +fn require_matching_hash(field: &str, actual: &str, expected: &str) -> Result<()> { + let normalize = |value: &str| value.strip_prefix("0x").unwrap_or(value).to_ascii_lowercase(); + let actual = normalize(actual); + let expected = normalize(expected); + if actual.len() != 64 || expected.len() != 64 || actual != expected { + bail!("{field} mismatch: compiled/materialized value does not match the signed Registry identity"); + } + Ok(()) +} + +fn unique_work_dir() -> Result { + let root = env::temp_dir(); + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).context("system clock is before the Unix epoch")?.as_nanos(); + for attempt in 0..100_u32 { + let candidate = root.join(format!("cellscript-registry-verify-{}-{timestamp}-{attempt}", std::process::id())); + if !candidate.exists() { + return Ok(candidate); + } + } + bail!("failed to allocate a unique verifier work directory") +} + +struct Cleanup(PathBuf); + +impl Drop for Cleanup { + fn drop(&mut self) { + if self.0.starts_with(env::temp_dir()) + && self.0.file_name().is_some_and(|name| name.to_string_lossy().starts_with("cellscript-registry-verify-")) + { + let _ = fs::remove_dir_all(&self.0); + } + } +} + +#[cfg(test)] +mod tests { + use base64::Engine as _; + use serde_json::json; + + use super::*; + + struct VerifiedCkbFixture { + source: Vec, + executable: Vec, + abi: Vec, + metadata: Vec, + lowering_record: Vec, + source_map: Vec, + } + + impl VerifiedCkbFixture { + fn new() -> Self { + let source = br#"module registry_fixture + +action main() { + verification +} +"# + .to_vec(); + let result = cellscript::compile( + std::str::from_utf8(&source).unwrap(), + cellscript::CompileOptions { target: Some("riscv64-elf".to_string()), ..Default::default() }, + ) + .unwrap(); + Self { + source, + executable: result.artifact_bytes, + abi: br#"{"actions":["main"]}"#.to_vec(), + metadata: serde_json::to_vec(&result.metadata).unwrap(), + lowering_record: cellscript_artifact_checker::canonical_bytes(result.verified_lowering_record.as_ref().unwrap()) + .unwrap(), + source_map: cellscript_artifact_checker::canonical_bytes(result.source_artifact_map.as_ref().unwrap()).unwrap(), + } + } + + fn objects(&self) -> Vec<(&str, &[u8])> { + vec![ + ("source", &self.source), + ("executable", &self.executable), + ("abi", &self.abi), + ("metadata", &self.metadata), + ("lowering_record", &self.lowering_record), + ("source_map", &self.source_map), + ] + } + } + + #[test] + fn exposes_stable_machine_codes_for_verification_boundaries() { + let cases = [ + (anyhow::anyhow!("missing required argument '--snapshot'"), "invalid_arguments"), + (anyhow::anyhow!("artifact_hash mismatch: signed identity differs"), "identity_hash_mismatch"), + (anyhow::anyhow!("artifact bundle must be valid JSON"), "artifact_bundle_invalid"), + (anyhow::anyhow!("CellScript package compilation failed"), "cellscript_compilation_failed"), + (anyhow::anyhow!("unsupported artifact profile 'unknown'"), "unsupported_profile"), + ]; + for (error, expected) in cases { + assert_eq!(verifier_error_code(&error), expected, "unexpected code for {error:#}"); + } + + let authenticated = anyhow::anyhow!("invalid file hash").context("source snapshot authentication failed"); + assert_eq!(verifier_error_code(&authenticated), "snapshot_authentication_failed"); + } + + #[test] + fn verifies_generated_snapshot_with_the_real_compiler() { + let source_root = tempfile::tempdir().unwrap(); + fs::create_dir_all(source_root.path().join("src")).unwrap(); + fs::write( + source_root.path().join("Cell.toml"), + r#"[package] +edition = "2026" +name = "demo" +version = "1.2.3" +namespace = "cellscript" +entry = "src/main.cell" +"#, + ) + .unwrap(); + fs::write( + source_root.path().join("src/main.cell"), + r#"module demo::main + +action identity(value: u64) -> u64 { + verification + value +} +"#, + ) + .unwrap(); + + let source_hash = cellscript::package::registry::compute_source_hash(source_root.path()).unwrap(); + let manager = cellscript::package::PackageManager::new(source_root.path()); + let manifest = manager.read_manifest().unwrap(); + let manifest_hash = cellscript::package::registry::compute_package_manifest_hash(&manifest).unwrap(); + let compile_root = Utf8PathBuf::from_path_buf(source_root.path().to_path_buf()).unwrap(); + let result = cellscript::compile_path(&compile_root, cellscript::CompileOptions::default()).unwrap(); + let compatibility_profile_hash = + hex::encode(cellscript::ckb_blake2b256(&serde_json::to_vec(&result.metadata.compatibility_profile).unwrap())); + + let mut files = Vec::new(); + for relative in ["Cell.toml", "src/main.cell"] { + let content = fs::read(source_root.path().join(relative)).unwrap(); + files.push(json!({ + "path": relative, + "blake2b256": hex::encode(cellscript::ckb_blake2b256(&content)), + "content_base64": base64::engine::general_purpose::STANDARD.encode(content), + })); + } + let snapshot = json!({ + "schema": "cellscript-source-snapshot-v1", + "generated_by": cellscript::VERSION, + "package": { "namespace": "cellscript", "name": "demo", "version": "1.2.3" }, + "files": files, + }); + let snapshot_path = source_root.path().join("snapshot.json"); + fs::write(&snapshot_path, serde_json::to_vec(&snapshot).unwrap()).unwrap(); + + let output = verify(Args { + snapshot: snapshot_path, + namespace: "cellscript".to_string(), + name: "demo".to_string(), + version: "1.2.3".to_string(), + source_hash: source_hash.clone(), + manifest_hash: manifest_hash.clone(), + artifact_kind: "source_library".to_string(), + profile: "cellscript_source".to_string(), + compatibility_profile_hash: Some(compatibility_profile_hash.clone()), + artifact_hash: None, + abi_hash: None, + build_recipe_hash: None, + }) + .unwrap(); + assert_eq!(output.status, "passed"); + assert_eq!(output.source_hash, source_hash); + assert_eq!(output.manifest_hash, manifest_hash); + assert_eq!(output.compatibility_profile_hash.as_deref(), Some(compatibility_profile_hash.as_str())); + assert_eq!(output.artifact_hash.as_deref().unwrap().len(), 64); + assert_eq!(output.metadata_hash.len(), 64); + } + + #[test] + fn hash_binds_ckb_executable_and_abi_bundle_objects() { + let fixture = VerifiedCkbFixture::new(); + let output = verify_bundle( + "ckb_executable", + &fixture.objects(), + Some(hex::encode(cellscript::ckb_blake2b256(&fixture.executable))), + Some(hex::encode(cellscript::ckb_blake2b256(&fixture.abi))), + None, + ) + .unwrap(); + assert_eq!(output.status, "passed"); + assert_eq!(output.verification_level, "structurally_verified"); + assert_eq!(output.artifact_format, "ckb-vm-executable"); + assert_eq!(output.checker_version.as_deref(), Some(cellscript_artifact_checker::CHECKER_VERSION)); + assert_eq!(output.checker_policy_schema.as_deref(), Some(cellscript_artifact_checker::CHECKER_POLICY_SCHEMA)); + assert_eq!(output.checker_report_hash.as_deref().unwrap().len(), 64); + } + + #[test] + fn deployed_ckb_executable_can_bind_a_reproducible_recipe() { + let fixture = VerifiedCkbFixture::new(); + let recipe = b"pinned build recipe"; + let mut objects = fixture.objects(); + objects.push(("build_recipe", recipe)); + let output = verify_bundle( + "ckb_executable", + &objects, + Some(hex::encode(cellscript::ckb_blake2b256(&fixture.executable))), + Some(hex::encode(cellscript::ckb_blake2b256(&fixture.abi))), + Some(hex::encode(cellscript::ckb_blake2b256(recipe))), + ) + .unwrap(); + assert_eq!(output.verification_level, "structurally_verified"); + } + + #[test] + fn distinguishes_reproducible_build_evidence_from_copy_material() { + let executable = b"reproducible-output"; + let recipe = b"FROM rust:latest"; + let reproducible = verify_bundle( + "reproducible_build", + &[("source", b"source"), ("executable", executable), ("build_recipe", recipe)], + Some(hex::encode(cellscript::ckb_blake2b256(executable))), + None, + Some(hex::encode(cellscript::ckb_blake2b256(recipe))), + ) + .unwrap(); + assert_eq!(reproducible.verification_level, "evidence_required"); + assert_eq!(reproducible.artifact_format, "reproducible-binary"); + + let copy = verify_bundle("copy_material", &[("source", b"starter")], None, None, None).unwrap(); + assert_eq!(copy.verification_level, "hash_bound"); + assert_eq!(copy.artifact_format, "copy-material"); + assert!(copy.artifact_hash.is_none()); + } + + #[test] + fn rejects_executable_bundle_when_published_hash_does_not_match() { + let fixture = VerifiedCkbFixture::new(); + let error = verify_bundle( + "ckb_executable", + &fixture.objects(), + Some("11".repeat(32)), + Some(hex::encode(cellscript::ckb_blake2b256(&fixture.abi))), + None, + ) + .unwrap_err(); + assert!(error.to_string().contains("artifact_hash mismatch")); + } + + #[test] + fn audited_contract_requires_an_immutable_audit_report_object() { + let encode = |role: &str| ArtifactBundleObject { + role: role.to_string(), + content_base64: base64::engine::general_purpose::STANDARD.encode(role), + }; + let contract = json!({ + "security": { "status": "audited", "audit_report_hash": "11".repeat(32) } + }); + let mut bundle = ArtifactBundle { + schema: "cellscript-registry-bundle".to_string(), + namespace: "cellscript".to_string(), + name: "demo".to_string(), + release: "1.2.3".to_string(), + profile: "ckb_executable".to_string(), + manifest_json: contract.to_string(), + objects: vec![ + encode("source"), + encode("executable"), + encode("abi"), + encode("metadata"), + encode("lowering_record"), + encode("source_map"), + ], + }; + + let error = validate_bundle_roles(&bundle, "ckb_executable", &contract).unwrap_err(); + assert!(error.to_string().contains("audit_report")); + + bundle.objects.push(encode("audit_report")); + validate_bundle_roles(&bundle, "ckb_executable", &contract).unwrap(); + } + + fn verify_bundle( + profile: &str, + objects: &[(&str, &[u8])], + artifact_hash: Option, + abi_hash: Option, + build_recipe_hash: Option, + ) -> Result { + let root = tempfile::tempdir().unwrap(); + let kind = match profile { + "ckb_executable" => "deployable_contract", + "reproducible_build" => "reproducible_binary", + "copy_material" => "template", + _ => unreachable!("test helper only supports generic artifact profiles"), + }; + let profile_contract = match profile { + "ckb_executable" => { + let reproducible = build_recipe_hash.is_some(); + let mut value = json!({ + "schema": cellscript::package::registry::ARTIFACT_PROFILE_CONTRACT_SCHEMA, + "artifact_kind": kind, + "profile": profile, + "build": { + "target": "riscv64imac-unknown-none-elf", + "toolchain": "rustc 1.97.1", + "profile": "release", + "source_revision": "0123456789abcdef", + "reproducible": reproducible + }, + "security": { "status": "review_required" }, + "ckb": { + "vm_version": "2", + "script_role": "type", + "hash_type": "data1", + "dep_type": "code", + "abi_hash": abi_hash.clone().unwrap() + } + }); + if reproducible { + value["reproduction"] = json!({ + "environment": "docker.io/library/rust:1.97.1@sha256:0123456789abcdef", + "command": "cargo build --locked --release", + "recipe_hash": build_recipe_hash.clone().unwrap(), + "expected_artifact_hash": artifact_hash.clone().unwrap() + }); + } + value + } + "reproducible_build" => json!({ + "schema": cellscript::package::registry::ARTIFACT_PROFILE_CONTRACT_SCHEMA, + "artifact_kind": kind, + "profile": profile, + "build": { + "target": "x86_64-unknown-linux-gnu", + "toolchain": "rustc 1.97.1", + "profile": "release", + "source_revision": "0123456789abcdef", + "reproducible": true + }, + "security": { "status": "review_required" }, + "reproduction": { + "environment": "docker.io/library/rust:1.97.1@sha256:0123456789abcdef", + "command": "cargo build --locked --release", + "recipe_hash": build_recipe_hash.clone().unwrap(), + "expected_artifact_hash": artifact_hash.clone().unwrap() + } + }), + "copy_material" => json!({ + "schema": cellscript::package::registry::ARTIFACT_PROFILE_CONTRACT_SCHEMA, + "artifact_kind": kind, + "profile": profile, + "copy": { "format": "file_map_v1", "entrypoint": "template.cell" } + }), + _ => unreachable!(), + }; + let manifest_json = cellscript::package::registry::canonical_artifact_contract_json(&profile_contract).unwrap(); + let bundle = json!({ + "schema": "cellscript-registry-bundle", + "namespace": "cellscript", + "name": "demo", + "release": "1.2.3", + "profile": profile, + "manifest_json": manifest_json, + "objects": objects.iter().map(|(role, bytes)| json!({ + "role": role, + "content_base64": base64::engine::general_purpose::STANDARD.encode(bytes), + })).collect::>(), + }); + let path = root.path().join("bundle.json"); + fs::write(&path, serde_json::to_vec(&bundle).unwrap()).unwrap(); + let source = objects.iter().find(|(role, _)| *role == "source").unwrap().1; + verify(Args { + snapshot: path, + namespace: "cellscript".to_string(), + name: "demo".to_string(), + version: "1.2.3".to_string(), + source_hash: hex::encode(cellscript::ckb_blake2b256(source)), + manifest_hash: hex::encode(cellscript::ckb_blake2b256(manifest_json.as_bytes())), + artifact_kind: kind.to_string(), + profile: profile.to_string(), + compatibility_profile_hash: None, + artifact_hash, + abi_hash, + build_recipe_hash, + }) + } +} diff --git a/src/assumptions.rs b/src/assumptions.rs index 42fcd8a5..71424bd5 100644 --- a/src/assumptions.rs +++ b/src/assumptions.rs @@ -536,10 +536,10 @@ fn validate_payload_array_items( if !concrete_fields.iter().any(|field| item_object.get(*field).is_some_and(non_empty_evidence_payload)) { mismatches.push(format!("{label} item {position} must include one of {}", concrete_fields.join(", "))); } - if let Some(required) = required_string_field { - if item_object.get(required).and_then(Value::as_str).is_none_or(|value| value.is_empty()) { - mismatches.push(format!("{label} item {position} must include non-empty {required}")); - } + if let Some(required) = required_string_field + && item_object.get(required).and_then(Value::as_str).is_none_or(|value| value.is_empty()) + { + mismatches.push(format!("{label} item {position} must include non-empty {required}")); } match item_object.get("index").and_then(Value::as_u64) { Some(index) => { diff --git a/src/cli/artifact.rs b/src/cli/artifact.rs new file mode 100644 index 00000000..4d32e287 --- /dev/null +++ b/src/cli/artifact.rs @@ -0,0 +1,2213 @@ +use crate::error::{CompileError, Result}; +use base64::Engine as _; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use sha2::{Digest, Sha256}; +use std::collections::{BTreeMap, BTreeSet}; +use std::io::Read; +use std::path::{Component, Path, PathBuf}; + +const MAX_REGISTRY_RESPONSE_BYTES: usize = 2 * 1024 * 1024; +const MAX_BUNDLE_BYTES: usize = 5 * 1024 * 1024; +const DEFAULT_CKB_MAINNET_RPC_URL: &str = "https://mainnet.ckb.dev/rpc"; +const DEFAULT_CKB_TESTNET_RPC_URL: &str = "https://testnet.ckb.dev/rpc"; +const DEFAULT_TESTNET_REGISTRY_API_URL: &str = "https://api.testnet.registry.cellscript.dev"; + +#[derive(Debug)] +pub struct ArtifactArgs { + pub operation: ArtifactOperation, +} + +#[derive(Debug)] +pub enum ArtifactOperation { + LsIdlValidate { + idl: PathBuf, + executable: Option, + json: bool, + }, + LsIdlBind { + idl: PathBuf, + executable: PathBuf, + output: PathBuf, + force: bool, + json: bool, + }, + LsIdlFetch { + code_hash: String, + hash_type: Option, + data_hash: Option, + network: String, + output: PathBuf, + api_url: Option, + force: bool, + json: bool, + }, + LsIdlBundle { + idl: PathBuf, + executable: PathBuf, + source: PathBuf, + namespace: String, + name: String, + release: String, + language: String, + hash_type: String, + dep_type: String, + toolchain: String, + source_revision: String, + output: PathBuf, + artifact_manifest_output: PathBuf, + force: bool, + json: bool, + }, + Fetch { + coordinate: String, + output: PathBuf, + receipt: Option, + api_url: Option, + force: bool, + json: bool, + }, + Verify { + bundle: PathBuf, + receipt: PathBuf, + json: bool, + }, + Pin { + coordinate: String, + output: PathBuf, + api_url: Option, + accept_hash_bound: bool, + force: bool, + json: bool, + }, + Copy { + coordinate: String, + destination: PathBuf, + api_url: Option, + accept_hash_bound: bool, + json: bool, + }, + CellDep { + coordinate: String, + output: PathBuf, + api_url: Option, + rpc_url: Option, + accept_hash_bound: bool, + force: bool, + json: bool, + }, + RecordDeployment { + coordinate: String, + network: String, + code_hash: String, + hash_type: String, + dep_type: String, + tx_hash: String, + index: u32, + capability_key_id: String, + capability_signature: Option, + api_url: Option, + print_payload: bool, + json: bool, + }, + SetAvailability { + coordinate: String, + status: String, + reason: Option, + capability_key_id: String, + capability_signature: Option, + api_url: Option, + print_payload: bool, + json: bool, + }, + ReproductionReport { + coordinate: String, + artifact: PathBuf, + build_log: PathBuf, + builder_id: String, + trust_domain: String, + builder_key_id: String, + builder_public_key: String, + output: PathBuf, + api_url: Option, + force: bool, + json: bool, + }, + ReproductionEvidence { + coordinate: String, + reports: Vec, + output: PathBuf, + api_url: Option, + force: bool, + json: bool, + }, + Commitment { + coordinate: String, + output: PathBuf, + api_url: Option, + force: bool, + json: bool, + }, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ArtifactBundle { + schema: String, + namespace: String, + name: String, + release: String, + profile: String, + manifest_json: String, + objects: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ArtifactBundleObject { + role: String, + content_base64: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct FetchReceipt { + schema: String, + coordinate: String, + registry_origin: String, + artifact: Value, + release: Value, + bundle_sha256: String, + bundle_url: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct TemplateFileMap { + schema: String, + files: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct TemplateFile { + path: String, + content_base64: String, + blake2b256: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct ReproductionReport { + schema: String, + builder_id: String, + trust_domain: String, + builder_public_key: String, + environment: String, + source_hash: String, + build_recipe_hash: String, + artifact_hash: String, + build_log_hash: String, + generated_at: String, + signature: ReproductionSignature, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct ReproductionSignature { + algorithm: String, + signature: String, +} + +struct Coordinate { + namespace: String, + name: String, + release: String, +} + +struct FetchedArtifact { + coordinate: Coordinate, + registry_origin: String, + artifact: Value, + release: Value, + bundle_url: String, + bundle: Vec, +} + +struct VerifiedBundle { + profile_contract: Value, + source: Vec, + object_hashes: BTreeMap, +} + +pub fn execute(args: ArtifactArgs) -> Result<()> { + match args.operation { + ArtifactOperation::LsIdlValidate { idl, executable, json } => { + let idl_bytes = read_limited(&idl, crate::package::registry::MAX_LS_IDL_BYTES, "LS-IDL document")?; + crate::package::registry::validate_ls_idl_document(&idl_bytes).map_err(error)?; + let digest = hex::encode(Sha256::digest(&idl_bytes)); + let executable_bound = if let Some(path) = executable.as_ref() { + let executable_bytes = read_limited(path, MAX_BUNDLE_BYTES, "CKB executable")?; + let expected: [u8; 32] = Sha256::digest(&idl_bytes).into(); + if !executable_bytes.ends_with(&expected) { + return Err(error("CKB executable does not end with the exact SHA-256 digest of the LS-IDL bytes")); + } + true + } else { + false + }; + emit( + json, + json!({ + "status": "valid", + "format": "ls-idl", + "format_version": "0.1", + "idl": idl, + "sha256": digest, + "executable_suffix_bound": executable_bound, + }), + format!("Validated LS-IDL 0.1 (sha256:{digest})"), + ) + } + ArtifactOperation::LsIdlBind { idl, executable, output, force, json } => { + let idl_bytes = read_limited(&idl, crate::package::registry::MAX_LS_IDL_BYTES, "LS-IDL document")?; + crate::package::registry::validate_ls_idl_document(&idl_bytes).map_err(error)?; + let mut executable_bytes = read_limited(&executable, MAX_BUNDLE_BYTES - 32, "CKB executable")?; + let digest: [u8; 32] = Sha256::digest(&idl_bytes).into(); + if !executable_bytes.ends_with(&digest) { + executable_bytes.extend_from_slice(&digest); + } + write_bytes(&output, &executable_bytes, force)?; + let digest_hex = hex::encode(digest); + emit( + json, + json!({ + "status": "bound", + "format": "ls-idl", + "format_version": "0.1", + "idl_sha256": digest_hex, + "output": output, + "artifact_hash": format!("0x{}", hex::encode(crate::ckb_blake2b256(&executable_bytes))), + }), + format!("Bound LS-IDL sha256:{digest_hex} to {}", output.display()), + ) + } + ArtifactOperation::LsIdlFetch { code_hash, hash_type, data_hash, network, output, api_url, force, json } => { + fetch_ls_idl(&code_hash, hash_type.as_deref(), data_hash.as_deref(), &network, &output, api_url.as_deref(), force, json) + } + ArtifactOperation::LsIdlBundle { + idl, + executable, + source, + namespace, + name, + release, + language, + hash_type, + dep_type, + toolchain, + source_revision, + output, + artifact_manifest_output, + force, + json, + } => build_ls_idl_bundle( + &idl, + &executable, + &source, + &namespace, + &name, + &release, + &language, + &hash_type, + &dep_type, + &toolchain, + &source_revision, + &output, + &artifact_manifest_output, + force, + json, + ), + ArtifactOperation::Fetch { coordinate, output, receipt, api_url, force, json } => { + let fetched = fetch(&coordinate, api_url.as_deref())?; + let verified = verify_fetched(&fetched)?; + write_bytes(&output, &fetched.bundle, force)?; + let receipt_path = receipt.unwrap_or_else(|| PathBuf::from(format!("{}.receipt.json", output.display()))); + let receipt = receipt_for(&fetched); + write_json(&receipt_path, &receipt, force)?; + emit( + json, + json!({ + "status": "fetched_and_verified", + "coordinate": coordinate, + "profile": fetched.artifact["profile"], + "verification_status": fetched.release["verification_status"], + "bundle": output, + "receipt": receipt_path, + "objects": verified.object_hashes, + }), + format!("Fetched and verified {coordinate}\n Bundle: {}\n Receipt: {}", output.display(), receipt_path.display()), + ) + } + ArtifactOperation::Verify { bundle, receipt, json } => { + let receipt: FetchReceipt = read_json(&receipt, "artifact fetch receipt")?; + if receipt.schema != "cellscript-artifact-fetch-receipt-v1" { + return Err(error("artifact fetch receipt schema is not supported")); + } + let bytes = read_limited(&bundle, MAX_BUNDLE_BYTES, "artifact bundle")?; + require_sha256(&bytes, &receipt.bundle_sha256, "bundle_sha256")?; + let coordinate = parse_coordinate(&receipt.coordinate)?; + let fetched = FetchedArtifact { + coordinate, + registry_origin: receipt.registry_origin, + artifact: receipt.artifact, + release: receipt.release, + bundle_url: receipt.bundle_url, + bundle: bytes, + }; + let verified = verify_fetched(&fetched)?; + emit( + json, + json!({ + "status": "verified", + "coordinate": receipt.coordinate, + "profile": fetched.artifact["profile"], + "verification_status": fetched.release["verification_status"], + "objects": verified.object_hashes, + "profile_contract": verified.profile_contract, + }), + format!("Verified {} with immutable bundle and profile-contract hashes", receipt.coordinate), + ) + } + ArtifactOperation::Pin { coordinate, output, api_url, accept_hash_bound, force, json } => { + let fetched = fetch(&coordinate, api_url.as_deref())?; + let verified = verify_fetched(&fetched)?; + require_assurance(&fetched.release, accept_hash_bound)?; + let pin = json!({ + "schema": "cellscript-artifact-lock-v1", + "coordinate": coordinate, + "registry_origin": fetched.registry_origin, + "artifact": fetched.artifact, + "release": fetched.release, + "bundle_sha256": sha256_identity(&fetched.bundle), + "bundle_url": fetched.bundle_url, + "object_hashes": verified.object_hashes, + "profile_contract": verified.profile_contract, + }); + write_json(&output, &pin, force)?; + emit( + json, + json!({ "status": "pinned", "coordinate": coordinate, "lockfile": output }), + format!("Pinned {coordinate} to {}", output.display()), + ) + } + ArtifactOperation::Copy { coordinate, destination, api_url, accept_hash_bound, json } => { + let fetched = fetch(&coordinate, api_url.as_deref())?; + let verified = verify_fetched(&fetched)?; + require_assurance(&fetched.release, accept_hash_bound)?; + if fetched.artifact["kind"].as_str() != Some("template") { + return Err(error("artifact copy only accepts kind=template")); + } + materialize_template(&verified.source, &verified.profile_contract, &destination)?; + emit( + json, + json!({ "status": "copied", "coordinate": coordinate, "destination": destination }), + format!("Copied {coordinate} into {}", destination.display()), + ) + } + ArtifactOperation::CellDep { coordinate, output, api_url, rpc_url, accept_hash_bound, force, json } => { + let fetched = fetch(&coordinate, api_url.as_deref())?; + let verified = verify_fetched(&fetched)?; + require_assurance(&fetched.release, accept_hash_bound)?; + if fetched.artifact["profile"].as_str() != Some("ckb_executable") { + return Err(error("artifact cell-dep only accepts profile=ckb_executable")); + } + let deployed = chain_verified_deployment(&fetched.release)?; + let release_identity = signed_release(&fetched.release)?; + let evidence = object_field(deployed, "evidence", "deployed evidence")?; + require_deployment_contract(&verified.profile_contract, evidence)?; + let evidence_network = map_string_field(evidence, "network", "deployed evidence")?; + let default_rpc = match evidence_network { + "mainnet" => DEFAULT_CKB_MAINNET_RPC_URL, + "testnet" => DEFAULT_CKB_TESTNET_RPC_URL, + other => return Err(error(format!("deployed evidence uses unsupported CKB network '{other}'"))), + }; + let rpc_url = rpc_url + .or_else(|| std::env::var(super::commands::CELLSCRIPT_CKB_RPC_URL_ENV).ok()) + .unwrap_or_else(|| default_rpc.to_string()); + revalidate_deployment(evidence, &rpc_url, evidence_network)?; + let descriptor = json!({ + "schema": "cellscript-registry-cell-dep-v1", + "coordinate": coordinate, + "artifact_hash": release_identity["artifact_hash"], + "abi_hash": release_identity["abi_hash"], + "profile_contract": verified.profile_contract, + "cell_dep": { + "out_point": evidence["out_point"], + "dep_type": evidence["dep_type"], + }, + "script": { + "code_hash": evidence["code_hash"], + "hash_type": evidence["hash_type"], + }, + "chain_verification": "get_live_cell:fresh", + "network": evidence_network, + "liveness_checked_at": super::commands::current_utc_timestamp(), + "resolved_code_out_point": evidence.get("resolved_code_out_point").cloned().unwrap_or(Value::Null), + "deployed_evidence_hash": deployed["evidence_hash"], + }); + write_json(&output, &descriptor, force)?; + emit( + json, + json!({ "status": "cell_dep_generated", "coordinate": coordinate, "output": output }), + format!("Generated chain-verified CellDep descriptor at {}", output.display()), + ) + } + ArtifactOperation::RecordDeployment { + coordinate, + network, + code_hash, + hash_type, + dep_type, + tx_hash, + index, + capability_key_id, + capability_signature, + api_url, + print_payload, + json, + } => record_deployment( + &coordinate, + &network, + &code_hash, + &hash_type, + &dep_type, + &tx_hash, + index, + &capability_key_id, + capability_signature.as_deref(), + api_url, + print_payload, + json, + ), + ArtifactOperation::SetAvailability { + coordinate, + status, + reason, + capability_key_id, + capability_signature, + api_url, + print_payload, + json, + } => set_availability( + &coordinate, + &status, + reason.as_deref(), + &capability_key_id, + capability_signature.as_deref(), + api_url, + print_payload, + json, + ), + ArtifactOperation::ReproductionReport { + coordinate, + artifact, + build_log, + builder_id, + trust_domain, + builder_key_id, + builder_public_key, + output, + api_url, + force, + json, + } => { + let fetched = fetch(&coordinate, api_url.as_deref())?; + let verified = verify_fetched(&fetched)?; + let report = build_signed_reproduction_report( + &fetched, + &verified, + &artifact, + &build_log, + &builder_id, + &trust_domain, + &builder_key_id, + &builder_public_key, + )?; + write_json(&output, &report, force)?; + emit( + json, + json!({ + "status": "reproduction_report_signed", + "coordinate": coordinate, + "builder_id": builder_id, + "trust_domain": trust_domain, + "output": output, + }), + format!("Signed reproduction report at {}", output.display()), + ) + } + ArtifactOperation::ReproductionEvidence { coordinate, reports, output, api_url, force, json } => { + let fetched = fetch(&coordinate, api_url.as_deref())?; + let verified = verify_fetched(&fetched)?; + let reports = + reports.iter().map(|path| read_json(path, "reproduction report")).collect::>>()?; + let promotion = build_reproduction_promotion(&fetched, &verified, reports)?; + write_json(&output, &promotion, force)?; + emit( + json, + json!({ "status": "reproduction_evidence_generated", "coordinate": coordinate, "output": output }), + format!("Generated independently reproduced build evidence at {}", output.display()), + ) + } + ArtifactOperation::Commitment { coordinate, output, api_url, force, json } => { + let fetched = fetch(&coordinate, api_url.as_deref())?; + verify_fetched(&fetched)?; + let network = registry_release_network(&fetched.release)?; + let deployed = chain_verified_deployment(&fetched.release)?; + let release_identity = signed_release(&fetched.release)?; + let deployed_evidence_hash = string_field(deployed, "evidence_hash", "deployed evidence")?; + let payload = json!({ + "schema": "cellscript-registry-commitment-v1", + "namespace": fetched.coordinate.namespace, + "name": fetched.coordinate.name, + "release": fetched.coordinate.release, + "source_hash": fetched.release["source_hash"], + "manifest_hash": fetched.release["manifest_hash"], + "artifact_hash": release_identity.get("artifact_hash").cloned().unwrap_or(Value::Null), + "deployed_evidence_hash": deployed_evidence_hash, + }); + let canonical = canonical_json(&payload)?; + let commitment_hash = format!("0x{}", hex::encode(crate::ckb_blake2b256(canonical.as_bytes()))); + let cell_data = format!("0x{}{}", hex::encode("CSREGv1"), commitment_hash.trim_start_matches("0x")); + let proof = fetch_commitment_proof(&fetched)?; + let transaction_intent = validate_commitment_proof(&proof, &payload, &commitment_hash, &cell_data, network)?; + let commitment = json!({ + "schema": "cellscript-registry-commitment-builder-v2", + "payload": payload, + "commitment_hash": commitment_hash, + "cell_data": cell_data, + "network": network, + "registry_type_hash": proof["registry_type_hash"], + "commitment_lock_hash": proof["commitment_lock_hash"], + "transaction_intent": transaction_intent, + }); + write_json(&output, &commitment, force)?; + emit( + json, + json!({ "status": "commitment_generated", "coordinate": coordinate, "output": output, "commitment_hash": commitment_hash }), + format!("Generated {network} Registry commitment at {}", output.display()), + ) + } + } +} + +#[allow(clippy::too_many_arguments)] +fn fetch_ls_idl( + code_hash: &str, + hash_type: Option<&str>, + data_hash: Option<&str>, + network: &str, + output: &Path, + api_url: Option<&str>, + force: bool, + json_output: bool, +) -> Result<()> { + require_hash_shape(code_hash, "code hash")?; + if let Some(value) = data_hash { + require_hash_shape(value, "data hash")?; + } + if !matches!(network, "mainnet" | "testnet") { + return Err(error("LS-IDL network must be mainnet or testnet")); + } + if let Some(value) = hash_type + && !matches!(value, "data" | "data1" | "data2" | "type") + { + return Err(error("LS-IDL hash type must be data, data1, data2, or type")); + } + if hash_type == Some("type") && data_hash.is_none() { + return Err(error("Type-hash LS-IDL lookup requires --data-hash to select the current code Cell bytes")); + } + let registry_origin = super::commands::resolve_registry_api_base(api_url.map(str::to_string))?; + let code_hash = code_hash.trim_start_matches("0x").to_ascii_lowercase(); + let mut url = + reqwest::Url::parse(&format!("{}/v1/ckb/scripts/{code_hash}/interfaces/ls-idl", registry_origin.trim_end_matches('/'))) + .map_err(|err| error(format!("LS-IDL Registry URL is invalid: {err}")))?; + { + let mut query = url.query_pairs_mut(); + query.append_pair("network", network); + if let Some(value) = hash_type { + query.append_pair("hash_type", value); + } + if let Some(value) = data_hash { + query.append_pair("data_hash", value); + } + } + let mut response = super::commands::registry_http_client()? + .get(url.clone()) + .header(reqwest::header::ACCEPT, crate::package::registry::LS_IDL_CONTENT_TYPE) + .header(reqwest::header::USER_AGENT, format!("cellc/{}", env!("CARGO_PKG_VERSION"))) + .send() + .map_err(|err| error(format!("LS-IDL Registry request '{url}' failed: {err}")))?; + let status = response.status(); + if !status.is_success() { + let mut body = Vec::new(); + let _ = response.by_ref().take(64 * 1024).read_to_end(&mut body); + let body = String::from_utf8_lossy(&body); + return Err(error(format!("LS-IDL Registry request returned HTTP {status}: {}", body.trim()))); + } + if response.content_length().is_some_and(|length| length > crate::package::registry::MAX_LS_IDL_BYTES as u64) { + return Err(error("LS-IDL Registry response exceeds the 256 KiB profile limit")); + } + let content_type = response + .headers() + .get(reqwest::header::CONTENT_TYPE) + .and_then(|value| value.to_str().ok()) + .unwrap_or_default() + .split(';') + .next() + .unwrap_or_default() + .trim(); + if content_type != crate::package::registry::LS_IDL_CONTENT_TYPE { + return Err(error("LS-IDL Registry response has an unexpected content type")); + } + let declared_digest = response + .headers() + .get("x-ls-idl-sha256") + .and_then(|value| value.to_str().ok()) + .filter(|value| !value.is_empty()) + .map(str::to_string) + .ok_or_else(|| error("LS-IDL Registry response is missing the digest header"))?; + if response.headers().get("x-ls-idl-verification").and_then(|value| value.to_str().ok()) != Some("schema-and-suffix-bound") { + return Err(error("LS-IDL Registry response is missing the schema-and-suffix verification contract")); + } + let coordinate = response.headers().get("x-ls-idl-coordinate").and_then(|value| value.to_str().ok()).map(str::to_string); + let mut bytes = Vec::new(); + response + .take(crate::package::registry::MAX_LS_IDL_BYTES as u64 + 1) + .read_to_end(&mut bytes) + .map_err(|err| error(format!("failed to read LS-IDL response: {err}")))?; + if bytes.len() > crate::package::registry::MAX_LS_IDL_BYTES { + return Err(error("LS-IDL Registry response exceeds the 256 KiB profile limit")); + } + crate::package::registry::validate_ls_idl_document(&bytes).map_err(error)?; + let digest = hex::encode(Sha256::digest(&bytes)); + if !digest.eq_ignore_ascii_case(declared_digest.trim_start_matches("0x")) { + return Err(error("LS-IDL response bytes do not match the Registry digest header")); + } + write_bytes(output, &bytes, force)?; + emit( + json_output, + json!({ + "status": "fetched_and_verified", + "format": "ls-idl", + "format_version": "0.1", + "code_hash": format!("0x{code_hash}"), + "sha256": digest, + "coordinate": coordinate, + "output": output, + }), + format!("Fetched and verified LS-IDL sha256:{digest} at {}", output.display()), + ) +} + +#[derive(Serialize)] +struct LsIdlArtifactManifest<'a> { + schema: &'static str, + namespace: &'a str, + name: &'a str, + release: &'a str, + kind: &'static str, + language: &'a str, + bundle: String, + description: String, + keywords: Vec<&'static str>, + categories: Vec<&'static str>, +} + +#[allow(clippy::too_many_arguments)] +fn build_ls_idl_bundle( + idl_path: &Path, + executable_path: &Path, + source_path: &Path, + namespace: &str, + name: &str, + release: &str, + language: &str, + hash_type: &str, + dep_type: &str, + toolchain: &str, + source_revision: &str, + output: &Path, + artifact_manifest_output: &Path, + force: bool, + json_output: bool, +) -> Result<()> { + parse_coordinate(&format!("{namespace}/{name}@{release}"))?; + if !matches!(language, "cellscript" | "rust" | "c" | "javascript" | "other") { + return Err(error("LS-IDL artifact language must be cellscript, rust, c, javascript, or other")); + } + if !matches!(hash_type, "data" | "data1" | "data2" | "type") { + return Err(error("LS-IDL hash type must be data, data1, data2, or type")); + } + if !matches!(dep_type, "code" | "dep_group") { + return Err(error("LS-IDL dep type must be code or dep_group")); + } + if toolchain.trim().is_empty() || toolchain.len() > 1024 { + return Err(error("LS-IDL bundle requires a non-empty toolchain identity no longer than 1024 bytes")); + } + if !matches!(source_revision.len(), 40 | 64) || !source_revision.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err(error("LS-IDL source revision must be an immutable 40- or 64-hex identity")); + } + if !force && (output.exists() || artifact_manifest_output.exists()) { + return Err(error("refusing to overwrite LS-IDL bundle outputs; pass --force explicitly")); + } + let idl = read_limited(idl_path, crate::package::registry::MAX_LS_IDL_BYTES, "LS-IDL document")?; + crate::package::registry::validate_ls_idl_document(&idl).map_err(error)?; + let executable = read_limited(executable_path, MAX_BUNDLE_BYTES, "CKB executable")?; + let digest: [u8; 32] = Sha256::digest(&idl).into(); + if !executable.ends_with(&digest) { + return Err(error("CKB executable does not carry the exact LS-IDL digest suffix; run 'cellc artifact ls-idl bind' first")); + } + let source = read_limited(source_path, MAX_BUNDLE_BYTES, "lock-script source")?; + let abi_hash = hex::encode(crate::ckb_blake2b256(&idl)); + let artifact_hash = hex::encode(crate::ckb_blake2b256(&executable)); + let source_hash = hex::encode(crate::ckb_blake2b256(&source)); + let digest_hex = hex::encode(digest); + let contract = json!({ + "schema": crate::package::registry::ARTIFACT_PROFILE_CONTRACT_SCHEMA, + "artifact_kind": "deployable_contract", + "profile": "ckb_executable", + "build": { + "target": "riscv64imac-unknown-none-elf", + "toolchain": toolchain, + "profile": "release", + "source_revision": source_revision, + "reproducible": false, + }, + "security": { "status": "review_required" }, + "ckb": { + "vm_version": "2", + "script_role": "lock", + "hash_type": hash_type, + "dep_type": dep_type, + "abi_hash": abi_hash, + }, + "interface": { + "schema": crate::package::registry::LS_IDL_INTERFACE_SCHEMA, + "format": "ls-idl", + "format_version": crate::package::registry::LS_IDL_FORMAT_VERSION, + "object_role": "abi", + "content_type": crate::package::registry::LS_IDL_CONTENT_TYPE, + "encoding": "linear-le-v0", + "commitment": { + "algorithm": "sha256", + "placement": "code-cell-data-suffix-32", + "digest": digest_hex, + }, + }, + }); + let manifest_json = crate::package::registry::canonical_artifact_contract_json(&contract).map_err(error)?; + let bundle = json!({ + "schema": "cellscript-registry-bundle", + "namespace": namespace, + "name": name, + "release": release, + "profile": "ckb_executable", + "manifest_json": manifest_json, + "objects": [ + { "role": "source", "content_base64": base64::engine::general_purpose::STANDARD.encode(&source) }, + { "role": "executable", "content_base64": base64::engine::general_purpose::STANDARD.encode(&executable) }, + { "role": "abi", "content_base64": base64::engine::general_purpose::STANDARD.encode(&idl) }, + ], + }); + let bundle_reference = if output.parent() == artifact_manifest_output.parent() { + output.file_name().map(PathBuf::from).unwrap_or_else(|| output.to_path_buf()) + } else if output.is_absolute() { + output.to_path_buf() + } else { + std::env::current_dir().map_err(|err| error(format!("failed to resolve LS-IDL bundle path: {err}")))?.join(output) + }; + let manifest = LsIdlArtifactManifest { + schema: "cellscript-registry-artifact", + namespace, + name, + release, + kind: "deployable_contract", + language, + bundle: bundle_reference.to_string_lossy().into_owned(), + description: format!("LS-IDL 0.1 interface for {namespace}/{name}"), + keywords: vec!["ckb", "lock-script", "ls-idl"], + categories: vec!["interface", "deployment"], + }; + let manifest_toml = + toml::to_string_pretty(&manifest).map_err(|err| error(format!("failed to serialize LS-IDL Artifact.toml: {err}")))?; + write_json(output, &bundle, force)?; + write_bytes(artifact_manifest_output, manifest_toml.as_bytes(), force)?; + emit( + json_output, + json!({ + "status": "bundle_created", + "coordinate": format!("{namespace}/{name}@{release}"), + "bundle": output, + "artifact_manifest": artifact_manifest_output, + "source_hash": source_hash, + "artifact_hash": artifact_hash, + "abi_hash": abi_hash, + "idl_sha256": digest_hex, + }), + format!("Created LS-IDL Registry bundle {} and manifest {}", output.display(), artifact_manifest_output.display()), + ) +} + +#[allow(clippy::too_many_arguments)] +fn build_signed_reproduction_report( + fetched: &FetchedArtifact, + verified: &VerifiedBundle, + artifact_path: &Path, + build_log_path: &Path, + builder_id: &str, + trust_domain: &str, + builder_key_id: &str, + builder_public_key: &str, +) -> Result { + if builder_id.trim().is_empty() || builder_id.len() > 200 { + return Err(error("builder id must contain 1 to 200 characters")); + } + if trust_domain.trim().is_empty() || trust_domain.len() > 200 { + return Err(error("trust domain must contain 1 to 200 characters")); + } + if !builder_public_key.starts_with("p256-spki:") { + return Err(error("builder public key must use p256-spki")); + } + let expected_key_id = format!("cap_{}", &hex::encode(Sha256::digest(builder_public_key.as_bytes()))[..32]); + if builder_key_id != expected_key_id { + return Err(error("builder key id does not match builder public key")); + } + let environment = verified + .profile_contract + .pointer("/reproduction/environment") + .and_then(Value::as_str) + .ok_or_else(|| error("artifact has no signed reproduction.environment"))?; + let release_identity = signed_release(&fetched.release)?; + let expected_artifact_hash = map_string_field(release_identity, "artifact_hash", "signed release")?; + let build_recipe_hash = map_string_field(release_identity, "build_recipe_hash", "signed release")?; + let artifact = read_limited(artifact_path, MAX_BUNDLE_BYTES, "reproduced artifact")?; + let artifact_hash = format!("0x{}", hex::encode(crate::ckb_blake2b256(&artifact))); + require_ckb_hash(&artifact_hash, expected_artifact_hash, "reproduced artifact hash")?; + let build_log = read_limited(build_log_path, MAX_BUNDLE_BYTES, "reproduction build log")?; + let build_log_hash = format!("0x{}", hex::encode(Sha256::digest(&build_log))); + let generated_at = super::commands::current_utc_timestamp(); + let unsigned = json!({ + "schema": "cellscript-reproduction-report-v2", + "builder_id": builder_id, + "trust_domain": trust_domain, + "builder_public_key": builder_public_key, + "environment": environment, + "source_hash": string_field(&fetched.release, "source_hash", "Registry release")?, + "build_recipe_hash": build_recipe_hash, + "artifact_hash": artifact_hash, + "build_log_hash": build_log_hash, + "generated_at": generated_at, + }); + let canonical = canonical_json(&unsigned)?; + let signature = super::commands::sign_registry_reproducer_payload(builder_key_id, &canonical)?; + let report = serde_json::from_value(json!({ + "schema": "cellscript-reproduction-report-v2", + "builder_id": builder_id, + "trust_domain": trust_domain, + "builder_public_key": builder_public_key, + "environment": environment, + "source_hash": string_field(&fetched.release, "source_hash", "Registry release")?, + "build_recipe_hash": build_recipe_hash, + "artifact_hash": artifact_hash, + "build_log_hash": build_log_hash, + "generated_at": generated_at, + "signature": { + "algorithm": "p256-sha256", + "signature": signature, + }, + })) + .map_err(|err| error(format!("failed to construct reproduction report: {err}")))?; + verify_reproduction_report_signature(&report)?; + Ok(report) +} + +fn fetch_commitment_proof(fetched: &FetchedArtifact) -> Result { + let url = format!( + "{}/v1/artifacts/{}/{}/releases/{}/commitment", + fetched.registry_origin.trim_end_matches('/'), + fetched.coordinate.namespace, + fetched.coordinate.name, + fetched.coordinate.release, + ); + let response = super::commands::registry_http_client()? + .get(&url) + .header(reqwest::header::ACCEPT, "application/json") + .header(reqwest::header::USER_AGENT, format!("cellc/{}", env!("CARGO_PKG_VERSION"))) + .send() + .map_err(|err| error(format!("Registry commitment request '{url}' failed: {err}")))?; + if !response.status().is_success() { + return Err(error(format!("Registry commitment request '{url}' returned HTTP {}", response.status()))); + } + let bytes = response.bytes().map_err(|err| error(format!("failed to read Registry commitment response: {err}")))?; + if bytes.is_empty() || bytes.len() > MAX_REGISTRY_RESPONSE_BYTES { + return Err(error("Registry commitment response is empty or exceeds 2 MiB")); + } + serde_json::from_slice(&bytes).map_err(|err| error(format!("Registry commitment response is invalid JSON: {err}"))) +} + +fn validate_commitment_proof( + proof: &Value, + payload: &Value, + commitment_hash: &str, + cell_data: &str, + expected_network: &str, +) -> Result { + if proof.get("schema").and_then(Value::as_str) != Some("cellscript-registry-commitment-proof-v1") { + return Err(error("Registry commitment proof schema is not supported")); + } + require_ckb_hash( + string_field(proof, "commitment_hash", "Registry commitment proof")?, + commitment_hash, + "Registry commitment hash", + )?; + if string_field(proof, "cell_data", "Registry commitment proof")? != cell_data { + return Err(error("Registry commitment Cell data does not match the locally verified release")); + } + let remote_payload = proof.get("payload").ok_or_else(|| error("Registry commitment proof has no payload"))?; + if canonical_json(remote_payload)? != canonical_json(payload)? { + return Err(error("Registry commitment payload does not match the locally verified release")); + } + let registry_type_hash = string_field(proof, "registry_type_hash", "Registry commitment proof")?; + let commitment_lock_hash = string_field(proof, "commitment_lock_hash", "Registry commitment proof")?; + require_hash_shape(registry_type_hash, "registry_type_hash")?; + require_hash_shape(commitment_lock_hash, "commitment_lock_hash")?; + let intent = proof + .get("transaction_intent") + .filter(|value| value.is_object()) + .cloned() + .ok_or_else(|| error("Registry commitment transaction construction is not configured by the service operator"))?; + if string_field(&intent, "schema", "Registry commitment transaction intent")? + != "cellscript-registry-commitment-transaction-intent-v1" + || string_field(&intent, "network", "Registry commitment transaction intent")? != expected_network + { + return Err(error("Registry commitment transaction intent schema or network is invalid")); + } + let output = object_field(&intent, "output", "Registry commitment transaction intent")?; + let output_data = map_string_field(output, "data", "Registry commitment output")?; + if output_data != cell_data { + return Err(error("Registry commitment transaction output data does not match the locally verified commitment")); + } + let type_script = output + .get("type") + .filter(|value| value.is_object()) + .ok_or_else(|| error("Registry commitment transaction output has no Type Script"))?; + let lock_script = output + .get("lock") + .filter(|value| value.is_object()) + .ok_or_else(|| error("Registry commitment transaction output has no Lock Script"))?; + require_ckb_hash( + &super::commands::ckb_script_hash_from_json(type_script)?, + registry_type_hash, + "Registry commitment transaction Type Script hash", + )?; + require_ckb_hash( + &super::commands::ckb_script_hash_from_json(lock_script)?, + commitment_lock_hash, + "Registry commitment transaction Lock Script hash", + )?; + let required_cell_deps = intent + .get("required_cell_deps") + .and_then(Value::as_array) + .filter(|items| !items.is_empty() && items.iter().all(Value::is_object)) + .ok_or_else(|| error("Registry commitment transaction intent has no valid Type Script CellDep"))?; + if required_cell_deps.len() > 16 || !intent.get("custody_cell_dep").is_some_and(Value::is_object) { + return Err(error("Registry commitment transaction intent has invalid Script CellDeps")); + } + Ok(intent) +} + +fn build_reproduction_promotion( + fetched: &FetchedArtifact, + verified: &VerifiedBundle, + reports: Vec, +) -> Result { + if verified.profile_contract.pointer("/build/reproducible").and_then(Value::as_bool) != Some(true) { + return Err(error("artifact does not declare profile_contract.build.reproducible=true")); + } + let environment = verified + .profile_contract + .pointer("/reproduction/environment") + .and_then(Value::as_str) + .ok_or_else(|| error("reproducible artifact has no signed reproduction.environment"))?; + let release_identity = signed_release(&fetched.release)?; + let artifact_hash = map_string_field(release_identity, "artifact_hash", "signed release")?; + let build_recipe_hash = map_string_field(release_identity, "build_recipe_hash", "signed release")?; + let source_hash = string_field(&fetched.release, "source_hash", "Registry release")?; + let manifest_hash = string_field(&fetched.release, "manifest_hash", "Registry release")?; + let verified_build = fetched + .release + .get("evidence") + .and_then(Value::as_array) + .and_then(|items| items.iter().rev().find(|item| item.get("kind").and_then(Value::as_str) == Some("verified_build"))) + .ok_or_else(|| error("Registry release has no accepted verified_build evidence to reproduce"))?; + let verified_build_hash = string_field(verified_build, "evidence_hash", "verified_build evidence")?; + let verified_build_body = object_field(verified_build, "evidence", "verified_build evidence")?; + require_ckb_hash( + map_string_field(verified_build_body, "artifact_hash", "verified_build evidence")?, + artifact_hash, + "verified_build artifact_hash", + )?; + + if !(2..=16).contains(&reports.len()) { + return Err(error("reproduction evidence requires between 2 and 16 reports")); + } + let mut builders = BTreeSet::new(); + let mut builder_keys = BTreeSet::new(); + let mut trust_domains = BTreeSet::new(); + for report in &reports { + if report.schema != "cellscript-reproduction-report-v2" { + return Err(error("reproduction report schema must be cellscript-reproduction-report-v2")); + } + if report.builder_id.trim().is_empty() || report.builder_id.len() > 200 || !builders.insert(report.builder_id.clone()) { + return Err(error("reproduction reports require distinct non-empty builder_id values")); + } + if report.environment != environment { + return Err(error("reproduction report environment does not match the signed profile contract")); + } + if report.trust_domain.trim().is_empty() + || report.trust_domain.len() > 200 + || !trust_domains.insert(report.trust_domain.clone()) + { + return Err(error("reproduction reports require distinct non-empty trust_domain values")); + } + if !report.builder_public_key.starts_with("p256-spki:") || !builder_keys.insert(report.builder_public_key.clone()) { + return Err(error("reproduction reports require distinct p256-spki builder_public_key values")); + } + require_ckb_hash(&report.source_hash, source_hash, "reproduction report source_hash")?; + require_ckb_hash(&report.build_recipe_hash, build_recipe_hash, "reproduction report build_recipe_hash")?; + require_ckb_hash(&report.artifact_hash, artifact_hash, "reproduction report artifact_hash")?; + require_hash_shape(&report.build_log_hash, "reproduction report build_log_hash")?; + if report.generated_at.trim().is_empty() || report.generated_at.len() > 40 { + return Err(error("reproduction report generated_at must be a non-empty ISO timestamp")); + } + verify_reproduction_report_signature(report)?; + } + let mut evidence = json!({ + "schema": "cellscript-registry-evidence", + "kind": "reproduced_build", + "producer": format!("cellc/{version}", version = env!("CARGO_PKG_VERSION")), + "generated_at": super::commands::current_utc_timestamp(), + "verification_status": "passed", + "verification_level": "reproduced", + "source_hash": source_hash, + "manifest_hash": manifest_hash, + "artifact_hash": artifact_hash, + "build_recipe_hash": build_recipe_hash, + "verified_build_evidence_hash": verified_build_hash, + "minimum_reproducers": 2, + "reproducers": reports, + }); + if let Some(profile_hash) = fetched.release.get("compatibility_profile_hash").and_then(Value::as_str) { + evidence["compatibility_profile_hash"] = Value::String(profile_hash.to_string()); + } + Ok(json!({ "kind": "reproduced_build", "evidence": evidence })) +} + +fn verify_reproduction_report_signature(report: &ReproductionReport) -> Result<()> { + if report.signature.algorithm != "p256-sha256" { + return Err(error("reproduction report signature algorithm must be p256-sha256")); + } + let encoded_key = report + .builder_public_key + .strip_prefix("p256-spki:") + .ok_or_else(|| error("reproduction report builder_public_key must use p256-spki"))?; + let spki = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(encoded_key) + .map_err(|err| error(format!("reproduction report builder_public_key is invalid base64url: {err}")))?; + const P256_SPKI_PREFIX: &[u8] = &[ + 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, + 0x01, 0x07, 0x03, 0x42, 0x00, + ]; + let public_key = spki + .strip_prefix(P256_SPKI_PREFIX) + .filter(|key| key.len() == 65 && key.first() == Some(&0x04)) + .ok_or_else(|| error("reproduction report builder_public_key is not a canonical P-256 SPKI key"))?; + let signature = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(report.signature.signature.trim()) + .map_err(|err| error(format!("reproduction report signature is invalid base64url: {err}")))?; + let payload = json!({ + "schema": report.schema, + "builder_id": report.builder_id, + "trust_domain": report.trust_domain, + "builder_public_key": report.builder_public_key, + "environment": report.environment, + "source_hash": report.source_hash, + "build_recipe_hash": report.build_recipe_hash, + "artifact_hash": report.artifact_hash, + "build_log_hash": report.build_log_hash, + "generated_at": report.generated_at, + }); + let canonical = canonical_json(&payload)?; + ring::signature::UnparsedPublicKey::new(&ring::signature::ECDSA_P256_SHA256_FIXED, public_key) + .verify(canonical.as_bytes(), &signature) + .map_err(|_| error(format!("reproduction report signature for '{}' is invalid", report.builder_id))) +} + +#[allow(clippy::too_many_arguments)] +fn record_deployment( + coordinate: &str, + network: &str, + code_hash: &str, + hash_type: &str, + dep_type: &str, + tx_hash: &str, + index: u32, + capability_key_id: &str, + capability_signature: Option<&str>, + api_url: Option, + print_payload: bool, + json_output: bool, +) -> Result<()> { + if !matches!(network, "mainnet" | "testnet") { + return Err(error("--network must be mainnet or testnet")); + } + if !matches!(hash_type, "data" | "data1" | "data2" | "type") { + return Err(error("--hash-type must be data, data1, data2, or type")); + } + if !matches!(dep_type, "code" | "dep_group") { + return Err(error("--dep-type must be code or dep_group")); + } + require_hash_shape(code_hash, "code_hash")?; + require_hash_shape(tx_hash, "tx_hash")?; + let api_base = if network == "testnet" + && api_url.is_none() + && std::env::var("CELLSCRIPT_REGISTRY_API_URL").is_err() + && std::env::var("CELLSCRIPT_REGISTRY_ORIGIN").is_err() + { + super::commands::resolve_registry_api_base(Some(DEFAULT_TESTNET_REGISTRY_API_URL.to_string()))? + } else { + super::commands::resolve_registry_api_base(api_url)? + }; + let registry_origin = super::commands::registry_origin_from_api_base(&api_base)?; + let fetched = fetch(coordinate, Some(&api_base))?; + let verified = verify_fetched(&fetched)?; + if fetched.artifact["profile"].as_str() != Some("ckb_executable") { + return Err(error("deployment evidence is valid only for profile=ckb_executable")); + } + require_deployment_contract_values(&verified.profile_contract, hash_type, dep_type)?; + let release = signed_release(&fetched.release)?; + let artifact_hash = map_string_field(release, "artifact_hash", "signed release")?; + if hash_type != "type" { + require_ckb_hash(code_hash, artifact_hash, "code_hash")?; + } + let issued_at = super::commands::current_utc_timestamp(); + let expires_at = super::commands::utc_timestamp_after_seconds(10 * 60); + let nonce_material = + format!("cellscript-registry-deployment\n{registry_origin}\n{coordinate}\n{artifact_hash}\n{tx_hash}\n{index}\n{issued_at}"); + let payload = json!({ + "protocol": "cellscript-registry-deployment", + "action": "record_deployment", + "registry_origin": registry_origin, + "namespace": fetched.coordinate.namespace, + "name": fetched.coordinate.name, + "release": fetched.coordinate.release, + "network": network, + "artifact_hash": artifact_hash, + "data_hash": artifact_hash, + "code_hash": code_hash, + "hash_type": hash_type, + "dep_type": dep_type, + "out_point": { "tx_hash": tx_hash, "index": index }, + "capability_key_id": capability_key_id, + "nonce": format!("0x{}", hex::encode(crate::ckb_blake2b256(nonce_material.as_bytes()))), + "issued_at": issued_at, + "expires_at": expires_at, + "cli_version": crate::VERSION, + }); + let canonical = canonical_json(&payload)?; + let endpoint = format!( + "{}/v1/artifacts/{}/{}/releases/{}/deployments", + api_base, fetched.coordinate.namespace, fetched.coordinate.name, fetched.coordinate.release + ); + if print_payload { + return emit( + json_output, + json!({ "endpoint": endpoint, "payload": payload, "canonical_payload": canonical }), + format!("{canonical}\n\nEndpoint: {endpoint}"), + ); + } + let signature = match capability_signature { + Some(value) => value.to_string(), + None => super::commands::sign_registry_capability_payload(capability_key_id, &canonical)?, + }; + let response = super::commands::registry_http_client()? + .post(&endpoint) + .json(&json!({ + "payload": payload, + "capability_signature": { "algorithm": "p256-sha256", "signature": signature } + })) + .send() + .map_err(|err| error(format!("failed to submit deployment evidence to '{endpoint}': {err}")))?; + let status = response.status(); + let body = response.text().map_err(|err| error(format!("failed to read deployment response: {err}")))?; + if !status.is_success() { + return Err(error(format!("deployment evidence request failed with HTTP {status}: {}", body.trim()))); + } + let response_json = serde_json::from_str::(&body).unwrap_or_else(|_| json!({ "response": body })); + emit(json_output, response_json, format!("Recorded and chain-verified {network} deployment for {coordinate}")) +} + +#[allow(clippy::too_many_arguments)] +fn set_availability( + coordinate: &str, + status: &str, + reason: Option<&str>, + capability_key_id: &str, + capability_signature: Option<&str>, + api_url: Option, + print_payload: bool, + json_output: bool, +) -> Result<()> { + if !matches!(status, "active" | "deprecated" | "yanked") { + return Err(error("--status must be active, deprecated, or yanked")); + } + let reason = reason.map(str::trim).filter(|value| !value.is_empty()); + if status == "yanked" && reason.is_none() { + return Err(error("--reason is required when yanking a release")); + } + if reason.is_some_and(|value| value.len() > 500) { + return Err(error("--reason must be no longer than 500 characters")); + } + if capability_key_id.len() != 36 + || !capability_key_id.starts_with("cap_") + || !capability_key_id[4..].bytes().all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) + { + return Err(error("--capability-key-id must be a Registry capability key id")); + } + let coordinate = parse_coordinate(coordinate)?; + let api_base = super::commands::resolve_registry_api_base(api_url)?; + let registry_origin = super::commands::registry_origin_from_api_base(&api_base)?; + let issued_at = super::commands::current_utc_timestamp(); + let expires_at = super::commands::utc_timestamp_after_seconds(10 * 60); + let nonce_material = format!( + "cellscript-registry-availability-v1\n{registry_origin}\n{}/{}/{}\n{status}\n{}\n{issued_at}", + coordinate.namespace, + coordinate.name, + coordinate.release, + reason.unwrap_or("") + ); + let mut payload = json!({ + "protocol": "cellscript-registry-availability-v1", + "action": "set_availability", + "registry_origin": registry_origin, + "namespace": coordinate.namespace, + "name": coordinate.name, + "release": coordinate.release, + "availability_status": status, + "capability_key_id": capability_key_id, + "nonce": format!("0x{}", hex::encode(crate::ckb_blake2b256(nonce_material.as_bytes()))), + "issued_at": issued_at, + "expires_at": expires_at, + "cli_version": crate::VERSION, + }); + if let Some(reason) = reason { + payload["reason"] = Value::String(reason.to_string()); + } + let canonical = canonical_json(&payload)?; + let endpoint = + format!("{}/v1/artifacts/{}/{}/releases/{}/availability", api_base, coordinate.namespace, coordinate.name, coordinate.release); + if print_payload { + return emit( + json_output, + json!({ "endpoint": endpoint, "payload": payload, "canonical_payload": canonical }), + format!("{canonical}\n\nEndpoint: {endpoint}"), + ); + } + let signature = match capability_signature { + Some(value) => value.to_string(), + None => super::commands::sign_registry_capability_payload(capability_key_id, &canonical)?, + }; + let response = super::commands::registry_http_client()? + .post(&endpoint) + .json(&json!({ + "payload": payload, + "capability_signature": { "algorithm": "p256-sha256", "signature": signature } + })) + .send() + .map_err(|err| error(format!("failed to submit availability update to '{endpoint}': {err}")))?; + let http_status = response.status(); + let body = response.text().map_err(|err| error(format!("failed to read availability response: {err}")))?; + if !http_status.is_success() { + return Err(error(format!("availability update failed with HTTP {http_status}: {}", body.trim()))); + } + let response_json = serde_json::from_str::(&body).unwrap_or_else(|_| json!({ "response": body })); + emit( + json_output, + response_json, + format!("Set {}/{}@{} availability to {status}", coordinate.namespace, coordinate.name, coordinate.release), + ) +} + +fn fetch(raw_coordinate: &str, api_url: Option<&str>) -> Result { + let coordinate = parse_coordinate(raw_coordinate)?; + let registry_origin = super::commands::resolve_registry_api_base(api_url.map(str::to_string))?; + let detail_url = format!("{}/v1/artifacts/{}/{}", registry_origin.trim_end_matches('/'), coordinate.namespace, coordinate.name); + let client = super::commands::registry_http_client()?; + let response = client + .get(&detail_url) + .header(reqwest::header::ACCEPT, "application/json") + .header(reqwest::header::USER_AGENT, format!("cellc/{}", env!("CARGO_PKG_VERSION"))) + .send() + .map_err(|err| error(format!("Registry request '{detail_url}' failed: {err}")))?; + if !response.status().is_success() { + return Err(error(format!("Registry request '{detail_url}' returned HTTP {}", response.status()))); + } + let detail_bytes = response.bytes().map_err(|err| error(format!("failed to read Registry response: {err}")))?; + if detail_bytes.is_empty() || detail_bytes.len() > MAX_REGISTRY_RESPONSE_BYTES { + return Err(error("Registry detail response is empty or exceeds 2 MiB")); + } + let detail: Value = + serde_json::from_slice(&detail_bytes).map_err(|err| error(format!("Registry detail response is invalid JSON: {err}")))?; + let releases = + detail.get("releases").and_then(Value::as_array).ok_or_else(|| error("Registry detail response has no releases array"))?; + let release = releases + .iter() + .find(|item| item.get("release").and_then(Value::as_str) == Some(coordinate.release.as_str())) + .cloned() + .ok_or_else(|| { + error(format!("Registry has no release '{}' for {}/{}", coordinate.release, coordinate.namespace, coordinate.name)) + })?; + if release.get("availability_status").and_then(Value::as_str) != Some("active") { + return Err(error("artifact release is not active")); + } + if matches!(release.get("verification_status").and_then(Value::as_str), None | Some("pending") | Some("rejected")) { + return Err(error("artifact release has no accepted verification evidence")); + } + let artifact = detail.get("artifact").cloned().ok_or_else(|| error("Registry detail response has no artifact descriptor"))?; + let immutable = object_field(&release, "immutable_bundle", "Registry release")?; + let bundle_url = map_string_field(immutable, "url", "immutable_bundle")?.to_string(); + validate_download_url(&bundle_url)?; + let response = client + .get(&bundle_url) + .header(reqwest::header::ACCEPT, "application/octet-stream, application/json") + .header(reqwest::header::USER_AGENT, format!("cellc/{}", env!("CARGO_PKG_VERSION"))) + .send() + .map_err(|err| error(format!("immutable bundle request '{bundle_url}' failed: {err}")))?; + if !response.status().is_success() { + return Err(error(format!("immutable bundle request returned HTTP {}", response.status()))); + } + let bundle = response.bytes().map_err(|err| error(format!("failed to read immutable bundle: {err}")))?.to_vec(); + if bundle.is_empty() || bundle.len() > MAX_BUNDLE_BYTES { + return Err(error("immutable artifact bundle is empty or exceeds 5 MiB")); + } + let expected_snapshot = map_string_field(immutable, "snapshot_hash", "immutable_bundle")?; + require_sha256(&bundle, expected_snapshot, "snapshot_hash")?; + Ok(FetchedArtifact { coordinate, registry_origin, artifact, release, bundle_url, bundle }) +} + +fn validate_download_url(value: &str) -> Result<()> { + let url = reqwest::Url::parse(value).map_err(|err| error(format!("immutable bundle URL is invalid: {err}")))?; + let host = url.host_str().ok_or_else(|| error("immutable bundle URL has no host"))?; + let host = host.trim_start_matches('[').trim_end_matches(']'); + let loopback = + host.eq_ignore_ascii_case("localhost") || host.parse::().is_ok_and(|address| address.is_loopback()); + if url.scheme() != "https" && !(url.scheme() == "http" && loopback) { + return Err(error("immutable bundle URL must use HTTPS; plaintext HTTP is allowed only for loopback development servers")); + } + if !url.username().is_empty() || url.password().is_some() || url.fragment().is_some() { + return Err(error("immutable bundle URL must not contain credentials or a fragment")); + } + Ok(()) +} + +fn verify_fetched(fetched: &FetchedArtifact) -> Result { + let bundle: ArtifactBundle = + serde_json::from_slice(&fetched.bundle).map_err(|err| error(format!("immutable artifact bundle is invalid: {err}")))?; + if bundle.schema != "cellscript-registry-bundle" + || bundle.namespace != fetched.coordinate.namespace + || bundle.name != fetched.coordinate.name + || bundle.release != fetched.coordinate.release + { + return Err(error("immutable artifact bundle identity does not match the Registry release")); + } + let kind = string_field(&fetched.artifact, "kind", "artifact descriptor")?; + let profile = string_field(&fetched.artifact, "profile", "artifact descriptor")?; + if bundle.profile != profile { + return Err(error("immutable artifact bundle profile does not match the Registry descriptor")); + } + let contract: Value = serde_json::from_str(&bundle.manifest_json) + .map_err(|err| error(format!("artifact profile contract is invalid JSON: {err}")))?; + let canonical_contract = crate::package::registry::canonical_artifact_contract_json(&contract).map_err(error)?; + let release_identity = signed_release(&fetched.release)?; + require_ckb_hash( + &hex::encode(crate::ckb_blake2b256(canonical_contract.as_bytes())), + string_field(&fetched.release, "manifest_hash", "Registry release")?, + "manifest_hash", + )?; + let mut required = match profile { + "ckb_executable" => vec!["source", "executable", "abi"], + "reproducible_build" => vec!["source", "executable", "build_recipe"], + "copy_material" => vec!["source"], + _ => return Err(error(format!("artifact profile '{profile}' is not a generic immutable-bundle profile"))), + }; + if contract.pointer("/security/audit_report_hash").is_some() { + required.push("audit_report"); + } + if profile == "ckb_executable" && contract.pointer("/build/reproducible").and_then(Value::as_bool) == Some(true) { + required.push("build_recipe"); + } + let mut objects = BTreeMap::new(); + for object in bundle.objects { + if !required.contains(&object.role.as_str()) || objects.contains_key(&object.role) { + return Err(error(format!("artifact bundle has duplicate or unsupported role '{}'", object.role))); + } + let bytes = base64::engine::general_purpose::STANDARD + .decode(&object.content_base64) + .map_err(|err| error(format!("artifact bundle role '{}' is not valid base64: {err}", object.role)))?; + if bytes.is_empty() { + return Err(error(format!("artifact bundle role '{}' is empty", object.role))); + } + objects.insert(object.role, bytes); + } + if required.iter().any(|role| !objects.contains_key(*role)) { + return Err(error("artifact bundle does not contain the exact required role set")); + } + let source = objects.remove("source").expect("required source role"); + require_ckb_hash( + &hex::encode(crate::ckb_blake2b256(&source)), + string_field(&fetched.release, "source_hash", "Registry release")?, + "source_hash", + )?; + let artifact_hash = objects.get("executable").map(|bytes| hex::encode(crate::ckb_blake2b256(bytes))); + let abi_hash = objects.get("abi").map(|bytes| hex::encode(crate::ckb_blake2b256(bytes))); + let (abi_sha256, executable_ls_idl_bound) = if contract.get("interface").is_some() { + let abi = objects.get("abi").ok_or_else(|| error("LS-IDL profile requires an abi object"))?; + crate::package::registry::validate_ls_idl_document(abi).map_err(error)?; + let digest = Sha256::digest(abi); + let executable = objects.get("executable").ok_or_else(|| error("LS-IDL profile requires an executable object"))?; + let digest: [u8; 32] = digest.into(); + (Some(hex::encode(digest)), Some(executable.ends_with(&digest))) + } else { + (None, None) + }; + let build_recipe_hash = objects.get("build_recipe").map(|bytes| hex::encode(crate::ckb_blake2b256(bytes))); + let audit_report_hash = objects.get("audit_report").map(|bytes| hex::encode(crate::ckb_blake2b256(bytes))); + if let Some(actual) = artifact_hash.as_deref() { + require_ckb_hash(actual, map_string_field(release_identity, "artifact_hash", "signed release")?, "artifact_hash")?; + } + if let Some(actual) = abi_hash.as_deref() { + require_ckb_hash(actual, map_string_field(release_identity, "abi_hash", "signed release")?, "abi_hash")?; + } + if let Some(actual) = build_recipe_hash.as_deref() { + require_ckb_hash(actual, map_string_field(release_identity, "build_recipe_hash", "signed release")?, "build_recipe_hash")?; + } + crate::package::registry::validate_artifact_profile_contract( + kind, + profile, + &contract, + crate::package::registry::ArtifactContractHashes { + artifact_hash: artifact_hash.as_deref(), + abi_hash: abi_hash.as_deref(), + abi_sha256: abi_sha256.as_deref(), + executable_ls_idl_bound, + build_recipe_hash: build_recipe_hash.as_deref(), + audit_report_hash: audit_report_hash.as_deref(), + }, + ) + .map_err(error)?; + let mut object_hashes = BTreeMap::new(); + object_hashes.insert("source".to_string(), hex::encode(crate::ckb_blake2b256(&source))); + if let Some(value) = artifact_hash { + object_hashes.insert("executable".to_string(), value); + } + if let Some(value) = abi_hash { + object_hashes.insert("abi".to_string(), value); + } + if let Some(value) = build_recipe_hash { + object_hashes.insert("build_recipe".to_string(), value); + } + if let Some(value) = audit_report_hash { + object_hashes.insert("audit_report".to_string(), value); + } + Ok(VerifiedBundle { profile_contract: contract, source, object_hashes }) +} + +fn materialize_template(source: &[u8], contract: &Value, destination: &Path) -> Result<()> { + let format = contract.pointer("/copy/format").and_then(Value::as_str).unwrap_or(""); + if format != "file_map_v1" { + return Err(error("template copy requires profile_contract.copy.format=file_map_v1")); + } + let entrypoint = + contract.pointer("/copy/entrypoint").and_then(Value::as_str).ok_or_else(|| error("template entrypoint is missing"))?; + let file_map: TemplateFileMap = + serde_json::from_slice(source).map_err(|err| error(format!("template file map is invalid: {err}")))?; + if file_map.schema != "cellscript-template-file-map-v1" || file_map.files.is_empty() || file_map.files.len() > 10_000 { + return Err(error("template source must be a non-empty cellscript-template-file-map-v1 with at most 10000 files")); + } + let mut paths = BTreeSet::new(); + let mut materialized = Vec::new(); + for file in file_map.files { + let relative = safe_relative_path(&file.path)?; + if !paths.insert(relative.clone()) { + return Err(error(format!("template contains duplicate path '{}'", file.path))); + } + let bytes = base64::engine::general_purpose::STANDARD + .decode(&file.content_base64) + .map_err(|err| error(format!("template file '{}' is not valid base64: {err}", file.path)))?; + require_ckb_hash(&hex::encode(crate::ckb_blake2b256(&bytes)), &file.blake2b256, "template file hash")?; + let target = destination.join(&relative); + if target.exists() { + return Err(error(format!("template copy refuses to overwrite '{}'", target.display()))); + } + materialized.push((target, bytes)); + } + let entrypoint_path = safe_relative_path(entrypoint)?; + if !paths.contains(&entrypoint_path) { + return Err(error("template copy.entrypoint is not present in the authenticated file map")); + } + for (target, _) in &materialized { + if let Some(parent) = target.parent() { + std::fs::create_dir_all(parent) + .map_err(|err| error(format!("failed to create template directory '{}': {err}", parent.display())))?; + } + } + for (target, bytes) in materialized { + let mut options = std::fs::OpenOptions::new(); + options.write(true).create_new(true); + let mut file = + options.open(&target).map_err(|err| error(format!("failed to create template file '{}': {err}", target.display())))?; + std::io::Write::write_all(&mut file, &bytes) + .map_err(|err| error(format!("failed to write template file '{}': {err}", target.display())))?; + } + Ok(()) +} + +fn safe_relative_path(value: &str) -> Result { + if value.is_empty() || value.contains('\\') || value.contains('\0') { + return Err(error("template paths must be non-empty portable forward-slash paths")); + } + let path = Path::new(value); + if path.is_absolute() || path.components().any(|component| !matches!(component, Component::Normal(_))) { + return Err(error(format!("template path '{value}' is not a safe relative path"))); + } + Ok(path.to_path_buf()) +} + +fn signed_release(release: &Value) -> Result<&serde_json::Map> { + let entry = object_field(release, "registry_entry", "Registry release")?; + let versions = entry.get("versions").and_then(Value::as_array).ok_or_else(|| error("signed registry_entry has no versions"))?; + let release_name = string_field(release, "release", "Registry release")?; + versions + .iter() + .find(|item| item.get("version").and_then(Value::as_str) == Some(release_name)) + .and_then(Value::as_object) + .ok_or_else(|| error("signed registry_entry does not contain the selected release")) +} + +fn require_deployment_contract(contract: &Value, evidence: &serde_json::Map) -> Result<()> { + require_deployment_contract_values( + contract, + map_string_field(evidence, "hash_type", "deployed evidence")?, + map_string_field(evidence, "dep_type", "deployed evidence")?, + ) +} + +fn require_deployment_contract_values(contract: &Value, hash_type: &str, dep_type: &str) -> Result<()> { + let contract_hash_type = contract + .pointer("/ckb/hash_type") + .and_then(Value::as_str) + .ok_or_else(|| error("signed profile contract has no ckb.hash_type"))?; + let contract_dep_type = contract + .pointer("/ckb/dep_type") + .and_then(Value::as_str) + .ok_or_else(|| error("signed profile contract has no ckb.dep_type"))?; + if hash_type != contract_hash_type { + return Err(error(format!( + "deployment hash_type '{hash_type}' does not match signed profile contract '{contract_hash_type}'" + ))); + } + if dep_type != contract_dep_type { + return Err(error(format!("deployment dep_type '{dep_type}' does not match signed profile contract '{contract_dep_type}'"))); + } + Ok(()) +} + +fn revalidate_deployment(evidence: &serde_json::Map, rpc_url: &str, expected_network: &str) -> Result<()> { + let chain = ckb_rpc_call(rpc_url, "get_blockchain_info", json!([]))?; + let chain_id = chain + .get("chain") + .or_else(|| chain.get("chain_id")) + .and_then(Value::as_str) + .ok_or_else(|| error("CKB RPC get_blockchain_info returned no chain identity"))?; + let normalized = chain_id.trim().to_ascii_lowercase().replace('_', "-"); + let matches_network = match expected_network { + "mainnet" => matches!(normalized.as_str(), "ckb" | "ckb-mainnet"), + "testnet" => matches!(normalized.as_str(), "ckb-testnet" | "pudge" | "pudge-testnet"), + _ => false, + }; + if !matches_network { + return Err(error(format!("artifact CellDep expects {expected_network}; RPC reports chain '{chain_id}'"))); + } + + let declared_out_point = + evidence.get("out_point").and_then(Value::as_object).ok_or_else(|| error("deployed evidence.out_point must be an object"))?; + let declared = get_live_cell(rpc_url, declared_out_point)?; + match map_string_field(evidence, "dep_type", "deployed evidence")? { + "code" => verify_live_code_cell(&declared, evidence), + "dep_group" => { + let content = declared + .pointer("/cell/data/content") + .and_then(Value::as_str) + .ok_or_else(|| error("live DepGroup Cell has no output data"))?; + let members = parse_dep_group_out_points(content)?; + if let Some(expected_size) = evidence.get("dep_group_size").and_then(Value::as_u64) + && expected_size != members.len() as u64 + { + return Err(error("live DepGroup member count no longer matches Registry deployment evidence")); + } + let resolved = evidence + .get("resolved_code_out_point") + .and_then(Value::as_object) + .ok_or_else(|| error("DepGroup deployment evidence has no resolved_code_out_point"))?; + let resolved_tx_hash = map_string_field(resolved, "tx_hash", "resolved_code_out_point")?; + let resolved_index = resolved + .get("index") + .and_then(Value::as_u64) + .and_then(|value| u32::try_from(value).ok()) + .ok_or_else(|| error("resolved_code_out_point.index must be a u32"))?; + if !members.iter().any(|(tx_hash, index)| tx_hash.eq_ignore_ascii_case(resolved_tx_hash) && *index == resolved_index) { + return Err(error("resolved code Cell is not a member of the live DepGroup")); + } + let code = get_live_cell(rpc_url, resolved)?; + verify_live_code_cell(&code, evidence) + } + other => Err(error(format!("unsupported deployment dep_type '{other}'"))), + } +} + +fn get_live_cell(rpc_url: &str, out_point: &serde_json::Map) -> Result { + let tx_hash = map_string_field(out_point, "tx_hash", "out_point")?; + require_hash_shape(tx_hash, "out_point.tx_hash")?; + let index = out_point + .get("index") + .and_then(Value::as_u64) + .and_then(|value| u32::try_from(value).ok()) + .ok_or_else(|| error("out_point.index must be a u32"))?; + let live = ckb_rpc_call(rpc_url, "get_live_cell", json!([{ "tx_hash": tx_hash, "index": format!("0x{index:x}") }, true, false]))?; + if live.get("status").and_then(Value::as_str) != Some("live") { + return Err(error(format!("deployment Cell {tx_hash}:0x{index:x} is no longer live"))); + } + if !live.get("cell").is_some_and(Value::is_object) { + return Err(error("CKB RPC live Cell response has no cell object")); + } + Ok(live) +} + +fn verify_live_code_cell(live: &Value, evidence: &serde_json::Map) -> Result<()> { + let data_hash = live + .pointer("/cell/data/hash") + .or_else(|| live.pointer("/cell/data_hash")) + .and_then(Value::as_str) + .ok_or_else(|| error("CKB RPC live Cell response has no data hash"))?; + require_ckb_hash(data_hash, map_string_field(evidence, "data_hash", "deployed evidence")?, "live Cell data_hash")?; + let expected_code_hash = map_string_field(evidence, "code_hash", "deployed evidence")?; + if map_string_field(evidence, "hash_type", "deployed evidence")? == "type" { + let type_script = live + .pointer("/cell/output/type") + .filter(|value| !value.is_null()) + .ok_or_else(|| error("type-hash deployment Cell has no Type Script"))?; + let actual = super::commands::ckb_script_hash_from_json(type_script)?; + require_ckb_hash(&actual, expected_code_hash, "live Cell type script hash")?; + } else { + require_ckb_hash(data_hash, expected_code_hash, "live Cell code_hash")?; + } + Ok(()) +} + +fn parse_dep_group_out_points(content: &str) -> Result> { + let bytes = hex::decode(content.strip_prefix("0x").unwrap_or(content)) + .map_err(|err| error(format!("DepGroup Cell data is not hexadecimal: {err}")))?; + if bytes.len() < 4 { + return Err(error("DepGroup Cell data is shorter than an OutPointVec header")); + } + let count = u32::from_le_bytes(bytes[..4].try_into().expect("four-byte slice")) as usize; + if count == 0 || count > 2048 || bytes.len() != 4 + count * 36 { + return Err(error("DepGroup Cell data is not a canonical non-empty Molecule OutPointVec")); + } + Ok((0..count) + .map(|item| { + let offset = 4 + item * 36; + let tx_hash = format!("0x{}", hex::encode(&bytes[offset..offset + 32])); + let index = u32::from_le_bytes(bytes[offset + 32..offset + 36].try_into().expect("four-byte slice")); + (tx_hash, index) + }) + .collect()) +} + +fn ckb_rpc_call(rpc_url: &str, method: &str, params: Value) -> Result { + validate_rpc_url(rpc_url)?; + let client = super::commands::registry_http_client()?; + let mut response = client + .post(rpc_url) + .header(reqwest::header::ACCEPT, "application/json") + .header(reqwest::header::USER_AGENT, format!("cellc/{}", env!("CARGO_PKG_VERSION"))) + .json(&json!({ "jsonrpc": "2.0", "id": 1, "method": method, "params": params })) + .send() + .map_err(|err| error(format!("CKB RPC request '{method}' failed: {err}")))?; + let status = response.status(); + if !status.is_success() { + return Err(error(format!("CKB RPC request '{method}' returned HTTP {status}"))); + } + let mut bytes = Vec::new(); + Read::by_ref(&mut response) + .take((MAX_REGISTRY_RESPONSE_BYTES + 1) as u64) + .read_to_end(&mut bytes) + .map_err(|err| error(format!("failed to read CKB RPC response '{method}': {err}")))?; + if bytes.is_empty() || bytes.len() > MAX_REGISTRY_RESPONSE_BYTES { + return Err(error("CKB RPC response is empty or exceeds 2 MiB")); + } + let rpc: Value = + serde_json::from_slice(&bytes).map_err(|err| error(format!("CKB RPC response '{method}' is invalid JSON: {err}")))?; + if let Some(rpc_error) = rpc.get("error") { + return Err(error(format!("CKB RPC request '{method}' failed: {rpc_error}"))); + } + rpc.get("result").cloned().ok_or_else(|| error(format!("CKB RPC response '{method}' has no result"))) +} + +fn validate_rpc_url(value: &str) -> Result<()> { + let url = reqwest::Url::parse(value).map_err(|err| error(format!("CKB RPC URL is invalid: {err}")))?; + let host = url.host_str().ok_or_else(|| error("CKB RPC URL has no host"))?; + let host = host.trim_start_matches('[').trim_end_matches(']'); + let loopback = + host.eq_ignore_ascii_case("localhost") || host.parse::().is_ok_and(|address| address.is_loopback()); + if url.scheme() != "https" && !(url.scheme() == "http" && loopback) { + return Err(error("CKB RPC URL must use HTTPS; plaintext HTTP is allowed only for loopback development servers")); + } + if !url.username().is_empty() || url.password().is_some() || url.fragment().is_some() { + return Err(error("CKB RPC URL must not contain credentials or a fragment")); + } + Ok(()) +} + +fn chain_verified_deployment(release: &Value) -> Result<&Value> { + if release.get("deployment_status").and_then(Value::as_str) != Some("chain_verified") { + return Err(error("a chain-verified deployment is required")); + } + release + .get("evidence") + .and_then(Value::as_array) + .and_then(|items| { + items.iter().rev().find(|item| { + item.get("kind").and_then(Value::as_str) == Some("deployed") + && item.pointer("/evidence/chain_verification").and_then(Value::as_str).is_some() + }) + }) + .ok_or_else(|| error("Registry release claims chain_verified but contains no RPC-verified deployment evidence")) +} + +fn registry_release_network(release: &Value) -> Result<&str> { + match release.get("network").and_then(Value::as_str).unwrap_or("mainnet") { + network @ ("mainnet" | "testnet") => Ok(network), + other => Err(error(format!("Registry release uses unsupported CKB network '{other}'"))), + } +} + +fn require_assurance(release: &Value, accept_hash_bound: bool) -> Result<()> { + match release.get("verification_status").and_then(Value::as_str) { + Some("verified") => Ok(()), + Some("hash_bound" | "evidence_required") if accept_hash_bound => Ok(()), + Some("hash_bound") => Err(error("artifact has hash-integrity evidence only; pass --accept-hash-bound to make that trust decision explicit")), + Some("evidence_required") => Err(error("artifact still requires external/reproducible evidence; pass --accept-hash-bound to pin its current immutable bytes explicitly")), + _ => Err(error("artifact has no acceptable verification status")), + } +} + +fn receipt_for(fetched: &FetchedArtifact) -> FetchReceipt { + FetchReceipt { + schema: "cellscript-artifact-fetch-receipt-v1".to_string(), + coordinate: format!("{}/{}@{}", fetched.coordinate.namespace, fetched.coordinate.name, fetched.coordinate.release), + registry_origin: fetched.registry_origin.clone(), + artifact: fetched.artifact.clone(), + release: fetched.release.clone(), + bundle_sha256: sha256_identity(&fetched.bundle), + bundle_url: fetched.bundle_url.clone(), + } +} + +fn parse_coordinate(value: &str) -> Result { + let (package, release) = value.rsplit_once('@').ok_or_else(|| error("artifact coordinate must be namespace/name@release"))?; + let (namespace, name) = package.split_once('/').ok_or_else(|| error("artifact coordinate must be namespace/name@release"))?; + for (label, token) in [("namespace", namespace), ("name", name)] { + let bytes = token.as_bytes(); + let edge = |byte: u8| byte.is_ascii_lowercase() || byte.is_ascii_digit(); + if token.is_empty() + || token.len() > 64 + || !edge(bytes[0]) + || !edge(*bytes.last().expect("non-empty identifier")) + || !bytes.iter().all(|byte| edge(*byte) || matches!(*byte, b'-' | b'_')) + { + return Err(error(format!( + "artifact {label} must be 1-64 lowercase letters or numbers, with '-' or '_' only between characters" + ))); + } + } + if release.is_empty() || release.len() > 80 || !release.bytes().all(|byte| byte.is_ascii_alphanumeric() || b".-+_".contains(&byte)) + { + return Err(error("artifact release is not a valid registry version token")); + } + Ok(Coordinate { namespace: namespace.to_string(), name: name.to_string(), release: release.to_string() }) +} + +fn object_field<'a>(value: &'a Value, key: &str, label: &str) -> Result<&'a serde_json::Map> { + value.get(key).and_then(Value::as_object).ok_or_else(|| error(format!("{label}.{key} must be an object"))) +} + +fn string_field<'a>(value: &'a Value, key: &str, label: &str) -> Result<&'a str> { + value + .get(key) + .and_then(Value::as_str) + .filter(|item| !item.is_empty()) + .ok_or_else(|| error(format!("{label}.{key} must be a string"))) +} + +fn map_string_field<'a>(value: &'a serde_json::Map, key: &str, label: &str) -> Result<&'a str> { + value + .get(key) + .and_then(Value::as_str) + .filter(|item| !item.is_empty()) + .ok_or_else(|| error(format!("{label}.{key} must be a string"))) +} + +fn require_hash_shape(value: &str, label: &str) -> Result<()> { + let bare = value.strip_prefix("0x").unwrap_or(value); + if bare.len() != 64 || !bare.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err(error(format!("{label} must be a 32-byte hexadecimal hash"))); + } + Ok(()) +} + +fn require_ckb_hash(actual: &str, expected: &str, label: &str) -> Result<()> { + let normalize = |value: &str| value.trim_start_matches("0x").to_ascii_lowercase(); + let actual = normalize(actual); + let expected = normalize(expected); + if actual.len() != 64 || expected.len() != 64 || actual != expected { + return Err(error(format!("{label} does not match the signed Registry identity"))); + } + Ok(()) +} + +fn require_sha256(bytes: &[u8], expected: &str, label: &str) -> Result<()> { + let actual = sha256_identity(bytes); + if !actual.eq_ignore_ascii_case(expected) { + return Err(error(format!("{label} does not match the downloaded immutable bundle"))); + } + Ok(()) +} + +fn sha256_identity(bytes: &[u8]) -> String { + format!("sha256:{}", hex::encode(Sha256::digest(bytes))) +} + +fn canonical_json(value: &Value) -> Result { + serde_json::to_string(&crate::package::registry::canonical_json_value(value)) + .map_err(|err| error(format!("failed to serialize canonical JSON: {err}"))) +} + +fn read_limited(path: &Path, limit: usize, label: &str) -> Result> { + let metadata = std::fs::metadata(path).map_err(|err| error(format!("failed to inspect {label} '{}': {err}", path.display())))?; + if !metadata.is_file() || metadata.len() == 0 || metadata.len() > limit as u64 { + return Err(error(format!("{label} must be a non-empty regular file no larger than {limit} bytes"))); + } + std::fs::read(path).map_err(|err| error(format!("failed to read {label} '{}': {err}", path.display()))) +} + +fn read_json Deserialize<'de>>(path: &Path, label: &str) -> Result { + let bytes = read_limited(path, MAX_REGISTRY_RESPONSE_BYTES, label)?; + serde_json::from_slice(&bytes).map_err(|err| error(format!("failed to parse {label} '{}': {err}", path.display()))) +} + +fn write_json(path: &Path, value: &impl Serialize, force: bool) -> Result<()> { + let mut bytes = + serde_json::to_vec_pretty(value).map_err(|err| error(format!("failed to serialize '{}': {err}", path.display())))?; + bytes.push(b'\n'); + write_bytes(path, &bytes, force) +} + +fn write_bytes(path: &Path, bytes: &[u8], force: bool) -> Result<()> { + if path.exists() && !force { + return Err(error(format!("refusing to overwrite '{}'; pass --force explicitly", path.display()))); + } + if let Some(parent) = path.parent().filter(|parent| !parent.as_os_str().is_empty()) { + std::fs::create_dir_all(parent).map_err(|err| error(format!("failed to create '{}': {err}", parent.display())))?; + } + std::fs::write(path, bytes).map_err(|err| error(format!("failed to write '{}': {err}", path.display()))) +} + +fn emit(json_output: bool, machine: Value, human: String) -> Result<()> { + if json_output { + println!( + "{}", + serde_json::to_string_pretty(&machine).map_err(|err| error(format!("failed to serialize command output: {err}")))? + ); + } else { + println!("{human}"); + } + Ok(()) +} + +fn error(message: impl Into) -> CompileError { + CompileError::without_span(message.into()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn verifies_generic_bundle_against_signed_release_and_contract() { + let source = b"source"; + let executable = b"elf"; + let abi = b"abi"; + let source_hash = hex::encode(crate::ckb_blake2b256(source)); + let artifact_hash = hex::encode(crate::ckb_blake2b256(executable)); + let abi_hash = hex::encode(crate::ckb_blake2b256(abi)); + let contract = json!({ + "schema": crate::package::registry::ARTIFACT_PROFILE_CONTRACT_SCHEMA, + "artifact_kind": "deployable_contract", + "profile": "ckb_executable", + "build": { + "target": "riscv64imac-unknown-none-elf", + "toolchain": "rustc 1.97.1", + "profile": "release", + "source_revision": "0123456789abcdef", + "reproducible": false + }, + "security": { "status": "review_required" }, + "ckb": { + "vm_version": "2", + "script_role": "type", + "hash_type": "data1", + "dep_type": "code", + "abi_hash": abi_hash + } + }); + let manifest_json = crate::package::registry::canonical_artifact_contract_json(&contract).unwrap(); + let manifest_hash = hex::encode(crate::ckb_blake2b256(manifest_json.as_bytes())); + let bundle = serde_json::to_vec(&json!({ + "schema": "cellscript-registry-bundle", + "namespace": "demo", + "name": "contract", + "release": "1.0.0", + "profile": "ckb_executable", + "manifest_json": manifest_json, + "objects": [ + { "role": "source", "content_base64": base64::engine::general_purpose::STANDARD.encode(source) }, + { "role": "executable", "content_base64": base64::engine::general_purpose::STANDARD.encode(executable) }, + { "role": "abi", "content_base64": base64::engine::general_purpose::STANDARD.encode(abi) } + ] + })) + .unwrap(); + let fetched = FetchedArtifact { + coordinate: parse_coordinate("demo/contract@1.0.0").unwrap(), + registry_origin: "https://registry.example".to_string(), + artifact: json!({ "kind": "deployable_contract", "profile": "ckb_executable" }), + release: json!({ + "release": "1.0.0", + "source_hash": source_hash, + "manifest_hash": manifest_hash, + "verification_status": "hash_bound", + "deployment_status": "undeployed", + "availability_status": "active", + "registry_entry": { + "versions": [{ + "version": "1.0.0", + "artifact_hash": artifact_hash, + "abi_hash": abi_hash + }] + } + }), + bundle_url: "https://registry.example/bundle".to_string(), + bundle, + }; + let verified = verify_fetched(&fetched).unwrap(); + assert_eq!(verified.object_hashes.get("executable"), Some(&artifact_hash)); + } + + #[test] + fn reproduction_promotion_requires_independent_matching_reports() { + let source_hash = format!("0x{}", "11".repeat(32)); + let artifact_hash = format!("0x{}", "22".repeat(32)); + let recipe_hash = format!("0x{}", "33".repeat(32)); + let environment = "docker.io/library/rust:1.97.1@sha256:0123456789abcdef"; + let fetched = FetchedArtifact { + coordinate: parse_coordinate("demo/contract@1.0.0").unwrap(), + registry_origin: "https://registry.example".to_string(), + artifact: json!({ "kind": "deployable_contract", "profile": "ckb_executable" }), + release: json!({ + "release": "1.0.0", + "source_hash": source_hash, + "manifest_hash": format!("0x{}", "44".repeat(32)), + "verification_status": "evidence_required", + "registry_entry": { + "versions": [{ + "version": "1.0.0", + "artifact_hash": artifact_hash, + "build_recipe_hash": recipe_hash + }] + }, + "evidence": [{ + "kind": "verified_build", + "evidence_hash": format!("sha256:{}", "55".repeat(32)), + "evidence": { "artifact_hash": artifact_hash } + }] + }), + bundle_url: "https://registry.example/bundle".to_string(), + bundle: Vec::new(), + }; + let verified = VerifiedBundle { + profile_contract: json!({ + "build": { "reproducible": true }, + "reproduction": { "environment": environment } + }), + source: Vec::new(), + object_hashes: BTreeMap::new(), + }; + let report = |builder_id: &str, trust_domain: &str| { + use ring::signature::KeyPair as _; + let rng = ring::rand::SystemRandom::new(); + let pkcs8 = + ring::signature::EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng).unwrap(); + let key_pair = + ring::signature::EcdsaKeyPair::from_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng) + .unwrap(); + let mut spki = vec![ + 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, + 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, + ]; + spki.extend_from_slice(key_pair.public_key().as_ref()); + let builder_public_key = format!("p256-spki:{}", base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(spki)); + let unsigned = json!({ + "schema": "cellscript-reproduction-report-v2", + "builder_id": builder_id, + "trust_domain": trust_domain, + "builder_public_key": builder_public_key, + "environment": environment, + "source_hash": source_hash, + "build_recipe_hash": recipe_hash, + "artifact_hash": artifact_hash, + "build_log_hash": format!("0x{}", "66".repeat(32)), + "generated_at": "2026-06-23T12:00:00Z", + }); + let signature = key_pair.sign(&rng, canonical_json(&unsigned).unwrap().as_bytes()).unwrap(); + serde_json::from_value(json!({ + "schema": "cellscript-reproduction-report-v2", + "builder_id": builder_id, + "trust_domain": trust_domain, + "builder_public_key": builder_public_key, + "environment": environment, + "source_hash": source_hash, + "build_recipe_hash": recipe_hash, + "artifact_hash": artifact_hash, + "build_log_hash": format!("0x{}", "66".repeat(32)), + "generated_at": "2026-06-23T12:00:00Z", + "signature": { + "algorithm": "p256-sha256", + "signature": base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(signature.as_ref()), + }, + })) + .unwrap() + }; + + let promotion = + build_reproduction_promotion(&fetched, &verified, vec![report("builder-a", "org-a"), report("builder-b", "org-b")]) + .unwrap(); + assert_eq!(promotion["kind"], "reproduced_build"); + assert_eq!(promotion["evidence"]["verification_level"], "reproduced"); + assert_eq!(promotion["evidence"]["reproducers"].as_array().unwrap().len(), 2); + assert!(build_reproduction_promotion(&fetched, &verified, vec![report("builder-a", "org-a"), report("builder-a", "org-b")]) + .is_err()); + } + + #[test] + fn commitment_proof_binds_the_wallet_transaction_intent() { + let payload = json!({ + "schema": "cellscript-registry-commitment-v1", + "namespace": "demo", + "name": "contract", + "release": "1.0.0" + }); + let commitment_hash = format!("0x{}", "11".repeat(32)); + let cell_data = format!("0x{}{}", hex::encode("CSREGv1"), commitment_hash.trim_start_matches("0x")); + let type_script = json!({ "code_hash": format!("0x{}", "22".repeat(32)), "hash_type": "data1", "args": "0x01" }); + let lock_script = json!({ "code_hash": format!("0x{}", "33".repeat(32)), "hash_type": "type", "args": "0x02" }); + let registry_type_hash = super::super::commands::ckb_script_hash_from_json(&type_script).unwrap(); + let commitment_lock_hash = super::super::commands::ckb_script_hash_from_json(&lock_script).unwrap(); + let intent = json!({ + "schema": "cellscript-registry-commitment-transaction-intent-v1", + "network": "mainnet", + "output": { "lock": lock_script, "type": type_script, "data": cell_data }, + "required_cell_deps": [{ "out_point": { "tx_hash": format!("0x{}", "44".repeat(32)), "index": "0x0" }, "dep_type": "code" }], + "custody_cell_dep": { "out_point": { "tx_hash": format!("0x{}", "55".repeat(32)), "index": "0x0" }, "dep_type": "code" } + }); + let proof = json!({ + "schema": "cellscript-registry-commitment-proof-v1", + "payload": payload, + "commitment_hash": commitment_hash, + "cell_data": cell_data, + "registry_type_hash": registry_type_hash, + "commitment_lock_hash": commitment_lock_hash, + "transaction_intent": intent + }); + + assert_eq!(validate_commitment_proof(&proof, &payload, &commitment_hash, &cell_data, "mainnet").unwrap(), intent); + assert!(validate_commitment_proof(&proof, &payload, &commitment_hash, &cell_data, "testnet").is_err()); + let mut mismatched = proof; + mismatched["cell_data"] = Value::String(format!("0x{}", "00".repeat(39))); + assert!(validate_commitment_proof(&mismatched, &payload, &commitment_hash, &cell_data, "mainnet").is_err()); + } + + #[test] + fn template_paths_fail_closed_on_traversal() { + assert!(safe_relative_path("src/main.cell").is_ok()); + assert!(safe_relative_path("../secret").is_err()); + assert!(safe_relative_path("/absolute").is_err()); + assert!(safe_relative_path("nested\\windows").is_err()); + } + + #[test] + fn immutable_bundle_downloads_require_safe_transport() { + assert!(validate_download_url("https://registry.example/bundle?version=1").is_ok()); + assert!(validate_download_url("http://127.0.0.1:8787/bundle").is_ok()); + assert!(validate_download_url("http://registry.example/bundle").is_err()); + assert!(validate_download_url("https://user:secret@registry.example/bundle").is_err()); + assert!(validate_download_url("https://registry.example/bundle#fragment").is_err()); + } + + #[test] + fn deployment_consumption_is_bound_to_the_signed_ckb_contract() { + let contract = json!({ "ckb": { "hash_type": "data1", "dep_type": "code" } }); + assert!(require_deployment_contract_values(&contract, "data1", "code").is_ok()); + assert!(require_deployment_contract_values(&contract, "type", "code").is_err()); + assert!(require_deployment_contract_values(&contract, "data1", "dep_group").is_err()); + } + + #[test] + fn dep_group_members_are_decoded_canonically() { + let mut bytes = vec![1, 0, 0, 0]; + bytes.extend([0x42; 32]); + bytes.extend(7_u32.to_le_bytes()); + let members = parse_dep_group_out_points(&format!("0x{}", hex::encode(bytes))).unwrap(); + assert_eq!(members, vec![(format!("0x{}", "42".repeat(32)), 7)]); + assert!(parse_dep_group_out_points("0x00000000").is_err()); + } + + #[test] + fn rpc_transport_requires_https_except_for_loopback_development() { + assert!(validate_rpc_url("https://mainnet.ckb.dev/rpc").is_ok()); + assert!(validate_rpc_url("http://127.0.0.1:8114").is_ok()); + assert!(validate_rpc_url("http://public.example/rpc").is_err()); + assert!(validate_rpc_url("https://user:secret@mainnet.ckb.dev/rpc").is_err()); + } +} diff --git a/src/cli/commands.rs b/src/cli/commands.rs index d5e7055c..5249ca6a 100644 --- a/src/cli/commands.rs +++ b/src/cli/commands.rs @@ -1,3 +1,4 @@ +use super::artifact::{ArtifactArgs, ArtifactOperation}; use crate::docgen::{DocGenerator, OutputFormat}; use crate::error::{CompileError, Result}; use crate::fmt::format_default; @@ -7,7 +8,8 @@ use crate::{ compile_path, compile_path_metadata_with_diagnostics, compile_path_with_entry_action, compile_path_with_entry_lock, default_metadata_path_for_artifact, default_output_path_for_input, load_modules_for_input, resolve_input_path, validate_artifact_metadata, validate_source_units_on_disk, ArtifactFormat, CompileMetadata, CompileOptions, EntryWitnessArg, - ParamMetadata, ProofPlanMetadata, TargetProfile, ENTRY_WITNESS_ABI, + ParamMetadata, ProofPlanMetadata, TargetProfile, ENTRY_WITNESS_ABI, ENTRY_WITNESS_PLACEMENT_ABI, ENTRY_WITNESS_PLACEMENT_FIELD, + ENTRY_WITNESS_PLACEMENT_SOURCE, }; use base64::Engine; use camino::Utf8Path; @@ -41,7 +43,7 @@ const ICKB_REQUIRED_PRODUCTION_EVIDENCE: [&str; 8] = [ ]; const ICKB_REQUIRED_HARDENING_EVIDENCE: [&str; 5] = ["mutation_coverage", "deterministic_fuzz_seed", "normalized_fixture_generator", "max_cellscript_cycles", "max_tx_size_bytes"]; -const CELLSCRIPT_CKB_RPC_URL_ENV: &str = "CELLSCRIPT_CKB_RPC_URL"; +pub(super) const CELLSCRIPT_CKB_RPC_URL_ENV: &str = "CELLSCRIPT_CKB_RPC_URL"; const NOVASEAL_CERTIFICATION_PLUGIN: &str = "novaseal-profile-v0"; const NOVASEAL_CERTIFICATION_REPORT_SCHEMA: &str = "cellscript-certification-report-v0.1"; const NOVASEAL_PLUGIN_REPORT_SCHEMA: &str = "novaseal-production-gates-v0.4"; @@ -120,6 +122,7 @@ pub enum Command { VerifyReceipt(VerifyReceiptArgs), VerifyArtifact(VerifyArtifactArgs), Run(RunArgs), + Artifact(ArtifactArgs), Publish(PublishArgs), Install(InstallArgs), RegistryVerify(RegistryVerifyArgs), @@ -127,6 +130,7 @@ pub enum Command { RegistryAdd(RegistryAddArgs), RegistryEdit(RegistryEditArgs), Certify(CertifyArgs), + Lock(PackageLockArgs), Update, Info(InfoArgs), Login(LoginArgs), @@ -134,6 +138,8 @@ pub enum Command { AuthCapabilityCreate(AuthCapabilityArgs), AuthCapabilitySubmit(AuthCapabilitySubmitArgs), AuthCapabilityRevoke(AuthCapabilityRevokeArgs), + AuthReproducerCreate(AuthReproducerCreateArgs), + AuthNamespaceClaim(AuthNamespaceClaimArgs), } #[derive(Debug, Default)] @@ -147,6 +153,10 @@ pub struct BuildArgs { pub features: Vec, pub all_features: bool, pub no_default_features: bool, + pub locked: bool, + pub frozen: bool, + pub offline: bool, + pub environment: Option, pub verbose: bool, pub json: bool, pub production: bool, @@ -163,6 +173,7 @@ pub struct BuildArgs { #[derive(Debug, Default)] pub struct TestArgs { pub filter: Option, + pub backend: Option, pub jobs: Option, pub release: bool, pub no_run: bool, @@ -170,6 +181,13 @@ pub struct TestArgs { pub fail_fast: bool, pub doc: bool, pub json: bool, + pub features: Vec, + pub all_features: bool, + pub no_default_features: bool, + pub locked: bool, + pub frozen: bool, + pub offline: bool, + pub environment: Option, } #[derive(Debug, Default)] @@ -209,6 +227,7 @@ pub struct NewArgs { #[derive(Debug, Default)] pub struct AddArgs { pub crates: Vec, + pub package: Option, pub dev: bool, pub build: bool, pub git: Option, @@ -236,11 +255,22 @@ pub struct InfoArgs { pub json: bool, } +#[derive(Debug, Default)] +pub struct PackageLockArgs { + pub json: bool, +} + #[derive(Debug, Default)] pub struct CheckArgs { pub all_targets: bool, pub target_profile: Option, pub features: Vec, + pub all_features: bool, + pub no_default_features: bool, + pub locked: bool, + pub frozen: bool, + pub offline: bool, + pub environment: Option, pub json: bool, pub production: bool, pub deny_fail_closed: bool, @@ -509,6 +539,8 @@ pub struct VerifyReceiptArgs { pub struct VerifyArtifactArgs { pub artifact: PathBuf, pub metadata: Option, + pub lowering_record: Option, + pub source_map: Option, pub receipt: Option, pub verify_sources: bool, pub json: bool, @@ -538,10 +570,14 @@ pub struct PublishArgs { pub allow_dirty: bool, pub api_url: Option, pub capability_key_id: Option, + pub authorise: bool, + pub no_open: bool, pub capability_signature: Option, pub idempotency_key: Option, pub payload: Option, pub source_snapshot: Option, + pub artifact_manifest: Option, + pub artifact_kind: Option, pub print_payload: bool, pub json: bool, } @@ -578,7 +614,7 @@ pub struct AuthCapabilityArgs { pub struct AuthCapabilitySubmitArgs { pub api_url: Option, pub payload: PathBuf, - pub joyid_signature: PathBuf, + pub wallet_signature: PathBuf, pub json: bool, } @@ -590,11 +626,28 @@ pub struct AuthCapabilityRevokeArgs { pub principal_id: Option, pub capability_key_id: Option, pub payload: Option, - pub joyid_signature: Option, + pub wallet_signature: Option, pub reason: Option, pub json: bool, } +#[derive(Debug, Default)] +pub struct AuthReproducerCreateArgs { + pub builder_id: String, + pub trust_domain: String, + pub private_key_output: Option, + pub json: bool, +} + +#[derive(Debug, Default)] +pub struct AuthNamespaceClaimArgs { + pub api_url: Option, + pub namespace: String, + pub payload: PathBuf, + pub wallet_signature: PathBuf, + pub json: bool, +} + #[derive(Debug, Default)] pub struct RegistryVerifyArgs { pub json: bool, @@ -742,6 +795,7 @@ fn run_entry_outcome(metadata: &CompileMetadata) -> Option { } impl CommandExecutor { + #[cfg(not(feature = "vm-runner"))] fn experimental_command(name: &str, detail: &str) -> Result<()> { Err(crate::error::CompileError::without_span(format!("cellc {} is still experimental: {}", name, detail))) } @@ -791,14 +845,18 @@ impl CommandExecutor { Command::VerifyReceipt(args) => Self::verify_receipt(args), Command::VerifyArtifact(args) => Self::verify_artifact(args), Command::Run(args) => Self::run(args), + Command::Artifact(args) => super::artifact::execute(args), Command::Publish(args) => Self::publish(args), Command::Install(args) => Self::install(args), + Command::Lock(args) => Self::lock(args), Command::Update => Self::update(), Command::Info(args) => Self::info(args), Command::Login(args) => Self::login(args), Command::AuthLogin(args) | Command::AuthCapabilityCreate(args) => Self::auth_capability(args), Command::AuthCapabilitySubmit(args) => Self::auth_capability_submit(args), Command::AuthCapabilityRevoke(args) => Self::auth_capability_revoke(args), + Command::AuthReproducerCreate(args) => Self::auth_reproducer_create(args), + Command::AuthNamespaceClaim(args) => Self::auth_namespace_claim(args), Command::RegistryVerify(args) => Self::registry_verify(args), Command::PackageVerify(args) => Self::package_verify(args), Command::RegistryAdd(args) => Self::registry_add(args), @@ -827,6 +885,7 @@ impl CommandExecutor { let opt_level = if args.release { 3 } else { 1 }; let input = Utf8Path::new("."); let options = CompileOptions { + edition: crate::CURRENT_EDITION, opt_level, output: None, debug: false, @@ -838,12 +897,15 @@ impl CommandExecutor { return Err(crate::error::CompileError::without_span("--entry-action and --entry-lock are mutually exclusive")); } let cache_options = options.clone(); - let result = match (args.entry_action.as_deref(), args.entry_lock.as_deref()) { - (Some(action), None) => compile_path_with_entry_action(input, options, action), - (None, Some(lock)) => compile_path_with_entry_lock(input, options, lock), - (None, None) => compile_path(input, options), - (Some(_), Some(_)) => unreachable!("validated above"), - }?; + let resolution_options = build_resolution_options(&args, crate::package::DependencyScope::Runtime); + let result = crate::package::with_resolution_options(resolution_options, || { + match (args.entry_action.as_deref(), args.entry_lock.as_deref()) { + (Some(action), None) => compile_path_with_entry_action(input, options, action), + (None, Some(lock)) => compile_path_with_entry_lock(input, options, lock), + (None, None) => compile_path(input, options), + (Some(_), Some(_)) => unreachable!("validated above"), + } + })?; let policy_args = effective_build_check_args(&args)?; validate_check_policy(&result.metadata, &policy_args)?; let resolved = resolve_input_path(input)?; @@ -851,8 +913,11 @@ impl CommandExecutor { result.write_to_path(&output_path)?; let metadata_path = default_metadata_path_for_artifact(&output_path); result.write_metadata_to_path(&metadata_path)?; + let verified_sidecars = result.write_verified_artifact_sidecars(&output_path)?; - refresh_lockfile_from_build(std::path::Path::new("."), &result.metadata)?; + if !args.frozen { + refresh_lockfile_from_build(std::path::Path::new("."), &result.metadata)?; + } if args.entry_action.is_none() && args.entry_lock.is_none() { crate::refresh_incremental_cache_for_input(input, &cache_options, &result)?; } @@ -861,7 +926,7 @@ impl CommandExecutor { || policy_args.deny_fail_closed || policy_args.deny_ckb_runtime || policy_args.deny_runtime_obligations; - let summary = serde_json::json!({ + let mut summary = serde_json::json!({ "status": "ok", "artifact": output_path.to_string(), "metadata": metadata_path.to_string(), @@ -903,6 +968,26 @@ impl CommandExecutor { "cache_hit": result.cache_hit, "constraints": &result.metadata.constraints, }); + if let Some(object) = summary.as_object_mut() { + object.insert( + "dependency_lock_mode".to_string(), + serde_json::json!(if args.frozen { + "frozen" + } else if args.locked { + "locked" + } else { + "authoritative" + }), + ); + object.insert("dependency_environment".to_string(), serde_json::json!(args.environment.as_deref())); + object.insert("dependency_offline".to_string(), serde_json::json!(args.offline || args.frozen)); + object.insert("dependency_features".to_string(), serde_json::json!(&args.features)); + object.insert("dependency_all_features".to_string(), serde_json::json!(args.all_features)); + object.insert("dependency_default_features".to_string(), serde_json::json!(!args.no_default_features)); + object + .insert("lowering_record".to_string(), serde_json::json!(verified_sidecars.as_ref().map(|paths| paths.0.to_string()))); + object.insert("source_map".to_string(), serde_json::json!(verified_sidecars.as_ref().map(|paths| paths.1.to_string()))); + } let mut human_lines = vec![ "Build complete".green().to_string(), format!(" Artifact format: {}", result.artifact_format.display_name()), @@ -953,6 +1038,7 @@ impl CommandExecutor { for member_dir in &members { let options = CompileOptions { + edition: crate::CURRENT_EDITION, opt_level, output: None, debug: false, @@ -961,11 +1047,14 @@ impl CommandExecutor { primitive_compat: args.primitive_compat.clone(), }; - let compile_result = match (args.entry_action.as_deref(), args.entry_lock.as_deref()) { - (Some(action), None) => compile_path_with_entry_action(member_dir, options, action), - (None, Some(lock)) => compile_path_with_entry_lock(member_dir, options, lock), - _ => compile_path(member_dir, options), - }; + let resolution_options = build_resolution_options(&args, crate::package::DependencyScope::Runtime); + let compile_result = crate::package::with_resolution_options(resolution_options, || { + match (args.entry_action.as_deref(), args.entry_lock.as_deref()) { + (Some(action), None) => compile_path_with_entry_action(member_dir, options, action), + (None, Some(lock)) => compile_path_with_entry_lock(member_dir, options, lock), + _ => compile_path(member_dir, options), + } + }); match compile_result { Ok(result) => { @@ -986,12 +1075,15 @@ impl CommandExecutor { result.write_to_path(&output_path)?; let metadata_path = default_metadata_path_for_artifact(&output_path); result.write_metadata_to_path(&metadata_path)?; + let verified_sidecars = result.write_verified_artifact_sidecars(&output_path)?; member_results.push(serde_json::json!({ "member": member_dir.as_str(), "status": "ok", "artifact": output_path.to_string(), "metadata": metadata_path.to_string(), + "lowering_record": verified_sidecars.as_ref().map(|paths| paths.0.to_string()), + "source_map": verified_sidecars.as_ref().map(|paths| paths.1.to_string()), "artifact_format": result.artifact_format.display_name(), "target_profile": result.metadata.target_profile.name, "artifact_hash": result.metadata.artifact_hash, @@ -1034,9 +1126,13 @@ impl CommandExecutor { lockfile.dependencies.insert( member_name.to_string(), crate::package::LockedDependency { + name: member_name.to_string(), + namespace: None, version: String::new(), source: crate::package::LockedSource::Path { path: member_name.to_string() }, source_hash: Some(artifact_hash.to_string()), + manifest_digest: "workspace-member-artifact".to_string(), + dependencies: BTreeMap::new(), build: None, }, ); @@ -1063,6 +1159,11 @@ impl CommandExecutor { } fn test(args: TestArgs) -> Result<()> { + let options = test_resolution_options(&args); + crate::package::with_resolution_options(options, || Self::test_inner(args)) + } + + fn test_inner(args: TestArgs) -> Result<()> { let doc_output = if args.doc { Some(Self::generate_docs(&DocArgs { output_format: OutputFormat::Markdown, ..Default::default() })?) } else { @@ -1070,15 +1171,32 @@ impl CommandExecutor { }; let mut test_inputs = collect_cell_files(Path::new("tests"))?; + let mut scenario_inputs = super::test_runner::collect_scenario_files(Path::new("tests"))?; if let Some(filter) = &args.filter { test_inputs.retain(|path| path.to_string_lossy().contains(filter)); + scenario_inputs.retain(|path| path.to_string_lossy().contains(filter)); } test_inputs.sort(); + scenario_inputs.sort(); + let backends = if args.no_run { + Vec::new() + } else { + let backend = args.backend.as_deref().ok_or_else(|| { + crate::error::CompileError::without_span("cellc test requires --backend simulator|ckb-vm|all unless --no-run is used") + })?; + if scenario_inputs.is_empty() { + return Err(crate::error::CompileError::without_span( + "cellc test cannot pass without an executable *.scenario.json fixture; use --no-run for compile-only checks", + )); + } + super::test_runner::TestBackend::parse(backend)? + }; if test_inputs.is_empty() { compile_path( ".", CompileOptions { + edition: crate::CURRENT_EDITION, opt_level: 0, output: None, debug: false, @@ -1110,7 +1228,9 @@ impl CommandExecutor { "execution": if args.no_run { "disabled" } else { "skipped-no-test-files" }, "docs_generated": args.doc, "doc_output": doc_output.as_ref().map(|path| path.display().to_string()), + "scenario_files": scenario_inputs.len(), "tests": [], + "scenarios": [], }), human_lines, } @@ -1131,6 +1251,7 @@ impl CommandExecutor { let result = compile_path( utf8, CompileOptions { + edition: crate::CURRENT_EDITION, opt_level: 0, output: None, debug: false, @@ -1182,16 +1303,64 @@ impl CommandExecutor { }))); } + let mut scenario_reports = Vec::new(); + let mut scenario_failures = Vec::new(); + if !args.no_run { + for scenario in &scenario_inputs { + for backend in &backends { + match super::test_runner::run_scenario(scenario, *backend) { + Ok(report) => scenario_reports.push(report), + Err(error) => { + let message = format!("{}: {}", scenario.display(), error); + scenario_reports.push(serde_json::json!({ + "schema": "cellscript-test-report-v1", + "status": "failed", + "scenario_path": scenario.to_string_lossy(), + "error": error.to_string(), + })); + scenario_failures.push(message); + if args.fail_fast { + break; + } + } + } + } + if args.fail_fast && !scenario_failures.is_empty() { + break; + } + } + } + if !scenario_failures.is_empty() { + return Err(crate::error::CompileError::without_span(format!( + "scenario test failed:\n - {}", + scenario_failures.join("\n - ") + )) + .with_details(serde_json::json!({ + "mode": "test", + "compile_test_files": test_inputs.len(), + "scenario_files": scenario_inputs.len(), + "backend": args.backend, + "scenario_runs_passed": scenario_reports.len().saturating_sub(scenario_failures.len()), + "scenario_runs_failed": scenario_failures.len(), + "scenarios": scenario_reports, + }))); + } + let mut human_lines = Vec::new(); if let Some(output) = &doc_output { human_lines.push("Documentation generated".green().to_string()); human_lines.push(format!(" Output: {}", output.display())); } - human_lines.push("Test compile complete".green().to_string()); + human_lines.push(if args.no_run { "Test compile complete" } else { "Test execution complete" }.green().to_string()); human_lines.push(format!(" Compiled {} test file(s)", passed)); - if !args.no_run { - human_lines - .push(" Execution: skipped; CellScript test execution is not enabled in the default toolchain yet".to_string()); + if args.no_run { + human_lines.push(" Execution: disabled by --no-run".to_string()); + } else { + human_lines.push(format!( + " Executed {} scenario/backend run(s) with {}", + scenario_reports.len(), + args.backend.as_deref().unwrap_or("unknown") + )); } CommandOutcome { machine: serde_json::json!({ @@ -1202,10 +1371,14 @@ impl CommandExecutor { "failed": 0, "fail_fast": args.fail_fast, "no_run": args.no_run, - "execution": if args.no_run { "disabled" } else { "skipped-default-toolchain" }, + "execution": if args.no_run { "disabled" } else { "executed" }, + "backend": args.backend, + "scenario_files": scenario_inputs.len(), + "scenario_runs": scenario_reports.len(), "docs_generated": args.doc, "doc_output": doc_output.as_ref().map(|path| path.display().to_string()), "tests": test_reports, + "scenarios": scenario_reports, }), human_lines, } @@ -1238,7 +1411,15 @@ impl CommandExecutor { let modules = load_modules_for_input(".")?; let compile_result = compile_path( ".", - CompileOptions { opt_level: 0, output: None, debug: false, target: None, target_profile: None, primitive_compat: None }, + CompileOptions { + edition: crate::CURRENT_EDITION, + opt_level: 0, + output: None, + debug: false, + target: None, + target_profile: None, + primitive_compat: None, + }, )?; let mut generator = DocGenerator::new(args.output_format); for module in &modules { @@ -1421,6 +1602,9 @@ impl CommandExecutor { if args.git.is_some() && args.path.is_some() { return Err(crate::error::CompileError::without_span("cellc add accepts either --git or --path, not both")); } + if args.package.is_some() && args.crates.len() != 1 { + return Err(crate::error::CompileError::without_span("cellc add --package requires exactly one local dependency alias")); + } let pm = PackageManager::new("."); let mut manifest = pm.read_manifest()?; @@ -1435,6 +1619,7 @@ impl CommandExecutor { } pm.write_manifest(&manifest)?; + refresh_lockfile_from_manifest(Path::new("."))?; CommandOutcome { machine: serde_json::json!({ @@ -1465,7 +1650,7 @@ impl CommandExecutor { } pm.write_manifest(&manifest)?; - if !args.dev && !args.build && !removed.is_empty() { + if !removed.is_empty() { refresh_lockfile_from_manifest(Path::new("."))?; } @@ -1535,6 +1720,7 @@ impl CommandExecutor { for target in targets { let compile_options = CompileOptions { + edition: crate::CURRENT_EDITION, opt_level: 0, output: None, debug: false, @@ -1542,13 +1728,15 @@ impl CommandExecutor { target_profile: compile_target_profile.clone(), primitive_compat: args.primitive_compat.clone(), }; - let result = match compile_path(".", compile_options.clone()) { - Ok(result) => result, - Err(error) => { - let diagnostics = compile_failure_diagnostics(Utf8Path::new("."), compile_options, error); - return Err(diagnostics_to_error(&diagnostics)); - } - }; + let resolution_options = check_resolution_options(&args, crate::package::DependencyScope::Runtime); + let result = + match crate::package::with_resolution_options(resolution_options, || compile_path(".", compile_options.clone())) { + Ok(result) => result, + Err(error) => { + let diagnostics = compile_failure_diagnostics(Utf8Path::new("."), compile_options, error); + return Err(diagnostics_to_error(&diagnostics)); + } + }; validate_check_policy(&result.metadata, &args)?; let target_profile_policy_violations = target_profile_policy_violations(&result.metadata, result.artifact_format, requested_profile); @@ -1652,17 +1840,18 @@ impl CommandExecutor { let mut failed = 0; for member_dir in &members { - let compile_result = compile_path( - member_dir, - CompileOptions { - opt_level: 0, - output: None, - debug: false, - target: None, - target_profile: args.target_profile.clone(), - primitive_compat: args.primitive_compat.clone(), - }, - ); + let compile_options = CompileOptions { + edition: crate::CURRENT_EDITION, + opt_level: 0, + output: None, + debug: false, + target: None, + target_profile: args.target_profile.clone(), + primitive_compat: args.primitive_compat.clone(), + }; + let resolution_options = check_resolution_options(&args, crate::package::DependencyScope::Runtime); + let compile_result = + crate::package::with_resolution_options(resolution_options, || compile_path(member_dir, compile_options)); match compile_result { Ok(result) => { @@ -1717,6 +1906,7 @@ impl CommandExecutor { let input = Utf8Path::from_path(&input_path) .ok_or_else(|| crate::error::CompileError::without_span(format!("path '{}' is not valid UTF-8", input_path.display())))?; let options = CompileOptions { + edition: crate::CURRENT_EDITION, opt_level: 0, output: None, debug: false, @@ -1754,6 +1944,7 @@ impl CommandExecutor { let input = Utf8Path::from_path(&input_path) .ok_or_else(|| crate::error::CompileError::without_span(format!("path '{}' is not valid UTF-8", input_path.display())))?; let options = CompileOptions { + edition: crate::CURRENT_EDITION, opt_level: 0, output: None, debug: false, @@ -1794,6 +1985,7 @@ impl CommandExecutor { let result = compile_path( input, CompileOptions { + edition: crate::CURRENT_EDITION, opt_level: 0, output: None, debug: false, @@ -1863,6 +2055,10 @@ impl CommandExecutor { let summary = serde_json::json!({ "status": if entry_constraints.unsupported { "fail" } else { "ok" }, "abi": ENTRY_WITNESS_ABI, + "placement_abi": ENTRY_WITNESS_PLACEMENT_ABI, + "witness_args_field": ENTRY_WITNESS_PLACEMENT_FIELD, + "witness_source": ENTRY_WITNESS_PLACEMENT_SOURCE, + "raw_v1_compatible": false, "target_profile": result.metadata.target_profile.name, "entry_kind": selected.kind, "entry": selected.name, @@ -1894,6 +2090,7 @@ impl CommandExecutor { let result = compile_path( input, CompileOptions { + edition: crate::CURRENT_EDITION, opt_level: 0, output: None, debug: false, @@ -2085,9 +2282,12 @@ impl CommandExecutor { }, "witness_args_policy": { "entry_payload_abi": ENTRY_WITNESS_ABI, + "placement_abi": ENTRY_WITNESS_PLACEMENT_ABI, "entry_payload_owner": "compiler", "final_witness_args_owner": "adapter", - "default_action_payload_field": "input_type", + "default_action_payload_field": ENTRY_WITNESS_PLACEMENT_FIELD, + "runtime_source": ENTRY_WITNESS_PLACEMENT_SOURCE, + "raw_v1_compatible": false, "lock_signature_policy": "explicit-adapter-owned-do-not-overwrite", "placement_requires_deployment_role": true, "ckb_reference": "ckb_types::packed::WitnessArgs", @@ -2165,6 +2365,7 @@ impl CommandExecutor { let result = compile_path( input, CompileOptions { + edition: crate::CURRENT_EDITION, opt_level: 0, output: None, debug: false, @@ -2206,6 +2407,7 @@ impl CommandExecutor { let result = compile_cli_input( args.input.as_ref(), CompileOptions { + edition: crate::CURRENT_EDITION, opt_level: 0, output: None, debug: false, @@ -2264,6 +2466,7 @@ impl CommandExecutor { let result = compile_cli_input( args.input.as_ref(), CompileOptions { + edition: crate::CURRENT_EDITION, opt_level: 0, output: None, debug: false, @@ -2318,6 +2521,7 @@ impl CommandExecutor { let result = compile_cli_input( args.input.as_ref(), CompileOptions { + edition: crate::CURRENT_EDITION, opt_level: 0, output: None, debug: false, @@ -2362,6 +2566,7 @@ impl CommandExecutor { let result = compile_cli_input( args.input.as_ref(), CompileOptions { + edition: crate::CURRENT_EDITION, opt_level: 0, output: None, debug: false, @@ -2387,6 +2592,7 @@ impl CommandExecutor { let result = compile_cli_input( args.input.as_ref(), CompileOptions { + edition: crate::CURRENT_EDITION, opt_level: 0, output: None, debug: false, @@ -2419,6 +2625,7 @@ impl CommandExecutor { let result = compile_cli_input( args.input.as_ref(), CompileOptions { + edition: crate::CURRENT_EDITION, opt_level: 0, output: None, debug: false, @@ -2462,6 +2669,7 @@ impl CommandExecutor { let result = compile_path( input, CompileOptions { + edition: crate::CURRENT_EDITION, opt_level: 0, output: None, debug: false, @@ -2510,6 +2718,7 @@ impl CommandExecutor { let result = compile_path( input, CompileOptions { + edition: crate::CURRENT_EDITION, opt_level: 0, output: None, debug: false, @@ -2552,6 +2761,7 @@ impl CommandExecutor { let result = compile_path( input, CompileOptions { + edition: crate::CURRENT_EDITION, opt_level, output: None, debug: false, @@ -2709,6 +2919,7 @@ impl CommandExecutor { let result = compile_path( input, CompileOptions { + edition: crate::CURRENT_EDITION, opt_level: 1, output: None, debug: false, @@ -2842,9 +3053,12 @@ impl CommandExecutor { "must_emit_lineage": true, "witness_policy": { "entry_payload_abi": ENTRY_WITNESS_ABI, + "placement_abi": ENTRY_WITNESS_PLACEMENT_ABI, "entry_payload_owner": "compiler", "final_witness_args_owner": "adapter", - "default_action_payload_field": "input_type", + "default_action_payload_field": ENTRY_WITNESS_PLACEMENT_FIELD, + "runtime_source": ENTRY_WITNESS_PLACEMENT_SOURCE, + "raw_v1_compatible": false, "lock_signature_policy": "explicit-adapter-owned-do-not-overwrite", "placement_requires_deployment_role": true, }, @@ -2956,6 +3170,7 @@ impl CommandExecutor { compile_path( input, CompileOptions { + edition: crate::CURRENT_EDITION, opt_level: 1, output: None, debug: false, @@ -3026,6 +3241,7 @@ impl CommandExecutor { let result = compile_path( input, CompileOptions { + edition: crate::CURRENT_EDITION, opt_level: 0, output: None, debug: false, @@ -3099,6 +3315,10 @@ impl CommandExecutor { machine: serde_json::json!({ "status": "ok", "abi": ENTRY_WITNESS_ABI, + "placement_abi": ENTRY_WITNESS_PLACEMENT_ABI, + "witness_args_field": ENTRY_WITNESS_PLACEMENT_FIELD, + "witness_source": ENTRY_WITNESS_PLACEMENT_SOURCE, + "raw_v1_compatible": false, "entry_kind": selected.kind, "entry": selected.name, "witness_hex": witness_hex, @@ -3119,7 +3339,12 @@ impl CommandExecutor { let input_path = resolve_input_path(input)?; let compile_result = compile_path( &input_path, - CompileOptions { target: args.target.clone(), target_profile: args.target_profile.clone(), ..CompileOptions::default() }, + CompileOptions { + edition: crate::CURRENT_EDITION, + target: args.target.clone(), + target_profile: args.target_profile.clone(), + ..CompileOptions::default() + }, )?; let receipt = compile_receipt_json(&compile_result.metadata)?; if let Some(parent) = args.output.parent() { @@ -3256,8 +3481,8 @@ impl CommandExecutor { let artifact_path = Utf8Path::from_path(&args.artifact).ok_or_else(|| { crate::error::CompileError::without_span(format!("artifact path '{}' is not valid UTF-8", args.artifact.display())) })?; - let metadata_path = match args.metadata { - Some(path) => path, + let metadata_path = match args.metadata.as_ref() { + Some(path) => path.clone(), None => default_metadata_path_for_artifact(artifact_path).into_std_path_buf(), }; @@ -3270,6 +3495,46 @@ impl CommandExecutor { let metadata: CompileMetadata = serde_json::from_slice(&metadata_bytes).map_err(|error| { crate::error::CompileError::without_span(format!("failed to parse metadata '{}': {}", metadata_path.display(), error)) })?; + let (checker_report, lowering_record_path, source_map_path) = if metadata.artifact_format == "RISC-V ELF" { + let lowering_record_path = args + .lowering_record + .clone() + .unwrap_or_else(|| crate::lowering_record_output_path_from_artifact(artifact_path).into_std_path_buf()); + let source_map_path = args + .source_map + .clone() + .unwrap_or_else(|| crate::source_map_output_path_from_artifact(artifact_path).into_std_path_buf()); + let lowering_record_bytes = std::fs::read(&lowering_record_path).map_err(|error| { + crate::error::CompileError::without_span(format!( + "failed to read lowering record '{}': {}", + lowering_record_path.display(), + error + )) + })?; + let source_map_bytes = std::fs::read(&source_map_path).map_err(|error| { + crate::error::CompileError::without_span(format!( + "failed to read source map '{}': {}", + source_map_path.display(), + error + )) + })?; + let report = cellscript_artifact_checker::check_bundle( + &artifact_bytes, + &metadata_bytes, + &lowering_record_bytes, + &source_map_bytes, + &crate::CheckerBudgets::default(), + ) + .map_err(|error| crate::error::CompileError::without_span(error.to_string()))?; + (Some(report), Some(lowering_record_path), Some(source_map_path)) + } else { + if args.lowering_record.is_some() || args.source_map.is_some() { + return Err(crate::error::CompileError::without_span( + "--lowering-record/--source-map are only valid for RISC-V ELF artifacts", + )); + } + (None, None, None) + }; let result = validate_artifact_metadata(artifact_bytes, metadata)?; if args.verify_sources { validate_source_units_on_disk(&result.metadata)?; @@ -3353,6 +3618,27 @@ impl CommandExecutor { "constraints": &result.metadata.constraints, }); if let Some(object) = summary.as_object_mut() { + object.insert( + "lowering_record".to_string(), + serde_json::json!(lowering_record_path.as_ref().map(|path| path.display().to_string())), + ); + object.insert( + "source_map".to_string(), + serde_json::json!(source_map_path.as_ref().map(|path| path.display().to_string())), + ); + object.insert("binding_verification".to_string(), serde_json::json!("verified")); + object.insert( + "structural_verification".to_string(), + serde_json::json!(if checker_report.is_some() { "verified" } else { "not-applicable" }), + ); + object.insert( + "lowering_record_verification".to_string(), + serde_json::json!(if checker_report.is_some() { "verified" } else { "not-applicable" }), + ); + object.insert("ckb_vm_evidence".to_string(), serde_json::json!("not-executed")); + object.insert("chain_evidence".to_string(), serde_json::json!("not-provided")); + object.insert("semantic_equivalence_claimed".to_string(), serde_json::json!(false)); + object.insert("checker_report".to_string(), serde_json::json!(&checker_report)); object.insert("receipt_verified".to_string(), serde_json::json!(receipt_report.is_some())); object.insert( "receipt_payload_hash".to_string(), @@ -3389,6 +3675,23 @@ impl CommandExecutor { println!(" Target profile: {}", result.metadata.target_profile.name); println!(" Hash: {}", result.metadata.artifact_hash.as_deref().unwrap_or("missing")); println!(" Size: {} bytes", result.artifact_bytes.len()); + println!(" Binding verification: verified"); + if checker_report.is_some() { + println!(" Structural verification: verified"); + println!(" Lowering-record verification: verified"); + if let Some(path) = lowering_record_path { + println!(" Lowering record: {}", path.display()); + } + if let Some(path) = source_map_path { + println!(" Source map: {}", path.display()); + } + } else { + println!(" Structural verification: not applicable to assembly"); + println!(" Lowering-record verification: not applicable to assembly"); + } + println!(" CKB-VM evidence: not executed"); + println!(" Chain evidence: not provided"); + println!(" Semantic equivalence: not claimed"); if expected_target_profile_verified { println!(" Expected target profile: verified"); } @@ -3418,6 +3721,7 @@ impl CommandExecutor { let compile_result = compile_path( ".", CompileOptions { + edition: crate::CURRENT_EDITION, opt_level, output: None, debug: false, @@ -3445,32 +3749,23 @@ impl CommandExecutor { .chain(result.metadata.locks.iter().filter(|lock| !lock.params.is_empty()).map(|lock| format!("lock {}", lock.name))) .collect::>(); if !parameterized_entries.is_empty() { - eprintln!( - "{}", - format!( - "Warning: {} requires transaction/parameter ABI context; falling back to simulate mode", - parameterized_entries.join(", ") - ) - .yellow() - ); - return Self::run_simulate(&result, &args); + return Err(crate::error::CompileError::without_span(format!( + "cellc run executes only no-argument pure ELF entrypoints; {} requires transaction/parameter ABI context; use --simulate explicitly for development interpretation", + parameterized_entries.join(", ") + ))); } if result.metadata.runtime.ckb_runtime_required { - eprintln!( - "{}", - format!( - "Warning: CKB runtime required ({}); falling back to simulate mode", - result.metadata.runtime.ckb_runtime_features.join(", ") - ) - .yellow() - ); - return Self::run_simulate(&result, &args); + return Err(crate::error::CompileError::without_span(format!( + "cellc run cannot provide CKB transaction/syscall context required by {}; use --simulate explicitly or an executable transaction scenario", + result.metadata.runtime.ckb_runtime_features.join(", ") + ))); } if !result.metadata.runtime.standalone_runner_compatible { - eprintln!("{}", "Warning: ELF is not standalone-compatible; falling back to simulate mode".yellow()); - return Self::run_simulate(&result, &args); + return Err(crate::error::CompileError::without_span( + "cellc run requires a standalone-compatible pure ELF; use --simulate explicitly or an executable transaction scenario", + )); } let vm_args = args.args.into_iter().map(|arg| arg.into_bytes()).collect::>(); @@ -3551,8 +3846,12 @@ impl CommandExecutor { } fn publish(args: PublishArgs) -> Result<()> { + if let Some(manifest_path) = args.artifact_manifest.clone() { + return publish_declared_artifact(args, &manifest_path); + } let pm = PackageManager::new("."); let manifest = pm.read_manifest()?; + let artifact = cellscript_artifact_descriptor(args.artifact_kind.as_deref())?; if args.dry_run { let mut issues = Vec::::new(); @@ -3648,19 +3947,23 @@ impl CommandExecutor { let api_base = resolve_registry_api_base(args.api_url)?; let registry_origin = registry_origin_from_api_base(&api_base)?; let endpoint = registry_publish_endpoint(&api_base, &namespace, &manifest.package.name); - let registry_entry = build_publish_registry_entry(&manifest, &namespace, version_entry)?; + let registry_entry = build_publish_registry_entry(&manifest, &namespace, version_entry, &artifact)?; let payload = if let Some(payload_path) = args.payload.as_deref() { read_registry_publish_payload(payload_path)? } else { - let capability_key_id = args - .capability_key_id - .or_else(|| std::env::var("CELLSCRIPT_CAPABILITY_KEY_ID").ok()) - .ok_or_else(|| { - crate::error::CompileError::without_span(format!( - "capability key id is required for public publish; connect JoyID through the registry submit page to derive , run `cellc auth capability create --principal-id --scope publish:{}/{} --expires 90d --json > capability-payload.json`, sign that payload with JoyID through CCC, then run `cellc auth capability submit --payload capability-payload.json --joyid-signature joyid-signature.json`; after registration, pass --capability-key-id or set CELLSCRIPT_CAPABILITY_KEY_ID", + let capability_key_id = match args.capability_key_id.or_else(|| std::env::var("CELLSCRIPT_CAPABILITY_KEY_ID").ok()) { + Some(key_id) => key_id, + None if args.authorise => { + authorise_registry_publish_key(&api_base, &namespace, &manifest.package.name, &artifact.kind, args.no_open)? + } + None => { + return Err(crate::error::CompileError::without_span(format!( + "publishing {}/{} requires a wallet-authorised publishing key; run `cellc publish --authorise` for the continuous browser flow, or pass an existing --capability-key-id", namespace, manifest.package.name )) - })?; + .with_category(crate::error::CompileErrorCategory::Authentication)); + } + }; let issued_at = current_utc_timestamp(); let expires_at = utc_timestamp_after_seconds(10 * 60); let nonce = registry_publish_nonce( @@ -3680,12 +3983,13 @@ impl CommandExecutor { name: manifest.package.name.clone(), version: manifest.package.version.clone(), source_hash: source_hash.clone(), - manifest_hash: Some(hash_json_value("package manifest", &manifest)?), + manifest_hash: crate::package::registry::compute_package_manifest_hash(&manifest)?, capability_key_id, nonce, issued_at, expires_at, cli_version: crate::VERSION.to_string(), + artifact: artifact.clone(), registry_entry, } }; @@ -3746,6 +4050,8 @@ impl CommandExecutor { let dep = DetailedDependency { version: args.version.clone().unwrap_or_else(|| "*".to_string()), namespace: None, + package: None, + resolver: None, git: Some(git_url.clone()), branch: None, tag: None, @@ -3754,6 +4060,8 @@ impl CommandExecutor { optional: false, features: Vec::new(), default_features: true, + allow_unverified: false, + allow_quarantined: false, }; pm.resolve_from_git(&crate_name, git_url, &dep)?; @@ -3773,6 +4081,8 @@ impl CommandExecutor { let dep = DetailedDependency { version: args.version.clone().unwrap_or_else(|| "*".to_string()), namespace: None, + package: None, + resolver: None, git: None, branch: None, tag: None, @@ -3781,6 +4091,8 @@ impl CommandExecutor { optional: false, features: Vec::new(), default_features: true, + allow_unverified: false, + allow_quarantined: false, }; let manifest_for_check = pm.read_manifest()?; @@ -3829,10 +4141,12 @@ impl CommandExecutor { }, )?; - let dep = if resolved_namespace.is_some() { + let dep = if resolved_namespace.is_some() || args.allow_unverified || args.allow_quarantined { Dependency::Detailed(DetailedDependency { version, namespace: resolved_namespace.clone(), + package: None, + resolver: None, git: None, branch: None, tag: None, @@ -3841,6 +4155,8 @@ impl CommandExecutor { optional: false, features: Vec::new(), default_features: true, + allow_unverified: args.allow_unverified, + allow_quarantined: args.allow_quarantined, }) } else { Dependency::Simple(version) @@ -3857,12 +4173,7 @@ impl CommandExecutor { println!("{}", format!("Installed {}/{} from registry", ns_display, resolved_name).green()); Ok(()) } else { - let mut pm = PackageManager::new("."); - pm.resolve_dependencies()?; - - let mut lockfile = Lockfile::read_from_root(std::path::Path::new("."))?.unwrap_or_default(); - lockfile.replace_with_resolved(pm.get_resolved()); - lockfile.write_to_root(std::path::Path::new("."))?; + refresh_lockfile_from_manifest(std::path::Path::new("."))?; println!("{}", "Dependencies resolved and lockfile updated".green()); Ok(()) @@ -3870,44 +4181,46 @@ impl CommandExecutor { } fn update() -> Result<()> { - let mut pm = PackageManager::new("."); - let manifest = pm.read_manifest()?; - - pm.resolve_dependencies()?; - - let mut lockfile = Lockfile::read_from_root(std::path::Path::new("."))?.unwrap_or_default(); - - lockfile.replace_with_resolved(pm.get_resolved()); - lockfile.write_to_root(std::path::Path::new("."))?; - - let resolved = pm.get_resolved(); - if resolved.is_empty() { + refresh_lockfile_from_manifest(std::path::Path::new("."))?; + let lockfile = Lockfile::read_from_root(std::path::Path::new("."))?.expect("lockfile was just written"); + if lockfile.dependencies.is_empty() { println!("{}", "No dependencies to update".green()); } else { - println!("{}", format!("Updated {} dependencies", resolved.len()).green()); - for (name, package) in resolved { + println!("{}", format!("Updated {} dependency nodes", lockfile.dependencies.len()).green()); + for (node_id, package) in &lockfile.dependencies { let source = match &package.source { - crate::package::PackageSource::Local(path) => format!("path: {}", path.display()), - crate::package::PackageSource::Git { url, revision } => format!("git: {}#{}", url, revision), - crate::package::PackageSource::Registry { registry, namespace, version, .. } => { + crate::package::LockedSource::Path { path } => format!("path: {}", path), + crate::package::LockedSource::Git { url, revision } => format!("git: {}#{}", url, revision), + crate::package::LockedSource::Registry { registry, namespace, version, .. } => { format!("registry: {}/{}@{}", registry, namespace, version) } }; - println!(" {} v{} ({})", name, package.version, source); - } - } - - let lockfile_issues = lockfile.consistency_issues_with_resolved(&manifest, resolved); - if !lockfile_issues.is_empty() { - println!("{}", "Warning: lockfile is not consistent with Cell.toml".yellow()); - for issue in lockfile_issues { - println!(" - {}", issue); + println!(" {} {} v{} ({})", node_id, package.name, package.version, source); } } Ok(()) } + fn lock(args: PackageLockArgs) -> Result<()> { + refresh_lockfile_from_manifest(std::path::Path::new("."))?; + let lockfile = Lockfile::read_from_root(std::path::Path::new("."))?.expect("lockfile was just written"); + CommandOutcome { + machine: serde_json::json!({ + "status": "ok", + "lockfile": "Cell.lock", + "schema": lockfile.schema, + "dependency_nodes": lockfile.dependencies.len(), + "environments": lockfile.environments.keys().collect::>(), + }), + human_lines: vec![ + "Dependency graph locked".green().to_string(), + format!(" {} dependency node(s)", lockfile.dependencies.len()), + ], + } + .emit(args.json) + } + fn info(args: InfoArgs) -> Result<()> { let pm = PackageManager::new("."); let manifest = pm.read_manifest()?; @@ -3929,6 +4242,10 @@ impl CommandExecutor { "package": manifest.package, "dependencies": manifest.dependencies, "dev_dependencies": manifest.dev_dependencies, + "features": manifest.features, + "environments": manifest.environments, + "dependency_overrides": manifest.dependency_overrides, + "resolvers": manifest.resolvers, "build": manifest.build, "policy": manifest.policy, "deploy": manifest.deploy, @@ -3950,9 +4267,12 @@ impl CommandExecutor { .unwrap_or_else(|| crate::package::registry::DEFAULT_PUBLIC_REGISTRY_ORIGIN.to_string()); let principal_type = args.principal_type.or_else(|| std::env::var("CELLSCRIPT_PRINCIPAL_TYPE").ok()).unwrap_or_else(|| "joyid_ckb".to_string()); + if principal_type != "joyid_ckb" && principal_type != "ckb_secp256k1" { + return Err(crate::error::CompileError::without_span("principal type must be joyid_ckb or ckb_secp256k1")); + } let principal_id = args.principal_id.or_else(|| std::env::var("CELLSCRIPT_PRINCIPAL_ID").ok()).ok_or_else(|| { crate::error::CompileError::without_span( - "principal id is required; pass --principal-id or set CELLSCRIPT_PRINCIPAL_ID to the normalized JoyID/CKB identity binding", + "principal id is required; pass --principal-id or set CELLSCRIPT_PRINCIPAL_ID to the normalized wallet identity binding", ) })?; let explicit_capability_pubkey = args.capability_pubkey.or_else(|| std::env::var("CELLSCRIPT_CAPABILITY_PUBKEY").ok()); @@ -3999,12 +4319,73 @@ impl CommandExecutor { format!(" Scopes: {}", payload.requested_scopes.join(", ")), format!(" Capability expires: {}", payload.capability_expires_at), String::new(), - "Sign this payload with JoyID, then submit the signed authorisation to the registry write API:".to_string(), + "Sign this payload with the matching JoyID or CKB wallet, then submit the signed authorisation to the registry write API:" + .to_string(), serde_json::to_string_pretty(&payload)?, ]); CommandOutcome { machine, human_lines }.emit(args.json) } + fn auth_reproducer_create(args: AuthReproducerCreateArgs) -> Result<()> { + let builder_id = args.builder_id.trim().to_string(); + if builder_id.is_empty() || builder_id.len() > 200 { + return Err(crate::error::CompileError::without_span("builder id must contain 1 to 200 characters")); + } + let trust_domain = args.trust_domain.trim().to_string(); + if trust_domain.is_empty() || trust_domain.len() > 200 { + return Err(crate::error::CompileError::without_span("trust domain must contain 1 to 200 characters")); + } + + let generated = generate_registry_key_material()?; + let (private_key_storage, storage_line) = if let Some(path) = args.private_key_output { + write_new_reproducer_private_key(&path, &generated.private_key_pkcs8)?; + let path_display = path.display().to_string(); + ( + serde_json::json!({ + "kind": "pkcs8_base64_file", + "path": &path_display, + "environment_variable": "CELLSCRIPT_REPRODUCER_PRIVATE_KEY_PKCS8_B64", + }), + format!(" Private key: restricted PKCS#8 base64 file at {path_display}"), + ) + } else { + store_registry_private_key(&generated.key_id, &generated.private_key_pkcs8)?; + ( + serde_json::json!({ + "kind": "os_keychain", + "service": "cellscript-registry", + "key_id": &generated.key_id, + }), + " Private key: stored in the OS keychain".to_string(), + ) + }; + let machine = serde_json::json!({ + "schema": "cellscript-reproducer-builder-enrollment-v1", + "builder_id": &builder_id, + "trust_domain": &trust_domain, + "builder_key_id": &generated.key_id, + "builder_public_key": &generated.public_key, + "policy_builder": { + "builder_id": &builder_id, + "trust_domain": &trust_domain, + "public_key": &generated.public_key, + }, + "private_key_storage": private_key_storage, + }); + let human_lines = vec![ + "Reproducer builder key created".green().to_string(), + format!(" Builder: {builder_id}"), + format!(" Trust domain: {trust_domain}"), + format!(" Builder key id: {}", generated.key_id), + format!(" Builder public key: {}", generated.public_key), + storage_line, + String::new(), + "Send only policy_builder to the Registry operator. Keep the private key inside this builder's independent custody." + .to_string(), + ]; + CommandOutcome { machine, human_lines }.emit(args.json) + } + fn auth_capability_submit(args: AuthCapabilitySubmitArgs) -> Result<()> { let api_base = resolve_registry_api_base(args.api_url)?; let registry_origin = registry_origin_from_api_base(&api_base)?; @@ -4015,10 +4396,10 @@ impl CommandExecutor { payload.registry_origin, registry_origin ))); } - let joyid_signature = read_json_value(&args.joyid_signature)?; + let wallet_signature = read_json_value(&args.wallet_signature)?; let body = serde_json::json!({ "payload": payload, - "joyid_signature": joyid_signature, + "wallet_signature": wallet_signature, }); let endpoint = format!("{}/v1/capabilities", api_base.trim_end_matches('/')); let response = submit_registry_json_request(&endpoint, &body, "Submitted capability authorisation", args.json)?; @@ -4033,10 +4414,50 @@ impl CommandExecutor { Ok(()) } + fn auth_namespace_claim(args: AuthNamespaceClaimArgs) -> Result<()> { + let api_base = resolve_registry_api_base(args.api_url)?; + let registry_origin = registry_origin_from_api_base(&api_base)?; + let namespace = args.namespace.trim(); + if namespace.is_empty() { + return Err(crate::error::CompileError::without_span("namespace is required for registry namespace claim")); + } + let payload = read_capability_authorisation_payload(&args.payload)?; + if payload.registry_origin != registry_origin { + return Err(crate::error::CompileError::without_span(format!( + "namespace claim payload registry_origin '{}' does not match API origin '{}'", + payload.registry_origin, registry_origin + ))); + } + let namespace_scope_prefix = format!("publish:{namespace}/"); + if !payload.requested_scopes.iter().any(|scope| scope.starts_with(&namespace_scope_prefix)) { + return Err(crate::error::CompileError::without_span(format!( + "namespace claim payload has no publish scope for namespace '{namespace}'" + ))); + } + let wallet_signature = read_json_value(&args.wallet_signature)?; + let body = serde_json::json!({ + "namespace": namespace, + "payload": payload, + "wallet_signature": wallet_signature, + }); + let endpoint = format!("{}/v1/namespaces/claim", api_base.trim_end_matches('/')); + let response = submit_registry_json_request(&endpoint, &body, "Claimed registry namespace", args.json)?; + if !args.json + && let Some(status) = response.get("status").and_then(serde_json::Value::as_str) + { + println!(" Namespace: {namespace}"); + println!(" Status: {status}"); + if status != "active" { + println!(" Publishing remains blocked until registry review activates the namespace."); + } + } + Ok(()) + } + fn auth_capability_revoke(args: AuthCapabilityRevokeArgs) -> Result<()> { - if args.payload.is_none() && args.joyid_signature.is_some() { + if args.payload.is_none() && args.wallet_signature.is_some() { return Err(crate::error::CompileError::without_span( - "capability revocation with --joyid-signature must use --payload from a previously generated revoke challenge", + "capability revocation with --wallet-signature must use --payload from a previously generated revoke challenge", )); } @@ -4051,9 +4472,12 @@ impl CommandExecutor { .principal_type .or_else(|| std::env::var("CELLSCRIPT_PRINCIPAL_TYPE").ok()) .unwrap_or_else(|| "joyid_ckb".to_string()); + if principal_type != "joyid_ckb" && principal_type != "ckb_secp256k1" { + return Err(crate::error::CompileError::without_span("principal type must be joyid_ckb or ckb_secp256k1")); + } let principal_id = args.principal_id.or_else(|| std::env::var("CELLSCRIPT_PRINCIPAL_ID").ok()).ok_or_else(|| { crate::error::CompileError::without_span( - "principal id is required for capability revoke; pass --principal-id or set CELLSCRIPT_PRINCIPAL_ID to the normalized JoyID/CKB identity binding", + "principal id is required for capability revoke; pass --principal-id or set CELLSCRIPT_PRINCIPAL_ID to the normalized wallet identity binding", ) })?; let capability_key_id = @@ -4077,7 +4501,7 @@ impl CommandExecutor { ) }; - let Some(signature_path) = args.joyid_signature.as_deref() else { + let Some(signature_path) = args.wallet_signature.as_deref() else { if args.json { print_json(&serde_json::to_value(&payload)?)?; } else { @@ -4088,8 +4512,8 @@ impl CommandExecutor { println!(" Principal: {}:{}", payload.principal_type, payload.principal_id); println!(" Capability key id: {}", payload.capability_key_id); println!(); - println!("Sign this payload with JoyID, then submit it with:"); - println!(" cellc auth capability revoke --payload --joyid-signature "); + println!("Sign this payload with the matching JoyID or CKB wallet, then submit it with:"); + println!(" cellc auth capability revoke --payload --wallet-signature "); println!("{}", serde_json::to_string_pretty(&payload)?); } return Ok(()); @@ -4103,10 +4527,10 @@ impl CommandExecutor { payload.registry_origin, registry_origin ))); } - let joyid_signature = read_json_value(signature_path)?; + let wallet_signature = read_json_value(signature_path)?; let mut body = serde_json::json!({ "payload": payload, - "joyid_signature": joyid_signature, + "wallet_signature": wallet_signature, }); if let Some(reason) = args.reason.filter(|reason| !reason.trim().is_empty()) { body["reason"] = serde_json::Value::String(reason); @@ -4153,6 +4577,12 @@ impl CommandExecutor { lockfile.package.version, deployed.package.version )); } + if lockfile.package.edition != deployed.package.edition { + violations.push(format!( + "package edition mismatch: Cell.lock has '{}', Deployed.toml has '{}'", + lockfile.package.edition, deployed.package.edition + )); + } if let (Some(lock_hash), Some(deployed_hash)) = (&lockfile.package.source_hash, &deployed.package.source_hash) { if lock_hash != deployed_hash { violations.push(format!("source_hash mismatch: Cell.lock has '{}', Deployed.toml has '{}'", lock_hash, deployed_hash)); @@ -4171,6 +4601,18 @@ impl CommandExecutor { } if let (Some(build), Some(deployed_build)) = (&lockfile.package_build, &deployed.build) { + if build.edition != deployed_build.edition { + violations.push(format!( + "build edition mismatch: Cell.lock has '{}', Deployed.toml has '{}'", + build.edition, deployed_build.edition + )); + } + if build.compatibility_profile_hash != deployed_build.compatibility_profile_hash { + violations.push(format!( + "compatibility_profile_hash mismatch: Cell.lock has '{}', Deployed.toml has '{}'", + build.compatibility_profile_hash, deployed_build.compatibility_profile_hash + )); + } compare_optional_build_field( "compiler_version", &build.compiler_version, @@ -4324,7 +4766,7 @@ impl CommandExecutor { fn package_verify(args: PackageVerifyArgs) -> Result<()> { let root = std::path::Path::new("."); - let mut pm = PackageManager::new(root); + let pm = PackageManager::new(root); let manifest = pm.read_manifest()?; // Read Cell.lock @@ -4345,6 +4787,12 @@ impl CommandExecutor { manifest.package.version, lockfile.package.version )); } + if lockfile.package.edition != manifest.package.edition { + violations.push(format!( + "package edition mismatch: Cell.toml has '{}', Cell.lock has '{}'", + manifest.package.edition, lockfile.package.edition + )); + } if lockfile.package.namespace != manifest.package.namespace { violations.push(format!( "package namespace mismatch: Cell.toml has '{:?}', Cell.lock has '{:?}'", @@ -4367,10 +4815,40 @@ impl CommandExecutor { None => violations.push("Cell.lock has no [package.build]; run 'cellc build' to populate build identity".to_string()), } - pm.resolve_dependencies()?; - for issue in lockfile.consistency_issues_with_resolved(&manifest, pm.get_resolved()) { + let computed_manifest_digest = crate::package::compute_manifest_digest(root)?; + if lockfile.root.manifest_digest != computed_manifest_digest { + violations.push(format!( + "manifest digest mismatch: Cell.lock has '{}', computed '{}'", + lockfile.root.manifest_digest, computed_manifest_digest + )); + } + for issue in lockfile.consistency_issues(&manifest) { violations.push(issue); } + let mut verification_modes = Vec::new(); + if manifest.dependency_overrides.is_empty() { + verification_modes.push(None); + } + verification_modes.extend(manifest.environments.keys().cloned().map(Some)); + let mut resolved = BTreeMap::new(); + for environment in verification_modes { + let options = crate::package::ResolutionOptions { + scope: crate::package::DependencyScope::Test, + all_features: true, + environment, + ..crate::package::ResolutionOptions::default() + }; + let mut mode_manager = PackageManager::new(root); + match mode_manager.resolve_locked_dependencies(&options) { + Ok(()) => resolved.extend(mode_manager.get_resolved().clone()), + Err(error) => violations.push(format!("locked dependency materialization failed: {}", error)), + } + } + for issue in lockfile.consistency_issues_with_resolved(&manifest, &resolved) { + if !violations.contains(&issue) { + violations.push(issue); + } + } for (name, locked) in &lockfile.dependencies { if matches!(locked.source, crate::package::LockedSource::Registry { .. }) && locked.source_hash.is_none() { violations.push(format!("registry dependency '{}' has no source_hash in Cell.lock", name)); @@ -4548,12 +5026,12 @@ fn civil_date_from_days(z: i32) -> (i32, u32, u32) { (y, m as u32, d as u32) } -fn current_utc_timestamp() -> String { +pub(super) fn current_utc_timestamp() -> String { let secs = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(); utc_timestamp_from_unix_secs(secs) } -fn utc_timestamp_after_seconds(delta_secs: u64) -> String { +pub(super) fn utc_timestamp_after_seconds(delta_secs: u64) -> String { let secs = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(); utc_timestamp_from_unix_secs(secs.saturating_add(delta_secs)) } @@ -4569,28 +5047,64 @@ fn utc_timestamp_from_unix_secs(secs: u64) -> String { } fn resolve_requested_scopes(mut scopes: Vec) -> Result> { - scopes.retain(|scope| !scope.trim().is_empty()); + if scopes.iter().any(|scope| scope.trim().is_empty()) { + return Err(invalid_capability_scope("")); + } + scopes = scopes.into_iter().map(|scope| scope.trim().to_string()).collect(); if !scopes.is_empty() { + validate_capability_scopes(&scopes)?; return Ok(scopes); } let manifest = PackageManager::new(".").read_manifest().map_err(|_| { crate::error::CompileError::without_span( - "at least one capability scope is required; pass --scope publish:/ outside a package directory", + "at least one capability scope is required outside a package directory; pass --scope :/", ) })?; let namespace = manifest.package.namespace.ok_or_else(|| { crate::error::CompileError::without_span( - "cannot infer capability scope because [package].namespace is missing; pass --scope publish:/", + "cannot infer the publish capability scope because [package].namespace is missing; pass --scope publish:/", ) })?; if manifest.package.name.is_empty() { return Err(crate::error::CompileError::without_span( - "cannot infer capability scope because [package].name is empty; pass --scope publish:/", + "cannot infer the publish capability scope because [package].name is empty; pass --scope publish:/", )); } - Ok(vec![format!("publish:{}/{}", namespace, manifest.package.name)]) + let coordinate = format!("{}/{}", namespace, manifest.package.name); + Ok(vec![format!("publish:{coordinate}")]) +} + +fn validate_capability_scopes(scopes: &[String]) -> Result<()> { + let mut seen = BTreeSet::new(); + for scope in scopes { + if !seen.insert(scope.as_str()) { + return Err(crate::error::CompileError::without_span(format!("duplicate capability scope '{scope}'")) + .with_category(crate::error::CompileErrorCategory::Usage)); + } + let Some((action, coordinate)) = scope.split_once(':') else { + return Err(invalid_capability_scope(scope)); + }; + if !matches!(action, "publish" | "deployment" | "availability") { + return Err(invalid_capability_scope(scope)); + } + let Some((namespace, name)) = coordinate.split_once('/') else { + return Err(invalid_capability_scope(scope)); + }; + validate_declared_artifact_ident(namespace, "capability scope namespace").map_err(|_| invalid_capability_scope(scope))?; + if name != "*" { + validate_declared_artifact_ident(name, "capability scope package").map_err(|_| invalid_capability_scope(scope))?; + } + } + Ok(()) +} + +fn invalid_capability_scope(scope: &str) -> CompileError { + crate::error::CompileError::without_span(format!( + "invalid capability scope '{scope}'; expected :/", + )) + .with_category(crate::error::CompileErrorCategory::Usage) } fn resolve_capability_expires_at(explicit_timestamp: Option, relative: Option) -> Result { @@ -4675,17 +5189,45 @@ struct GeneratedRegistryCapabilityKey { capability_pubkey: String, } -fn generate_and_store_registry_capability_key() -> Result { +struct GeneratedRegistryKeyMaterial { + key_id: String, + public_key: String, + private_key_pkcs8: Vec, +} + +const REGISTRY_KEYCHAIN_SECRET_SCHEMA: &str = "cellscript-registry-private-key-v1"; + +#[derive(serde::Deserialize, serde::Serialize)] +struct RegistryKeychainSecret { + schema: String, + status: String, + pkcs8_b64: String, + #[serde(skip_serializing_if = "Option::is_none")] + session_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + expires_at: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pending_expires_at_unix_seconds: Option, +} + +fn generate_registry_key_material() -> Result { let rng = ring::rand::SystemRandom::new(); let pkcs8 = ring::signature::EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng) - .map_err(|error| crate::error::CompileError::without_span(format!("failed to generate capability key: {:?}", error)))?; + .map_err(|error| crate::error::CompileError::without_span(format!("failed to generate P-256 registry key: {:?}", error)))?; let key_pair = ring::signature::EcdsaKeyPair::from_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng) - .map_err(|error| crate::error::CompileError::without_span(format!("failed to load generated capability key: {:?}", error)))?; + .map_err(|error| { + crate::error::CompileError::without_span(format!("failed to load generated P-256 registry key: {:?}", error)) + })?; let spki = p256_spki_der_from_uncompressed_public_key(key_pair.public_key().as_ref())?; - let capability_pubkey = format!("p256-spki:{}", base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(spki)); - let key_id = registry_capability_key_id(&capability_pubkey); - store_registry_capability_private_key(&key_id, pkcs8.as_ref())?; - Ok(GeneratedRegistryCapabilityKey { key_id, capability_pubkey }) + let public_key = format!("p256-spki:{}", base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(spki)); + let key_id = registry_capability_key_id(&public_key); + Ok(GeneratedRegistryKeyMaterial { key_id, public_key, private_key_pkcs8: pkcs8.as_ref().to_vec() }) +} + +fn generate_and_store_registry_capability_key() -> Result { + let generated = generate_registry_key_material()?; + store_registry_private_key(&generated.key_id, &generated.private_key_pkcs8)?; + Ok(GeneratedRegistryCapabilityKey { key_id: generated.key_id, capability_pubkey: generated.public_key }) } fn registry_capability_key_id(capability_pubkey: &str) -> String { @@ -4701,7 +5243,7 @@ fn p256_spki_der_from_uncompressed_public_key(public_key: &[u8]) -> Result Result Result<()> { - let secret = base64::engine::general_purpose::STANDARD.encode(pkcs8); - let entry = keyring::Entry::new("cellscript-registry", key_id).map_err(|error| { - crate::error::CompileError::without_span(format!("failed to open OS keychain: {}", error)) - .with_category(crate::error::CompileErrorCategory::Authentication) - .with_source(error) - })?; - entry.set_password(&secret).map_err(|error| { - crate::error::CompileError::without_span(format!( - "failed to store capability private key '{}' in OS keychain: {}", - key_id, error - )) - .with_category(crate::error::CompileErrorCategory::Authentication) +fn store_registry_private_key(key_id: &str, pkcs8: &[u8]) -> Result<()> { + store_registry_keychain_secret( + key_id, + &RegistryKeychainSecret { + schema: REGISTRY_KEYCHAIN_SECRET_SCHEMA.to_string(), + status: "active".to_string(), + pkcs8_b64: base64::engine::general_purpose::STANDARD.encode(pkcs8), + session_id: None, + expires_at: None, + pending_expires_at_unix_seconds: None, + }, + ) +} + +fn store_pending_registry_private_key(key_id: &str, pkcs8: &[u8], session_id: &str, expires_at: &str) -> Result<()> { + let pending_expires_at_unix_seconds = + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs().saturating_add(15 * 60); + store_registry_keychain_secret( + key_id, + &RegistryKeychainSecret { + schema: REGISTRY_KEYCHAIN_SECRET_SCHEMA.to_string(), + status: "pending".to_string(), + pkcs8_b64: base64::engine::general_purpose::STANDARD.encode(pkcs8), + session_id: Some(session_id.to_string()), + expires_at: Some(expires_at.to_string()), + pending_expires_at_unix_seconds: Some(pending_expires_at_unix_seconds), + }, + ) +} + +fn store_registry_keychain_secret(key_id: &str, secret: &RegistryKeychainSecret) -> Result<()> { + let encoded = serde_json::to_string(secret).map_err(|error| { + crate::error::CompileError::without_span(format!("failed to encode registry private-key state: {error}")) + .with_category(crate::error::CompileErrorCategory::Authentication) + })?; + let entry = keyring::Entry::new("cellscript-registry", key_id).map_err(|error| { + crate::error::CompileError::without_span(format!("failed to open OS keychain: {}", error)) + .with_category(crate::error::CompileErrorCategory::Authentication) + .with_source(error) + })?; + entry.set_password(&encoded).map_err(|error| { + crate::error::CompileError::without_span(format!( + "failed to store registry P-256 private key '{}' in OS keychain: {}", + key_id, error + )) + .with_category(crate::error::CompileErrorCategory::Authentication) .with_source(error) }) } +fn remove_registry_private_key(key_id: &str) -> Result<()> { + let entry = keyring::Entry::new("cellscript-registry", key_id).map_err(|error| { + crate::error::CompileError::without_span(format!("failed to open OS keychain: {}", error)) + .with_category(crate::error::CompileErrorCategory::Authentication) + .with_source(error) + })?; + match entry.delete_credential() { + Ok(()) | Err(keyring::Error::NoEntry) => Ok(()), + Err(error) => Err(crate::error::CompileError::without_span(format!( + "failed to remove pending registry private key '{}' from OS keychain: {}", + key_id, error + )) + .with_category(crate::error::CompileErrorCategory::Authentication) + .with_source(error)), + } +} + +fn write_new_reproducer_private_key(path: &Path, pkcs8: &[u8]) -> Result<()> { + #[cfg(not(unix))] + { + let _ = (path, pkcs8); + return Err(crate::error::CompileError::without_span( + "--private-key-output requires Unix mode-0600 permission semantics; use the OS keychain on this platform", + ) + .with_category(crate::error::CompileErrorCategory::Authentication)); + } + + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt as _; + + let mut options = std::fs::OpenOptions::new(); + options.write(true).create_new(true); + options.mode(0o600); + let mut file = options.open(path).map_err(|error| { + crate::error::CompileError::without_span(format!( + "failed to create reproducer private-key file '{}': {}", + path.display(), + error + )) + .with_category(crate::error::CompileErrorCategory::Authentication) + .with_source(error) + })?; + let secret = base64::engine::general_purpose::STANDARD.encode(pkcs8); + file.write_all(secret.as_bytes()).and_then(|_| file.write_all(b"\n")).map_err(|error| { + crate::error::CompileError::without_span(format!( + "failed to write reproducer private-key file '{}': {}", + path.display(), + error + )) + .with_category(crate::error::CompileErrorCategory::Authentication) + .with_source(error) + })?; + file.sync_all().map_err(|error| { + crate::error::CompileError::without_span(format!( + "failed to sync reproducer private-key file '{}': {}", + path.display(), + error + )) + .with_category(crate::error::CompileErrorCategory::Authentication) + .with_source(error) + }) + } +} + fn sign_registry_publish_payload(key_id: &str, canonical_payload: &str) -> Result { + sign_registry_capability_payload(key_id, canonical_payload) +} + +pub(super) fn sign_registry_capability_payload(key_id: &str, canonical_payload: &str) -> Result { let Some(pkcs8) = load_registry_capability_private_key(key_id)? else { return Err( crate::error::CompileError::without_span(format!( @@ -4741,6 +5385,19 @@ fn sign_registry_publish_payload(key_id: &str, canonical_payload: &str) -> Resul sign_registry_publish_payload_with_pkcs8(&pkcs8, canonical_payload) } +pub(super) fn sign_registry_reproducer_payload(key_id: &str, canonical_payload: &str) -> Result { + let Some(pkcs8) = load_registry_reproducer_private_key(key_id)? else { + return Err( + crate::error::CompileError::without_span(format!( + "reproducer signature key '{}' was not found in the OS keychain; set CELLSCRIPT_REPRODUCER_PRIVATE_KEY_PKCS8_B64 for an isolated builder", + key_id + )) + .with_category(crate::error::CompileErrorCategory::Authentication), + ); + }; + sign_registry_publish_payload_with_pkcs8(&pkcs8, canonical_payload) +} + fn load_registry_capability_private_key(key_id: &str) -> Result>> { if let Ok(value) = std::env::var("CELLSCRIPT_CAPABILITY_PRIVATE_KEY_PKCS8_B64") { let trimmed = value.trim(); @@ -4755,6 +5412,27 @@ fn load_registry_capability_private_key(key_id: &str) -> Result>> } } + load_registry_keychain_private_key(key_id) +} + +fn load_registry_reproducer_private_key(key_id: &str) -> Result>> { + if let Ok(value) = std::env::var("CELLSCRIPT_REPRODUCER_PRIVATE_KEY_PKCS8_B64") { + let trimmed = value.trim(); + if !trimmed.is_empty() { + let decoded = base64::engine::general_purpose::STANDARD.decode(trimmed).map_err(|error| { + crate::error::CompileError::without_span(format!( + "failed to decode CELLSCRIPT_REPRODUCER_PRIVATE_KEY_PKCS8_B64: {}", + error + )) + })?; + return Ok(Some(decoded)); + } + } + + load_registry_keychain_private_key(key_id) +} + +fn load_registry_keychain_private_key(key_id: &str) -> Result>> { let entry = keyring::Entry::new("cellscript-registry", key_id).map_err(|error| { crate::error::CompileError::without_span(format!("failed to open OS keychain: {}", error)) .with_category(crate::error::CompileErrorCategory::Authentication) @@ -4762,7 +5440,31 @@ fn load_registry_capability_private_key(key_id: &str) -> Result>> })?; match entry.get_password() { Ok(secret) => { - let decoded = base64::engine::general_purpose::STANDARD.decode(secret.trim()).map_err(|error| { + let trimmed = secret.trim(); + let encoded = if trimmed.starts_with('{') { + let stored: RegistryKeychainSecret = serde_json::from_str(trimmed).map_err(|error| { + crate::error::CompileError::without_span(format!( + "failed to decode registry private-key state '{}' from OS keychain: {}", + key_id, error + )) + .with_category(crate::error::CompileErrorCategory::Authentication) + })?; + if stored.schema != REGISTRY_KEYCHAIN_SECRET_SCHEMA || !matches!(stored.status.as_str(), "pending" | "active") { + return Err(crate::error::CompileError::without_span(format!( + "registry private key '{}' has an unsupported OS keychain state", + key_id + )) + .with_category(crate::error::CompileErrorCategory::Authentication)); + } + // A process may exit after the wallet commits authority but before the CLI + // observes it. Only an observed terminal session state may remove a pending + // key; local time alone cannot distinguish that case from an abandoned session. + stored.pkcs8_b64 + } else { + // Compatibility with keys written before the key lifecycle envelope was introduced. + trimmed.to_string() + }; + let decoded = base64::engine::general_purpose::STANDARD.decode(encoded.trim()).map_err(|error| { crate::error::CompileError::without_span(format!( "failed to decode capability private key '{}' from OS keychain: {}", key_id, error @@ -4813,6 +5515,401 @@ fn registry_publish_nonce( format!("0x{}", hex::encode(crate::ckb_blake2b256(material.as_bytes()))) } +#[derive(Debug, serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct DeclaredArtifactManifest { + schema: String, + namespace: String, + name: String, + release: String, + kind: String, + language: String, + bundle: PathBuf, + #[serde(default)] + description: String, + #[serde(default)] + repository: String, + #[serde(default)] + homepage: String, + #[serde(default)] + documentation: String, + #[serde(default)] + keywords: Vec, + #[serde(default)] + categories: Vec, +} + +#[derive(Debug, serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct DeclaredArtifactBundle { + schema: String, + namespace: String, + name: String, + release: String, + profile: String, + manifest_json: String, + objects: Vec, +} + +#[derive(Debug, serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct DeclaredArtifactBundleObject { + role: String, + content_base64: String, +} + +fn publish_declared_artifact(args: PublishArgs, manifest_path: &Path) -> Result<()> { + if args.payload.is_some() || args.source_snapshot.is_some() { + return Err(crate::error::CompileError::without_span( + "--artifact-manifest owns the publish payload and immutable bundle; do not combine it with --payload or --source-snapshot", + )); + } + let manifest_text = std::fs::read_to_string(manifest_path).map_err(|error| { + crate::error::CompileError::without_span(format!("failed to read artifact manifest '{}': {}", manifest_path.display(), error)) + })?; + let manifest: DeclaredArtifactManifest = toml::from_str(&manifest_text).map_err(|error| { + crate::error::CompileError::without_span(format!("failed to parse artifact manifest '{}': {}", manifest_path.display(), error)) + })?; + if manifest.schema != "cellscript-registry-artifact" { + return Err(crate::error::CompileError::without_span("Artifact.toml schema must be 'cellscript-registry-artifact'")); + } + validate_declared_artifact_ident(&manifest.namespace, "namespace")?; + validate_declared_artifact_ident(&manifest.name, "name")?; + validate_declared_artifact_release(&manifest.release)?; + let artifact = declared_artifact_descriptor(&manifest.kind, &manifest.language)?; + let manifest_dir = manifest_path.parent().unwrap_or_else(|| Path::new(".")); + let bundle_path = if manifest.bundle.is_absolute() { manifest.bundle.clone() } else { manifest_dir.join(&manifest.bundle) }; + let bundle_bytes = std::fs::read(&bundle_path).map_err(|error| { + crate::error::CompileError::without_span(format!("failed to read artifact bundle '{}': {}", bundle_path.display(), error)) + })?; + if bundle_bytes.is_empty() || bundle_bytes.len() > 5 * 1024 * 1024 { + return Err(crate::error::CompileError::without_span("artifact bundle must be a non-empty JSON file no larger than 5 MiB")); + } + let bundle: DeclaredArtifactBundle = serde_json::from_slice(&bundle_bytes).map_err(|error| { + crate::error::CompileError::without_span(format!("failed to parse artifact bundle '{}': {}", bundle_path.display(), error)) + })?; + if bundle.schema != "cellscript-registry-bundle" + || bundle.namespace != manifest.namespace + || bundle.name != manifest.name + || bundle.release != manifest.release + || bundle.profile != artifact.profile + { + return Err(crate::error::CompileError::without_span( + "artifact bundle schema, coordinate, release, or profile does not match Artifact.toml", + )); + } + let manifest_json: serde_json::Value = serde_json::from_str(&bundle.manifest_json).map_err(|error| { + crate::error::CompileError::without_span(format!("artifact bundle manifest_json must be valid JSON: {error}")) + })?; + if !manifest_json.is_object() { + return Err(crate::error::CompileError::without_span("artifact bundle manifest_json must encode a JSON object")); + } + validate_declared_bundle_roles(&bundle, &artifact.profile, &manifest_json)?; + let source = declared_bundle_object(&bundle, "source")?; + let source_hash = hex::encode(crate::ckb_blake2b256(&source)); + let mut artifact_hash = None; + let mut abi_hash = None; + let mut abi_sha256 = None; + let mut executable_ls_idl_bound = None; + let mut build_recipe_hash = None; + let audit_report_hash = if manifest_json.pointer("/security/audit_report_hash").is_some() { + Some(hex::encode(crate::ckb_blake2b256(&declared_bundle_object(&bundle, "audit_report")?))) + } else { + None + }; + if artifact.profile == "ckb_executable" { + let executable = declared_bundle_object(&bundle, "executable")?; + let abi = declared_bundle_object(&bundle, "abi")?; + artifact_hash = Some(hex::encode(crate::ckb_blake2b256(&executable))); + abi_hash = Some(hex::encode(crate::ckb_blake2b256(&abi))); + if manifest_json.get("interface").is_some() { + use sha2::Digest as _; + crate::package::registry::validate_ls_idl_document(&abi).map_err(crate::error::CompileError::without_span)?; + let digest = sha2::Sha256::digest(&abi); + abi_sha256 = Some(hex::encode(digest)); + executable_ls_idl_bound = Some(executable.ends_with(digest.as_slice())); + } + if manifest_json.pointer("/build/reproducible").and_then(serde_json::Value::as_bool) == Some(true) { + build_recipe_hash = Some(hex::encode(crate::ckb_blake2b256(&declared_bundle_object(&bundle, "build_recipe")?))); + } + } else if artifact.profile == "reproducible_build" { + artifact_hash = Some(hex::encode(crate::ckb_blake2b256(&declared_bundle_object(&bundle, "executable")?))); + build_recipe_hash = Some(hex::encode(crate::ckb_blake2b256(&declared_bundle_object(&bundle, "build_recipe")?))); + } + crate::package::registry::validate_artifact_profile_contract( + &artifact.kind, + &artifact.profile, + &manifest_json, + crate::package::registry::ArtifactContractHashes { + artifact_hash: artifact_hash.as_deref(), + abi_hash: abi_hash.as_deref(), + abi_sha256: abi_sha256.as_deref(), + executable_ls_idl_bound, + build_recipe_hash: build_recipe_hash.as_deref(), + audit_report_hash: audit_report_hash.as_deref(), + }, + ) + .map_err(crate::error::CompileError::without_span)?; + let canonical_manifest_json = crate::package::registry::canonical_artifact_contract_json(&manifest_json) + .map_err(crate::error::CompileError::without_span)?; + let manifest_hash = hex::encode(crate::ckb_blake2b256(canonical_manifest_json.as_bytes())); + let mut release = serde_json::json!({ + "version": manifest.release, + "tag": format!("v{}", manifest.release), + "source_hash": source_hash, + "verification_status": "pending", + "deployment_status": if artifact.profile == "ckb_executable" { "undeployed" } else { "not_applicable" }, + "availability_status": "active", + "profile_contract": manifest_json, + }); + if let Some(value) = artifact_hash { + release["artifact_hash"] = serde_json::Value::String(value); + } + if let Some(value) = abi_hash { + release["abi_hash"] = serde_json::Value::String(value); + } + if let Some(value) = build_recipe_hash { + release["build_recipe_hash"] = serde_json::Value::String(value); + } + let mut registry_entry = serde_json::json!({ + "schema_version": crate::package::registry::RegistryIndex::CURRENT_SCHEMA_VERSION, + "namespace": manifest.namespace, + "name": manifest.name, + "artifact": artifact, + "versions": [release], + }); + let entry = registry_entry.as_object_mut().expect("registry entry JSON object"); + for (key, value) in [ + ("description", &manifest.description), + ("repository", &manifest.repository), + ("homepage", &manifest.homepage), + ("documentation", &manifest.documentation), + ] { + if !value.is_empty() { + entry.insert(key.to_string(), serde_json::Value::String(value.clone())); + } + } + if !manifest.keywords.is_empty() { + entry.insert( + "keywords".to_string(), + serde_json::to_value(&manifest.keywords).map_err(|error| { + crate::error::CompileError::without_span(format!("failed to serialize artifact keywords: {error}")) + })?, + ); + } + if !manifest.categories.is_empty() { + entry.insert( + "categories".to_string(), + serde_json::to_value(&manifest.categories).map_err(|error| { + crate::error::CompileError::without_span(format!("failed to serialize artifact categories: {error}")) + })?, + ); + } + + if args.dry_run { + let summary = serde_json::json!({ + "status": "valid", + "coordinate": format!("{}/{}@{}", manifest.namespace, manifest.name, manifest.release), + "artifact": artifact, + "source_hash": source_hash, + "manifest_hash": manifest_hash, + "bundle": bundle_path, + }); + if args.json { + return print_json(&summary); + } + println!("{}", "Artifact publish dry-run passed".green()); + println!(" Coordinate: {}/{}@{}", manifest.namespace, manifest.name, manifest.release); + println!(" Kind: {}", artifact.kind); + println!(" Profile: {}", artifact.profile); + println!(" Source hash: {}", source_hash); + return Ok(()); + } + + let api_base = resolve_registry_api_base(args.api_url)?; + let registry_origin = registry_origin_from_api_base(&api_base)?; + let endpoint = registry_publish_endpoint(&api_base, &manifest.namespace, &manifest.name); + let capability_key_id = match args.capability_key_id.or_else(|| std::env::var("CELLSCRIPT_CAPABILITY_KEY_ID").ok()) { + Some(key_id) => key_id, + None if args.authorise => { + authorise_registry_publish_key(&api_base, &manifest.namespace, &manifest.name, &artifact.kind, args.no_open)? + } + None => { + return Err(crate::error::CompileError::without_span(format!( + "publishing {}/{} requires a wallet-authorised publishing key; run `cellc publish --authorise` for the continuous browser flow, or pass an existing --capability-key-id", + manifest.namespace, manifest.name + )) + .with_category(crate::error::CompileErrorCategory::Authentication)); + } + }; + let issued_at = current_utc_timestamp(); + let expires_at = utc_timestamp_after_seconds(10 * 60); + let nonce = registry_publish_nonce( + ®istry_origin, + &manifest.namespace, + &manifest.name, + &manifest.release, + &source_hash, + &capability_key_id, + &issued_at, + ); + let payload = crate::package::registry::RegistryPublishPayload { + protocol: crate::package::registry::REGISTRY_PUBLISH_PROTOCOL.to_string(), + action: crate::package::registry::PUBLISH_ACTION.to_string(), + registry_origin, + namespace: manifest.namespace, + name: manifest.name, + version: manifest.release, + source_hash: source_hash.clone(), + manifest_hash, + capability_key_id, + nonce, + issued_at, + expires_at, + cli_version: crate::VERSION.to_string(), + artifact, + registry_entry, + }; + let canonical_payload = registry_publish_canonical_payload(&payload)?; + if args.print_payload { + return print_json(&serde_json::json!({ "endpoint": endpoint, "payload": payload, "canonical_payload": canonical_payload })); + } + let capability_signature = + if let Some(signature) = args.capability_signature.or_else(|| std::env::var("CELLSCRIPT_CAPABILITY_SIGNATURE").ok()) { + signature + } else { + sign_registry_publish_payload(&payload.capability_key_id, &canonical_payload)? + }; + let request = crate::package::registry::RegistryPublishRequest { + payload, + capability_signature: crate::package::registry::RegistryCapabilitySignature { + algorithm: "p256-sha256".to_string(), + signature: capability_signature, + }, + source_snapshot: crate::package::registry::RegistrySourceSnapshot { + content_base64: base64::engine::general_purpose::STANDARD.encode(&bundle_bytes), + content_type: "application/vnd.cellscript.artifact-bundle+json".to_string(), + size_bytes: bundle_bytes.len() as u64, + source_hash, + }, + }; + let idempotency_key = resolve_registry_publish_idempotency_key(args.idempotency_key.as_deref(), &request)?; + submit_registry_publish_request(&endpoint, &request, &idempotency_key, args.json) +} + +fn declared_artifact_descriptor(kind: &str, language: &str) -> Result { + let allowed_language = match kind { + "runtime_verifier" | "deployable_contract" => matches!(language, "cellscript" | "rust" | "c" | "javascript" | "other"), + "reproducible_binary" => matches!(language, "rust" | "c" | "other"), + "template" => matches!(language, "cellscript" | "rust" | "c" | "javascript" | "other" | "unspecified"), + "source_library" | "profile_library" => { + return Err(crate::error::CompileError::without_span( + "source_library and profile_library use the native CellScript package publish path", + )); + } + _ => return Err(crate::error::CompileError::without_span(format!("unknown artifact kind '{kind}'"))), + }; + if !allowed_language { + return Err(crate::error::CompileError::without_span(format!( + "language '{language}' is not valid for artifact kind '{kind}'" + ))); + } + let (profile, consumption_mode) = match kind { + "runtime_verifier" => ("ckb_executable", "tcb"), + "deployable_contract" => ("ckb_executable", "deployment"), + "reproducible_binary" => ("reproducible_build", "tcb"), + "template" => ("copy_material", "copy"), + _ => unreachable!("artifact kind was checked above"), + }; + Ok(crate::package::registry::RegistryArtifactDescriptor { + kind: kind.to_string(), + profile: profile.to_string(), + consumption_mode: consumption_mode.to_string(), + language: language.to_string(), + }) +} + +fn declared_bundle_object(bundle: &DeclaredArtifactBundle, role: &str) -> Result> { + let mut objects = bundle.objects.iter().filter(|object| object.role == role); + let object = objects + .next() + .ok_or_else(|| crate::error::CompileError::without_span(format!("artifact bundle is missing required '{role}' object")))?; + if objects.next().is_some() { + return Err(crate::error::CompileError::without_span(format!("artifact bundle contains more than one '{role}' object"))); + } + let bytes = base64::engine::general_purpose::STANDARD.decode(&object.content_base64).map_err(|error| { + crate::error::CompileError::without_span(format!("artifact bundle '{role}' object is not valid base64: {error}")) + })?; + if bytes.is_empty() { + return Err(crate::error::CompileError::without_span(format!("artifact bundle '{role}' object must not be empty"))); + } + Ok(bytes) +} + +fn validate_declared_bundle_roles(bundle: &DeclaredArtifactBundle, profile: &str, contract: &serde_json::Value) -> Result<()> { + let mut required = match profile { + "ckb_executable" => vec!["source", "executable", "abi"], + "reproducible_build" => vec!["source", "executable", "build_recipe"], + "copy_material" => vec!["source"], + other => { + return Err(crate::error::CompileError::without_span(format!("unsupported declared artifact profile '{other}'"))); + } + }; + if contract.pointer("/security/audit_report_hash").is_some() { + required.push("audit_report"); + } + if profile == "ckb_executable" && contract.pointer("/build/reproducible").and_then(serde_json::Value::as_bool) == Some(true) { + required.push("build_recipe"); + } + let mut seen = BTreeSet::new(); + for object in &bundle.objects { + if !required.contains(&object.role.as_str()) { + return Err(crate::error::CompileError::without_span(format!( + "artifact bundle role '{}' is not allowed for profile '{profile}'", + object.role + ))); + } + if !seen.insert(object.role.as_str()) { + return Err(crate::error::CompileError::without_span(format!( + "artifact bundle contains more than one '{}' object", + object.role + ))); + } + } + for role in required { + if !seen.contains(role) { + return Err(crate::error::CompileError::without_span(format!("artifact bundle is missing required '{role}' object"))); + } + } + Ok(()) +} + +fn validate_declared_artifact_ident(value: &str, field: &str) -> Result<()> { + let bytes = value.as_bytes(); + let edge = |byte: u8| byte.is_ascii_lowercase() || byte.is_ascii_digit(); + if bytes.is_empty() + || bytes.len() > 64 + || !edge(bytes[0]) + || !edge(*bytes.last().expect("non-empty identifier")) + || !bytes.iter().all(|byte| edge(*byte) || matches!(*byte, b'_' | b'-')) + { + return Err(crate::error::CompileError::without_span(format!( + "artifact {field} must be 1-64 lowercase letters or numbers, with '_' or '-' only between characters" + ))); + } + Ok(()) +} + +fn validate_declared_artifact_release(value: &str) -> Result<()> { + let mut core = value.split(['-', '+']).next().unwrap_or_default().split('.'); + let valid = (0..3).all(|_| core.next().is_some_and(|part| !part.is_empty() && part.bytes().all(|byte| byte.is_ascii_digit()))) + && core.next().is_none(); + if !valid { + return Err(crate::error::CompileError::without_span("artifact release must be semver-like")); + } + Ok(()) +} + fn build_publish_registry_version( manifest: &PackageManifest, result: &crate::CompileResult, @@ -4835,6 +5932,8 @@ fn build_publish_registry_version( tag: format!("v{}", manifest.package.version), source_hash: source_hash.to_string(), cellscript_version: result.metadata.compiler_version.clone(), + edition: result.metadata.edition, + compatibility_profile_hash: hash_json_value("compatibility_profile", &result.metadata.compatibility_profile)?, dependencies: deps, abi_index: Some(metadata_abi_hash(&result.metadata)?), schema_hash: Some(result.metadata.molecule_schema_manifest.manifest_hash.clone()), @@ -4853,6 +5952,7 @@ fn build_publish_registry_entry( manifest: &PackageManifest, namespace: &str, version_entry: crate::package::registry::RegistryVersion, + artifact: &crate::package::registry::RegistryArtifactDescriptor, ) -> Result { let index = crate::package::registry::RegistryIndex { schema_version: crate::package::registry::RegistryIndex::CURRENT_SCHEMA_VERSION, @@ -4866,6 +5966,23 @@ fn build_publish_registry_entry( let Some(object) = value.as_object_mut() else { return Err(crate::error::CompileError::without_span("registry entry did not serialize as a JSON object")); }; + object.insert( + "artifact".to_string(), + serde_json::to_value(artifact).map_err(|error| { + crate::error::CompileError::without_span(format!("failed to serialize CellScript artifact descriptor: {}", error)) + })?, + ); + let published = object + .get_mut("versions") + .and_then(serde_json::Value::as_array_mut) + .and_then(|versions| versions.first_mut()) + .and_then(serde_json::Value::as_object_mut) + .ok_or_else(|| crate::error::CompileError::without_span("registry entry has no publish release"))?; + published.remove("status"); + published.remove("yanked"); + published.insert("verification_status".to_string(), serde_json::Value::String("pending".to_string())); + published.insert("deployment_status".to_string(), serde_json::Value::String("not_applicable".to_string())); + published.insert("availability_status".to_string(), serde_json::Value::String("active".to_string())); if !manifest.package.repository.is_empty() { object.insert("repository".to_string(), serde_json::Value::String(manifest.package.repository.clone())); } @@ -4897,7 +6014,22 @@ fn build_publish_registry_entry( Ok(value) } -fn resolve_registry_api_base(api_url: Option) -> Result { +fn cellscript_artifact_descriptor(kind: Option<&str>) -> Result { + let kind = kind.unwrap_or("source_library"); + if !matches!(kind, "source_library" | "profile_library") { + return Err(crate::error::CompileError::without_span( + "CellScript package artifact kind must be source_library or profile_library", + )); + } + Ok(crate::package::registry::RegistryArtifactDescriptor { + kind: kind.to_string(), + profile: "cellscript_source".to_string(), + consumption_mode: "dependency".to_string(), + language: "cellscript".to_string(), + }) +} + +pub(super) fn resolve_registry_api_base(api_url: Option) -> Result { let value = api_url .or_else(|| std::env::var("CELLSCRIPT_REGISTRY_API_URL").ok()) .or_else(|| std::env::var("CELLSCRIPT_REGISTRY_ORIGIN").ok()) @@ -4910,7 +6042,7 @@ fn resolve_registry_api_base(api_url: Option) -> Result { Ok(trimmed.to_string()) } -fn registry_origin_from_api_base(api_base: &str) -> Result { +pub(super) fn registry_origin_from_api_base(api_base: &str) -> Result { Ok(parse_registry_api_url(api_base)?.origin().ascii_serialization()) } @@ -4948,7 +6080,7 @@ fn parse_registry_api_url(api_base: &str) -> Result { } fn registry_publish_endpoint(api_base: &str, namespace: &str, name: &str) -> String { - format!("{}/v1/packages/{}/{}/versions", api_base.trim_end_matches('/'), namespace, name) + format!("{}/v1/artifacts/{}/{}/releases", api_base.trim_end_matches('/'), namespace, name) } fn resolve_registry_publish_idempotency_key( @@ -5038,6 +6170,15 @@ fn validate_publish_payload_matches_local_package( payload.source_hash, source_hash ))); } + if !matches!(payload.artifact.kind.as_str(), "source_library" | "profile_library") + || payload.artifact.profile != "cellscript_source" + || payload.artifact.consumption_mode != "dependency" + || payload.artifact.language != "cellscript" + { + return Err(crate::error::CompileError::without_span( + "CellScript package publish payload must declare the source_library/cellscript_source dependency contract", + )); + } Ok(()) } @@ -5232,7 +6373,7 @@ fn submit_registry_publish_request( Ok(()) } -fn registry_http_client() -> Result { +pub(super) fn registry_http_client() -> Result { reqwest::blocking::Client::builder().timeout(Duration::from_secs(30)).redirect(reqwest::redirect::Policy::none()).build().map_err( |error| { crate::error::CompileError::without_span(format!("failed to build registry HTTP client: {}", error)) @@ -5242,6 +6383,298 @@ fn registry_http_client() -> Result { ) } +#[derive(serde::Serialize)] +struct RegistryAuthorisationSessionCreateRequest { + capability_pubkey: String, + requested_scopes: Vec, + artifact_kind: String, + capability_expires_at: String, + cli_version: String, +} + +#[derive(serde::Deserialize)] +struct RegistryAuthorisationSessionCreateResponse { + session_id: String, + poll_token: String, + browser_url: String, + expires_at: String, +} + +#[derive(serde::Deserialize)] +struct RegistryAuthorisationSessionPollResponse { + status: String, + capability_key_id: Option, + namespace_status: Option, +} + +enum RegistryAuthorisationSessionPollOutcome { + Expired, + State(RegistryAuthorisationSessionPollResponse), +} + +fn fetch_registry_authorisation_session( + client: &reqwest::blocking::Client, + endpoint: &str, + poll_token: &str, +) -> Result { + let response = client.get(endpoint).bearer_auth(poll_token).send().map_err(|error| { + crate::error::CompileError::without_span(format!("failed to poll browser authorisation session: {error}")) + .with_category(crate::error::CompileErrorCategory::Network) + .with_source(error) + })?; + let status = response.status(); + let body = response.text().map_err(|error| { + crate::error::CompileError::without_span(format!("failed to read authorisation session status: {error}")) + .with_category(crate::error::CompileErrorCategory::Network) + .with_source(error) + })?; + if status == reqwest::StatusCode::GONE { + return Ok(RegistryAuthorisationSessionPollOutcome::Expired); + } + if !status.is_success() { + return Err(crate::error::CompileError::without_span(format!( + "browser authorisation session failed with HTTP {status}: {}", + body.trim() + )) + .with_category(registry_http_error_category(status))); + } + let poll = serde_json::from_str::(&body).map_err(|error| { + crate::error::CompileError::without_span(format!("registry returned an invalid authorisation status: {error}")) + .with_category(crate::error::CompileErrorCategory::Network) + .with_source(error) + })?; + Ok(RegistryAuthorisationSessionPollOutcome::State(poll)) +} + +fn registry_authorisation_status_removes_pending_key(status: &str) -> bool { + matches!(status, "cancelled" | "expired") +} + +fn resolve_registry_authorisation_session_poll( + poll: RegistryAuthorisationSessionPollResponse, + generated: &GeneratedRegistryKeyMaterial, + namespace: &str, +) -> Result> { + activate_registry_key_after_wallet_approval(&poll.status, poll.capability_key_id.as_deref(), &generated.key_id, || { + store_registry_private_key(&generated.key_id, &generated.private_key_pkcs8) + })?; + match poll.status.as_str() { + "pending" => Ok(None), + "authorised" => { + let key_id = poll + .capability_key_id + .ok_or_else(|| crate::error::CompileError::without_span("authorised session did not return a capability key"))?; + eprintln!("Publishing authorisation confirmed; continuing with cellc."); + Ok(Some(key_id)) + } + "review_pending" => { + let key_id = poll.capability_key_id.as_deref().ok_or_else(|| { + crate::error::CompileError::without_span("review_pending session did not return a capability key") + .with_category(crate::error::CompileErrorCategory::Authentication) + })?; + Err(crate::error::CompileError::without_span(format!( + "wallet authorisation succeeded, but namespace '{namespace}' is awaiting Registry review; rerun publish with --capability-key-id {key_id} after approval" + )) + .with_category(crate::error::CompileErrorCategory::Authentication)) + } + status if registry_authorisation_status_removes_pending_key(status) => { + remove_registry_private_key(&generated.key_id)?; + Err(crate::error::CompileError::without_span(format!( + "browser authorisation session was {status}; run `cellc publish --authorise` again" + )) + .with_category(crate::error::CompileErrorCategory::Authentication)) + } + other => Err(crate::error::CompileError::without_span(format!( + "registry returned unknown authorisation session status '{other}' (namespace status: {})", + poll.namespace_status.as_deref().unwrap_or("unknown") + )) + .with_category(crate::error::CompileErrorCategory::Network)), + } +} + +fn activate_registry_key_after_wallet_approval( + status: &str, + returned_key_id: Option<&str>, + generated_key_id: &str, + persist: F, +) -> Result<()> +where + F: FnOnce() -> Result<()>, +{ + if !matches!(status, "authorised" | "review_pending") { + return Ok(()); + } + let Some(returned_key_id) = returned_key_id else { + return Err(crate::error::CompileError::without_span(format!("{status} session did not return a publishing key")) + .with_category(crate::error::CompileErrorCategory::Authentication)); + }; + if returned_key_id != generated_key_id { + return Err(crate::error::CompileError::without_span( + "registry approved a different publishing key than the one held locally", + ) + .with_category(crate::error::CompileErrorCategory::Authentication)); + } + persist() +} + +fn authorise_registry_publish_key(api_base: &str, namespace: &str, name: &str, artifact_kind: &str, no_open: bool) -> Result { + let generated = generate_registry_key_material()?; + let client = registry_http_client()?; + let endpoint = format!("{}/v1/authorisation-sessions", api_base.trim_end_matches('/')); + let response = client + .post(&endpoint) + .json(&RegistryAuthorisationSessionCreateRequest { + capability_pubkey: generated.public_key.clone(), + requested_scopes: vec![format!("publish:{namespace}/{name}")], + artifact_kind: artifact_kind.to_string(), + capability_expires_at: utc_timestamp_after_seconds(90 * 24 * 60 * 60), + cli_version: crate::VERSION.to_string(), + }) + .send() + .map_err(|error| { + crate::error::CompileError::without_span(format!("failed to create browser authorisation session: {error}")) + .with_category(crate::error::CompileErrorCategory::Network) + .with_source(error) + })?; + let status = response.status(); + let body = response.text().map_err(|error| { + crate::error::CompileError::without_span(format!("failed to read browser authorisation session response: {error}")) + .with_category(crate::error::CompileErrorCategory::Network) + .with_source(error) + })?; + if !status.is_success() { + return Err(crate::error::CompileError::without_span(format!( + "registry refused browser authorisation session with HTTP {status}: {}", + body.trim() + )) + .with_category(registry_http_error_category(status))); + } + let session: RegistryAuthorisationSessionCreateResponse = serde_json::from_str(&body).map_err(|error| { + crate::error::CompileError::without_span(format!("registry returned an invalid authorisation session: {error}")) + .with_category(crate::error::CompileErrorCategory::Network) + .with_source(error) + })?; + validate_registry_authorisation_session(&session, api_base)?; + if generated.key_id != registry_capability_key_id(&generated.public_key) { + return Err(crate::error::CompileError::without_span( + "generated capability key identity changed before browser authorisation", + ) + .with_category(crate::error::CompileErrorCategory::Authentication)); + } + store_pending_registry_private_key(&generated.key_id, &generated.private_key_pkcs8, &session.session_id, &session.expires_at)?; + eprintln!("Authorise publishing {namespace}/{name} in your CKB wallet:"); + eprintln!(" {}", session.browser_url); + eprintln!("Pending publishing key: {}", generated.key_id); + if !no_open && let Err(error) = open_registry_authorisation_url(&session.browser_url) { + eprintln!("Browser did not open automatically: {error}"); + } + eprintln!("Waiting for wallet approval…"); + + let poll_endpoint = format!("{}/v1/authorisation-sessions/{}", api_base.trim_end_matches('/'), session.session_id); + let deadline = std::time::Instant::now() + Duration::from_secs(15 * 60); + while std::time::Instant::now() < deadline { + match fetch_registry_authorisation_session(&client, &poll_endpoint, &session.poll_token)? { + RegistryAuthorisationSessionPollOutcome::Expired => { + remove_registry_private_key(&generated.key_id)?; + return Err(crate::error::CompileError::without_span( + "browser authorisation session expired before wallet approval; run `cellc publish --authorise` again", + ) + .with_category(crate::error::CompileErrorCategory::Authentication)); + } + RegistryAuthorisationSessionPollOutcome::State(poll) => { + if let Some(key_id) = resolve_registry_authorisation_session_poll(poll, &generated, namespace)? { + return Ok(key_id); + } + std::thread::sleep(Duration::from_secs(2)); + } + } + } + + // The server may have committed wallet approval immediately before the local + // deadline. One final authoritative read closes that race. A still-pending or + // unreachable session keeps its pending key so a later CLI invocation can recover it. + match fetch_registry_authorisation_session(&client, &poll_endpoint, &session.poll_token)? { + RegistryAuthorisationSessionPollOutcome::Expired => { + remove_registry_private_key(&generated.key_id)?; + Err(crate::error::CompileError::without_span( + "browser authorisation session expired before wallet approval; run `cellc publish --authorise` again", + ) + .with_category(crate::error::CompileErrorCategory::Authentication)) + } + RegistryAuthorisationSessionPollOutcome::State(poll) => { + if let Some(key_id) = resolve_registry_authorisation_session_poll(poll, &generated, namespace)? { + return Ok(key_id); + } + Err(crate::error::CompileError::without_span(format!( + "browser authorisation is still pending; publishing key {} remains in the OS keychain for recovery", + generated.key_id + )) + .with_category(crate::error::CompileErrorCategory::Authentication)) + } + } +} + +fn validate_registry_authorisation_session(session: &RegistryAuthorisationSessionCreateResponse, api_base: &str) -> Result<()> { + if !session.session_id.starts_with("auth_") + || session.session_id.len() != 37 + || !session.session_id[5..].bytes().all(|byte| byte.is_ascii_hexdigit()) + || !session.poll_token.starts_with("poll_") + || session.poll_token.len() != 37 + || !session.poll_token[5..].bytes().all(|byte| byte.is_ascii_hexdigit()) + { + return Err(crate::error::CompileError::without_span("registry returned malformed authorisation credentials") + .with_category(crate::error::CompileErrorCategory::Network)); + } + let browser = reqwest::Url::parse(&session.browser_url).map_err(|error| { + crate::error::CompileError::without_span(format!("registry returned an invalid browser URL: {error}")) + .with_category(crate::error::CompileErrorCategory::Network) + })?; + let api = parse_registry_api_url(api_base)?; + let browser_host = browser.host_str().unwrap_or_default(); + let fragment = browser.fragment().unwrap_or_default(); + let fragment_value = |name: &str| { + fragment.split('&').find_map(|part| { + let (key, value) = part.split_once('=')?; + (key == name).then_some(value) + }) + }; + let browser_session_id = fragment_value("authorisation_session"); + let browser_token = fragment_value("browser_token").unwrap_or_default(); + let loopback = browser_host.parse::().is_ok_and(|address| address.is_loopback()) + || browser_host.eq_ignore_ascii_case("localhost"); + if (browser.scheme() != "https" && !(browser.scheme() == "http" && loopback)) + || !browser.username().is_empty() + || browser.password().is_some() + || browser.query().is_some() + || browser_session_id != Some(session.session_id.as_str()) + || !browser_token.starts_with("browser_") + || browser_token.len() != 40 + || !browser_token[8..].bytes().all(|byte| byte.is_ascii_hexdigit()) + || !browser.path().ends_with("/registry/submit") + || (api.scheme() == "https" && browser.scheme() != "https") + { + return Err(crate::error::CompileError::without_span("registry returned an unsafe browser authorisation URL") + .with_category(crate::error::CompileErrorCategory::Network)); + } + Ok(()) +} + +fn open_registry_authorisation_url(url: &str) -> std::io::Result<()> { + #[cfg(target_os = "macos")] + let status = std::process::Command::new("open").arg(url).status()?; + #[cfg(target_os = "windows")] + let status = std::process::Command::new("rundll32").args(["url.dll,FileProtocolHandler", url]).status()?; + #[cfg(all(unix, not(target_os = "macos")))] + let status = std::process::Command::new("xdg-open").arg(url).status()?; + #[cfg(not(any(unix, windows)))] + return Err(std::io::Error::other("automatic browser opening is unsupported on this platform")); + if status.success() { + Ok(()) + } else { + Err(std::io::Error::other("browser launcher returned a failure status")) + } +} + fn submit_registry_publish_request_with_retry( client: &reqwest::blocking::Client, endpoint: &str, @@ -5676,17 +7109,22 @@ fn read_lockfile_path(path: &Path) -> Result { let content = std::fs::read_to_string(path).map_err(|error| { crate::error::CompileError::without_span(format!("failed to read lockfile '{}': {}", path.display(), error)) })?; - toml::from_str(&content) - .map_err(|error| crate::error::CompileError::without_span(format!("failed to parse lockfile '{}': {}", path.display(), error))) + let lockfile: Lockfile = toml::from_str(&content).map_err(|error| { + crate::error::CompileError::without_span(format!("failed to parse lockfile '{}': {}", path.display(), error)) + })?; + lockfile.validate_schema()?; + Ok(lockfile) } fn read_deployed_manifest_path(path: &Path) -> Result { let content = std::fs::read_to_string(path).map_err(|error| { crate::error::CompileError::without_span(format!("failed to read deployed manifest '{}': {}", path.display(), error)) })?; - toml::from_str(&content).map_err(|error| { + let manifest: crate::package::DeployedManifest = toml::from_str(&content).map_err(|error| { crate::error::CompileError::without_span(format!("failed to parse deployed manifest '{}': {}", path.display(), error)) - }) + })?; + manifest.validate_schema()?; + Ok(manifest) } fn verify_builder_lockfile_identity( @@ -5697,6 +7135,12 @@ fn verify_builder_lockfile_identity( let lockfile = read_lockfile_path(lockfile_path)?; let expected_build = locked_build_info_from_metadata(metadata)?; let mut violations = Vec::new(); + if lockfile.package.edition != metadata.edition { + violations.push(format!( + "edition mismatch: Cell.lock package has '{}' but metadata has '{}'", + lockfile.package.edition, metadata.edition + )); + } let locked_compiler_source_hash = lockfile.package.compiler_source_hash.as_ref().or(lockfile.package.source_hash.as_ref()); let locked_source_label = if lockfile.package.compiler_source_hash.is_some() { "compiler_source_hash" } else { "source_hash" }; @@ -5711,6 +7155,18 @@ fn verify_builder_lockfile_identity( match &lockfile.package_build { Some(build) => { push_missing_locked_build_identity("Cell.lock [package.build]", build, &mut violations); + if build.edition != metadata.edition { + violations.push(format!( + "edition mismatch: Cell.lock build has '{}' but metadata has '{}'", + build.edition, metadata.edition + )); + } + if build.compatibility_profile_hash != expected_build.compatibility_profile_hash { + violations.push(format!( + "compatibility_profile_hash mismatch: Cell.lock has '{}', metadata has '{}'", + build.compatibility_profile_hash, expected_build.compatibility_profile_hash + )); + } compare_builder_identity_field( "compiler_version", &build.compiler_version, @@ -5751,6 +7207,8 @@ fn verify_builder_lockfile_identity( "build": lockfile.package_build, "verified_fields": [ locked_source_label, + "edition", + "compatibility_profile_hash", "compiler_version", "target_profile", "artifact_hash", @@ -5774,6 +7232,11 @@ fn verify_builder_deployment_identity( let deployed = read_deployed_manifest_path(deployed_path)?; let expected_build = locked_build_info_from_metadata(metadata)?; let mut violations = Vec::new(); + for (label, edition) in [("Cell.lock package", lockfile.package.edition), ("Deployed.toml package", deployed.package.edition)] { + if edition != metadata.edition { + violations.push(format!("edition mismatch: {} has '{}' but metadata has '{}'", label, edition, metadata.edition)); + } + } match (&lockfile.package.source_hash, &deployed.package.source_hash) { (Some(locked), Some(deployed_hash)) if locked == deployed_hash => {} @@ -5797,6 +7260,18 @@ fn verify_builder_deployment_identity( match &deployed.build { Some(build) => { push_missing_deployed_build_identity("Deployed.toml [build]", build, &mut violations); + if build.edition != metadata.edition { + violations.push(format!( + "edition mismatch: Deployed.toml build has '{}' but metadata has '{}'", + build.edition, metadata.edition + )); + } + if build.compatibility_profile_hash != expected_build.compatibility_profile_hash { + violations.push(format!( + "compatibility_profile_hash mismatch: Deployed.toml has '{}', metadata has '{}'", + build.compatibility_profile_hash, expected_build.compatibility_profile_hash + )); + } compare_builder_deployed_field( "compiler_version", &build.compiler_version, @@ -5829,6 +7304,18 @@ fn verify_builder_deployment_identity( continue; } push_deployment_status_violation(deployment, &mut violations); + if deployment.edition != metadata.edition { + violations.push(format!( + "edition mismatch for network '{}': deployment has '{}' but metadata has '{}'", + deployment.network, deployment.edition, metadata.edition + )); + } + if deployment.compatibility_profile_hash != expected_build.compatibility_profile_hash { + violations.push(format!( + "compatibility_profile_hash mismatch for network '{}': Deployed.toml has '{}', metadata has '{}'", + deployment.network, deployment.compatibility_profile_hash, expected_build.compatibility_profile_hash + )); + } compare_builder_deployment_record_field( "artifact_hash", &deployment.artifact_hash, @@ -5947,6 +7434,8 @@ fn verify_builder_deployment_identity( "verified_fields": [ "source_hash", "compiler_source_hash", + "edition", + "compatibility_profile_hash", "compiler_version", "artifact_hash", "metadata_hash", @@ -6239,11 +7728,13 @@ fn typescript_builder_manifest( deployment_identity: Option<&serde_json::Value>, ) -> serde_json::Value { serde_json::json!({ - "schema": "cellscript-generated-action-builder-v0.20", + "schema": "cellscript-generated-action-builder-v0.23-edition-2026", "target": "typescript", "package_name": package_name, "module": metadata.module, "compiler_version": metadata.compiler_version, + "edition": metadata.edition, + "compatibility_profile": metadata.compatibility_profile, "metadata_schema_version": metadata.metadata_schema_version, "metadata_schema_versions": metadata_schema_versions_json(metadata), "metadata_hash": metadata_hash, @@ -6355,7 +7846,7 @@ fn typescript_builder_index( let metadata_json = json_string_pretty("metadata", metadata)?; let mut ts = String::new(); - ts.push_str("export const CELLSCRIPT_BUILDER_SCHEMA = \"cellscript-generated-action-builder-v0.20\" as const;\n"); + ts.push_str("export const CELLSCRIPT_BUILDER_SCHEMA = \"cellscript-generated-action-builder-v0.23-edition-2026\" as const;\n"); ts.push_str("export const ACTION_SCAN_SELECTORS_SCHEMA = \"cellscript-action-scan-selectors-v0.21\" as const;\n"); ts.push_str(&format!("export const builderManifest = {manifest_json} as const;\n")); ts.push_str(&format!("export const metadata = {metadata_json} as const;\n")); @@ -6385,6 +7876,7 @@ fn typescript_builder_index( script_field?: string | null;\n\ };\n\n\ export interface CellScriptLockfilePackage {\n\ + edition: \"2026\";\n\ name?: string;\n\ version?: string;\n\ namespace?: string | null;\n\ @@ -6392,6 +7884,8 @@ fn typescript_builder_index( compiler_source_hash?: string | null;\n\ }\n\n\ export interface CellScriptLockfileBuild {\n\ + edition: \"2026\";\n\ + compatibility_profile_hash: string;\n\ compiler_version?: string | null;\n\ target_profile?: string | null;\n\ artifact_hash?: string | null;\n\ @@ -6414,6 +7908,7 @@ fn typescript_builder_index( deployment?: Record;\n\ }\n\n\ export interface CellScriptDeploymentRecord {\n\ + edition: \"2026\";\n\ network: string;\n\ chain_id: string;\n\ tx_hash: string;\n\ @@ -6430,6 +7925,7 @@ fn typescript_builder_index( abi_hash?: string | null;\n\ constraints_hash?: string | null;\n\ compiler_version?: string | null;\n\ + compatibility_profile_hash: string;\n\ type_id?: string | null;\n\ status?: string | null;\n\ audit_report_hash?: string | null;\n\ @@ -6560,6 +8056,8 @@ fn typescript_builder_index( const GENERATED_ARTIFACT_HASH: string | null = {};\n\ const GENERATED_SOURCE_HASH: string | null = {};\n\ const GENERATED_COMPILER_VERSION = {};\n\ + const GENERATED_EDITION = {};\n\ + const GENERATED_COMPATIBILITY_PROFILE_HASH = {};\n\ const GENERATED_TARGET_PROFILE = {};\n\ const GENERATED_SCHEMA_HASH = {};\n\ const GENERATED_CELL_DATA_CODEC_MANIFEST_HASH = {};\n\ @@ -6572,6 +8070,8 @@ fn typescript_builder_index( metadata.artifact_hash.as_deref().map(typescript_string_literal).unwrap_or_else(|| "null".to_string()), metadata.source_hash.as_deref().map(typescript_string_literal).unwrap_or_else(|| "null".to_string()), typescript_string_literal(&metadata.compiler_version), + typescript_string_literal(metadata.edition.as_str()), + typescript_string_literal(&hash_json_value("compatibility_profile", &metadata.compatibility_profile,)?), typescript_string_literal(&metadata.target_profile.name), typescript_string_literal(&metadata.molecule_schema_manifest.manifest_hash), typescript_string_literal(&metadata.cell_data_codec_manifest.manifest_hash), @@ -6704,12 +8204,15 @@ fn typescript_builder_index( if (!pkg) {\n\ violations.push(\"Cell.lock has no [package]\");\n\ } else {\n\ + compareRequiredIdentity(\"edition\", pkg.edition, GENERATED_EDITION, violations);\n\ compareRequiredIdentity(\"compiler_source_hash\", pkg.compiler_source_hash ?? pkg.source_hash, GENERATED_SOURCE_HASH, violations);\n\ }\n\ const build = lockfile.package_build;\n\ if (!build) {\n\ violations.push(\"Cell.lock has no [package.build]\");\n\ } else {\n\ + compareRequiredIdentity(\"edition\", build.edition, GENERATED_EDITION, violations);\n\ + compareRequiredIdentity(\"compatibility_profile_hash\", build.compatibility_profile_hash, GENERATED_COMPATIBILITY_PROFILE_HASH, violations);\n\ compareRequiredIdentity(\"compiler_version\", build.compiler_version, GENERATED_COMPILER_VERSION, violations);\n\ compareRequiredIdentity(\"target_profile\", build.target_profile, GENERATED_TARGET_PROFILE, violations);\n\ compareRequiredIdentity(\"artifact_hash\", build.artifact_hash, GENERATED_ARTIFACT_HASH, violations);\n\ @@ -6742,6 +8245,8 @@ fn typescript_builder_index( return violations;\n\ }\n\ violations.push(...validateCellScriptDeploymentTrust(deployment, trustPolicy));\n\ + compareDeploymentIdentity(\"edition\", deployment.edition, GENERATED_EDITION, violations);\n\ + compareDeploymentIdentity(\"compatibility_profile_hash\", deployment.compatibility_profile_hash, GENERATED_COMPATIBILITY_PROFILE_HASH, violations);\n\ if (!deployment.status) {\n\ violations.push(\"deployment record has no status; expected 'active'\");\n\ } else if (deployment.status !== \"active\") {\n\ @@ -8053,19 +9558,17 @@ fn validate_ickb_claim_thresholds( let Some(row) = matrix_rows.get(scenario) else { continue; }; - if let (Some(max), Some(actual)) = (max_cycles, row["execution"]["cellscript_cycles"].as_u64()) { - if actual > max { - issues.push(format!( - "iCKB claim branch {family_id}/{branch_id} scenario {scenario} cellscript_cycles {actual} exceeds {max}" - )); - } + if let (Some(max), Some(actual)) = (max_cycles, row["execution"]["cellscript_cycles"].as_u64()) + && actual > max + { + issues.push(format!( + "iCKB claim branch {family_id}/{branch_id} scenario {scenario} cellscript_cycles {actual} exceeds {max}" + )); } - if let (Some(max), Some(actual)) = (max_tx_size, row["execution"]["tx_size_bytes"].as_u64()) { - if actual > max { - issues.push(format!( - "iCKB claim branch {family_id}/{branch_id} scenario {scenario} tx_size_bytes {actual} exceeds {max}" - )); - } + if let (Some(max), Some(actual)) = (max_tx_size, row["execution"]["tx_size_bytes"].as_u64()) + && actual > max + { + issues.push(format!("iCKB claim branch {family_id}/{branch_id} scenario {scenario} tx_size_bytes {actual} exceeds {max}")); } } } @@ -9610,6 +11113,61 @@ fn proof_plan_read_label(read: &str) -> String { } } +fn resolution_options( + features: &[String], + all_features: bool, + no_default_features: bool, + environment: Option<&str>, + offline: bool, + frozen: bool, + scope: crate::package::DependencyScope, +) -> crate::package::ResolutionOptions { + crate::package::ResolutionOptions { + scope, + features: features.iter().cloned().collect(), + all_features, + no_default_features, + environment: environment.map(str::to_string), + offline: offline || frozen, + } +} + +fn build_resolution_options(args: &BuildArgs, scope: crate::package::DependencyScope) -> crate::package::ResolutionOptions { + resolution_options( + &args.features, + args.all_features, + args.no_default_features, + args.environment.as_deref(), + args.offline, + args.frozen, + scope, + ) +} + +fn check_resolution_options(args: &CheckArgs, scope: crate::package::DependencyScope) -> crate::package::ResolutionOptions { + resolution_options( + &args.features, + args.all_features, + args.no_default_features, + args.environment.as_deref(), + args.offline, + args.frozen, + scope, + ) +} + +fn test_resolution_options(args: &TestArgs) -> crate::package::ResolutionOptions { + resolution_options( + &args.features, + args.all_features, + args.no_default_features, + args.environment.as_deref(), + args.offline, + args.frozen, + crate::package::DependencyScope::Test, + ) +} + fn effective_check_args(mut args: CheckArgs) -> Result { // In a workspace root (virtual manifest without [package]), fall back to default policy. let policy = PackageManager::new(".").read_manifest().map(|m| m.policy).unwrap_or_default(); @@ -9706,6 +11264,11 @@ fn validate_dependency_target_flags(dev: bool, build: bool) -> Result<()> { if dev && build { return Err(crate::error::CompileError::without_span("dependency target flags --dev and --build are mutually exclusive")); } + if build { + return Err(crate::error::CompileError::without_span( + "--build dependencies are reserved until isolated build-script execution is implemented", + )); + } Ok(()) } @@ -9722,18 +11285,18 @@ fn validate_not_self_dependency(crate_name: &str, dep: &Dependency, manifest: &c manifest.package.name ))); } - if let Dependency::Detailed(detailed) = dep { - if let Some(dep_path) = &detailed.path { - let dep_canon = std::path::Path::new(dep_path); - let manifest_dir = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); - let dep_abs = dep_canon.canonicalize().unwrap_or_else(|_| manifest_dir.join(dep_canon)); - let manifest_abs = manifest_dir.canonicalize().unwrap_or_else(|_| manifest_dir.clone()); - if dep_abs == manifest_abs { - return Err(crate::error::CompileError::without_span(format!( - "refusing to add self-dependency: path '{}' resolves to the current package root", - dep_path - ))); - } + if let Dependency::Detailed(detailed) = dep + && let Some(dep_path) = &detailed.path + { + let dep_canon = std::path::Path::new(dep_path); + let manifest_dir = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); + let dep_abs = dep_canon.canonicalize().unwrap_or_else(|_| manifest_dir.join(dep_canon)); + let manifest_abs = manifest_dir.canonicalize().unwrap_or_else(|_| manifest_dir.clone()); + if dep_abs == manifest_abs { + return Err(crate::error::CompileError::without_span(format!( + "refusing to add self-dependency: path '{}' resolves to the current package root", + dep_path + ))); } } Ok(()) @@ -9764,6 +11327,8 @@ fn dependency_from_add_args(args: &AddArgs) -> Dependency { (Some(git), _) => Dependency::Detailed(DetailedDependency { version: "*".to_string(), namespace: None, + package: args.package.clone(), + resolver: None, git: Some(git.clone()), branch: None, tag: None, @@ -9772,10 +11337,14 @@ fn dependency_from_add_args(args: &AddArgs) -> Dependency { optional: false, features: Vec::new(), default_features: true, + allow_unverified: false, + allow_quarantined: false, }), (_, Some(path)) => Dependency::Detailed(DetailedDependency { version: "*".to_string(), namespace: None, + package: args.package.clone(), + resolver: None, git: None, branch: None, tag: None, @@ -9784,6 +11353,24 @@ fn dependency_from_add_args(args: &AddArgs) -> Dependency { optional: false, features: Vec::new(), default_features: true, + allow_unverified: false, + allow_quarantined: false, + }), + _ if args.package.is_some() => Dependency::Detailed(DetailedDependency { + version: "*".to_string(), + namespace: None, + package: args.package.clone(), + resolver: None, + git: None, + branch: None, + tag: None, + rev: None, + path: None, + optional: false, + features: Vec::new(), + default_features: true, + allow_unverified: false, + allow_quarantined: false, }), _ => Dependency::Simple("*".to_string()), } @@ -9806,7 +11393,26 @@ fn auth_capability_submit_args_from_matches(m: &clap::ArgMatches) -> AuthCapabil AuthCapabilitySubmitArgs { api_url: m.get_one::("api-url").cloned(), payload: m.get_one::("payload").map(PathBuf::from).expect("required payload"), - joyid_signature: m.get_one::("joyid-signature").map(PathBuf::from).expect("required joyid-signature"), + wallet_signature: m.get_one::("wallet-signature").map(PathBuf::from).expect("required wallet-signature"), + json: json_output(m), + } +} + +fn auth_reproducer_create_args_from_matches(m: &clap::ArgMatches) -> AuthReproducerCreateArgs { + AuthReproducerCreateArgs { + builder_id: m.get_one::("builder-id").cloned().expect("required builder-id"), + trust_domain: m.get_one::("trust-domain").cloned().expect("required trust-domain"), + private_key_output: m.get_one::("private-key-output").map(PathBuf::from), + json: json_output(m), + } +} + +fn auth_namespace_claim_args_from_matches(m: &clap::ArgMatches) -> AuthNamespaceClaimArgs { + AuthNamespaceClaimArgs { + api_url: m.get_one::("api-url").cloned(), + namespace: m.get_one::("namespace").cloned().expect("required namespace"), + payload: m.get_one::("payload").map(PathBuf::from).expect("required payload"), + wallet_signature: m.get_one::("wallet-signature").map(PathBuf::from).expect("required wallet-signature"), json: json_output(m), } } @@ -9819,32 +11425,74 @@ fn auth_capability_revoke_args_from_matches(m: &clap::ArgMatches) -> AuthCapabil principal_id: m.get_one::("principal-id").cloned(), capability_key_id: m.get_one::("capability-key-id").cloned(), payload: m.get_one::("payload").map(PathBuf::from), - joyid_signature: m.get_one::("joyid-signature").map(PathBuf::from), + wallet_signature: m.get_one::("wallet-signature").map(PathBuf::from), reason: m.get_one::("reason").cloned(), json: json_output(m), } } fn refresh_lockfile_from_manifest(root: &Path) -> Result<()> { - let mut manager = PackageManager::new(root); - manager.resolve_dependencies()?; - - let mut lockfile = Lockfile::read_from_root(root)?.unwrap_or_default(); - lockfile.replace_with_resolved(manager.get_resolved()); + let manager = PackageManager::new(root); + let manifest = manager.read_manifest()?; + let mut lockfile = read_lockfile_for_explicit_repin(root)?; + lockfile.dependencies.clear(); + lockfile.root = crate::package::LockedRootGraph::default(); + lockfile.environments.clear(); + lockfile.package = lockfile_package_info(root, &manifest)?; + + let mut environments: Vec> = Vec::new(); + if manifest.dependency_overrides.is_empty() { + environments.push(None); + } + environments.extend(manifest.environments.keys().cloned().map(Some)); + for environment in environments { + let mut manager = PackageManager::new(root); + let options = crate::package::ResolutionOptions { + scope: crate::package::DependencyScope::Test, + all_features: true, + environment, + ..crate::package::ResolutionOptions::default() + }; + manager.resolve_dependencies_with_options(&options)?; + lockfile.merge_resolution(&manager, &manifest, &options)?; + } lockfile.write_to_root(root)?; Ok(()) } +fn read_lockfile_for_explicit_repin(root: &Path) -> Result { + match Lockfile::read_from_root(root) { + Ok(Some(lockfile)) => Ok(lockfile), + Ok(None) => Ok(Lockfile::new()), + Err(error) => { + let path = root.join("Cell.lock"); + let source = std::fs::read_to_string(&path).map_err(|_| error.clone())?; + let value: toml::Value = toml::from_str(&source).map_err(|_| error.clone())?; + if value.get("version").and_then(toml::Value::as_integer).is_some_and(|version| matches!(version, 1 | 2)) { + Ok(Lockfile::new()) + } else { + Err(error) + } + } + } +} + fn refresh_lockfile_from_build(root: &Path, metadata: &CompileMetadata) -> Result<()> { - let mut manager = PackageManager::new(root); + let manager = PackageManager::new(root); let manifest = manager.read_manifest()?; - manager.resolve_dependencies()?; let mut lockfile = Lockfile::read_from_root(root)?.unwrap_or_default(); + if (!manifest.dependencies.is_empty() || !manifest.dev_dependencies.is_empty()) && lockfile.root.manifest_digest.is_empty() { + return Err(crate::error::CompileError::without_span( + "Cell.lock has no pinned dependency graph; run 'cellc lock' or 'cellc update' before building", + )); + } + if manifest.dependencies.is_empty() && manifest.dev_dependencies.is_empty() { + lockfile.root.manifest_digest = crate::package::compute_manifest_digest(root)?; + } let mut package = lockfile_package_info(root, &manifest)?; package.compiler_source_hash = metadata.source_hash.clone(); lockfile.package = package; - lockfile.replace_with_resolved(manager.get_resolved()); lockfile.package_build = Some(locked_build_info_from_metadata(metadata)?); refresh_lockfile_deployment_refs(root, &mut lockfile); lockfile.write_to_root(root)?; @@ -9897,6 +11545,7 @@ fn refresh_lockfile_deployment_refs(root: &Path, lockfile: &mut crate::package:: fn lockfile_package_info(root: &Path, manifest: &crate::package::PackageManifest) -> Result { Ok(crate::package::LockfilePackageInfo { + edition: manifest.package.edition, name: manifest.package.name.clone(), version: manifest.package.version.clone(), namespace: manifest.package.namespace.clone(), @@ -9907,6 +11556,8 @@ fn lockfile_package_info(root: &Path, manifest: &crate::package::PackageManifest fn locked_build_info_from_metadata(metadata: &CompileMetadata) -> Result { Ok(crate::package::LockedBuildInfo { + edition: metadata.edition, + compatibility_profile_hash: hash_json_value("compatibility_profile", &metadata.compatibility_profile)?, compiler_version: Some(metadata.compiler_version.clone()), target_profile: Some(metadata.target_profile.name.clone()), artifact_hash: metadata.artifact_hash.clone(), @@ -9931,6 +11582,8 @@ fn metadata_abi_hash(metadata: &CompileMetadata) -> Result { let abi = serde_json::json!({ "metadata_schema_version": metadata.metadata_schema_version, "metadata_schema_versions": metadata_schema_versions_json(metadata), + "edition": metadata.edition, + "compatibility_profile": &metadata.compatibility_profile, "target_profile": metadata.target_profile.name.as_str(), "types": &metadata.types, "actions": &metadata.actions, @@ -9964,8 +11617,11 @@ fn compile_receipt_json(metadata: &CompileMetadata) -> Result serde_json::Value::String(hash_json_value("template_layouts", &metadata.template_layouts)?) }; Ok(serde_json::json!({ - "schema": "cellscript-compile-receipt-v1", + "schema": "cellscript-compile-receipt-v2", "compiler_version": metadata.compiler_version, + "edition": metadata.edition, + "compatibility_profile": metadata.compatibility_profile, + "compatibility_profile_hash": hash_json_value("compatibility_profile", &metadata.compatibility_profile)?, "rust_toolchain": cellscript_rust_toolchain(), "target": metadata.artifact_format, "target_profile": metadata.target_profile.name, @@ -9993,6 +11649,9 @@ fn verify_compile_receipt_against_metadata( let expected = compile_receipt_json(metadata)?; for pointer in [ "/compiler_version", + "/edition", + "/compatibility_profile", + "/compatibility_profile_hash", "/rust_toolchain", "/target", "/target_profile", @@ -10021,9 +11680,9 @@ fn verify_compile_receipt_against_metadata( fn validate_compile_receipt_schema(receipt: &serde_json::Value) -> Result<()> { match receipt.get("schema").and_then(serde_json::Value::as_str) { - Some("cellscript-compile-receipt-v1") => Ok(()), + Some("cellscript-compile-receipt-v2") => Ok(()), Some(schema) => Err(crate::error::CompileError::without_span(format!( - "unsupported compile receipt schema '{}'; expected cellscript-compile-receipt-v1", + "unsupported compile receipt schema '{}'; expected cellscript-compile-receipt-v2", schema ))), None => Err(crate::error::CompileError::without_span("compile receipt is missing schema")), @@ -10447,6 +12106,9 @@ fn cellfabric_app_conflict_key_templates(app_namespace: &str, action: &crate::Ac } fn push_missing_locked_build_identity(label: &str, build: &crate::package::LockedBuildInfo, violations: &mut Vec) { + if build.compatibility_profile_hash.is_empty() { + violations.push(format!("{} has no compatibility_profile_hash", label)); + } if build.compiler_version.is_none() { violations.push(format!("{} has no compiler_version", label)); } @@ -10474,6 +12136,9 @@ fn push_missing_locked_build_identity(label: &str, build: &crate::package::Locke } fn push_missing_deployed_build_identity(label: &str, build: &crate::package::DeployedBuildInfo, violations: &mut Vec) { + if build.compatibility_profile_hash.is_empty() { + violations.push(format!("{} has no compatibility_profile_hash", label)); + } if build.compiler_version.is_none() { violations.push(format!("{} has no compiler_version", label)); } @@ -10569,13 +12234,13 @@ fn verify_live_deployments( let rpc_code_hash = live_cell_code_hash_for_deployment(&live, deployment, rpc_data_hash.as_deref(), &mut deployment_violations); - if let Some(hash) = rpc_code_hash.as_deref() { - if !hex_eq(hash, &deployment.code_hash) { - deployment_violations.push(format!( - "live code_hash mismatch for network '{}': RPC has '{}', Deployed.toml has '{}'", - deployment.network, hash, deployment.code_hash - )); - } + if let Some(hash) = rpc_code_hash.as_deref() + && !hex_eq(hash, &deployment.code_hash) + { + deployment_violations.push(format!( + "live code_hash mismatch for network '{}': RPC has '{}', Deployed.toml has '{}'", + deployment.network, hash, deployment.code_hash + )); } if let Some(type_id) = &deployment.type_id { @@ -10676,7 +12341,7 @@ fn live_cell_code_hash_for_deployment( } } -fn ckb_script_hash_from_json(script: &serde_json::Value) -> Result { +pub(super) fn ckb_script_hash_from_json(script: &serde_json::Value) -> Result { let code_hash = script .get("code_hash") .and_then(|value| value.as_str()) @@ -10834,6 +12499,12 @@ fn effective_build_check_args(args: &BuildArgs) -> Result { all_targets: false, target_profile: args.target_profile.clone(), features: args.features.clone(), + all_features: args.all_features, + no_default_features: args.no_default_features, + locked: args.locked, + frozen: args.frozen, + offline: args.offline, + environment: args.environment.clone(), json: false, production: args.production, deny_fail_closed: args.deny_fail_closed, @@ -11301,6 +12972,12 @@ impl CompileTestExpectation { all_targets: false, target_profile: None, features: Vec::new(), + all_features: false, + no_default_features: false, + locked: true, + frozen: false, + offline: false, + environment: None, json: false, production: self.production, deny_fail_closed: self.deny_fail_closed, @@ -11499,30 +13176,30 @@ fn validate_compile_test_metadata( expectation: &CompileTestExpectation, metadata: &crate::CompileMetadata, ) -> Result<()> { - if let Some(expected) = &expectation.expected_artifact_format { - if &metadata.artifact_format != expected { - return Err(crate::error::CompileError::without_span(format!( - "{}: expected artifact_format='{}', got '{}'", - path, expected, metadata.artifact_format - ))); - } + if let Some(expected) = &expectation.expected_artifact_format + && &metadata.artifact_format != expected + { + return Err(crate::error::CompileError::without_span(format!( + "{}: expected artifact_format='{}', got '{}'", + path, expected, metadata.artifact_format + ))); } - if let Some(expected) = expectation.expect_standalone { - if metadata.runtime.standalone_runner_compatible != expected { - return Err(crate::error::CompileError::without_span(format!( - "{}: expected standalone_runner_compatible={}, got {}", - path, expected, metadata.runtime.standalone_runner_compatible - ))); - } + if let Some(expected) = expectation.expect_standalone + && metadata.runtime.standalone_runner_compatible != expected + { + return Err(crate::error::CompileError::without_span(format!( + "{}: expected standalone_runner_compatible={}, got {}", + path, expected, metadata.runtime.standalone_runner_compatible + ))); } - if let Some(expected) = expectation.expect_ckb_runtime { - if metadata.runtime.ckb_runtime_required != expected { - return Err(crate::error::CompileError::without_span(format!( - "{}: expected ckb_runtime_required={}, got {}", - path, expected, metadata.runtime.ckb_runtime_required - ))); - } + if let Some(expected) = expectation.expect_ckb_runtime + && metadata.runtime.ckb_runtime_required != expected + { + return Err(crate::error::CompileError::without_span(format!( + "{}: expected ckb_runtime_required={}, got {}", + path, expected, metadata.runtime.ckb_runtime_required + ))); } if let Some(expected) = expectation.expect_fail_closed { let actual = !metadata.runtime.fail_closed_runtime_features.is_empty() @@ -11909,15 +13586,15 @@ fn decode_hex_arg(name: &str, value: &str, expected_len: Option) -> Resul Ok((high << 4) | low) }) .collect::>>()?; - if let Some(expected_len) = expected_len { - if bytes.len() != expected_len { - return Err(crate::error::CompileError::without_span(format!( - "parameter '{}' expects {} byte(s), got {}", - name, - expected_len, - bytes.len() - ))); - } + if let Some(expected_len) = expected_len + && bytes.len() != expected_len + { + return Err(crate::error::CompileError::without_span(format!( + "parameter '{}' expects {} byte(s), got {}", + name, + expected_len, + bytes.len() + ))); } Ok(bytes) } @@ -12025,6 +13702,13 @@ impl CliParser { .help("Compile only this lock as the artifact entrypoint"), ) .arg(Arg::new("jobs").long("jobs").short('j').value_name("N").help("Number of parallel jobs")) + .arg(Arg::new("features").long("features").value_delimiter(',').num_args(1..).value_name("FEATURES").help("Activate package features")) + .arg(Arg::new("all-features").long("all-features").action(ArgAction::SetTrue).help("Activate all package features")) + .arg(Arg::new("no-default-features").long("no-default-features").action(ArgAction::SetTrue).help("Do not activate the default feature")) + .arg(Arg::new("locked").long("locked").action(ArgAction::SetTrue).help("Require the existing Cell.lock dependency graph")) + .arg(Arg::new("frozen").long("frozen").action(ArgAction::SetTrue).help("Require Cell.lock and cached sources without network or lockfile writes")) + .arg(Arg::new("offline").long("offline").action(ArgAction::SetTrue).help("Do not access the network while materializing locked sources")) + .arg(Arg::new("environment").long("environment").value_name("NAME").help("Select an explicitly declared CKB dependency environment")) .arg( Arg::new("production") @@ -12081,6 +13765,13 @@ impl CliParser { ClapCommand::new("test") .about("Run the tests") .arg(Arg::new("filter").value_name("FILTER").help("Filter tests by name")) + .arg( + Arg::new("backend") + .long("backend") + .value_name("BACKEND") + .value_parser(["simulator", "ckb-vm", "all"]) + .help("Execution backend; required unless --no-run: simulator, ckb-vm, or all"), + ) .arg( Arg::new("no-run") .long("no-run") @@ -12090,6 +13781,13 @@ impl CliParser { .arg(Arg::new("nocapture").long("nocapture").action(ArgAction::SetTrue).help("Don't capture stdout")) .arg(Arg::new("fail-fast").long("fail-fast").action(ArgAction::SetTrue).help("Stop on first failure")) .arg(Arg::new("doc").long("doc").action(ArgAction::SetTrue).help("Generate docs before compiling tests")) + .arg(Arg::new("features").long("features").value_delimiter(',').num_args(1..).value_name("FEATURES").help("Activate package features")) + .arg(Arg::new("all-features").long("all-features").action(ArgAction::SetTrue).help("Activate all package features")) + .arg(Arg::new("no-default-features").long("no-default-features").action(ArgAction::SetTrue).help("Do not activate the default feature")) + .arg(Arg::new("locked").long("locked").action(ArgAction::SetTrue).help("Require the existing Cell.lock dependency graph")) + .arg(Arg::new("frozen").long("frozen").action(ArgAction::SetTrue).help("Require cached locked sources without network or lockfile writes")) + .arg(Arg::new("offline").long("offline").action(ArgAction::SetTrue).help("Do not access the network while materializing locked sources")) + .arg(Arg::new("environment").long("environment").value_name("NAME").help("Select an explicitly declared CKB dependency environment")) , ) .subcommand( @@ -12143,6 +13841,7 @@ impl CliParser { ClapCommand::new("add") .about("Add dependencies") .arg(Arg::new("crates").value_name("CRATES").required(true).num_args(1..).help("Crates to add")) + .arg(Arg::new("package-name").long("package").value_name("NAME").help("Declared package name when it differs from the local alias")) .arg(Arg::new("dev").long("dev").action(ArgAction::SetTrue).help("Add as dev dependency")) .arg(Arg::new("build").long("build").action(ArgAction::SetTrue).help("Add as build dependency")) .arg(Arg::new("git").long("git").value_name("URL").help("Add a git dependency source")) @@ -12175,6 +13874,13 @@ impl CliParser { .help("Also check the current ELF-compatible target path"), ) .arg(Arg::new("target-profile").long("target-profile").value_name("PROFILE").help("Target profile: ckb")) + .arg(Arg::new("features").long("features").value_delimiter(',').num_args(1..).value_name("FEATURES").help("Activate package features")) + .arg(Arg::new("all-features").long("all-features").action(ArgAction::SetTrue).help("Activate all package features")) + .arg(Arg::new("no-default-features").long("no-default-features").action(ArgAction::SetTrue).help("Do not activate the default feature")) + .arg(Arg::new("locked").long("locked").action(ArgAction::SetTrue).help("Require the existing Cell.lock dependency graph")) + .arg(Arg::new("frozen").long("frozen").action(ArgAction::SetTrue).help("Require cached locked sources without network or lockfile writes")) + .arg(Arg::new("offline").long("offline").action(ArgAction::SetTrue).help("Do not access the network while materializing locked sources")) + .arg(Arg::new("environment").long("environment").value_name("NAME").help("Select an explicitly declared CKB dependency environment")) .arg( Arg::new("production") @@ -12730,6 +14436,18 @@ impl CliParser { .value_name("FILE") .help("Also verify a compile receipt against the artifact and metadata"), ) + .arg( + Arg::new("lowering-record") + .long("lowering-record") + .value_name("FILE") + .help("Canonical lowering record; defaults to ARTIFACT.lowering.json for ELF"), + ) + .arg( + Arg::new("source-map") + .long("source-map") + .value_name("FILE") + .help("Canonical source map; defaults to ARTIFACT.sourcemap.json for ELF"), + ) .arg( Arg::new("verify-sources") .long("verify-sources") @@ -12819,6 +14537,259 @@ impl CliParser { ) .arg(Arg::new("args").value_name("ARGS").num_args(0..).trailing_var_arg(true)), ) + .subcommand( + ClapCommand::new("artifact") + .display_order(105) + .about("Fetch, verify, pin, copy, and consume non-CellScript Registry artifacts") + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand( + ClapCommand::new("ls-idl") + .about("Validate, bind, or fetch an exact-byte LS-IDL lock-script interface") + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand( + ClapCommand::new("validate") + .about("Validate an LS-IDL 0.1 document and optionally verify its executable suffix") + .arg(Arg::new("idl").long("idl").value_name("FILE").required(true)) + .arg(Arg::new("executable").long("executable").value_name("CKB_ELF")), + ) + .subcommand( + ClapCommand::new("bind") + .about("Append SHA-256(raw idl.json bytes) to a CKB executable") + .arg(Arg::new("idl").long("idl").value_name("FILE").required(true)) + .arg(Arg::new("executable").long("executable").value_name("CKB_ELF").required(true)) + .arg(Arg::new("output").long("output").short('o').value_name("CKB_ELF").required(true)) + .arg(Arg::new("force").long("force").action(ArgAction::SetTrue)), + ) + .subcommand( + ClapCommand::new("fetch") + .about("Fetch exact LS-IDL bytes by a chain-verified CKB script identity") + .arg(Arg::new("code-hash").long("code-hash").value_name("HASH").required(true)) + .arg( + Arg::new("hash-type") + .long("hash-type") + .value_name("TYPE") + .value_parser(["data", "data1", "data2", "type"]), + ) + .arg(Arg::new("data-hash").long("data-hash").value_name("HASH")) + .arg( + Arg::new("network") + .long("network") + .value_name("NETWORK") + .value_parser(["mainnet", "testnet"]) + .default_value("mainnet"), + ) + .arg(Arg::new("output").long("output").short('o').value_name("FILE").required(true)) + .arg(Arg::new("api-url").long("api-url").value_name("URL")) + .arg(Arg::new("force").long("force").action(ArgAction::SetTrue)), + ) + .subcommand( + ClapCommand::new("bundle") + .about("Create a publish-ready LS-IDL Registry bundle and Artifact.toml") + .arg(Arg::new("idl").long("idl").value_name("FILE").required(true)) + .arg(Arg::new("executable").long("executable").value_name("CKB_ELF").required(true)) + .arg(Arg::new("source").long("source").value_name("FILE").required(true)) + .arg(Arg::new("namespace").long("namespace").value_name("NAME").required(true)) + .arg(Arg::new("name").long("name").value_name("NAME").required(true)) + .arg(Arg::new("release").long("release").value_name("VERSION").required(true)) + .arg( + Arg::new("language") + .long("language") + .value_name("LANGUAGE") + .value_parser(["cellscript", "rust", "c", "javascript", "other"]) + .required(true), + ) + .arg( + Arg::new("hash-type") + .long("hash-type") + .value_name("TYPE") + .value_parser(["data", "data1", "data2", "type"]) + .default_value("data1"), + ) + .arg( + Arg::new("dep-type") + .long("dep-type") + .value_name("TYPE") + .value_parser(["code", "dep_group"]) + .default_value("code"), + ) + .arg(Arg::new("toolchain").long("toolchain").value_name("IDENTITY").required(true)) + .arg( + Arg::new("source-revision") + .long("source-revision") + .value_name("REVISION") + .required(true), + ) + .arg( + Arg::new("output") + .long("output") + .short('o') + .value_name("BUNDLE_JSON") + .default_value("bundle.json"), + ) + .arg( + Arg::new("artifact-manifest-output") + .long("artifact-manifest-output") + .value_name("ARTIFACT_TOML") + .default_value("Artifact.toml"), + ) + .arg(Arg::new("force").long("force").action(ArgAction::SetTrue)), + ), + ) + .subcommand( + ClapCommand::new("fetch") + .about("Download an immutable artifact bundle and write an authenticated receipt") + .arg(Arg::new("coordinate").value_name("NAMESPACE/NAME@RELEASE").required(true)) + .arg(Arg::new("output").long("output").short('o').value_name("FILE").required(true)) + .arg(Arg::new("receipt").long("receipt").value_name("FILE")) + .arg(Arg::new("api-url").long("api-url").value_name("URL")) + .arg(Arg::new("force").long("force").action(ArgAction::SetTrue)), + ) + .subcommand( + ClapCommand::new("verify") + .about("Verify a downloaded bundle against its signed Registry receipt") + .arg(Arg::new("bundle").long("bundle").value_name("FILE").required(true)) + .arg(Arg::new("receipt").long("receipt").value_name("FILE").required(true)), + ) + .subcommand( + ClapCommand::new("pin") + .about("Write a deterministic TCB/deployment artifact lock") + .arg(Arg::new("coordinate").value_name("NAMESPACE/NAME@RELEASE").required(true)) + .arg(Arg::new("output").long("output").short('o').value_name("FILE").default_value("Artifacts.lock")) + .arg(Arg::new("api-url").long("api-url").value_name("URL")) + .arg( + Arg::new("accept-hash-bound") + .long("accept-hash-bound") + .action(ArgAction::SetTrue) + .help("Explicitly pin immutable bytes that have integrity evidence but no semantic/security certification"), + ) + .arg(Arg::new("force").long("force").action(ArgAction::SetTrue)), + ) + .subcommand( + ClapCommand::new("copy") + .about("Materialize an authenticated template file map without overwriting files") + .arg(Arg::new("coordinate").value_name("NAMESPACE/NAME@RELEASE").required(true)) + .arg(Arg::new("destination").long("destination").short('d').value_name("DIR").default_value(".")) + .arg(Arg::new("api-url").long("api-url").value_name("URL")) + .arg(Arg::new("accept-hash-bound").long("accept-hash-bound").action(ArgAction::SetTrue)), + ) + .subcommand( + ClapCommand::new("cell-dep") + .visible_alias("celldep") + .about("Generate a transaction-builder CellDep descriptor from chain-verified deployment evidence") + .arg(Arg::new("coordinate").value_name("NAMESPACE/NAME@RELEASE").required(true)) + .arg(Arg::new("output").long("output").short('o').value_name("FILE").required(true)) + .arg(Arg::new("api-url").long("api-url").value_name("URL")) + .arg( + Arg::new("rpc-url") + .long("rpc-url") + .value_name("URL") + .help("Mainnet CKB RPC used to re-check Cell liveness (defaults to CELLSCRIPT_CKB_RPC_URL or mainnet.ckb.dev)"), + ) + .arg( + Arg::new("accept-hash-bound") + .long("accept-hash-bound") + .action(ArgAction::SetTrue) + .help("Explicitly consume chain-verified deployment bytes that have integrity evidence but no semantic/security certification"), + ) + .arg(Arg::new("force").long("force").action(ArgAction::SetTrue)), + ) + .subcommand( + ClapCommand::new("record-deployment") + .about("Sign, submit, and RPC-verify a CKB deployment record") + .arg(Arg::new("coordinate").value_name("NAMESPACE/NAME@RELEASE").required(true)) + .arg( + Arg::new("network") + .long("network") + .value_name("NETWORK") + .value_parser(["mainnet", "testnet"]) + .default_value("mainnet") + .help("CKB network whose live Cell will be verified; testnet defaults to the isolated Pudge Registry API"), + ) + .arg(Arg::new("code-hash").long("code-hash").value_name("HASH").required(true)) + .arg( + Arg::new("hash-type") + .long("hash-type") + .value_name("TYPE") + .value_parser(["data", "data1", "data2", "type"]) + .required(true), + ) + .arg( + Arg::new("dep-type") + .long("dep-type") + .value_name("TYPE") + .value_parser(["code", "dep_group"]) + .required(true), + ) + .arg(Arg::new("tx-hash").long("tx-hash").value_name("HASH").required(true)) + .arg(Arg::new("index").long("index").value_name("U32").value_parser(clap::value_parser!(u32)).required(true)) + .arg(Arg::new("capability-key-id").long("capability-key-id").value_name("KEY_ID").required(true)) + .arg(Arg::new("capability-signature").long("capability-signature").value_name("SIGNATURE")) + .arg(Arg::new("api-url").long("api-url").value_name("URL")) + .arg(Arg::new("print-payload").long("print-payload").action(ArgAction::SetTrue)), + ) + .subcommand( + ClapCommand::new("set-availability") + .about("Set a release active, deprecated, or yanked with a publisher capability") + .arg(Arg::new("coordinate").value_name("NAMESPACE/NAME@RELEASE").required(true)) + .arg( + Arg::new("status") + .long("status") + .value_name("STATUS") + .value_parser(["active", "deprecated", "yanked"]) + .required(true), + ) + .arg(Arg::new("reason").long("reason").value_name("TEXT")) + .arg(Arg::new("capability-key-id").long("capability-key-id").value_name("KEY_ID").required(true)) + .arg(Arg::new("capability-signature").long("capability-signature").value_name("SIGNATURE")) + .arg(Arg::new("api-url").long("api-url").value_name("URL")) + .arg(Arg::new("print-payload").long("print-payload").action(ArgAction::SetTrue)), + ) + .subcommand( + ClapCommand::new("reproduction-report") + .about("Hash a clean reproduction and sign a builder-authenticated report") + .arg(Arg::new("coordinate").value_name("NAMESPACE/NAME@RELEASE").required(true)) + .arg(Arg::new("artifact").long("artifact").value_name("FILE").required(true)) + .arg(Arg::new("build-log").long("build-log").value_name("FILE").required(true)) + .arg(Arg::new("builder-id").long("builder-id").value_name("ID").required(true)) + .arg(Arg::new("trust-domain").long("trust-domain").value_name("DOMAIN").required(true)) + .arg(Arg::new("builder-key-id").long("builder-key-id").value_name("KEY_ID").required(true)) + .arg( + Arg::new("builder-public-key") + .long("builder-public-key") + .value_name("P256_SPKI") + .required(true), + ) + .arg(Arg::new("output").long("output").short('o').value_name("FILE").required(true)) + .arg(Arg::new("api-url").long("api-url").value_name("URL")) + .arg(Arg::new("force").long("force").action(ArgAction::SetTrue)), + ) + .subcommand( + ClapCommand::new("reproduction-evidence") + .about("Validate signed independent reproduction reports and generate an admin promotion request") + .arg(Arg::new("coordinate").value_name("NAMESPACE/NAME@RELEASE").required(true)) + .arg( + Arg::new("report") + .long("report") + .value_name("FILE") + .action(ArgAction::Append) + .required(true) + .help("Signed independent cellscript-reproduction-report-v2 JSON; pass once per trusted builder"), + ) + .arg(Arg::new("output").long("output").short('o').value_name("FILE").required(true)) + .arg(Arg::new("api-url").long("api-url").value_name("URL")) + .arg(Arg::new("force").long("force").action(ArgAction::SetTrue)), + ) + .subcommand( + ClapCommand::new("commitment") + .about("Generate the canonical mainnet Registry commitment payload and Cell data") + .arg(Arg::new("coordinate").value_name("NAMESPACE/NAME@RELEASE").required(true)) + .arg(Arg::new("output").long("output").short('o').value_name("FILE").required(true)) + .arg(Arg::new("api-url").long("api-url").value_name("URL")) + .arg(Arg::new("force").long("force").action(ArgAction::SetTrue)), + ), + ) .subcommand( ClapCommand::new("publish") .display_order(110) @@ -12851,7 +14822,21 @@ impl CliParser { Arg::new("capability-key-id") .long("capability-key-id") .value_name("KEY_ID") - .help("Registry capability key id authorised by JoyID"), + .help("Registry capability key id authorised by a root wallet"), + ) + .arg( + Arg::new("authorise") + .long("authorise") + .action(ArgAction::SetTrue) + .conflicts_with_all(["capability-key-id", "capability-signature", "payload", "offline", "dry-run", "print-payload"]) + .help("Create a short-lived browser wallet session, wait for approval, then continue publishing"), + ) + .arg( + Arg::new("no-open") + .long("no-open") + .action(ArgAction::SetTrue) + .requires("authorise") + .help("Print the browser authorisation URL without opening it automatically"), ) .arg( Arg::new("capability-signature") @@ -12877,6 +14862,21 @@ impl CliParser { .value_name("FILE") .help("Immutable source snapshot bytes to upload; defaults to a generated CellScript source snapshot"), ) + .arg( + Arg::new("artifact-manifest") + .long("artifact-manifest") + .value_name("FILE") + .conflicts_with("offline") + .help("Publish a non-package artifact described by Artifact.toml and its immutable bundle"), + ) + .arg( + Arg::new("artifact-kind") + .long("artifact-kind") + .value_name("KIND") + .value_parser(["source_library", "profile_library"]) + .conflicts_with("artifact-manifest") + .help("CellScript dependency kind; defaults to source_library"), + ) .arg( Arg::new("print-payload") .long("print-payload") @@ -12907,7 +14907,8 @@ impl CliParser { .help("Allow explicit incident-review install of quarantined registry entries"), ), ) - .subcommand(ClapCommand::new("update").about("Experimental: update dependencies")) + .subcommand(ClapCommand::new("lock").about("Resolve and write the complete Cell.lock dependency graph")) + .subcommand(ClapCommand::new("update").about("Explicitly repin dependencies and rewrite Cell.lock")) .subcommand( ClapCommand::new("info") .about("Show package information") @@ -12921,18 +14922,18 @@ impl CliParser { ) .subcommand( ClapCommand::new("auth") - .about("Manage JoyID-rooted registry capability authorisation") + .about("Manage wallet-rooted registry capability authorisation") .subcommand_required(true) .arg_required_else_help(true) .subcommand( ClapCommand::new("login") .hide(true) - .about("Create a JoyID capability authorisation payload") + .about("Create a wallet capability authorisation payload") .arg( Arg::new("registry-origin") .long("registry-origin") .value_name("URL") - .help("Registry origin bound into the JoyID capability challenge"), + .help("Registry origin bound into the wallet capability challenge"), ) .arg( Arg::new("principal-type") @@ -12944,7 +14945,7 @@ impl CliParser { Arg::new("principal-id") .long("principal-id") .value_name("ID") - .help("Normalized JoyID/CKB principal binding derived from the CCC JoyID signer"), + .help("Normalized principal binding derived from a supported CCC CKB signer"), ) .arg( Arg::new("capability-pubkey") @@ -12957,7 +14958,7 @@ impl CliParser { .long("scope") .value_name("SCOPE") .action(ArgAction::Append) - .help("Capability scope, e.g. publish:namespace/package"), + .help("Repeatable least-privilege scope: publish, deployment, or availability for namespace/package (or namespace/*)"), ) .arg( Arg::new("expires") @@ -12986,12 +14987,12 @@ impl CliParser { .arg_required_else_help(true) .subcommand( ClapCommand::new("create") - .about("Create a JoyID capability authorisation payload for CI or local publishing") + .about("Create a wallet capability authorisation payload for CI or local publishing") .arg( Arg::new("registry-origin") .long("registry-origin") .value_name("URL") - .help("Registry origin bound into the JoyID capability challenge"), + .help("Registry origin bound into the wallet capability challenge"), ) .arg( Arg::new("principal-type") @@ -13003,7 +15004,7 @@ impl CliParser { Arg::new("principal-id") .long("principal-id") .value_name("ID") - .help("Normalized JoyID/CKB principal binding derived from the CCC JoyID signer"), + .help("Normalized principal binding derived from a supported CCC CKB signer"), ) .arg( Arg::new("capability-pubkey") @@ -13016,7 +15017,7 @@ impl CliParser { .long("scope") .value_name("SCOPE") .action(ArgAction::Append) - .help("Capability scope, e.g. publish:namespace/package"), + .help("Repeatable least-privilege scope: publish, deployment, or availability for namespace/package (or namespace/*)"), ) .arg( Arg::new("expires") @@ -13040,7 +15041,7 @@ impl CliParser { ) .subcommand( ClapCommand::new("submit") - .about("Submit a JoyID-signed capability authorisation payload to the registry") + .about("Submit a wallet-signed capability authorisation payload to the registry") .arg( Arg::new("api-url") .long("api-url") @@ -13055,11 +15056,12 @@ impl CliParser { .help("Capability authorisation payload JSON created by auth capability create"), ) .arg( - Arg::new("joyid-signature") - .long("joyid-signature") + Arg::new("wallet-signature") + .long("wallet-signature") + .visible_alias("joyid-signature") .value_name("FILE") .required(true) - .help("JoyID signature JSON whose challenge is the canonical payload"), + .help("JoyID or CKB wallet signature JSON whose challenge is the canonical payload"), ) .arg( Arg::new("json") @@ -13070,7 +15072,7 @@ impl CliParser { ) .subcommand( ClapCommand::new("revoke") - .about("Create or submit a JoyID-signed capability revocation payload") + .about("Create or submit a wallet-signed capability revocation payload") .arg( Arg::new("api-url") .long("api-url") @@ -13081,7 +15083,7 @@ impl CliParser { Arg::new("registry-origin") .long("registry-origin") .value_name("URL") - .help("Registry origin bound into the JoyID revocation challenge"), + .help("Registry origin bound into the wallet revocation challenge"), ) .arg( Arg::new("principal-type") @@ -13093,7 +15095,7 @@ impl CliParser { Arg::new("principal-id") .long("principal-id") .value_name("ID") - .help("Normalized JoyID/CKB principal binding derived from the CCC JoyID signer"), + .help("Normalized principal binding derived from a supported CCC CKB signer"), ) .arg( Arg::new("capability-key-id") @@ -13108,10 +15110,11 @@ impl CliParser { .help("Previously generated capability revocation payload JSON"), ) .arg( - Arg::new("joyid-signature") - .long("joyid-signature") + Arg::new("wallet-signature") + .long("wallet-signature") + .visible_alias("joyid-signature") .value_name("FILE") - .help("JoyID signature JSON whose challenge is the canonical revoke payload"), + .help("JoyID or CKB wallet signature JSON whose challenge is the canonical revoke payload"), ) .arg( Arg::new("reason") @@ -13126,6 +15129,88 @@ impl CliParser { .help("Emit machine-readable capability revocation output"), ), ), + ) + .subcommand( + ClapCommand::new("reproducer") + .about("Manage independent reproducibility builder identities") + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand( + ClapCommand::new("create") + .about("Create a P-256 reproducer key and public policy enrollment record") + .arg( + Arg::new("builder-id") + .long("builder-id") + .value_name("ID") + .required(true) + .help("Stable builder identifier assigned by the independent operator"), + ) + .arg( + Arg::new("trust-domain") + .long("trust-domain") + .value_name("DOMAIN") + .required(true) + .help("Administrative and private-key custody domain for this builder"), + ) + .arg( + Arg::new("private-key-output") + .long("private-key-output") + .value_name("FILE") + .help( + "On Unix, write PKCS#8 base64 to a new mode-0600 file for CI secret enrollment instead of the OS keychain", + ), + ) + .arg( + Arg::new("json") + .long("json") + .action(ArgAction::SetTrue) + .help("Emit the public builder enrollment record as JSON without private-key material"), + ), + ), + ) + .subcommand( + ClapCommand::new("namespace") + .about("Manage Registry namespace ownership") + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand( + ClapCommand::new("claim") + .about("Claim a namespace with a wallet-signed capability authorisation payload") + .arg( + Arg::new("api-url") + .long("api-url") + .value_name("URL") + .help("Registry write API base URL; defaults to CELLSCRIPT_REGISTRY_API_URL"), + ) + .arg( + Arg::new("namespace") + .long("namespace") + .value_name("NAMESPACE") + .required(true) + .help("Namespace to claim; the signed payload must contain a matching publish scope"), + ) + .arg( + Arg::new("payload") + .long("payload") + .value_name("FILE") + .required(true) + .help("Capability authorisation payload JSON created by auth capability create"), + ) + .arg( + Arg::new("wallet-signature") + .long("wallet-signature") + .visible_alias("joyid-signature") + .value_name("FILE") + .required(true) + .help("JoyID or CKB wallet signature JSON whose challenge is the canonical capability payload"), + ) + .arg( + Arg::new("json") + .long("json") + .action(ArgAction::SetTrue) + .help("Emit machine-readable namespace claim output"), + ), + ), ), ) .subcommand( @@ -13167,11 +15252,11 @@ impl CliParser { , ) .subcommand( - ClapCommand::new("package").about("Package integrity commands").subcommand_required(true).subcommand( - ClapCommand::new("verify") - .about("Verify package integrity against Cell.lock and source tree") - , - ), + ClapCommand::new("package") + .about("Package integrity commands") + .subcommand_required(true) + .subcommand(ClapCommand::new("lock").about("Resolve and write the complete Cell.lock dependency graph")) + .subcommand(ClapCommand::new("verify").about("Verify package integrity against Cell.lock and source tree")), ) .subcommand( ClapCommand::new("registry") @@ -13260,6 +15345,13 @@ impl CliParser { entry_action: m.get_one::("entry-action").cloned(), entry_lock: m.get_one::("entry-lock").cloned(), jobs: m.get_one::("jobs").and_then(|s| s.parse().ok()), + features: m.get_many::("features").map(|values| values.cloned().collect()).unwrap_or_default(), + all_features: m.get_flag("all-features"), + no_default_features: m.get_flag("no-default-features"), + locked: m.get_flag("locked"), + frozen: m.get_flag("frozen"), + offline: m.get_flag("offline"), + environment: m.get_one::("environment").cloned(), json: json_output(m), production: m.get_flag("production"), deny_fail_closed: m.get_flag("deny-fail-closed"), @@ -13275,10 +15367,18 @@ impl CliParser { }), Some(("test", m)) => Command::Test(TestArgs { filter: m.get_one::("filter").cloned(), + backend: m.get_one::("backend").cloned(), no_run: m.get_flag("no-run"), nocapture: m.get_flag("nocapture"), fail_fast: m.get_flag("fail-fast"), doc: m.get_flag("doc"), + features: m.get_many::("features").map(|values| values.cloned().collect()).unwrap_or_default(), + all_features: m.get_flag("all-features"), + no_default_features: m.get_flag("no-default-features"), + locked: m.get_flag("locked"), + frozen: m.get_flag("frozen"), + offline: m.get_flag("offline"), + environment: m.get_one::("environment").cloned(), json: json_output(m), ..Default::default() }), @@ -13313,6 +15413,7 @@ impl CliParser { }), Some(("add", m)) => Command::Add(AddArgs { crates: m.get_many::("crates").map(|v| v.cloned().collect()).unwrap_or_default(), + package: m.get_one::("package-name").cloned(), dev: m.get_flag("dev"), build: m.get_flag("build"), git: m.get_one::("git").cloned(), @@ -13339,7 +15440,13 @@ impl CliParser { m.get_one::("primitive-compat").cloned(), m.get_one::("primitive-strict").cloned(), ), - features: Vec::new(), + features: m.get_many::("features").map(|values| values.cloned().collect()).unwrap_or_default(), + all_features: m.get_flag("all-features"), + no_default_features: m.get_flag("no-default-features"), + locked: m.get_flag("locked"), + frozen: m.get_flag("frozen"), + offline: m.get_flag("offline"), + environment: m.get_one::("environment").cloned(), package: m.get_one::("package").cloned(), workspace: m.get_flag("workspace"), }), @@ -13651,6 +15758,8 @@ impl CliParser { Some(("verify-artifact", m)) => Command::VerifyArtifact(VerifyArtifactArgs { artifact: m.get_one::("artifact").map(PathBuf::from).expect("required artifact"), metadata: m.get_one::("metadata").map(PathBuf::from), + lowering_record: m.get_one::("lowering-record").map(PathBuf::from), + source_map: m.get_one::("source-map").map(PathBuf::from), receipt: m.get_one::("receipt").map(PathBuf::from), verify_sources: m.get_flag("verify-sources"), json: json_output(m), @@ -13673,16 +15782,166 @@ impl CliParser { simulate: m.get_flag("simulate"), json: json_output(m), }), + Some(("artifact", m)) => Command::Artifact(ArtifactArgs { + operation: match m.subcommand() { + Some(("ls-idl", action)) => match action.subcommand() { + Some(("validate", command)) => ArtifactOperation::LsIdlValidate { + idl: command.get_one::("idl").map(PathBuf::from).expect("required IDL"), + executable: command.get_one::("executable").map(PathBuf::from), + json: json_output(command), + }, + Some(("bind", command)) => ArtifactOperation::LsIdlBind { + idl: command.get_one::("idl").map(PathBuf::from).expect("required IDL"), + executable: command.get_one::("executable").map(PathBuf::from).expect("required executable"), + output: command.get_one::("output").map(PathBuf::from).expect("required output"), + force: command.get_flag("force"), + json: json_output(command), + }, + Some(("fetch", command)) => ArtifactOperation::LsIdlFetch { + code_hash: command.get_one::("code-hash").cloned().expect("required code hash"), + hash_type: command.get_one::("hash-type").cloned(), + data_hash: command.get_one::("data-hash").cloned(), + network: command.get_one::("network").cloned().expect("defaulted network"), + output: command.get_one::("output").map(PathBuf::from).expect("required output"), + api_url: command.get_one::("api-url").cloned(), + force: command.get_flag("force"), + json: json_output(command), + }, + Some(("bundle", command)) => ArtifactOperation::LsIdlBundle { + idl: command.get_one::("idl").map(PathBuf::from).expect("required IDL"), + executable: command.get_one::("executable").map(PathBuf::from).expect("required executable"), + source: command.get_one::("source").map(PathBuf::from).expect("required source"), + namespace: command.get_one::("namespace").cloned().expect("required namespace"), + name: command.get_one::("name").cloned().expect("required name"), + release: command.get_one::("release").cloned().expect("required release"), + language: command.get_one::("language").cloned().expect("required language"), + hash_type: command.get_one::("hash-type").cloned().expect("defaulted hash type"), + dep_type: command.get_one::("dep-type").cloned().expect("defaulted dep type"), + toolchain: command.get_one::("toolchain").cloned().expect("required toolchain"), + source_revision: command.get_one::("source-revision").cloned().expect("required source revision"), + output: command.get_one::("output").map(PathBuf::from).expect("defaulted output"), + artifact_manifest_output: command + .get_one::("artifact-manifest-output") + .map(PathBuf::from) + .expect("defaulted artifact manifest output"), + force: command.get_flag("force"), + json: json_output(command), + }, + _ => unreachable!(), + }, + Some(("fetch", action)) => ArtifactOperation::Fetch { + coordinate: action.get_one::("coordinate").cloned().expect("required coordinate"), + output: action.get_one::("output").map(PathBuf::from).expect("required output"), + receipt: action.get_one::("receipt").map(PathBuf::from), + api_url: action.get_one::("api-url").cloned(), + force: action.get_flag("force"), + json: json_output(action), + }, + Some(("verify", action)) => ArtifactOperation::Verify { + bundle: action.get_one::("bundle").map(PathBuf::from).expect("required bundle"), + receipt: action.get_one::("receipt").map(PathBuf::from).expect("required receipt"), + json: json_output(action), + }, + Some(("pin", action)) => ArtifactOperation::Pin { + coordinate: action.get_one::("coordinate").cloned().expect("required coordinate"), + output: action.get_one::("output").map(PathBuf::from).expect("defaulted output"), + api_url: action.get_one::("api-url").cloned(), + accept_hash_bound: action.get_flag("accept-hash-bound"), + force: action.get_flag("force"), + json: json_output(action), + }, + Some(("copy", action)) => ArtifactOperation::Copy { + coordinate: action.get_one::("coordinate").cloned().expect("required coordinate"), + destination: action.get_one::("destination").map(PathBuf::from).expect("defaulted destination"), + api_url: action.get_one::("api-url").cloned(), + accept_hash_bound: action.get_flag("accept-hash-bound"), + json: json_output(action), + }, + Some(("cell-dep", action)) => ArtifactOperation::CellDep { + coordinate: action.get_one::("coordinate").cloned().expect("required coordinate"), + output: action.get_one::("output").map(PathBuf::from).expect("required output"), + api_url: action.get_one::("api-url").cloned(), + rpc_url: action.get_one::("rpc-url").cloned(), + accept_hash_bound: action.get_flag("accept-hash-bound"), + force: action.get_flag("force"), + json: json_output(action), + }, + Some(("record-deployment", action)) => ArtifactOperation::RecordDeployment { + coordinate: action.get_one::("coordinate").cloned().expect("required coordinate"), + network: action.get_one::("network").cloned().expect("defaulted network"), + code_hash: action.get_one::("code-hash").cloned().expect("required code hash"), + hash_type: action.get_one::("hash-type").cloned().expect("required hash type"), + dep_type: action.get_one::("dep-type").cloned().expect("required dep type"), + tx_hash: action.get_one::("tx-hash").cloned().expect("required tx hash"), + index: *action.get_one::("index").expect("required index"), + capability_key_id: action.get_one::("capability-key-id").cloned().expect("required capability key id"), + capability_signature: action.get_one::("capability-signature").cloned(), + api_url: action.get_one::("api-url").cloned(), + print_payload: action.get_flag("print-payload"), + json: json_output(action), + }, + Some(("set-availability", action)) => ArtifactOperation::SetAvailability { + coordinate: action.get_one::("coordinate").cloned().expect("required coordinate"), + status: action.get_one::("status").cloned().expect("required status"), + reason: action.get_one::("reason").cloned(), + capability_key_id: action.get_one::("capability-key-id").cloned().expect("required capability key id"), + capability_signature: action.get_one::("capability-signature").cloned(), + api_url: action.get_one::("api-url").cloned(), + print_payload: action.get_flag("print-payload"), + json: json_output(action), + }, + Some(("reproduction-report", action)) => ArtifactOperation::ReproductionReport { + coordinate: action.get_one::("coordinate").cloned().expect("required coordinate"), + artifact: action.get_one::("artifact").map(PathBuf::from).expect("required artifact"), + build_log: action.get_one::("build-log").map(PathBuf::from).expect("required build log"), + builder_id: action.get_one::("builder-id").cloned().expect("required builder id"), + trust_domain: action.get_one::("trust-domain").cloned().expect("required trust domain"), + builder_key_id: action.get_one::("builder-key-id").cloned().expect("required builder key id"), + builder_public_key: action + .get_one::("builder-public-key") + .cloned() + .expect("required builder public key"), + output: action.get_one::("output").map(PathBuf::from).expect("required output"), + api_url: action.get_one::("api-url").cloned(), + force: action.get_flag("force"), + json: json_output(action), + }, + Some(("reproduction-evidence", action)) => ArtifactOperation::ReproductionEvidence { + coordinate: action.get_one::("coordinate").cloned().expect("required coordinate"), + reports: action + .get_many::("report") + .expect("required reproduction reports") + .map(PathBuf::from) + .collect(), + output: action.get_one::("output").map(PathBuf::from).expect("required output"), + api_url: action.get_one::("api-url").cloned(), + force: action.get_flag("force"), + json: json_output(action), + }, + Some(("commitment", action)) => ArtifactOperation::Commitment { + coordinate: action.get_one::("coordinate").cloned().expect("required coordinate"), + output: action.get_one::("output").map(PathBuf::from).expect("required output"), + api_url: action.get_one::("api-url").cloned(), + force: action.get_flag("force"), + json: json_output(action), + }, + _ => unreachable!(), + }, + }), Some(("publish", m)) => Command::Publish(PublishArgs { dry_run: m.get_flag("dry-run"), offline: m.get_flag("offline"), allow_dirty: m.get_flag("allow-dirty"), api_url: m.get_one::("api-url").cloned(), capability_key_id: m.get_one::("capability-key-id").cloned(), + authorise: m.get_flag("authorise"), + no_open: m.get_flag("no-open"), capability_signature: m.get_one::("capability-signature").cloned(), idempotency_key: m.get_one::("idempotency-key").cloned(), payload: m.get_one::("payload").map(PathBuf::from), source_snapshot: m.get_one::("source-snapshot").map(PathBuf::from), + artifact_manifest: m.get_one::("artifact-manifest").map(PathBuf::from), + artifact_kind: m.get_one::("artifact-kind").cloned(), print_payload: m.get_flag("print-payload"), json: json_output(m), }), @@ -13695,6 +15954,7 @@ impl CliParser { allow_unverified: m.get_flag("allow-unverified"), allow_quarantined: m.get_flag("allow-quarantined"), }), + Some(("lock", m)) => Command::Lock(PackageLockArgs { json: json_output(m) }), Some(("update", _)) => Command::Update, Some(("info", m)) => Command::Info(InfoArgs { json: json_output(m) }), Some(("login", m)) => { @@ -13709,6 +15969,14 @@ impl CliParser { Some(("revoke", revoke)) => Command::AuthCapabilityRevoke(auth_capability_revoke_args_from_matches(revoke)), _ => unreachable!(), }, + Some(("reproducer", reproducer)) => match reproducer.subcommand() { + Some(("create", create)) => Command::AuthReproducerCreate(auth_reproducer_create_args_from_matches(create)), + _ => unreachable!(), + }, + Some(("namespace", namespace)) => match namespace.subcommand() { + Some(("claim", claim)) => Command::AuthNamespaceClaim(auth_namespace_claim_args_from_matches(claim)), + _ => unreachable!(), + }, _ => unreachable!(), }, Some(("certify", m)) => Command::Certify(CertifyArgs { @@ -13720,6 +15988,7 @@ impl CliParser { require_production: m.get_flag("require-production"), }), Some(("package", m)) => match m.subcommand() { + Some(("lock", lock)) => Command::Lock(PackageLockArgs { json: json_output(lock) }), Some(("verify", verify)) => Command::PackageVerify(PackageVerifyArgs { json: json_output(verify) }), _ => unreachable!(), }, @@ -13793,6 +16062,141 @@ mod tests { let _cmd = Command::Clean(CleanArgs::default()); } + #[test] + fn publish_parses_continuous_browser_authorisation() { + let matches = CliParser::command().try_get_matches_from(["cellc", "publish", "--authorise", "--no-open"]).unwrap(); + let Command::Publish(args) = CliParser::parse_matches(matches) else { + panic!("expected publish command"); + }; + assert!(args.authorise); + assert!(args.no_open); + assert!(args.capability_key_id.is_none()); + } + + #[test] + fn publish_authorisation_rejects_an_existing_key_override() { + let result = CliParser::command().try_get_matches_from([ + "cellc", + "publish", + "--authorise", + "--capability-key-id", + "cap_0123456789abcdef0123456789abcdef", + ]); + assert!(result.is_err()); + } + + #[test] + fn browser_authorisation_activates_only_the_server_confirmed_key() { + for status in ["pending", "cancelled", "expired"] { + let persisted = std::cell::Cell::new(false); + activate_registry_key_after_wallet_approval(status, None, "cap_local", || { + persisted.set(true); + Ok(()) + }) + .unwrap(); + assert!(!persisted.get(), "{status} must not activate a publishing key"); + } + + for status in ["authorised", "review_pending"] { + let persisted = std::cell::Cell::new(false); + activate_registry_key_after_wallet_approval(status, Some("cap_local"), "cap_local", || { + persisted.set(true); + Ok(()) + }) + .unwrap(); + assert!(persisted.get(), "{status} must preserve the approved publishing key"); + } + + let persisted = std::cell::Cell::new(false); + let mismatch = activate_registry_key_after_wallet_approval("authorised", Some("cap_remote"), "cap_local", || { + persisted.set(true); + Ok(()) + }); + assert!(mismatch.is_err()); + assert!(!persisted.get(), "a mismatched server key must not be persisted"); + + for status in ["authorised", "review_pending"] { + let activated = std::cell::Cell::new(false); + let missing = activate_registry_key_after_wallet_approval(status, None, "cap_local", || { + activated.set(true); + Ok(()) + }); + assert!(missing.is_err(), "{status} must require a returned key id"); + assert!(!activated.get(), "{status} must not activate a key without its id"); + } + } + + #[test] + fn browser_authorisation_removes_pending_keys_only_for_explicit_terminal_failure() { + for status in ["pending", "authorised", "review_pending", "unreachable", "deadline_elapsed"] { + assert!(!registry_authorisation_status_removes_pending_key(status), "{status} must preserve the pending key"); + } + for status in ["cancelled", "expired"] { + assert!(registry_authorisation_status_removes_pending_key(status), "{status} must remove the pending key"); + } + } + + #[test] + fn registry_keychain_state_distinguishes_pending_and_active_keys() { + let pending = RegistryKeychainSecret { + schema: REGISTRY_KEYCHAIN_SECRET_SCHEMA.to_string(), + status: "pending".to_string(), + pkcs8_b64: "cGVuZGluZw==".to_string(), + session_id: Some("auth_0123456789abcdef0123456789abcdef".to_string()), + expires_at: Some("2026-08-05T12:00:00.000Z".to_string()), + pending_expires_at_unix_seconds: Some(1_786_000_000), + }; + let encoded = serde_json::to_string(&pending).unwrap(); + let decoded: RegistryKeychainSecret = serde_json::from_str(&encoded).unwrap(); + assert_eq!(decoded.schema, REGISTRY_KEYCHAIN_SECRET_SCHEMA); + assert_eq!(decoded.status, "pending"); + assert_eq!(decoded.session_id.as_deref(), Some("auth_0123456789abcdef0123456789abcdef")); + + let active = RegistryKeychainSecret { + schema: REGISTRY_KEYCHAIN_SECRET_SCHEMA.to_string(), + status: "active".to_string(), + pkcs8_b64: pending.pkcs8_b64, + session_id: None, + expires_at: None, + pending_expires_at_unix_seconds: None, + }; + assert_eq!(serde_json::from_str::(&serde_json::to_string(&active).unwrap()).unwrap().status, "active"); + } + + #[test] + fn record_deployment_parses_explicit_testnet_network() { + let code_hash = format!("0x{}", "11".repeat(32)); + let tx_hash = format!("0x{}", "22".repeat(32)); + let matches = CliParser::command() + .try_get_matches_from([ + "cellc", + "artifact", + "record-deployment", + "acme/demo@1.0.0", + "--network", + "testnet", + "--code-hash", + &code_hash, + "--hash-type", + "data1", + "--dep-type", + "code", + "--tx-hash", + &tx_hash, + "--index", + "0", + "--capability-key-id", + "cap_test", + ]) + .unwrap(); + let Command::Artifact(ArtifactArgs { operation: ArtifactOperation::RecordDeployment { network, .. } }) = + CliParser::parse_matches(matches) + else { + panic!("expected artifact record-deployment command"); + }; + assert_eq!(network, "testnet"); + } + #[test] fn registry_api_urls_require_https_except_for_loopback() { assert_eq!(registry_origin_from_api_base("https://registry.example/api").unwrap(), "https://registry.example"); diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 322fe4ec..95dcf0d6 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,8 +1,10 @@ //! CLI module //! Command-line interface and subcommand implementation +mod artifact; pub mod commands; mod novaseal_certification; +mod test_runner; use crate::error::Result; use commands::{CliParser, CommandExecutor}; diff --git a/src/cli/novaseal_certification.rs b/src/cli/novaseal_certification.rs index 8ced1c19..82d3c1ef 100644 --- a/src/cli/novaseal_certification.rs +++ b/src/cli/novaseal_certification.rs @@ -1753,8 +1753,8 @@ fn build_stateful_acceptance_report(repo_root: &Path, agreement_conformance: &Va "proposals/novaseal/fungible-xudt-profile-v0/src", "proposals/novaseal/fungible-xudt-profile-v0/schemas", VERIFIER_ROOT, - "scripts/novaseal_planned_profiles_devnet_stateful_live.py", - "scripts/novaseal_devnet_stateful_live.py", + "crates/cellscript-tools/src/novaseal_planned_fungible.rs", + "crates/cellscript-tools/src/ckb_devnet.rs", ], &[("issue", "/issue/commit/tx_hash"), ("transfer", "/transfer/commit/tx_hash"), ("settle", "/settle/commit/tx_hash")], &[ @@ -1783,8 +1783,8 @@ fn build_stateful_acceptance_report(repo_root: &Path, agreement_conformance: &Va "proposals/novaseal/rwa-receipt-profile-v0/src", "proposals/novaseal/rwa-receipt-profile-v0/schemas", VERIFIER_ROOT, - "scripts/novaseal_planned_profiles_devnet_stateful_live.py", - "scripts/novaseal_devnet_stateful_live.py", + "crates/cellscript-tools/src/novaseal_planned_rwa.rs", + "crates/cellscript-tools/src/ckb_devnet.rs", ], &[("materialize", "/materialize/commit/tx_hash"), ("claim", "/claim/commit/tx_hash"), ("settle", "/settle/commit/tx_hash")], &[ @@ -1813,8 +1813,8 @@ fn build_stateful_acceptance_report(repo_root: &Path, agreement_conformance: &Va "proposals/novaseal/btc-transaction-commitment-profile-v0/src", "proposals/novaseal/btc-transaction-commitment-profile-v0/schemas", VERIFIER_ROOT, - "scripts/novaseal_planned_profiles_devnet_stateful_live.py", - "scripts/novaseal_devnet_stateful_live.py", + "crates/cellscript-tools/src/novaseal_planned_btc_tx.rs", + "crates/cellscript-tools/src/ckb_devnet.rs", ], &[("commit_transaction", "/commit_transaction/commit/tx_hash")], &[ @@ -1840,8 +1840,8 @@ fn build_stateful_acceptance_report(repo_root: &Path, agreement_conformance: &Va "proposals/novaseal/btc-utxo-seal-profile-v0/src", "proposals/novaseal/btc-utxo-seal-profile-v0/schemas", VERIFIER_ROOT, - "scripts/novaseal_planned_profiles_devnet_stateful_live.py", - "scripts/novaseal_devnet_stateful_live.py", + "crates/cellscript-tools/src/novaseal_planned_btc_utxo.rs", + "crates/cellscript-tools/src/ckb_devnet.rs", ], &[("close_utxo_seal", "/close_utxo_seal/commit/tx_hash")], &[ @@ -1867,8 +1867,8 @@ fn build_stateful_acceptance_report(repo_root: &Path, agreement_conformance: &Va "proposals/novaseal/dual-seal-profile-v0/src", "proposals/novaseal/dual-seal-profile-v0/schemas", VERIFIER_ROOT, - "scripts/novaseal_planned_profiles_devnet_stateful_live.py", - "scripts/novaseal_devnet_stateful_live.py", + "crates/cellscript-tools/src/novaseal_planned_dual.rs", + "crates/cellscript-tools/src/ckb_devnet.rs", ], &[("finalize_dual_seal", "/finalize_dual_seal/commit/tx_hash")], &[ @@ -1893,8 +1893,8 @@ fn build_stateful_acceptance_report(repo_root: &Path, agreement_conformance: &Va "proposals/novaseal/fiber-candidate-profile-v0/src", "proposals/novaseal/fiber-candidate-profile-v0/schemas", VERIFIER_ROOT, - "scripts/novaseal_planned_profiles_devnet_stateful_live.py", - "scripts/novaseal_devnet_stateful_live.py", + "crates/cellscript-tools/src/novaseal_planned_fiber.rs", + "crates/cellscript-tools/src/ckb_devnet.rs", ], &[("settle_fiber_candidate", "/settle_fiber_candidate/commit/tx_hash")], &[ @@ -2961,7 +2961,8 @@ fn live_core_summary(repo_root: &Path, report: Option<&Value>) -> Result "proposals/novaseal/v0-mvp-skeleton/src", "proposals/novaseal/v0-mvp-skeleton/schemas", VERIFIER_ROOT, - "scripts/novaseal_devnet_stateful_live.py", + "crates/cellscript-tools/src/novaseal_core_live.rs", + "crates/cellscript-tools/src/ckb_devnet.rs", ], )?; Ok(json!({ @@ -2998,8 +2999,8 @@ fn live_agreement_summary(repo_root: &Path, report: Option<&Value>) -> Result { + ($name:literal, $value:expr) => { checks.insert($name.to_string(), Value::Bool($value)); }; } @@ -8384,7 +8385,7 @@ mod tests { } #[test] - fn novaseal_handoff_hash_matches_python_generator_vector() { + fn novaseal_handoff_hash_matches_reference_generator_vector() { let value = json!({ "z": 1, "a": ["b", true, null], diff --git a/src/cli/test_runner.rs b/src/cli/test_runner.rs new file mode 100644 index 00000000..1b0d66bc --- /dev/null +++ b/src/cli/test_runner.rs @@ -0,0 +1,821 @@ +use crate::error::{CompileError, Result}; +use crate::runtime_errors::CellScriptRuntimeError; +use crate::simulate::{SimValue, SimulateError, SimulateInterpreter}; +use crate::{compile_path_with_entry_action, compile_path_with_entry_lock, CompileOptions, CompileResult}; +use camino::Utf8PathBuf; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::collections::{BTreeMap, BTreeSet}; +use std::path::{Component, Path, PathBuf}; + +#[cfg(feature = "vm-runner")] +use ckb_vm::{ + cost_model::estimate_cycles, machine::VERSION2, Bytes, DefaultCoreMachine, DefaultMachineBuilder, DefaultMachineRunner, + Error as VmError, SparseMemory, SupportMachine, TraceMachine, WXorXMemory, ISA_B, ISA_IMC, ISA_MOP, +}; + +const SCENARIO_SCHEMA: &str = "cellscript-test-scenario-v1"; +const MAX_SCENARIO_BYTES: u64 = 1024 * 1024; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum TestBackend { + Simulator, + CkbVm, +} + +impl TestBackend { + pub(super) fn parse(value: &str) -> Result> { + match value { + "simulator" => Ok(vec![Self::Simulator]), + "ckb-vm" => Ok(vec![Self::CkbVm]), + "all" => Ok(vec![Self::Simulator, Self::CkbVm]), + _ => Err(CompileError::without_span("invalid test backend; expected simulator, ckb-vm, or all")), + } + } + + fn name(self) -> &'static str { + match self { + Self::Simulator => "simulator", + Self::CkbVm => "ckb-vm", + } + } + + fn evidence_tier(self) -> &'static str { + match self { + Self::Simulator => "development-non-consensus", + Self::CkbVm => "authoritative-runtime", + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct Scenario { + schema: String, + name: String, + source: String, + target_profile: String, + entry: ScenarioEntry, + initial_cells: Vec, + steps: Vec, + limits: ScenarioLimits, + #[serde(default)] + oracle: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct ScenarioEntry { + kind: String, + name: String, + args: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct ScenarioArgument { + name: String, + ty: String, + value: Value, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct ScenarioCell { + name: String, + capacity: u64, + data: String, + lock: ScenarioScript, + #[serde(rename = "type")] + type_script: Option, + #[serde(default)] + prior_output: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct ScenarioScript { + code_hash: String, + hash_type: String, + args: String, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct ScenarioStep { + name: String, + consumes: Vec, + outputs: Vec, + cell_deps: Vec, + header_deps: Vec, + since: BTreeMap, + witnesses: Vec, + expectation: ScenarioExpectation, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct ScenarioCellDep { + name: String, + tx_hash: String, + index: u32, + dep_type: String, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct ScenarioHeaderDep { + name: String, + hash: String, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct ScenarioWitness { + input: String, + lock: Option, + input_type: Option, + output_type: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct ScenarioExpectation { + status: String, + #[serde(default)] + result: Option, + #[serde(default)] + runtime_error: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct ExpectedRuntimeError { + code: u64, + name: String, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct ScenarioLimits { + max_steps: u64, + max_cycles: u64, + max_transaction_bytes: u64, + minimum_cell_capacity: u64, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct ScenarioOracle { + kind: String, + scenario_id: String, + evidence_path: String, +} + +#[derive(Debug)] +struct StateReport { + initial_live: Vec, + final_live: Vec, + transitions: Vec, +} + +#[derive(Debug)] +enum Observation { + Passed { result: String, steps: Option, cycles: Option, trace: Vec }, + RuntimeError { code: u64, name: String, steps: Option, cycles: Option, trace: Vec }, +} + +pub(super) fn collect_scenario_files(root: &Path) -> Result> { + if !root.exists() { + return Ok(Vec::new()); + } + let mut files = Vec::new(); + let mut stack = vec![root.to_path_buf()]; + while let Some(path) = stack.pop() { + for entry in std::fs::read_dir(&path)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + stack.push(path); + } else if path.file_name().and_then(|name| name.to_str()).is_some_and(|name| name.ends_with(".scenario.json")) { + files.push(path); + } + } + } + files.sort(); + Ok(files) +} + +pub(super) fn run_scenario(path: &Path, backend: TestBackend) -> Result { + let bytes = std::fs::read(path) + .map_err(|error| CompileError::without_span(format!("failed to read scenario '{}': {error}", path.display())))?; + if bytes.is_empty() || bytes.len() as u64 > MAX_SCENARIO_BYTES { + return Err(CompileError::without_span(format!( + "scenario '{}' must be non-empty and no larger than {MAX_SCENARIO_BYTES} bytes", + path.display() + ))); + } + let scenario: Scenario = serde_json::from_slice(&bytes) + .map_err(|error| CompileError::without_span(format!("invalid scenario '{}': {error}", path.display())))?; + validate_scenario_shape(path, &scenario, bytes.len() as u64)?; + let source = resolve_scenario_source(path, &scenario.source)?; + let state = validate_state_transitions(&scenario)?; + validate_oracle(path, scenario.oracle.as_ref())?; + + let options = CompileOptions { + target: Some("riscv64-elf".to_string()), + target_profile: Some("ckb".to_string()), + ..CompileOptions::default() + }; + let result = match scenario.entry.kind.as_str() { + "action" => compile_path_with_entry_action(&source, options, scenario.entry.name.clone())?, + "lock" => compile_path_with_entry_lock(&source, options, scenario.entry.name.clone())?, + _ => unreachable!("validated entry kind"), + }; + let checker_report = checker_report(&result)?; + + let args = scenario.entry.args.iter().map(scenario_argument_value).collect::>>()?; + let mut step_reports = Vec::with_capacity(scenario.steps.len()); + for step in &scenario.steps { + let observation = match backend { + TestBackend::Simulator => run_simulator(&result, &scenario, &args)?, + TestBackend::CkbVm => run_ckb_vm(&result, &scenario)?, + }; + validate_expectation(&step.expectation, &observation, &step.name)?; + step_reports.push(observation_report(step, observation)); + } + + let coverage = coverage_report(&result, &scenario, backend); + Ok(json!({ + "schema": "cellscript-test-report-v1", + "status": "passed", + "scenario": scenario.name, + "scenario_path": path.to_string_lossy(), + "backend": backend.name(), + "evidence_tier": backend.evidence_tier(), + "compiler_version": result.metadata.compiler_version, + "artifact_hash": result.metadata.artifact_hash, + "checker_name": result.metadata.verified_artifact.checker_name, + "checker_version": checker_report.checker_version, + "checker_policy_schema": checker_report.checker_policy_schema, + "lowering_record_hash": result.metadata.verified_artifact.lowering_record_hash, + "source_map_hash": result.metadata.verified_artifact.source_map_hash, + "target_profile": result.metadata.target_profile.name, + "compatibility_profile": result.metadata.compatibility_profile, + "entry": { + "kind": scenario.entry.kind, + "name": scenario.entry.name, + "inputs": scenario.entry.args.iter().map(|arg| json!({"name": arg.name, "type": arg.ty, "value": arg.value})).collect::>() + }, + "state": { + "validation": "local-live-cell-replacement-v1", + "initial_live": state.initial_live, + "final_live": state.final_live, + "transitions": state.transitions + }, + "oracle": scenario.oracle.as_ref().map(|oracle| json!({ + "kind": oracle.kind, + "scenario_id": oracle.scenario_id, + "evidence_path": oracle.evidence_path, + "state": "declared-not-promoted-by-package-runner" + })), + "steps": step_reports, + "coverage": coverage, + })) +} + +fn validate_scenario_shape(path: &Path, scenario: &Scenario, scenario_bytes: u64) -> Result<()> { + if scenario.schema != SCENARIO_SCHEMA || scenario.name.is_empty() || scenario.target_profile != "ckb" { + return Err(CompileError::without_span(format!( + "scenario '{}' has an unsupported schema, empty name, or non-CKB target profile", + path.display() + ))); + } + if !matches!(scenario.entry.kind.as_str(), "action" | "lock") || scenario.entry.name.is_empty() { + return Err(CompileError::without_span(format!("scenario '{}' has an invalid entry", path.display()))); + } + if scenario.steps.is_empty() + || scenario.limits.max_steps == 0 + || scenario.limits.max_cycles == 0 + || scenario.limits.max_transaction_bytes == 0 + || scenario_bytes > scenario.limits.max_transaction_bytes + { + return Err(CompileError::without_span(format!("scenario '{}' has empty steps or invalid/exceeded limits", path.display()))); + } + let mut argument_names = BTreeSet::new(); + for argument in &scenario.entry.args { + if argument.name.is_empty() || argument.ty.is_empty() || !argument_names.insert(argument.name.as_str()) { + return Err(CompileError::without_span(format!("scenario '{}' has duplicate or empty entry arguments", path.display()))); + } + } + let mut step_names = BTreeSet::new(); + for step in &scenario.steps { + if step.name.is_empty() || !step_names.insert(step.name.as_str()) { + return Err(CompileError::without_span(format!("scenario '{}' has duplicate or empty step names", path.display()))); + } + validate_step_contract(path, step, &scenario.limits)?; + } + Ok(()) +} + +fn validate_step_contract(path: &Path, step: &ScenarioStep, limits: &ScenarioLimits) -> Result<()> { + let expected = &step.expectation; + match expected.status.as_str() { + "pass" if expected.runtime_error.is_none() => {} + "runtime-error" => { + let error = expected.runtime_error.as_ref().ok_or_else(|| { + CompileError::without_span(format!("scenario '{}' step '{}' omits its exact runtime error", path.display(), step.name)) + })?; + let registered = CellScriptRuntimeError::from_code(error.code).ok_or_else(|| { + CompileError::without_span(format!( + "scenario '{}' step '{}' uses unknown runtime code {}", + path.display(), + step.name, + error.code + )) + })?; + if registered.name() != error.name { + return Err(CompileError::without_span(format!( + "scenario '{}' step '{}' runtime code/name mismatch", + path.display(), + step.name + ))); + } + } + _ => { + return Err(CompileError::without_span(format!( + "scenario '{}' step '{}' has an invalid expectation", + path.display(), + step.name + ))); + } + } + let estimate = serde_json::to_vec(step) + .map_err(|error| CompileError::without_span(format!("failed to size scenario step: {error}")))? + .len() as u64; + if estimate > limits.max_transaction_bytes { + return Err(CompileError::without_span(format!("scenario step '{}' exceeds max_transaction_bytes", step.name))); + } + let mut deps = BTreeSet::new(); + for dep in &step.cell_deps { + if dep.name.is_empty() + || !deps.insert(dep.name.as_str()) + || !valid_hash(&dep.tx_hash) + || !matches!(dep.dep_type.as_str(), "code" | "dep-group") + { + return Err(CompileError::without_span(format!("scenario step '{}' has an invalid CellDep", step.name))); + } + let _ = dep.index; + } + let mut headers = BTreeSet::new(); + for header in &step.header_deps { + if header.name.is_empty() || !headers.insert(header.name.as_str()) || !valid_hash(&header.hash) { + return Err(CompileError::without_span(format!("scenario step '{}' has an invalid HeaderDep", step.name))); + } + } + for cell in step.since.keys() { + if !step.consumes.contains(cell) { + return Err(CompileError::without_span(format!("scenario step '{}' has since for a non-consumed Cell", step.name))); + } + } + let mut witnessed = BTreeSet::new(); + for witness in &step.witnesses { + if !step.consumes.contains(&witness.input) || !witnessed.insert(witness.input.as_str()) { + return Err(CompileError::without_span(format!("scenario step '{}' has a stale or duplicate witness input", step.name))); + } + for bytes in [&witness.lock, &witness.input_type, &witness.output_type].into_iter().flatten() { + validate_hex("witness", bytes)?; + } + } + Ok(()) +} + +fn validate_state_transitions(scenario: &Scenario) -> Result { + let mut live = BTreeMap::::new(); + let mut all_names = BTreeSet::new(); + for cell in &scenario.initial_cells { + validate_cell(cell, &scenario.limits)?; + if !all_names.insert(cell.name.clone()) || live.insert(cell.name.clone(), cell.clone()).is_some() { + return Err(CompileError::without_span(format!("scenario '{}' has duplicate initial Cell names", scenario.name))); + } + } + let initial_live = live.keys().cloned().collect(); + let mut transitions = Vec::new(); + for step in &scenario.steps { + let mut consumed = BTreeSet::new(); + for name in &step.consumes { + if !consumed.insert(name.clone()) || live.remove(name).is_none() { + return Err(CompileError::without_span(format!( + "scenario '{}' step '{}' consumes a missing, dead, or duplicate Cell '{}'", + scenario.name, step.name, name + ))); + } + } + let mut produced = Vec::new(); + for cell in &step.outputs { + validate_cell(cell, &scenario.limits)?; + if all_names.contains(&cell.name) || live.contains_key(&cell.name) { + return Err(CompileError::without_span(format!( + "scenario '{}' step '{}' reuses Cell name '{}'", + scenario.name, step.name, cell.name + ))); + } + if let Some(prior) = &cell.prior_output + && !consumed.contains(prior) + { + return Err(CompileError::without_span(format!( + "scenario '{}' step '{}' output '{}' names unconsumed prior output '{}'", + scenario.name, step.name, cell.name, prior + ))); + } + all_names.insert(cell.name.clone()); + produced.push(cell.name.clone()); + live.insert(cell.name.clone(), cell.clone()); + } + transitions.push(json!({ + "step": step.name, + "consumed_became_dead": consumed, + "outputs_became_live": produced, + "live_after": live.keys().cloned().collect::>() + })); + } + Ok(StateReport { initial_live, final_live: live.keys().cloned().collect(), transitions }) +} + +fn validate_cell(cell: &ScenarioCell, limits: &ScenarioLimits) -> Result<()> { + if cell.name.is_empty() || cell.capacity < limits.minimum_cell_capacity { + return Err(CompileError::without_span(format!("Cell '{}' has an empty name or insufficient capacity", cell.name))); + } + validate_hex("Cell data", &cell.data)?; + validate_script(&cell.lock)?; + if let Some(script) = &cell.type_script { + validate_script(script)?; + } + Ok(()) +} + +fn validate_script(script: &ScenarioScript) -> Result<()> { + if !valid_hash(&script.code_hash) || !matches!(script.hash_type.as_str(), "data" | "type" | "data1" | "data2") { + return Err(CompileError::without_span("scenario contains an invalid Script identity")); + } + validate_hex("Script args", &script.args) +} + +fn validate_hex(label: &str, value: &str) -> Result<()> { + let raw = value.strip_prefix("0x").unwrap_or(value); + if !raw.len().is_multiple_of(2) || !raw.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err(CompileError::without_span(format!("{label} must be even-length hexadecimal"))); + } + Ok(()) +} + +fn valid_hash(value: &str) -> bool { + let raw = value.strip_prefix("0x").unwrap_or(value); + raw.len() == 64 && raw.bytes().all(|byte| byte.is_ascii_hexdigit()) +} + +fn resolve_scenario_source(scenario_path: &Path, source: &str) -> Result { + let source_path = Path::new(source); + if source_path.is_absolute() + || source_path + .components() + .any(|component| matches!(component, Component::ParentDir | Component::RootDir | Component::Prefix(_))) + { + return Err(CompileError::without_span("scenario source path must be confined and relative")); + } + let parent = scenario_path.parent().unwrap_or_else(|| Path::new(".")); + let root = std::fs::canonicalize(parent) + .map_err(|error| CompileError::without_span(format!("failed to resolve scenario directory: {error}")))?; + let resolved = std::fs::canonicalize(parent.join(source)) + .map_err(|error| CompileError::without_span(format!("failed to resolve scenario source '{source}': {error}")))?; + if !resolved.starts_with(&root) || resolved.extension().and_then(|extension| extension.to_str()) != Some("cell") { + return Err(CompileError::without_span("scenario source escapes its directory or is not a .cell file")); + } + Utf8PathBuf::from_path_buf(resolved) + .map_err(|path| CompileError::without_span(format!("scenario source path '{}' is not valid UTF-8", path.display()))) +} + +fn validate_oracle(scenario_path: &Path, oracle: Option<&ScenarioOracle>) -> Result<()> { + let Some(oracle) = oracle else { + return Ok(()); + }; + if oracle.kind != "cellscript-ckb-stateful-scenario-v1" || oracle.scenario_id.is_empty() { + return Err(CompileError::without_span("scenario oracle has an unsupported kind or empty id")); + } + let evidence = Path::new(&oracle.evidence_path); + if evidence.is_absolute() || evidence.components().any(|component| matches!(component, Component::ParentDir)) { + return Err(CompileError::without_span("scenario oracle evidence path must be confined and relative")); + } + let _ = scenario_path; + Ok(()) +} + +fn scenario_argument_value(argument: &ScenarioArgument) -> Result { + match argument.ty.as_str() { + "u8" | "u16" | "u32" | "u64" => argument + .value + .as_u64() + .map(SimValue::Integer) + .ok_or_else(|| CompileError::without_span(format!("argument '{}' must be an unsigned integer", argument.name))), + "bool" => argument + .value + .as_bool() + .map(SimValue::Bool) + .ok_or_else(|| CompileError::without_span(format!("argument '{}' must be a bool", argument.name))), + "string" => argument + .value + .as_str() + .map(|value| SimValue::String(value.to_string())) + .ok_or_else(|| CompileError::without_span(format!("argument '{}' must be a string", argument.name))), + other => Err(CompileError::without_span(format!("scenario argument type '{other}' is not supported by the v1 runner"))), + } +} + +fn run_simulator(result: &CompileResult, scenario: &Scenario, args: &[SimValue]) -> Result { + let mut interpreter = SimulateInterpreter::new(&result.ast, scenario.limits.max_steps); + let observed = match scenario.entry.kind.as_str() { + "action" => interpreter.simulate_action(&scenario.entry.name, args), + "lock" => interpreter.simulate_lock(&scenario.entry.name, args), + _ => unreachable!("validated entry kind"), + }; + match observed { + Ok(observed) => Ok(Observation::Passed { + result: observed.return_value.to_string(), + steps: Some(observed.steps), + cycles: None, + trace: observed.trace.iter().map(ToString::to_string).collect(), + }), + Err(SimulateError::RuntimeError { code, name }) => { + Ok(Observation::RuntimeError { code, name, steps: None, cycles: None, trace: Vec::new() }) + } + Err(error) => Err(CompileError::without_span(format!("scenario simulator failed: {error}"))), + } +} + +#[cfg(feature = "vm-runner")] +fn run_ckb_vm(result: &CompileResult, scenario: &Scenario) -> Result { + if !scenario.entry.args.is_empty() { + return Err(CompileError::without_span( + "ckb-vm scenario entry arguments require a transaction syscall harness; use an imported stateful oracle", + )); + } + type ScenarioMachine = TraceMachine>>>; + let core_machine = <::Inner as SupportMachine>::new( + ISA_IMC | ISA_B | ISA_MOP, + VERSION2, + scenario.limits.max_cycles, + ); + let builder = DefaultMachineBuilder::new(core_machine).instruction_cycle_func(Box::new(estimate_cycles)); + let mut machine = ScenarioMachine::new(builder.build()); + let program = Bytes::copy_from_slice(crate::strip_vm_abi_trailer(&result.artifact_bytes)); + machine + .load_program(&program, std::iter::empty::>()) + .map_err(|error| CompileError::without_span(format!("scenario CKB-VM failed to load ELF: {error}")))?; + let exit_code = machine.run().map_err(|error| CompileError::without_span(format!("scenario CKB-VM execution failed: {error}")))?; + let cycles = machine.machine.cycles(); + if exit_code == 0 { + Ok(Observation::Passed { result: "()".to_string(), steps: None, cycles: Some(cycles), trace: Vec::new() }) + } else { + let code = u64::try_from(exit_code) + .map_err(|_| CompileError::without_span(format!("scenario CKB-VM returned negative exit code {exit_code}")))?; + let runtime = CellScriptRuntimeError::from_code(code) + .ok_or_else(|| CompileError::without_span(format!("scenario CKB-VM returned unregistered runtime code {code}")))?; + Ok(Observation::RuntimeError { code, name: runtime.name().to_string(), steps: None, cycles: Some(cycles), trace: Vec::new() }) + } +} + +#[cfg(not(feature = "vm-runner"))] +fn run_ckb_vm(_result: &CompileResult, _scenario: &Scenario) -> Result { + Err(CompileError::without_span("ckb-vm test backend is unavailable because the binary was built without vm-runner")) +} + +fn validate_expectation(expectation: &ScenarioExpectation, observed: &Observation, step: &str) -> Result<()> { + match (expectation.status.as_str(), observed) { + ("pass", Observation::Passed { result, .. }) => { + if expectation.result.as_ref().is_some_and(|expected| expected != result) { + return Err(CompileError::without_span(format!( + "scenario step '{step}' result mismatch: expected {:?}, observed '{result}'", + expectation.result + ))); + } + Ok(()) + } + ("runtime-error", Observation::RuntimeError { code, name, .. }) => { + let expected = expectation.runtime_error.as_ref().expect("validated runtime error"); + if expected.code == *code && expected.name == *name { + Ok(()) + } else { + Err(CompileError::without_span(format!( + "scenario step '{step}' runtime error mismatch: expected {} ({}), observed {} ({})", + expected.code, expected.name, code, name + ))) + } + } + (expected, observed) => Err(CompileError::without_span(format!( + "scenario step '{step}' expected '{expected}' but observed {}", + observation_status(observed) + ))), + } +} + +fn observation_report(step: &ScenarioStep, observation: Observation) -> Value { + match observation { + Observation::Passed { result, steps, cycles, trace } => json!({ + "name": step.name, + "status": "passed", + "result": result, + "runtime_error": null, + "steps": steps, + "cycles": cycles, + "trace": trace, + "transaction": transaction_shape_report(step) + }), + Observation::RuntimeError { code, name, steps, cycles, trace } => json!({ + "name": step.name, + "status": "expected-runtime-error", + "result": null, + "runtime_error": {"code": code, "name": name}, + "steps": steps, + "cycles": cycles, + "trace": trace, + "transaction": transaction_shape_report(step) + }), + } +} + +fn transaction_shape_report(step: &ScenarioStep) -> Value { + json!({ + "consumes": step.consumes, + "outputs": step.outputs.iter().map(|cell| cell.name.as_str()).collect::>(), + "cell_deps": step.cell_deps.iter().map(|dep| dep.name.as_str()).collect::>(), + "header_deps": step.header_deps.iter().map(|dep| dep.name.as_str()).collect::>(), + "since_inputs": step.since.keys().collect::>(), + "witness_inputs": step.witnesses.iter().map(|witness| witness.input.as_str()).collect::>() + }) +} + +fn observation_status(observation: &Observation) -> &'static str { + match observation { + Observation::Passed { .. } => "pass", + Observation::RuntimeError { .. } => "runtime-error", + } +} + +fn checker_report(result: &CompileResult) -> Result { + let record = result + .verified_lowering_record + .as_ref() + .ok_or_else(|| CompileError::without_span("scenario ELF has no verified lowering record"))?; + let source_map = + result.source_artifact_map.as_ref().ok_or_else(|| CompileError::without_span("scenario ELF has no source map"))?; + let metadata = serde_json::to_value(&result.metadata) + .map_err(|error| CompileError::without_span(format!("failed to encode scenario metadata: {error}")))?; + cellscript_artifact_checker::check_bundle_values( + &result.artifact_bytes, + &metadata, + record, + source_map, + &cellscript_artifact_checker::CheckerBudgets::default(), + ) + .map_err(|error| CompileError::without_span(format!("scenario artifact checker rejected the build: {error}"))) +} + +fn coverage_report(result: &CompileResult, scenario: &Scenario, backend: TestBackend) -> Value { + let Some(record) = result.verified_lowering_record.as_ref() else { + return Value::Null; + }; + let Some(source_map) = result.source_artifact_map.as_ref() else { + return Value::Null; + }; + let entry = &scenario.entry; + let entry_record = record.entries.iter().find(|candidate| candidate.name == entry.name); + let entry_id = entry_record.map(|entry| entry.id.as_str()); + let blocks = record + .blocks + .iter() + .filter(|block| Some(block.owner_entry.as_str()) == entry_id) + .map(|block| block.id.clone()) + .collect::>(); + let entry_block = entry_record.map(|entry| entry.entry_block.clone()); + let intervals = source_map + .intervals + .iter() + .filter(|interval| Some(interval.entry_id.as_str()) == entry_id) + .map(|interval| { + json!({ + "source_path": interval.source_path, + "source_start": interval.source_start, + "source_end": interval.source_end, + "block_id": interval.block_id, + "machine_range": interval.machine_range, + "proof_ids": interval.proof_ids, + "runtime_error_codes": interval.runtime_error_codes + }) + }) + .collect::>(); + let proofs = entry_record.map(|entry| entry.proof_ids.clone()).unwrap_or_default(); + let runtime_errors = record + .runtime_error_exits + .iter() + .filter(|exit| blocks.contains(&exit.block_id)) + .map(|exit| json!({"code": exit.code, "name": exit.name, "block_id": exit.block_id})) + .collect::>(); + let observed_runtime_codes = scenario + .steps + .iter() + .filter_map(|step| step.expectation.runtime_error.as_ref().map(|error| error.code as i32)) + .collect::>(); + let observed_runtime_errors = record + .runtime_error_exits + .iter() + .filter(|exit| blocks.contains(&exit.block_id) && observed_runtime_codes.contains(&exit.code)) + .map(|exit| json!({"code": exit.code, "name": exit.name, "block_id": exit.block_id})) + .collect::>(); + let syscalls = record + .syscall_sites + .iter() + .filter(|site| blocks.contains(&site.block_id)) + .map(|site| json!({"block_id": site.block_id, "address": site.address, "contract": site.contract})) + .collect::>(); + json!({ + "claim": "observed-entry-only;unexecuted-branches-not-claimed", + "evidence_tier": backend.evidence_tier(), + "entries": {"declared": [entry.name.clone()], "observed": [entry.name.clone()]}, + "lowering_blocks": {"declared": blocks, "observed": entry_block.into_iter().collect::>()}, + "proof_plan_obligations": {"declared": proofs, "observed": []}, + "runtime_error_paths": {"declared": runtime_errors, "observed": observed_runtime_errors}, + "syscall_sites": {"declared": syscalls, "observed": []}, + "source_links": intervals + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn script() -> ScenarioScript { + ScenarioScript { code_hash: "11".repeat(32), hash_type: "data1".to_string(), args: String::new() } + } + + fn cell(name: &str, prior: Option<&str>) -> ScenarioCell { + ScenarioCell { + name: name.to_string(), + capacity: 100, + data: String::new(), + lock: script(), + type_script: None, + prior_output: prior.map(str::to_string), + } + } + + #[test] + fn local_state_transition_rejects_reuse_of_consumed_cells() { + let scenario = Scenario { + schema: SCENARIO_SCHEMA.to_string(), + name: "reuse".to_string(), + source: "main.cell".to_string(), + target_profile: "ckb".to_string(), + entry: ScenarioEntry { kind: "action".to_string(), name: "main".to_string(), args: Vec::new() }, + initial_cells: vec![cell("c0", None)], + steps: vec![ + ScenarioStep { + name: "first".to_string(), + consumes: vec!["c0".to_string()], + outputs: vec![cell("c1", Some("c0"))], + cell_deps: Vec::new(), + header_deps: Vec::new(), + since: BTreeMap::new(), + witnesses: Vec::new(), + expectation: ScenarioExpectation { status: "pass".to_string(), result: None, runtime_error: None }, + }, + ScenarioStep { + name: "second".to_string(), + consumes: vec!["c0".to_string()], + outputs: Vec::new(), + cell_deps: Vec::new(), + header_deps: Vec::new(), + since: BTreeMap::new(), + witnesses: Vec::new(), + expectation: ScenarioExpectation { status: "pass".to_string(), result: None, runtime_error: None }, + }, + ], + limits: ScenarioLimits { + max_steps: 100, + max_cycles: 1_000_000, + max_transaction_bytes: 1_000_000, + minimum_cell_capacity: 1, + }, + oracle: None, + }; + assert!(validate_state_transitions(&scenario).unwrap_err().to_string().contains("missing, dead")); + } + + #[test] + fn unknown_scenario_fields_fail_closed() { + let error = serde_json::from_str::( + r#"{"schema":"cellscript-test-scenario-v1","name":"x","source":"x.cell","target_profile":"ckb","entry":{"kind":"action","name":"main","args":[]},"initial_cells":[],"steps":[],"limits":{"max_steps":1,"max_cycles":1,"max_transaction_bytes":1,"minimum_cell_capacity":1},"unknown":true}"#, + ) + .unwrap_err(); + assert!(error.to_string().contains("unknown field")); + } +} diff --git a/src/codegen/abi.rs b/src/codegen/abi.rs new file mode 100644 index 00000000..c97ddb58 --- /dev/null +++ b/src/codegen/abi.rs @@ -0,0 +1,698 @@ +use super::*; + +impl CodeGenerator { + pub(super) fn emit_entry_abi_marker(&mut self, name: &str) { + self.assembly.push(format!("# cellscript entry abi: {} requires-explicit-parameter-abi", name)); + } + + pub(super) fn emit_entry_direct_wrapper(&mut self, target: &str) { + self.emit_global(ENTRY_WITNESS_LABEL); + self.emit_label(ENTRY_WITNESS_LABEL); + self.emit(format!("# cellscript entry abi: {} tail-calls no-arg {}", ENTRY_WITNESS_LABEL, target)); + self.emit(format!("j {}", target)); + } + + pub(super) fn emit_entry_witness_wrapper(&mut self, target: &str, params: &[IrParam]) -> Result<()> { + self.entry_frame_sizes.insert(ENTRY_WITNESS_LABEL.to_string(), ENTRY_WITNESS_FRAME_SIZE as u32); + let callable_abi = self.callable_abis.get(target).cloned(); + let type_hash_param_indices = callable_abi.as_ref().map(|abi| abi.type_hash_param_indices.clone()).unwrap_or_default(); + let runtime_bound_param_indices = callable_abi.as_ref().map(|abi| abi.runtime_bound_param_indices.clone()).unwrap_or_default(); + let outgoing_stack_arg_bytes = align_stack_arg_bytes(entry_abi_arg_count(params, callable_abi.as_ref()).saturating_sub(8) * 8); + let payload = entry_witness_payload_layout(params, &runtime_bound_param_indices, &self.enum_layouts); + let payload_len = payload.iter().map(|arg| arg.width).sum::(); + let has_witness_payload = payload.iter().any(|arg| arg.width > 0 || arg.unsupported); + let has_lock_args = params.iter().any(|param| param.source == ParamSource::LockArgs); + let has_dynamic_payload = payload.iter().any(|arg| arg.schema_dynamic); + let min_witness_len = ENTRY_WITNESS_HEADER_SIZE + payload_len; + let loaded_label = self.fresh_label("entry_witness_loaded"); + let try_group_output_label = self.fresh_label("entry_witness_try_group_output"); + let buffer_ok_label = self.fresh_label("entry_witness_buffer_ok"); + let size_ok_label = self.fresh_label("entry_witness_size_ok"); + let fail_label = self.fresh_label("entry_witness_fail"); + let done_label = self.fresh_label("entry_witness_done"); + + self.emit_global(ENTRY_WITNESS_LABEL); + self.emit_label(ENTRY_WITNESS_LABEL); + self.emit(format!( + "# cellscript entry abi: {} loads GroupInput#0 witness args for {} and falls back to GroupOutput#0", + ENTRY_WITNESS_LABEL, target + )); + self.emit("# cellscript entry abi: placement profile requires CSARGv1 inside WitnessArgs.input_type"); + self.emit_large_addi("sp", "sp", -(ENTRY_WITNESS_FRAME_SIZE as i64)); + self.emit_stack_store("ra", ENTRY_WITNESS_RA_OFFSET); + if has_lock_args { + self.emit_entry_load_script_args(&fail_label); + } + if has_witness_payload { + self.emit_load_witness_syscall_to_offsets( + "entry_args", + CKB_SOURCE_GROUP_INPUT, + 0, + ENTRY_WITNESS_SIZE_OFFSET, + ENTRY_WITNESS_BUFFER_OFFSET, + ENTRY_WITNESS_BUFFER_SIZE, + ); + self.emit(format!("beqz a0, {}", loaded_label)); + self.emit(format!("j {}", try_group_output_label)); + self.emit_label(&try_group_output_label); + self.emit_load_witness_syscall_to_offsets( + "entry_args_fallback_group_output", + CKB_SOURCE_GROUP_OUTPUT, + 0, + ENTRY_WITNESS_SIZE_OFFSET, + ENTRY_WITNESS_BUFFER_OFFSET, + ENTRY_WITNESS_BUFFER_SIZE, + ); + self.emit(format!("beqz a0, {}", loaded_label)); + self.emit(format!("j {}", fail_label)); + self.emit_label(&loaded_label); + + self.emit_stack_load("t0", ENTRY_WITNESS_SIZE_OFFSET); + self.emit("# cellscript entry abi: reject witnesses larger than the local entry buffer"); + self.emit(format!("li t1, {}", ENTRY_WITNESS_BUFFER_SIZE + 1)); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", buffer_ok_label)); + self.emit(format!("j {}", fail_label)); + self.emit_label(&buffer_ok_label); + + self.emit_entry_normalize_witness_args_input_type_v2(&fail_label); + + self.emit_stack_load("t0", ENTRY_WITNESS_SIZE_OFFSET); + self.emit(format!("li t1, {}", min_witness_len)); + self.emit("sltu t2, t0, t1"); + self.emit(format!("beqz t2, {}", size_ok_label)); + self.emit(format!("j {}", fail_label)); + self.emit_label(&size_ok_label); + + for (index, byte) in ENTRY_WITNESS_MAGIC.iter().enumerate() { + self.emit_stack_load_byte("t0", ENTRY_WITNESS_BUFFER_OFFSET + index); + self.emit(format!("li t1, {}", byte)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", fail_label)); + } + + if !has_dynamic_payload { + let exact_size_label = self.fresh_label("entry_witness_exact_size_ok"); + self.emit("# cellscript entry abi: reject trailing witness payload bytes"); + self.emit_stack_load("t0", ENTRY_WITNESS_SIZE_OFFSET); + self.emit(format!("li t1, {}", min_witness_len)); + self.emit("sub t2, t0, t1"); + self.emit(format!("beqz t2, {}", exact_size_label)); + self.emit(format!("j {}", fail_label)); + self.emit_label(&exact_size_label); + } + } + + if payload.iter().any(|arg| arg.unsupported) { + self.emit("# cellscript entry abi: unsupported witness parameter shape; fail closed"); + self.emit(format!("j {}", fail_label)); + } else if has_dynamic_payload { + let mut abi_index = 0usize; + self.emit("# cellscript entry abi: witness payload contains schema-backed dynamic segments"); + self.emit_stack_load("t5", ENTRY_WITNESS_SIZE_OFFSET); + self.emit(format!("li t6, {}", ENTRY_WITNESS_HEADER_SIZE)); + for (param_index, param) in params.iter().enumerate() { + let param_is_runtime_bound = + runtime_bound_param_indices.contains(¶m_index) || matches!(param.ty, IrType::Ref(_) | IrType::MutRef(_)); + if param.source == ParamSource::LockArgs { + self.emit_entry_lock_args_param(&mut abi_index, param, outgoing_stack_arg_bytes, &fail_label); + } else if param_is_runtime_bound { + self.emit(format!("# cellscript entry abi: runtime-bound param {} is loaded from transaction cells", param.name)); + self.emit_entry_abi_zero_arg(abi_index, outgoing_stack_arg_bytes); + self.emit_entry_abi_zero_arg(abi_index + 1, outgoing_stack_arg_bytes); + abi_index += 2; + if type_hash_param_indices.contains(¶m_index) { + self.emit(format!( + "# cellscript entry abi: runtime-bound param {} TypeHash witness bytes unavailable; pass null ABI bytes", + param.name + )); + self.emit_entry_abi_zero_arg(abi_index, outgoing_stack_arg_bytes); + self.emit_entry_abi_zero_arg(abi_index + 1, outgoing_stack_arg_bytes); + abi_index += 2; + } + } else if entry_witness_dynamic_schema_param(¶m.ty) && self.payload_enum_width(¶m.ty).is_none() { + let len_ok_label = self.fresh_label("entry_witness_schema_len_ok"); + let bytes_ok_label = self.fresh_label("entry_witness_schema_bytes_ok"); + self.emit(format!( + "# cellscript entry abi: schema param {} -> {}={} {}={} (length-prefixed witness bytes)", + param.name, + abi_arg_label(abi_index), + "ptr", + abi_arg_label(abi_index + 1), + "len" + )); + self.emit("addi t1, t6, 4"); + self.emit("sltu t2, t5, t1"); + self.emit(format!("beqz t2, {}", len_ok_label)); + self.emit(format!("j {}", fail_label)); + self.emit_label(&len_ok_label); + self.emit("add t0, sp, t6"); + self.emit(format!("addi t0, t0, {}", ENTRY_WITNESS_BUFFER_OFFSET)); + self.emit("li t4, 0"); + for byte_index in 0..4 { + self.emit(format!("lbu t1, {}(t0)", byte_index)); + if byte_index != 0 { + self.emit(format!("slli t1, t1, {}", byte_index * 8)); + } + self.emit("or t4, t4, t1"); + } + self.emit("addi t1, t6, 4"); + self.emit("add t1, t1, t4"); + self.emit("sltu t2, t5, t1"); + self.emit(format!("beqz t2, {}", bytes_ok_label)); + self.emit(format!("j {}", fail_label)); + self.emit_label(&bytes_ok_label); + self.emit_entry_abi_pointer_from_dynamic_offset(abi_index, "t6", 4, "t0", outgoing_stack_arg_bytes); + self.emit_entry_abi_reg_arg(abi_index + 1, "t4", outgoing_stack_arg_bytes); + abi_index += 2; + self.emit("addi t6, t6, 4"); + self.emit("add t6, t6, t4"); + if type_hash_param_indices.contains(¶m_index) { + self.emit(format!( + "# cellscript entry abi: schema param {} TypeHash witness bytes unavailable; pass null ABI bytes", + param.name + )); + self.emit_entry_abi_zero_arg(abi_index, outgoing_stack_arg_bytes); + self.emit_entry_abi_zero_arg(abi_index + 1, outgoing_stack_arg_bytes); + abi_index += 2; + } + } else if let Some(width) = self + .payload_enum_width(¶m.ty) + .or_else(|| fixed_byte_pointer_param_width(¶m.ty).or_else(|| fixed_aggregate_pointer_param_width(¶m.ty))) + { + let bytes_ok_label = self.fresh_label("entry_witness_fixed_bytes_ok"); + self.emit(format!( + "# cellscript entry abi: fixed-byte param {} pointer={} length={} size={}", + param.name, + abi_arg_label(abi_index), + abi_arg_label(abi_index + 1), + width + )); + self.emit(format!("addi t1, t6, {}", width)); + self.emit("sltu t2, t5, t1"); + self.emit(format!("beqz t2, {}", bytes_ok_label)); + self.emit(format!("j {}", fail_label)); + self.emit_label(&bytes_ok_label); + self.emit_entry_abi_pointer_from_dynamic_offset(abi_index, "t6", 0, "t0", outgoing_stack_arg_bytes); + self.emit_entry_abi_immediate_arg(abi_index + 1, width as u64, outgoing_stack_arg_bytes); + self.emit(format!("addi t6, t6, {}", width)); + abi_index += 2; + } else if let Some(width) = entry_witness_register_param_width(¶m.ty) { + let bytes_ok_label = self.fresh_label("entry_witness_scalar_bytes_ok"); + self.emit(format!( + "# cellscript entry abi: scalar param {} -> {} size={}", + param.name, + abi_arg_label(abi_index), + width + )); + self.emit(format!("addi t1, t6, {}", width)); + self.emit("sltu t2, t5, t1"); + self.emit(format!("beqz t2, {}", bytes_ok_label)); + self.emit(format!("j {}", fail_label)); + self.emit_label(&bytes_ok_label); + self.emit("add t0, sp, t6"); + self.emit(format!("addi t0, t0, {}", ENTRY_WITNESS_BUFFER_OFFSET)); + if abi_index < 8 { + self.emit_entry_witness_scalar_load_from_reg( + &format!("a{}", abi_index), + "t0", + "t1", + width, + param.ty == IrType::I32, + ); + } else { + let caller_stack_offset = (abi_index - 8) * 8; + self.emit_entry_witness_scalar_load_from_reg("t3", "t0", "t1", width, param.ty == IrType::I32); + self.emit(format!( + "# cellscript entry abi: scalar param {} stored to caller stack +{}", + param.name, caller_stack_offset + )); + self.emit_entry_abi_reg_arg(abi_index, "t3", outgoing_stack_arg_bytes); + } + self.emit(format!("addi t6, t6, {}", width)); + abi_index += 1; + } else { + self.emit(format!("# cellscript entry abi: unsupported param {} shape; fail closed", param.name)); + self.emit(format!("j {}", fail_label)); + } + } + let exact_size_label = self.fresh_label("entry_witness_exact_size_ok"); + self.emit("# cellscript entry abi: reject trailing witness payload bytes"); + self.emit_stack_load("t5", ENTRY_WITNESS_SIZE_OFFSET); + self.emit("sub t2, t5, t6"); + self.emit(format!("beqz t2, {}", exact_size_label)); + self.emit(format!("j {}", fail_label)); + self.emit_label(&exact_size_label); + if has_lock_args { + self.emit_entry_lock_args_exact_size_check(&fail_label); + } + self.emit_entry_call_target(target, outgoing_stack_arg_bytes); + self.emit(format!("j {}", done_label)); + } else { + let mut abi_index = 0usize; + let mut payload_cursor = 0usize; + for (param_index, param) in params.iter().enumerate() { + let param_is_runtime_bound = + runtime_bound_param_indices.contains(¶m_index) || matches!(param.ty, IrType::Ref(_) | IrType::MutRef(_)); + if param.source == ParamSource::LockArgs { + self.emit_entry_lock_args_param(&mut abi_index, param, outgoing_stack_arg_bytes, &fail_label); + } else if param_is_runtime_bound { + self.emit(format!("# cellscript entry abi: runtime-bound param {} is loaded from transaction cells", param.name)); + self.emit_entry_abi_zero_arg(abi_index, outgoing_stack_arg_bytes); + self.emit_entry_abi_zero_arg(abi_index + 1, outgoing_stack_arg_bytes); + abi_index += 2; + if type_hash_param_indices.contains(¶m_index) { + self.emit(format!( + "# cellscript entry abi: runtime-bound param {} TypeHash witness bytes unavailable; pass null ABI bytes", + param.name + )); + self.emit_entry_abi_zero_arg(abi_index, outgoing_stack_arg_bytes); + self.emit_entry_abi_zero_arg(abi_index + 1, outgoing_stack_arg_bytes); + abi_index += 2; + } + } else if entry_witness_dynamic_schema_param(¶m.ty) && self.payload_enum_width(¶m.ty).is_none() { + self.emit(format!("# cellscript entry abi: schema param {} is runtime-loaded; pass null ABI bytes", param.name)); + self.emit_entry_abi_zero_arg(abi_index, outgoing_stack_arg_bytes); + self.emit_entry_abi_zero_arg(abi_index + 1, outgoing_stack_arg_bytes); + abi_index += 2; + if type_hash_param_indices.contains(¶m_index) { + self.emit(format!( + "# cellscript entry abi: schema param {} TypeHash witness bytes unavailable; pass null ABI bytes", + param.name + )); + self.emit_entry_abi_zero_arg(abi_index, outgoing_stack_arg_bytes); + self.emit_entry_abi_zero_arg(abi_index + 1, outgoing_stack_arg_bytes); + abi_index += 2; + } + } else if let Some(width) = self + .payload_enum_width(¶m.ty) + .or_else(|| fixed_byte_pointer_param_width(¶m.ty).or_else(|| fixed_aggregate_pointer_param_width(¶m.ty))) + { + self.emit(format!( + "# cellscript entry abi: fixed-byte param {} pointer={} length={} size={}", + param.name, + abi_arg_label(abi_index), + abi_arg_label(abi_index + 1), + width + )); + self.emit_entry_abi_pointer_arg( + abi_index, + ENTRY_WITNESS_BUFFER_OFFSET + ENTRY_WITNESS_HEADER_SIZE + payload_cursor, + outgoing_stack_arg_bytes, + ); + self.emit_entry_abi_immediate_arg(abi_index + 1, width as u64, outgoing_stack_arg_bytes); + payload_cursor += width; + abi_index += 2; + } else if let Some(width) = entry_witness_register_param_width(¶m.ty) { + self.emit(format!( + "# cellscript entry abi: scalar param {} -> {} size={}", + param.name, + abi_arg_label(abi_index), + width + )); + let stack_offset = ENTRY_WITNESS_BUFFER_OFFSET + ENTRY_WITNESS_HEADER_SIZE + payload_cursor; + if abi_index < 8 { + self.emit_entry_witness_scalar_load(&format!("a{}", abi_index), stack_offset, width, param.ty == IrType::I32); + } else { + let caller_stack_offset = (abi_index - 8) * 8; + self.emit_entry_witness_scalar_load("t3", stack_offset, width, param.ty == IrType::I32); + self.emit(format!( + "# cellscript entry abi: scalar param {} stored to caller stack +{}", + param.name, caller_stack_offset + )); + self.emit_entry_abi_reg_arg(abi_index, "t3", outgoing_stack_arg_bytes); + } + payload_cursor += width; + abi_index += 1; + } else { + self.emit(format!("# cellscript entry abi: unsupported param {} shape; fail closed", param.name)); + self.emit(format!("j {}", fail_label)); + } + } + if has_lock_args { + self.emit_entry_lock_args_exact_size_check(&fail_label); + } + self.emit_entry_call_target(target, outgoing_stack_arg_bytes); + self.emit(format!("j {}", done_label)); + } + + self.emit_label(&fail_label); + self.emit_runtime_error_comment(CellScriptRuntimeError::EntryWitnessAbiInvalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::EntryWitnessAbiInvalid.code())); + self.emit_label(&done_label); + self.emit_stack_load("ra", ENTRY_WITNESS_RA_OFFSET); + self.emit_large_addi("sp", "sp", ENTRY_WITNESS_FRAME_SIZE as i64); + self.emit("ret"); + Ok(()) + } + + /// Normalize the selected entry placement ABI into the payload buffer + /// shape consumed by the positional decoder. + /// + /// The wrapper requires a canonical CKB `WitnessArgs` from the current + /// script group and copies its `input_type` Bytes payload to the start of + /// the local buffer. A raw `CSARGv1\0` witness is not a valid alias. + pub(super) fn emit_entry_normalize_witness_args_input_type_v2(&mut self, fail_label: &str) { + let validate_loop_label = self.fresh_label("entry_witness_v2_validate_loop"); + let field_end_ready_label = self.fresh_label("entry_witness_v2_field_end_ready"); + let field_done_label = self.fresh_label("entry_witness_v2_field_done"); + let copy_loop_label = self.fresh_label("entry_witness_v2_copy_loop"); + let copy_done_label = self.fresh_label("entry_witness_v2_copy_done"); + + self.emit("# cellscript entry placement profile: validate the exact three-field WitnessArgs table"); + self.emit_stack_load("t0", ENTRY_WITNESS_SIZE_OFFSET); + self.emit("li t1, 16"); + self.emit(format!("bltu t0, t1, {}", fail_label)); + self.emit_sp_addi("t3", ENTRY_WITNESS_BUFFER_OFFSET); + + // The table header and local buffer are eight-byte aligned, so load its + // four u32 words in two pairs. Keep variable-offset Bytes lengths below + // on byte loads because Molecule payload offsets need not be aligned. + self.emit("ld a4, 0(t3)"); + self.emit("slli t1, a4, 32"); + self.emit("srli t1, t1, 32"); + self.emit(format!("bne t1, t0, {}", fail_label)); + self.emit("srli t4, a4, 32"); + self.emit("li t1, 16"); + self.emit(format!("bne t4, t1, {}", fail_label)); + self.emit("ld a4, 8(t3)"); + self.emit("slli t5, a4, 32"); + self.emit("srli t5, t5, 32"); + self.emit(format!("bltu t5, t4, {}", fail_label)); + self.emit("srli t6, a4, 32"); + self.emit(format!("bltu t6, t5, {}", fail_label)); + self.emit(format!("bltu t0, t6, {}", fail_label)); + + // Validate lock, input_type, and output_type through one compact loop. + // a5 is the field index and t4 the current start. The three ends are + // the preserved input_type offset, output_type offset, and total_size. + self.emit("li t4, 16"); + self.emit("li a5, 0"); + self.emit_label(&validate_loop_label); + self.emit("addi a6, t5, 0"); + self.emit(format!("beqz a5, {}", field_end_ready_label)); + self.emit("addi a6, t6, 0"); + self.emit("li a0, 1"); + self.emit(format!("beq a5, a0, {}", field_end_ready_label)); + self.emit("addi a6, t0, 0"); + self.emit_label(&field_end_ready_label); + self.emit("sub a1, a6, t4"); + self.emit(format!("beqz a1, {}", field_done_label)); + self.emit("li a0, 4"); + self.emit(format!("bltu a1, a0, {}", fail_label)); + self.emit("add a2, t3, t4"); + self.emit_u32_le_from_base_to("t1", "a2", 0, "t2"); + self.emit("addi a1, a1, -4"); + self.emit(format!("bne t1, a1, {}", fail_label)); + self.emit_label(&field_done_label); + self.emit("addi t4, a6, 0"); + self.emit("addi a5, a5, 1"); + self.emit("li a0, 3"); + self.emit(format!("bltu a5, a0, {}", validate_loop_label)); + + // input_type is mandatory for v2, while lock and output_type remain + // optional. t5 and t6 still hold its start and end offsets. + self.emit("sub t1, t6, t5"); + self.emit(format!("beqz t1, {}", fail_label)); + self.emit("addi t1, t1, -4"); + self.emit("add t4, t3, t5"); + + self.emit("# cellscript entry placement v2: copy input_type payload over the table envelope"); + self.emit("addi t4, t4, 4"); + self.emit_sp_addi("t5", ENTRY_WITNESS_BUFFER_OFFSET); + self.emit("li t2, 0"); + self.emit_label(©_loop_label); + self.emit("sltu t6, t2, t1"); + self.emit(format!("beqz t6, {}", copy_done_label)); + self.emit("add t3, t4, t2"); + self.emit("lbu t6, 0(t3)"); + self.emit("add t3, t5, t2"); + self.emit("sb t6, 0(t3)"); + self.emit("addi t2, t2, 1"); + self.emit(format!("j {}", copy_loop_label)); + self.emit_label(©_done_label); + self.emit_stack_store("t1", ENTRY_WITNESS_SIZE_OFFSET); + } + + pub(super) fn emit_entry_call_target(&mut self, target: &str, outgoing_stack_arg_bytes: usize) { + if outgoing_stack_arg_bytes > 0 { + self.emit(format!("# cellscript entry abi: reserve {} bytes for outgoing stack call arguments", outgoing_stack_arg_bytes)); + self.emit_large_addi("sp", "sp", -(outgoing_stack_arg_bytes as i64)); + } + self.emit(format!("call {}", target)); + if outgoing_stack_arg_bytes > 0 { + self.emit_large_addi("sp", "sp", outgoing_stack_arg_bytes as i64); + } + } + + pub(super) fn emit_entry_abi_zero_arg(&mut self, abi_index: usize, outgoing_stack_arg_bytes: usize) { + self.emit_entry_abi_immediate_arg(abi_index, 0, outgoing_stack_arg_bytes); + } + + pub(super) fn emit_entry_abi_reg_arg(&mut self, abi_index: usize, source_reg: &str, outgoing_stack_arg_bytes: usize) { + if abi_index < 8 { + self.emit(format!("addi a{}, {}, 0", abi_index, source_reg)); + } else { + self.emit_entry_outgoing_stack_arg_store(source_reg, abi_index, outgoing_stack_arg_bytes); + } + } + + pub(super) fn emit_entry_abi_immediate_arg(&mut self, abi_index: usize, value: u64, outgoing_stack_arg_bytes: usize) { + if abi_index < 8 { + self.emit(format!("li a{}, {}", abi_index, value)); + } else { + self.emit(format!("# cellscript entry abi: stack arg{} <- {}", abi_index, value)); + self.emit(format!("li t0, {}", value)); + self.emit_entry_outgoing_stack_arg_store("t0", abi_index, outgoing_stack_arg_bytes); + } + } + + pub(super) fn emit_entry_abi_pointer_arg(&mut self, abi_index: usize, stack_offset: usize, outgoing_stack_arg_bytes: usize) { + if abi_index < 8 { + self.emit_sp_addi(&format!("a{}", abi_index), stack_offset); + } else { + self.emit(format!("# cellscript entry abi: stack arg{} <- sp+{}", abi_index, stack_offset)); + self.emit_sp_addi("t0", stack_offset); + self.emit_entry_outgoing_stack_arg_store("t0", abi_index, outgoing_stack_arg_bytes); + } + } + + pub(super) fn emit_entry_abi_pointer_from_dynamic_offset( + &mut self, + abi_index: usize, + offset_reg: &str, + extra_offset: usize, + temp_reg: &str, + outgoing_stack_arg_bytes: usize, + ) { + self.emit(format!("add {}, sp, {}", temp_reg, offset_reg)); + if ENTRY_WITNESS_BUFFER_OFFSET + extra_offset != 0 { + self.emit(format!("addi {}, {}, {}", temp_reg, temp_reg, ENTRY_WITNESS_BUFFER_OFFSET + extra_offset)); + } + self.emit_entry_abi_reg_arg(abi_index, temp_reg, outgoing_stack_arg_bytes); + } + + pub(super) fn emit_entry_outgoing_stack_arg_store(&mut self, register: &str, abi_index: usize, outgoing_stack_arg_bytes: usize) { + let stack_slot_offset = (abi_index - 8) * 8; + let offset = i64::try_from(stack_slot_offset).expect("entry call stack slot should fit in i64") + - i64::try_from(outgoing_stack_arg_bytes).expect("entry call stack argument area should fit in i64"); + self.emit(format!( + "# cellscript entry abi: stage stack arg{} at pre-call sp{}{}", + abi_index, + if offset < 0 { "" } else { "+" }, + offset + )); + self.emit_sp_store_signed(register, offset); + } + + pub(super) fn emit_entry_witness_scalar_load(&mut self, dest_reg: &str, stack_offset: usize, width: usize, signed_i32: bool) { + self.emit(format!("li {}, 0", dest_reg)); + for byte_index in 0..width { + self.emit_stack_load_byte("t0", stack_offset + byte_index); + if byte_index != 0 { + self.emit(format!("slli t0, t0, {}", byte_index * 8)); + } + self.emit(format!("or {}, {}, t0", dest_reg, dest_reg)); + } + if signed_i32 { + self.emit_sign_extend_i32(dest_reg); + } + } + + pub(super) fn emit_entry_witness_scalar_load_from_reg( + &mut self, + dest_reg: &str, + base_reg: &str, + byte_reg: &str, + width: usize, + signed_i32: bool, + ) { + debug_assert_ne!(dest_reg, base_reg, "entry scalar decoder destination must not alias its base"); + debug_assert_ne!(byte_reg, base_reg, "entry scalar decoder scratch must not alias its base"); + debug_assert_ne!(byte_reg, dest_reg, "entry scalar decoder scratch must not alias its destination"); + self.emit(format!("li {}, 0", dest_reg)); + for byte_index in 0..width { + self.emit(format!("lbu {}, {}({})", byte_reg, byte_index, base_reg)); + if byte_index != 0 { + self.emit(format!("slli {}, {}, {}", byte_reg, byte_reg, byte_index * 8)); + } + self.emit(format!("or {}, {}, {}", dest_reg, dest_reg, byte_reg)); + } + if signed_i32 { + self.emit_sign_extend_i32(dest_reg); + } + } + + pub(super) fn emit_entry_load_u32_from_stack(&mut self, dest_reg: &str, stack_offset: usize) { + self.emit(format!("li {}, 0", dest_reg)); + for byte_index in 0..4 { + self.emit_stack_load_byte("t0", stack_offset + byte_index); + if byte_index != 0 { + self.emit(format!("slli t0, t0, {}", byte_index * 8)); + } + self.emit(format!("or {}, {}, t0", dest_reg, dest_reg)); + } + } + + pub(super) fn emit_entry_load_u32_from_reg(&mut self, dest_reg: &str, base_reg: &str, byte_reg: &str) { + debug_assert_ne!(dest_reg, base_reg, "entry u32 decoder destination must not alias its base"); + debug_assert_ne!(byte_reg, base_reg, "entry u32 decoder scratch must not alias its base"); + debug_assert_ne!(byte_reg, dest_reg, "entry u32 decoder scratch must not alias its destination"); + self.emit(format!("li {}, 0", dest_reg)); + for byte_index in 0..4 { + self.emit(format!("lbu {}, {}({})", byte_reg, byte_index, base_reg)); + if byte_index != 0 { + self.emit(format!("slli {}, {}, {}", byte_reg, byte_reg, byte_index * 8)); + } + self.emit(format!("or {}, {}, {}", dest_reg, dest_reg, byte_reg)); + } + } + + pub(super) fn emit_entry_load_script_args(&mut self, fail_label: &str) { + let loaded_label = self.fresh_label("entry_script_loaded"); + let buffer_ok_label = self.fresh_label("entry_script_buffer_ok"); + let total_ok_label = self.fresh_label("entry_script_total_ok"); + let table_header_ok_label = self.fresh_label("entry_script_table_header_ok"); + let args_offset_min_ok_label = self.fresh_label("entry_script_args_offset_min_ok"); + let args_offset_ok_label = self.fresh_label("entry_script_args_offset_ok"); + let args_span_ok_label = self.fresh_label("entry_script_args_span_ok"); + + self.emit("# cellscript entry abi: lock_args parameters are decoded from the executing Script.args bytes"); + self.emit_load_script_syscall_to_offsets( + "entry_lock_args", + ENTRY_SCRIPT_SIZE_OFFSET, + ENTRY_SCRIPT_BUFFER_OFFSET, + ENTRY_SCRIPT_BUFFER_SIZE, + ); + self.emit(format!("beqz a0, {}", loaded_label)); + self.emit(format!("j {}", fail_label)); + self.emit_label(&loaded_label); + + self.emit_stack_load("t0", ENTRY_SCRIPT_SIZE_OFFSET); + self.emit(format!("li t1, {}", ENTRY_SCRIPT_BUFFER_SIZE + 1)); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", buffer_ok_label)); + self.emit(format!("j {}", fail_label)); + self.emit_label(&buffer_ok_label); + + self.emit_entry_load_u32_from_stack("t3", ENTRY_SCRIPT_BUFFER_OFFSET); + self.emit_stack_load("t0", ENTRY_SCRIPT_SIZE_OFFSET); + self.emit("sub t2, t0, t3"); + self.emit(format!("beqz t2, {}", total_ok_label)); + self.emit(format!("j {}", fail_label)); + self.emit_label(&total_ok_label); + + self.emit("li t1, 16"); + self.emit("sltu t2, t3, t1"); + self.emit(format!("beqz t2, {}", table_header_ok_label)); + self.emit(format!("j {}", fail_label)); + self.emit_label(&table_header_ok_label); + + self.emit_entry_load_u32_from_stack("t4", ENTRY_SCRIPT_BUFFER_OFFSET + 12); + self.emit("li t1, 16"); + self.emit("sltu t2, t4, t1"); + self.emit(format!("beqz t2, {}", args_offset_min_ok_label)); + self.emit(format!("j {}", fail_label)); + self.emit_label(&args_offset_min_ok_label); + self.emit("addi t1, t4, 4"); + self.emit("sltu t2, t3, t1"); + self.emit(format!("beqz t2, {}", args_offset_ok_label)); + self.emit(format!("j {}", fail_label)); + self.emit_label(&args_offset_ok_label); + + self.emit_sp_addi("t0", ENTRY_SCRIPT_BUFFER_OFFSET); + self.emit("add t0, t0, t4"); + self.emit_entry_load_u32_from_reg("t5", "t0", "t1"); + self.emit("addi t6, t4, 4"); + self.emit("add t1, t6, t5"); + self.emit("sltu t2, t3, t1"); + self.emit(format!("beqz t2, {}", args_span_ok_label)); + self.emit(format!("j {}", fail_label)); + self.emit_label(&args_span_ok_label); + self.emit_stack_store_with_avoid("t6", ENTRY_SCRIPT_ARGS_START_OFFSET, &["t5"]); + self.emit_stack_store("t5", ENTRY_SCRIPT_ARGS_LEN_OFFSET); + self.emit("li t0, 0"); + self.emit_stack_store("t0", ENTRY_SCRIPT_ARGS_CURSOR_OFFSET); + } + + pub(super) fn emit_entry_lock_args_param( + &mut self, + abi_index: &mut usize, + param: &IrParam, + outgoing_stack_arg_bytes: usize, + fail_label: &str, + ) { + let fixed_byte_width = self + .payload_enum_width(¶m.ty) + .or_else(|| fixed_byte_pointer_param_width(¶m.ty).or_else(|| fixed_aggregate_pointer_param_width(¶m.ty))); + let scalar_width = entry_witness_register_param_width(¶m.ty); + let Some(width) = fixed_byte_width.or(scalar_width) else { + self.emit(format!("# cellscript entry abi: unsupported lock_args param {} shape; fail closed", param.name)); + self.emit(format!("j {}", fail_label)); + return; + }; + let bytes_ok_label = self.fresh_label("entry_lock_args_bytes_ok"); + self.emit(format!("# cellscript entry abi: lock_args param {} consumes {} script arg byte(s)", param.name, width)); + let witness_cursor_live = ["t5", "t6"]; + let witness_and_script_cursor_live = ["t3", "t5", "t6"]; + self.emit_stack_load_with_avoid("t3", ENTRY_SCRIPT_ARGS_CURSOR_OFFSET, &witness_cursor_live); + self.emit_stack_load_with_avoid("t4", ENTRY_SCRIPT_ARGS_LEN_OFFSET, &witness_and_script_cursor_live); + self.emit(format!("addi t1, t3, {}", width)); + self.emit("sltu t2, t4, t1"); + self.emit(format!("beqz t2, {}", bytes_ok_label)); + self.emit(format!("j {}", fail_label)); + self.emit_label(&bytes_ok_label); + self.emit_stack_load_with_avoid("t4", ENTRY_SCRIPT_ARGS_START_OFFSET, &witness_and_script_cursor_live); + self.emit("add t4, t4, t3"); + self.emit_sp_addi("t0", ENTRY_SCRIPT_BUFFER_OFFSET); + self.emit("add t0, t0, t4"); + + if fixed_byte_width.is_some() { + self.emit_entry_abi_reg_arg(*abi_index, "t0", outgoing_stack_arg_bytes); + self.emit_entry_abi_immediate_arg(*abi_index + 1, width as u64, outgoing_stack_arg_bytes); + *abi_index += 2; + } else if *abi_index < 8 { + self.emit_entry_witness_scalar_load_from_reg(&format!("a{}", *abi_index), "t0", "t1", width, param.ty == IrType::I32); + *abi_index += 1; + } else { + self.emit_entry_witness_scalar_load_from_reg("t4", "t0", "t1", width, param.ty == IrType::I32); + self.emit_entry_abi_reg_arg(*abi_index, "t4", outgoing_stack_arg_bytes); + *abi_index += 1; + } + + self.emit_stack_load_with_avoid("t3", ENTRY_SCRIPT_ARGS_CURSOR_OFFSET, &witness_cursor_live); + self.emit(format!("addi t3, t3, {}", width)); + self.emit_stack_store_with_avoid("t3", ENTRY_SCRIPT_ARGS_CURSOR_OFFSET, &witness_cursor_live); + } + + pub(super) fn emit_entry_lock_args_exact_size_check(&mut self, fail_label: &str) { + let exact_label = self.fresh_label("entry_lock_args_exact_size_ok"); + self.emit("# cellscript entry abi: reject trailing Script.args bytes after typed lock_args"); + self.emit_stack_load("t0", ENTRY_SCRIPT_ARGS_CURSOR_OFFSET); + self.emit_stack_load("t1", ENTRY_SCRIPT_ARGS_LEN_OFFSET); + self.emit("sub t2, t1, t0"); + self.emit(format!("beqz t2, {}", exact_label)); + self.emit(format!("j {}", fail_label)); + self.emit_label(&exact_label); + } +} diff --git a/src/codegen/assembler.rs b/src/codegen/assembler.rs new file mode 100644 index 00000000..9c7a0849 --- /dev/null +++ b/src/codegen/assembler.rs @@ -0,0 +1,2838 @@ +use super::*; + +const ELF_HEADER_SIZE: usize = 64; +const ELF_PROGRAM_HEADER_SIZE: usize = 56; +const ELF_SECTION_HEADER_SIZE: usize = 64; +const ELF_SEGMENT_ALIGN: usize = 0x1000; +const ELF_PF_X: u32 = 1; +#[cfg(test)] +const ELF_PF_W: u32 = 2; +const ELF_PF_R: u32 = 4; +const ELF_BASE_ADDR: u64 = 0x10000; +const START_TRAMPOLINE_SIZE: usize = 20; +const EXIT_SYSCALL_NUMBER: i64 = 93; +const ELF_SECTION_NAMES: &[u8] = b"\0.text\0.rodata\0.shstrtab\0"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum SectionKind { + Text, + Rodata, +} + +#[derive(Debug, Clone)] +enum AsmOp { + Label(String), + Instruction(Instruction), + Word(u32), + Byte(u8), + Ascii(Vec), + Align(usize), +} + +#[derive(Debug, Clone, Copy)] +struct SymbolDef { + section: SectionKind, + offset: usize, +} + +#[derive(Debug, Clone, Copy)] +struct SectionLayout { + text_base: u64, + text_user_base: u64, + rodata_base: u64, +} + +impl SectionLayout { + fn for_text_user_size(text_user_size: usize) -> Self { + let rodata_offset = align_up(START_TRAMPOLINE_SIZE + text_user_size, 8); + Self { + text_base: ELF_BASE_ADDR, + text_user_base: ELF_BASE_ADDR + START_TRAMPOLINE_SIZE as u64, + rodata_base: ELF_BASE_ADDR + rodata_offset as u64, + } + } + + fn rodata_offset(&self) -> Result { + usize::try_from(self.rodata_base - self.text_base) + .map_err(|_| CompileError::new("ELF rodata offset does not fit usize", crate::error::Span::default())) + } +} + +#[derive(Debug)] +pub(super) struct MachineLayoutPlan { + parsed: ParsedAssembly, + layout: SectionLayout, + cfg: MachineCfg, + order: MachineLayoutOrder, + pub(super) metrics: BackendLayoutMetrics, +} + +#[derive(Debug, Clone, Copy, Default)] +pub(super) struct BackendLayoutMetrics { + text_size: usize, + rodata_size: usize, + executable_text_op_count: usize, + covered_text_op_count: usize, + relaxed_branch_count: usize, + max_cond_branch_abs_distance: u64, + machine_block_count: usize, + max_machine_block_size: usize, + conditional_branch_block_count: usize, + labeled_machine_block_count: usize, + machine_cfg_edge_count: usize, + machine_call_edge_count: usize, + unreachable_machine_block_count: usize, + layout_order_block_count: usize, + layout_order_text_size: usize, +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)] +pub struct BackendShapeMetrics { + pub text_size: usize, + pub rodata_size: usize, + pub executable_text_op_count: usize, + pub covered_text_op_count: usize, + pub relaxed_branch_count: usize, + pub max_cond_branch_abs_distance: u64, + pub machine_block_count: usize, + pub max_machine_block_size: usize, + pub conditional_branch_block_count: usize, + pub labeled_machine_block_count: usize, + pub machine_cfg_edge_count: usize, + pub machine_call_edge_count: usize, + pub unreachable_machine_block_count: usize, + pub layout_order_block_count: usize, + pub layout_order_text_size: usize, +} + +impl From for BackendShapeMetrics { + fn from(metrics: BackendLayoutMetrics) -> Self { + Self { + text_size: metrics.text_size, + rodata_size: metrics.rodata_size, + executable_text_op_count: metrics.executable_text_op_count, + covered_text_op_count: metrics.covered_text_op_count, + relaxed_branch_count: metrics.relaxed_branch_count, + max_cond_branch_abs_distance: metrics.max_cond_branch_abs_distance, + machine_block_count: metrics.machine_block_count, + max_machine_block_size: metrics.max_machine_block_size, + conditional_branch_block_count: metrics.conditional_branch_block_count, + labeled_machine_block_count: metrics.labeled_machine_block_count, + machine_cfg_edge_count: metrics.machine_cfg_edge_count, + machine_call_edge_count: metrics.machine_call_edge_count, + unreachable_machine_block_count: metrics.unreachable_machine_block_count, + layout_order_block_count: metrics.layout_order_block_count, + layout_order_text_size: metrics.layout_order_text_size, + } + } +} + +#[derive(Debug, Clone)] +enum Instruction { + Addi { rd: u8, rs1: u8, imm: i64 }, + Add { rd: u8, rs1: u8, rs2: u8 }, + Sub { rd: u8, rs1: u8, rs2: u8 }, + And { rd: u8, rs1: u8, rs2: u8 }, + Or { rd: u8, rs1: u8, rs2: u8 }, + Xor { rd: u8, rs1: u8, rs2: u8 }, + Mul { rd: u8, rs1: u8, rs2: u8 }, + Mulhu { rd: u8, rs1: u8, rs2: u8 }, + Div { rd: u8, rs1: u8, rs2: u8 }, + Divu { rd: u8, rs1: u8, rs2: u8 }, + Rem { rd: u8, rs1: u8, rs2: u8 }, + Remu { rd: u8, rs1: u8, rs2: u8 }, + Slt { rd: u8, rs1: u8, rs2: u8 }, + Sltu { rd: u8, rs1: u8, rs2: u8 }, + Sgt { rd: u8, rs1: u8, rs2: u8 }, + Xori { rd: u8, rs1: u8, imm: i64 }, + Seqz { rd: u8, rs: u8 }, + Snez { rd: u8, rs: u8 }, + Neg { rd: u8, rs: u8 }, + Ld { rd: u8, rs1: u8, imm: i64 }, + Lbu { rd: u8, rs1: u8, imm: i64 }, + Sb { rs2: u8, rs1: u8, imm: i64 }, + Sh { rs2: u8, rs1: u8, imm: i64 }, + Sw { rs2: u8, rs1: u8, imm: i64 }, + Sd { rs2: u8, rs1: u8, imm: i64 }, + Slli { rd: u8, rs1: u8, shamt: i64 }, + Srai { rd: u8, rs1: u8, shamt: i64 }, + Srli { rd: u8, rs1: u8, shamt: i64 }, + Li { rd: u8, imm: i128 }, + La { rd: u8, label: String }, + Call { label: String }, + Jump { label: String }, + Beq { rs1: u8, rs2: u8, label: String }, + Bne { rs1: u8, rs2: u8, label: String }, + Blt { rs1: u8, rs2: u8, label: String }, + Bge { rs1: u8, rs2: u8, label: String }, + Bltu { rs1: u8, rs2: u8, label: String }, + Bgeu { rs1: u8, rs2: u8, label: String }, + Beqz { rs: u8, label: String }, + Bnez { rs: u8, label: String }, + Ret, + Ecall, +} + +fn reject_unresolved_calls(lines: &[String]) -> Result<()> { + let mut labels = BTreeSet::new(); + let mut calls = BTreeSet::new(); + + for line in lines { + let Some(clean) = strip_comment(line) else { + continue; + }; + if let Some(label) = clean.strip_suffix(':') { + labels.insert(label.trim().to_string()); + continue; + } + if let Some(target) = clean.strip_prefix("call ") { + let target = target.trim(); + if !target.is_empty() { + calls.insert(target.to_string()); + } + } + } + + let missing = calls.difference(&labels).cloned().collect::>(); + if missing.is_empty() { + return Ok(()); + } + + Err(CompileError::without_span(format!( + "unresolved call target(s) in generated assembly: {}; production ELF emission requires all call targets to be lowered", + missing.join(", ") + ))) +} + +fn entry_requires_explicit_parameter_abi(lines: &[String], entry_label: &str) -> bool { + let marker = format!("# cellscript entry abi: {} requires-explicit-parameter-abi", entry_label); + lines.iter().any(|line| line.trim() == marker) +} + +pub(super) fn assemble_generated_elf(lines: &[String]) -> Result> { + reject_unresolved_calls(lines).map_err(|error| with_codegen_code(error, "E2200"))?; + assemble_elf_internal(lines) +} + +fn assemble_elf_internal(lines: &[String]) -> Result> { + let plan = MachineLayoutPlan::build(lines).map_err(|error| with_codegen_code(error, "E2201"))?; + let parsed = &plan.parsed; + let layout = plan.layout; + let _layout_control_metrics = ( + plan.metrics.executable_text_op_count, + plan.metrics.covered_text_op_count, + plan.metrics.relaxed_branch_count, + plan.metrics.max_cond_branch_abs_distance, + plan.metrics.machine_block_count, + plan.metrics.max_machine_block_size, + plan.metrics.conditional_branch_block_count, + plan.metrics.labeled_machine_block_count, + plan.metrics.machine_cfg_edge_count, + plan.metrics.machine_call_edge_count, + plan.metrics.unreachable_machine_block_count, + plan.metrics.layout_order_block_count, + plan.metrics.layout_order_text_size, + plan.cfg.blocks.len(), + plan.cfg.edges.len(), + plan.order.block_order.len(), + plan.order.placed_blocks.len(), + plan.order.text_size, + ); + let entry_label = parsed.entry_label.as_deref().ok_or_else(|| { + CompileError::new("ELF target requires at least one action or lock entry point", crate::error::Span::default()) + })?; + let text_user_size = plan.metrics.text_size; + let rodata_size = plan.metrics.rodata_size; + let rodata_offset = layout.rodata_offset()?; + let mut text_bytes = Vec::with_capacity(START_TRAMPOLINE_SIZE + text_user_size); + if entry_requires_explicit_parameter_abi(lines, entry_label) { + encode_li_sequence(&mut text_bytes, 10, 25)?; + } else { + let entry_addr = parsed.symbol_address(entry_label, &layout)?; + encode_call_sequence(&mut text_bytes, layout.text_base, entry_addr)?; + } + encode_li_sequence(&mut text_bytes, 17, i128::from(EXIT_SYSCALL_NUMBER))?; + text_bytes.extend_from_slice(&encode_ecall().to_le_bytes()); + debug_assert_eq!(text_bytes.len(), START_TRAMPOLINE_SIZE); + parsed + .encode_section(SectionKind::Text, &mut text_bytes, &layout, START_TRAMPOLINE_SIZE) + .map_err(|error| with_codegen_code(error, "E2202"))?; + + let mut rodata_bytes = Vec::with_capacity(rodata_size); + parsed.encode_section(SectionKind::Rodata, &mut rodata_bytes, &layout, 0).map_err(|error| with_codegen_code(error, "E2202"))?; + + let segment_file_payload_size = rodata_offset + rodata_bytes.len(); + let segment_file_offset = align_up(ELF_HEADER_SIZE + ELF_PROGRAM_HEADER_SIZE, ELF_SEGMENT_ALIGN); + let load_segment_offset = 0u64; + let load_segment_vaddr = layout.text_base.checked_sub(segment_file_offset as u64).ok_or_else(|| { + CompileError::new("ELF text base is smaller than the load segment file offset", crate::error::Span::default()) + })?; + let load_segment_file_size = segment_file_offset + segment_file_payload_size; + let section_names_offset = align_up(load_segment_file_size, 8); + let section_header_offset = align_up(section_names_offset + ELF_SECTION_NAMES.len(), 8); + let section_count = 4usize; + let elf_size = section_header_offset + section_count * ELF_SECTION_HEADER_SIZE; + let mut elf = vec![0u8; elf_size]; + write_elf_header(&mut elf[..ELF_HEADER_SIZE], layout.text_base, 1, section_header_offset as u64, section_count as u16, 3)?; + write_program_header( + &mut elf[ELF_HEADER_SIZE..ELF_HEADER_SIZE + ELF_PROGRAM_HEADER_SIZE], + ELF_PF_R | ELF_PF_X, + load_segment_offset, + load_segment_vaddr, + load_segment_file_size as u64, + load_segment_file_size as u64, + )?; + + let segment = &mut elf[segment_file_offset..segment_file_offset + segment_file_payload_size]; + segment[..text_bytes.len()].copy_from_slice(&text_bytes); + segment[rodata_offset..rodata_offset + rodata_bytes.len()].copy_from_slice(&rodata_bytes); + elf[section_names_offset..section_names_offset + ELF_SECTION_NAMES.len()].copy_from_slice(ELF_SECTION_NAMES); + let section_headers = &mut elf[section_header_offset..section_header_offset + section_count * ELF_SECTION_HEADER_SIZE]; + write_section_header( + &mut section_headers[ELF_SECTION_HEADER_SIZE..2 * ELF_SECTION_HEADER_SIZE], + 1, + 1, + 0x2 | 0x4, + layout.text_base, + segment_file_offset as u64, + text_bytes.len() as u64, + 4, + )?; + write_section_header( + &mut section_headers[2 * ELF_SECTION_HEADER_SIZE..3 * ELF_SECTION_HEADER_SIZE], + 7, + 1, + 0x2, + layout.rodata_base, + (segment_file_offset + rodata_offset) as u64, + rodata_bytes.len() as u64, + 8, + )?; + write_section_header( + &mut section_headers[3 * ELF_SECTION_HEADER_SIZE..4 * ELF_SECTION_HEADER_SIZE], + 15, + 3, + 0, + 0, + section_names_offset as u64, + ELF_SECTION_NAMES.len() as u64, + 1, + )?; + Ok(elf) +} + +#[derive(Debug, Default)] +struct ParsedAssembly { + text_ops: Vec, + rodata_ops: Vec, + text_size: usize, + rodata_size: usize, + symbols: HashMap, + globals: BTreeSet, + entry_label: Option, + relaxed_text_branches: BTreeSet, +} + +impl ParsedAssembly { + fn from_lines_relaxed(lines: &[String], layout: &SectionLayout) -> Result { + let conservative = Self::from_lines_with_branch_mode(lines, BranchSizeMode::Conservative)?; + let relaxed_text_branches = conservative.relaxed_branch_indices(layout)?; + Self::from_lines_with_branch_mode(lines, BranchSizeMode::Exact(&relaxed_text_branches)) + } + + fn from_lines_with_branch_mode(lines: &[String], branch_size_mode: BranchSizeMode<'_>) -> Result { + let mut current_section = SectionKind::Text; + let mut text_size = 0usize; + let mut rodata_size = 0usize; + let mut text_ops = Vec::new(); + let mut rodata_ops = Vec::new(); + let mut symbols = HashMap::new(); + let mut globals = BTreeSet::new(); + let mut entry_label = None; + let mut fallback_entry = None; + + for line in lines { + let Some(clean) = strip_comment(line) else { + continue; + }; + if clean.is_empty() { + continue; + } + + if let Some(section) = parse_section_directive(clean)? { + current_section = section; + continue; + } + if clean.starts_with(".option ") || clean.starts_with(".type ") { + continue; + } + if let Some(symbol) = clean.strip_prefix(".global ") { + globals.insert(symbol.trim().to_string()); + continue; + } + + let (ops, offset) = match current_section { + SectionKind::Text => (&mut text_ops, &mut text_size), + SectionKind::Rodata => (&mut rodata_ops, &mut rodata_size), + }; + let op_index = ops.len(); + + if let Some(label) = clean.strip_suffix(':') { + let label = label.trim().to_string(); + let symbol = SymbolDef { section: current_section, offset: *offset }; + if symbols.insert(label.clone(), symbol).is_some() { + return Err(CompileError::new(format!("duplicate assembly label '{}'", label), crate::error::Span::default())); + } + if current_section == SectionKind::Text && globals.contains(&label) { + if fallback_entry.is_none() { + fallback_entry = Some(label.clone()); + } + if !label.starts_with("__") && entry_label.is_none() { + entry_label = Some(label.clone()); + } + } + ops.push(AsmOp::Label(label)); + continue; + } + + let op = parse_asm_op(clean)?; + *offset += op_size(&op, *offset, current_section, op_index, branch_size_mode); + ops.push(op); + } + + Ok(Self { + text_ops, + rodata_ops, + text_size, + rodata_size, + symbols, + globals, + entry_label: entry_label.or(fallback_entry), + relaxed_text_branches: branch_size_mode.relaxed_text_branches().cloned().unwrap_or_default(), + }) + } + + fn relaxed_branch_indices(&self, layout: &SectionLayout) -> Result> { + let mut relaxed = BTreeSet::new(); + let mut offset = 0usize; + for (index, op) in self.text_ops.iter().enumerate() { + if let AsmOp::Instruction(inst) = op + && conditional_branch_parts(inst).is_some() + { + let pc = layout.text_user_base + offset as u64; + let target = branch_target(inst, self, layout)?; + if !signed_bits_fit(relative_offset(pc, target)?, 13) { + relaxed.insert(index); + } + } + offset += op_size(op, offset, SectionKind::Text, index, BranchSizeMode::Conservative); + } + Ok(relaxed) + } + + fn section_size(&self, section: SectionKind) -> usize { + match section { + SectionKind::Text => self.text_size, + SectionKind::Rodata => self.rodata_size, + } + } + + fn symbol_address(&self, label: &str, layout: &SectionLayout) -> Result { + let symbol = self + .symbols + .get(label) + .ok_or_else(|| CompileError::new(format!("unknown assembly label '{}'", label), crate::error::Span::default()))?; + Ok(match symbol.section { + SectionKind::Text => layout.text_user_base + symbol.offset as u64, + SectionKind::Rodata => layout.rodata_base + symbol.offset as u64, + }) + } + + fn encode_section(&self, section: SectionKind, out: &mut Vec, layout: &SectionLayout, base_bias: usize) -> Result<()> { + let ops = match section { + SectionKind::Text => &self.text_ops, + SectionKind::Rodata => &self.rodata_ops, + }; + let section_base = match section { + SectionKind::Text => layout.text_user_base, + SectionKind::Rodata => layout.rodata_base, + }; + + for (op_index, op) in ops.iter().enumerate() { + match op { + AsmOp::Label(_) => {} + AsmOp::Word(word) => out.extend_from_slice(&word.to_le_bytes()), + AsmOp::Byte(byte) => out.push(*byte), + AsmOp::Ascii(bytes) => out.extend_from_slice(bytes), + AsmOp::Align(bytes) => pad_to_alignment(out, *bytes), + AsmOp::Instruction(inst) => { + let section_offset = out.len().checked_sub(base_bias).ok_or_else(|| { + CompileError::new("assembly output offset is smaller than section base bias", crate::error::Span::default()) + })?; + let pc = section_base + section_offset as u64; + encode_instruction( + out, + inst, + pc, + self, + layout, + section == SectionKind::Text && self.relaxed_text_branches.contains(&op_index), + )?; + } + } + } + + Ok(()) + } +} + +impl MachineLayoutPlan { + pub(super) fn build(lines: &[String]) -> Result { + let preliminary = ParsedAssembly::from_lines_with_branch_mode(lines, BranchSizeMode::Conservative)?; + let preliminary_layout = SectionLayout::for_text_user_size(preliminary.section_size(SectionKind::Text)); + let parsed = ParsedAssembly::from_lines_relaxed(lines, &preliminary_layout)?; + let layout = SectionLayout::for_text_user_size(parsed.section_size(SectionKind::Text)); + let cfg = machine_cfg(&parsed)?; + let coverage = validate_machine_block_coverage(&parsed, &cfg)?; + let order = machine_layout_order(&cfg)?; + let metrics = parsed.layout_metrics(&layout, &cfg, &order, coverage)?; + Ok(Self { parsed, layout, cfg, order, metrics }) + } +} + +pub(super) fn machine_layout_evidence( + lines: &[String], + entry_frame_sizes: &BTreeMap, + ir: &IrModule, +) -> Result { + let plan = MachineLayoutPlan::build(lines)?; + let runtime_error_labels = ir_runtime_error_labels(ir); + let text_start = plan.layout.text_user_base; + let text_end = text_start + .checked_add(plan.metrics.text_size as u64) + .ok_or_else(|| CompileError::new("machine evidence text range overflows u64", crate::error::Span::default()))?; + let entry_label = plan + .parsed + .entry_label + .clone() + .ok_or_else(|| CompileError::new("machine evidence requires an entry label", crate::error::Span::default()))?; + let blocks = plan + .cfg + .blocks + .iter() + .enumerate() + .map(|(index, block)| MachineBlockEvidence { + index, + label: block.label.clone(), + start: text_start + block.byte_start as u64, + end: text_start + block.byte_start as u64 + block.byte_size as u64, + terminator: match block.terminator { + MachineTerminator::Fallthrough => MachineTerminatorEvidence::Fallthrough, + MachineTerminator::Jump { .. } => MachineTerminatorEvidence::Jump, + MachineTerminator::ConditionalBranch { .. } => MachineTerminatorEvidence::ConditionalBranch, + MachineTerminator::Return => MachineTerminatorEvidence::Return, + }, + runtime_error_codes: block.label.as_ref().and_then(|label| runtime_error_labels.get(label)).cloned().unwrap_or_default(), + }) + .collect(); + let edges = plan + .cfg + .edges + .iter() + .map(|edge| MachineEdgeEvidence { + from: edge.from, + to: edge.to, + kind: match edge.kind { + MachineCfgEdgeKind::Fallthrough => MachineEdgeKindEvidence::Fallthrough, + MachineCfgEdgeKind::Jump => MachineEdgeKindEvidence::Jump, + MachineCfgEdgeKind::ConditionalTaken => MachineEdgeKindEvidence::ConditionalTaken, + MachineCfgEdgeKind::ConditionalFallthrough => MachineEdgeKindEvidence::ConditionalFallthrough, + MachineCfgEdgeKind::Call => MachineEdgeKindEvidence::Call, + }, + }) + .collect(); + let symbols = plan + .parsed + .symbols + .iter() + .filter_map(|(name, symbol)| { + (symbol.section == SectionKind::Text).then_some((name.clone(), text_start + symbol.offset as u64)) + }) + .collect(); + Ok(MachineLayoutEvidence { + text_start, + text_end, + entry_label, + blocks, + edges, + symbols, + globals: plan.parsed.globals.clone(), + entry_frame_sizes: entry_frame_sizes.clone(), + }) +} + +fn ir_runtime_error_labels(ir: &IrModule) -> BTreeMap> { + let mut labels = BTreeMap::new(); + for item in &ir.items { + let (name, body) = match item { + IrItem::Action(action) => (action.name.as_str(), &action.body), + IrItem::Lock(lock) => (lock.name.as_str(), &lock.body), + IrItem::PureFn(function) => (function.name.as_str(), &function.body), + IrItem::TypeDef(_) | IrItem::Invariant(_) => continue, + }; + for block in &body.blocks { + if let Some(error) = block.runtime_error { + labels.insert(format!(".L{}_block_{}", name, block.id.0), vec![error.code()]); + } + } + } + labels +} + +#[derive(Debug, Clone, Copy)] +struct TextOpLayout { + op_index: usize, + offset: usize, + size: usize, +} + +#[derive(Debug, Clone)] +struct MachineBlock { + label: Option, + op_start: usize, + op_end: usize, + byte_start: usize, + byte_size: usize, + terminator: MachineTerminator, +} + +#[derive(Debug, Clone)] +struct MachineCfg { + blocks: Vec, + edges: Vec, +} + +#[derive(Debug, Clone, Copy, Default)] +struct MachineBlockCoverage { + executable_text_op_count: usize, + covered_text_op_count: usize, +} + +#[derive(Debug, Clone)] +struct MachineLayoutOrder { + block_order: Vec, + placed_blocks: Vec, + text_size: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct MachinePlacedBlock { + block_index: usize, + byte_start: usize, + byte_size: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct MachineCfgEdge { + from: usize, + to: usize, + kind: MachineCfgEdgeKind, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MachineCfgEdgeKind { + Fallthrough, + Jump, + ConditionalTaken, + ConditionalFallthrough, + Call, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum MachineTerminator { + Fallthrough, + Jump { target: String }, + ConditionalBranch { target: String }, + Return, +} + +fn text_op_layouts(parsed: &ParsedAssembly) -> Vec { + let mut offset = 0usize; + let mut layouts = Vec::with_capacity(parsed.text_ops.len()); + for (op_index, op) in parsed.text_ops.iter().enumerate() { + let size = op_size(op, offset, SectionKind::Text, op_index, BranchSizeMode::Exact(&parsed.relaxed_text_branches)); + layouts.push(TextOpLayout { op_index, offset, size }); + offset += size; + } + layouts +} + +fn machine_blocks(parsed: &ParsedAssembly) -> Vec { + let layouts = text_op_layouts(parsed); + let mut blocks = Vec::new(); + let mut block_start = 0usize; + let mut block_label = None; + + for (op_index, op) in parsed.text_ops.iter().enumerate() { + if let AsmOp::Label(label) = op { + if block_has_executable_ops(&parsed.text_ops[block_start..op_index]) { + blocks.push(build_machine_block(parsed, &layouts, block_start, op_index, block_label.take())); + block_start = op_index; + } + if block_label.is_none() { + block_label = Some(label.clone()); + } + continue; + } + + if instruction_terminator(op).is_some() { + blocks.push(build_machine_block(parsed, &layouts, block_start, op_index + 1, block_label.take())); + block_start = op_index + 1; + } + } + + if block_start < parsed.text_ops.len() && block_has_executable_ops(&parsed.text_ops[block_start..]) { + blocks.push(build_machine_block(parsed, &layouts, block_start, parsed.text_ops.len(), block_label)); + } + + blocks +} + +fn machine_cfg(parsed: &ParsedAssembly) -> Result { + let blocks = machine_blocks(parsed); + let label_to_block = machine_label_to_block(parsed, &blocks); + let mut edges = Vec::new(); + + for (index, block) in blocks.iter().enumerate() { + for target in machine_block_call_targets(parsed, block) { + if let Some(&target_block) = label_to_block.get(&target) { + edges.push(MachineCfgEdge { from: index, to: target_block, kind: MachineCfgEdgeKind::Call }); + } + } + match &block.terminator { + MachineTerminator::Fallthrough => { + if index + 1 < blocks.len() { + edges.push(MachineCfgEdge { from: index, to: index + 1, kind: MachineCfgEdgeKind::Fallthrough }); + } + } + MachineTerminator::Jump { target } => { + edges.push(MachineCfgEdge { + from: index, + to: machine_cfg_target_block(target, &label_to_block)?, + kind: MachineCfgEdgeKind::Jump, + }); + } + MachineTerminator::ConditionalBranch { target } => { + edges.push(MachineCfgEdge { + from: index, + to: machine_cfg_target_block(target, &label_to_block)?, + kind: MachineCfgEdgeKind::ConditionalTaken, + }); + if index + 1 < blocks.len() { + edges.push(MachineCfgEdge { from: index, to: index + 1, kind: MachineCfgEdgeKind::ConditionalFallthrough }); + } + } + MachineTerminator::Return => {} + } + } + + Ok(MachineCfg { blocks, edges }) +} + +fn validate_machine_block_coverage(parsed: &ParsedAssembly, cfg: &MachineCfg) -> Result { + let executable_text_op_count = parsed.text_ops.iter().filter(|op| !matches!(op, AsmOp::Label(_))).count(); + let mut covered = BTreeSet::new(); + + for block in &cfg.blocks { + if block.op_start >= block.op_end || block.op_end > parsed.text_ops.len() { + return Err(CompileError::new( + format!("machine block has invalid op range {}..{}", block.op_start, block.op_end), + crate::error::Span::default(), + )); + } + if !block_has_executable_ops(&parsed.text_ops[block.op_start..block.op_end]) { + return Err(CompileError::new("machine block contains no executable instructions", crate::error::Span::default())); + } + for op_index in block.op_start..block.op_end { + if matches!(parsed.text_ops[op_index], AsmOp::Label(_)) { + continue; + } + if !covered.insert(op_index) { + return Err(CompileError::new( + format!("machine block coverage overlaps text op {}", op_index), + crate::error::Span::default(), + )); + } + } + } + + if covered.len() != executable_text_op_count { + return Err(CompileError::new( + format!("machine blocks cover {} executable text ops but assembly contains {}", covered.len(), executable_text_op_count), + crate::error::Span::default(), + )); + } + + Ok(MachineBlockCoverage { executable_text_op_count, covered_text_op_count: covered.len() }) +} + +fn machine_layout_order(cfg: &MachineCfg) -> Result { + let block_order = (0..cfg.blocks.len()).collect::>(); + build_machine_layout_order(cfg, block_order) +} + +fn build_machine_layout_order(cfg: &MachineCfg, block_order: Vec) -> Result { + validate_machine_layout_order(cfg, &block_order)?; + let mut byte_start = 0usize; + let mut placed_blocks = Vec::with_capacity(block_order.len()); + for &block_index in &block_order { + let block = &cfg.blocks[block_index]; + placed_blocks.push(MachinePlacedBlock { block_index, byte_start, byte_size: block.byte_size }); + byte_start += block.byte_size; + } + Ok(MachineLayoutOrder { block_order, placed_blocks, text_size: byte_start }) +} + +fn validate_machine_layout_order(cfg: &MachineCfg, block_order: &[usize]) -> Result<()> { + if block_order.len() != cfg.blocks.len() { + return Err(CompileError::new( + format!("machine layout order contains {} blocks but CFG contains {}", block_order.len(), cfg.blocks.len()), + crate::error::Span::default(), + )); + } + + let mut seen = BTreeSet::new(); + for &block_index in block_order { + if block_index >= cfg.blocks.len() { + return Err(CompileError::new( + format!("machine layout order references missing block {}", block_index), + crate::error::Span::default(), + )); + } + if !seen.insert(block_index) { + return Err(CompileError::new( + format!("machine layout order repeats block {}", block_index), + crate::error::Span::default(), + )); + } + } + + Ok(()) +} + +fn machine_label_to_block(parsed: &ParsedAssembly, blocks: &[MachineBlock]) -> HashMap { + let mut label_to_block = HashMap::new(); + for (label, symbol) in &parsed.symbols { + if symbol.section != SectionKind::Text { + continue; + } + if let Some((block_index, _)) = blocks.iter().enumerate().find(|(_, block)| block.byte_start == symbol.offset) { + label_to_block.insert(label.clone(), block_index); + } + } + label_to_block +} + +fn machine_cfg_target_block(target: &str, label_to_block: &HashMap) -> Result { + label_to_block.get(target).copied().ok_or_else(|| { + CompileError::new(format!("assembly branch target '{}' does not start a machine block", target), crate::error::Span::default()) + }) +} + +fn machine_block_call_targets(parsed: &ParsedAssembly, block: &MachineBlock) -> Vec { + parsed.text_ops[block.op_start..block.op_end] + .iter() + .filter_map(|op| match op { + AsmOp::Instruction(Instruction::Call { label }) => Some(label.clone()), + _ => None, + }) + .collect() +} + +fn unreachable_machine_block_count(parsed: &ParsedAssembly, cfg: &MachineCfg) -> usize { + if cfg.blocks.is_empty() { + return 0; + } + let label_to_block = machine_label_to_block(parsed, &cfg.blocks); + let mut roots = parsed.entry_label.as_ref().and_then(|label| label_to_block.get(label).copied()).into_iter().collect::>(); + if roots.is_empty() { + roots.push(0); + } + let mut reachable = BTreeSet::new(); + let mut stack = roots; + while let Some(block) = stack.pop() { + if !reachable.insert(block) { + continue; + } + for edge in cfg.edges.iter().filter(|edge| edge.from == block) { + stack.push(edge.to); + } + } + cfg.blocks.len().saturating_sub(reachable.len()) +} + +fn block_has_executable_ops(ops: &[AsmOp]) -> bool { + ops.iter().any(|op| !matches!(op, AsmOp::Label(_))) +} + +fn build_machine_block( + parsed: &ParsedAssembly, + layouts: &[TextOpLayout], + op_start: usize, + op_end: usize, + label: Option, +) -> MachineBlock { + let byte_start = layouts.get(op_start).map(|layout| layout.offset).unwrap_or(0); + let byte_end = + op_end.checked_sub(1).and_then(|last| layouts.get(last).map(|layout| layout.offset + layout.size)).unwrap_or(byte_start); + let terminator = + parsed.text_ops[op_start..op_end].iter().rev().find_map(instruction_terminator).unwrap_or(MachineTerminator::Fallthrough); + MachineBlock { label, op_start, op_end, byte_start, byte_size: byte_end.saturating_sub(byte_start), terminator } +} + +fn instruction_terminator(op: &AsmOp) -> Option { + match op { + AsmOp::Instruction(Instruction::Jump { label }) => Some(MachineTerminator::Jump { target: label.clone() }), + AsmOp::Instruction(Instruction::Ret) => Some(MachineTerminator::Return), + AsmOp::Instruction(inst) => { + conditional_branch_parts(inst).map(|(_, _, label, _)| MachineTerminator::ConditionalBranch { target: label.to_string() }) + } + _ => None, + } +} + +impl ParsedAssembly { + fn layout_metrics( + &self, + layout: &SectionLayout, + machine_cfg: &MachineCfg, + machine_order: &MachineLayoutOrder, + coverage: MachineBlockCoverage, + ) -> Result { + let text_op_layouts = text_op_layouts(self); + let text_size = text_op_layouts.iter().map(|op| op.size).sum(); + let mut max_cond_branch_abs_distance = 0u64; + for op_layout in text_op_layouts { + let AsmOp::Instruction(inst) = &self.text_ops[op_layout.op_index] else { + continue; + }; + if conditional_branch_parts(inst).is_none() { + continue; + }; + let pc = layout.text_user_base + op_layout.offset as u64; + let target = branch_target(inst, self, layout)?; + let distance = relative_offset(pc, target)?.unsigned_abs(); + max_cond_branch_abs_distance = max_cond_branch_abs_distance.max(distance); + } + let machine_block_count = machine_cfg.blocks.len(); + let max_machine_block_size = machine_cfg.blocks.iter().map(|block| block.byte_size).max().unwrap_or_default(); + let conditional_branch_block_count = + machine_cfg.blocks.iter().filter(|block| matches!(block.terminator, MachineTerminator::ConditionalBranch { .. })).count(); + let labeled_machine_block_count = machine_cfg.blocks.iter().filter(|block| block.label.is_some()).count(); + let machine_cfg_edge_count = machine_cfg.edges.len(); + let machine_call_edge_count = machine_cfg.edges.iter().filter(|edge| edge.kind == MachineCfgEdgeKind::Call).count(); + let unreachable_machine_block_count = unreachable_machine_block_count(self, machine_cfg); + let layout_order_block_count = machine_order.block_order.len(); + let layout_order_text_size = machine_order.text_size; + let _covered_text_ops = machine_cfg.blocks.iter().map(|block| block.op_end.saturating_sub(block.op_start)).sum::(); + let _first_block_byte_start = machine_cfg.blocks.first().map(|block| block.byte_start).unwrap_or_default(); + Ok(BackendLayoutMetrics { + text_size, + rodata_size: self.section_size(SectionKind::Rodata), + executable_text_op_count: coverage.executable_text_op_count, + covered_text_op_count: coverage.covered_text_op_count, + relaxed_branch_count: self.relaxed_text_branches.len(), + max_cond_branch_abs_distance, + machine_block_count, + max_machine_block_size, + conditional_branch_block_count, + labeled_machine_block_count, + machine_cfg_edge_count, + machine_call_edge_count, + unreachable_machine_block_count, + layout_order_block_count, + layout_order_text_size, + }) + } +} + +fn parse_section_directive(line: &str) -> Result> { + if let Some(section) = line.strip_prefix(".section ") { + return match section.trim() { + ".text" => Ok(Some(SectionKind::Text)), + ".rodata" => Ok(Some(SectionKind::Rodata)), + other => Err(CompileError::new(format!("unsupported assembly section '{}'", other), crate::error::Span::default())), + }; + } + Ok(None) +} + +fn parse_asm_op(line: &str) -> Result { + if let Some(value) = line.strip_prefix(".word ") { + let value = parse_immediate(value.trim())?; + return Ok(AsmOp::Word( + u32::try_from(value).map_err(|_| { + CompileError::new(format!("'.word' value '{}' does not fit u32", value), crate::error::Span::default()) + })?, + )); + } + if let Some(value) = line.strip_prefix(".byte ") { + let value = parse_immediate(value.trim())?; + return Ok(AsmOp::Byte( + u8::try_from(value) + .map_err(|_| CompileError::new(format!("'.byte' value '{}' does not fit u8", value), crate::error::Span::default()))?, + )); + } + if let Some(value) = line.strip_prefix(".ascii ") { + return Ok(AsmOp::Ascii(parse_ascii_literal(value.trim())?)); + } + if let Some(value) = line.strip_prefix(".align ") { + let align_pow = parse_immediate(value.trim())?; + if !(0..=16).contains(&align_pow) { + return Err(CompileError::new(format!("unsupported .align value '{}'", align_pow), crate::error::Span::default())); + } + return Ok(AsmOp::Align(1usize << (align_pow as usize))); + } + Ok(AsmOp::Instruction(parse_instruction(line)?)) +} + +fn parse_instruction(line: &str) -> Result { + let mut parts = line.splitn(2, char::is_whitespace); + let opcode = parts.next().unwrap().trim(); + let args = parts.next().unwrap_or("").trim(); + let args = if args.is_empty() { Vec::new() } else { args.split(',').map(|arg| arg.trim().to_string()).collect() }; + + match opcode { + "addi" => Ok(Instruction::Addi { + rd: parse_register(arg(&args, 0)?)?, + rs1: parse_register(arg(&args, 1)?)?, + imm: parse_immediate(arg(&args, 2)?)?, + }), + "add" => Ok(Instruction::Add { + rd: parse_register(arg(&args, 0)?)?, + rs1: parse_register(arg(&args, 1)?)?, + rs2: parse_register(arg(&args, 2)?)?, + }), + "sub" => Ok(Instruction::Sub { + rd: parse_register(arg(&args, 0)?)?, + rs1: parse_register(arg(&args, 1)?)?, + rs2: parse_register(arg(&args, 2)?)?, + }), + "and" => Ok(Instruction::And { + rd: parse_register(arg(&args, 0)?)?, + rs1: parse_register(arg(&args, 1)?)?, + rs2: parse_register(arg(&args, 2)?)?, + }), + "or" => Ok(Instruction::Or { + rd: parse_register(arg(&args, 0)?)?, + rs1: parse_register(arg(&args, 1)?)?, + rs2: parse_register(arg(&args, 2)?)?, + }), + "xor" => Ok(Instruction::Xor { + rd: parse_register(arg(&args, 0)?)?, + rs1: parse_register(arg(&args, 1)?)?, + rs2: parse_register(arg(&args, 2)?)?, + }), + "mul" => Ok(Instruction::Mul { + rd: parse_register(arg(&args, 0)?)?, + rs1: parse_register(arg(&args, 1)?)?, + rs2: parse_register(arg(&args, 2)?)?, + }), + "mulhu" => Ok(Instruction::Mulhu { + rd: parse_register(arg(&args, 0)?)?, + rs1: parse_register(arg(&args, 1)?)?, + rs2: parse_register(arg(&args, 2)?)?, + }), + "div" => Ok(Instruction::Div { + rd: parse_register(arg(&args, 0)?)?, + rs1: parse_register(arg(&args, 1)?)?, + rs2: parse_register(arg(&args, 2)?)?, + }), + "divu" => Ok(Instruction::Divu { + rd: parse_register(arg(&args, 0)?)?, + rs1: parse_register(arg(&args, 1)?)?, + rs2: parse_register(arg(&args, 2)?)?, + }), + "rem" => Ok(Instruction::Rem { + rd: parse_register(arg(&args, 0)?)?, + rs1: parse_register(arg(&args, 1)?)?, + rs2: parse_register(arg(&args, 2)?)?, + }), + "remu" => Ok(Instruction::Remu { + rd: parse_register(arg(&args, 0)?)?, + rs1: parse_register(arg(&args, 1)?)?, + rs2: parse_register(arg(&args, 2)?)?, + }), + "slt" => Ok(Instruction::Slt { + rd: parse_register(arg(&args, 0)?)?, + rs1: parse_register(arg(&args, 1)?)?, + rs2: parse_register(arg(&args, 2)?)?, + }), + "sltu" => Ok(Instruction::Sltu { + rd: parse_register(arg(&args, 0)?)?, + rs1: parse_register(arg(&args, 1)?)?, + rs2: parse_register(arg(&args, 2)?)?, + }), + "sgt" => Ok(Instruction::Sgt { + rd: parse_register(arg(&args, 0)?)?, + rs1: parse_register(arg(&args, 1)?)?, + rs2: parse_register(arg(&args, 2)?)?, + }), + "xori" => Ok(Instruction::Xori { + rd: parse_register(arg(&args, 0)?)?, + rs1: parse_register(arg(&args, 1)?)?, + imm: parse_immediate(arg(&args, 2)?)?, + }), + "seqz" => Ok(Instruction::Seqz { rd: parse_register(arg(&args, 0)?)?, rs: parse_register(arg(&args, 1)?)? }), + "snez" => Ok(Instruction::Snez { rd: parse_register(arg(&args, 0)?)?, rs: parse_register(arg(&args, 1)?)? }), + "neg" => Ok(Instruction::Neg { rd: parse_register(arg(&args, 0)?)?, rs: parse_register(arg(&args, 1)?)? }), + "ld" => { + let (imm, rs1) = parse_memory_operand(arg(&args, 1)?)?; + Ok(Instruction::Ld { rd: parse_register(arg(&args, 0)?)?, rs1, imm }) + } + "lbu" => { + let (imm, rs1) = parse_memory_operand(arg(&args, 1)?)?; + Ok(Instruction::Lbu { rd: parse_register(arg(&args, 0)?)?, rs1, imm }) + } + "sb" => { + let (imm, rs1) = parse_memory_operand(arg(&args, 1)?)?; + Ok(Instruction::Sb { rs2: parse_register(arg(&args, 0)?)?, rs1, imm }) + } + "sh" => { + let (imm, rs1) = parse_memory_operand(arg(&args, 1)?)?; + Ok(Instruction::Sh { rs2: parse_register(arg(&args, 0)?)?, rs1, imm }) + } + "sw" => { + let (imm, rs1) = parse_memory_operand(arg(&args, 1)?)?; + Ok(Instruction::Sw { rs2: parse_register(arg(&args, 0)?)?, rs1, imm }) + } + "sd" => { + let (imm, rs1) = parse_memory_operand(arg(&args, 1)?)?; + Ok(Instruction::Sd { rs2: parse_register(arg(&args, 0)?)?, rs1, imm }) + } + "slli" => Ok(Instruction::Slli { + rd: parse_register(arg(&args, 0)?)?, + rs1: parse_register(arg(&args, 1)?)?, + shamt: parse_immediate(arg(&args, 2)?)?, + }), + "srai" => Ok(Instruction::Srai { + rd: parse_register(arg(&args, 0)?)?, + rs1: parse_register(arg(&args, 1)?)?, + shamt: parse_immediate(arg(&args, 2)?)?, + }), + "srli" => Ok(Instruction::Srli { + rd: parse_register(arg(&args, 0)?)?, + rs1: parse_register(arg(&args, 1)?)?, + shamt: parse_immediate(arg(&args, 2)?)?, + }), + "li" => Ok(Instruction::Li { rd: parse_register(arg(&args, 0)?)?, imm: parse_li_immediate(arg(&args, 1)?)? }), + "mv" => Ok(Instruction::Addi { rd: parse_register(arg(&args, 0)?)?, rs1: parse_register(arg(&args, 1)?)?, imm: 0 }), + "la" => Ok(Instruction::La { rd: parse_register(arg(&args, 0)?)?, label: arg(&args, 1)?.to_string() }), + "call" => Ok(Instruction::Call { label: arg(&args, 0)?.to_string() }), + "j" => Ok(Instruction::Jump { label: arg(&args, 0)?.to_string() }), + "bgt" => Ok(Instruction::Blt { + rs1: parse_register(arg(&args, 1)?)?, + rs2: parse_register(arg(&args, 0)?)?, + label: arg(&args, 2)?.to_string(), + }), + "bgez" => Ok(Instruction::Bge { rs1: parse_register(arg(&args, 0)?)?, rs2: 0, label: arg(&args, 1)?.to_string() }), + "beq" | "bne" | "blt" | "bge" | "bltu" | "bgeu" => { + let rs1 = parse_register(arg(&args, 0)?)?; + let rs2 = parse_register(arg(&args, 1)?)?; + let label = arg(&args, 2)?.to_string(); + match opcode { + "beq" => Ok(Instruction::Beq { rs1, rs2, label }), + "bne" => Ok(Instruction::Bne { rs1, rs2, label }), + "blt" => Ok(Instruction::Blt { rs1, rs2, label }), + "bge" => Ok(Instruction::Bge { rs1, rs2, label }), + "bltu" => Ok(Instruction::Bltu { rs1, rs2, label }), + "bgeu" => Ok(Instruction::Bgeu { rs1, rs2, label }), + _ => unreachable!("branch opcode matched above"), + } + } + "beqz" => Ok(Instruction::Beqz { rs: parse_register(arg(&args, 0)?)?, label: arg(&args, 1)?.to_string() }), + "bnez" => Ok(Instruction::Bnez { rs: parse_register(arg(&args, 0)?)?, label: arg(&args, 1)?.to_string() }), + "ret" => Ok(Instruction::Ret), + "ecall" => Ok(Instruction::Ecall), + other => Err(CompileError::new(format!("unsupported assembly instruction '{}'", other), crate::error::Span::default())), + } +} + +#[derive(Debug, Clone, Copy)] +enum BranchSizeMode<'a> { + Conservative, + Exact(&'a BTreeSet), +} + +impl<'a> BranchSizeMode<'a> { + fn relaxed_text_branches(self) -> Option<&'a BTreeSet> { + match self { + Self::Conservative => None, + Self::Exact(branches) => Some(branches), + } + } +} + +fn branch_target(inst: &Instruction, parsed: &ParsedAssembly, layout: &SectionLayout) -> Result { + if let Some((_, _, label, _)) = conditional_branch_parts(inst) { + parsed.symbol_address(label, layout) + } else { + Err(CompileError::new("instruction is not a conditional branch", crate::error::Span::default())) + } +} + +fn conditional_branch_parts(inst: &Instruction) -> Option<(u8, u8, &str, u32)> { + match inst { + Instruction::Beq { rs1, rs2, label } => Some((*rs1, *rs2, label.as_str(), 0b000)), + Instruction::Bne { rs1, rs2, label } => Some((*rs1, *rs2, label.as_str(), 0b001)), + Instruction::Blt { rs1, rs2, label } => Some((*rs1, *rs2, label.as_str(), 0b100)), + Instruction::Bge { rs1, rs2, label } => Some((*rs1, *rs2, label.as_str(), 0b101)), + Instruction::Bltu { rs1, rs2, label } => Some((*rs1, *rs2, label.as_str(), 0b110)), + Instruction::Bgeu { rs1, rs2, label } => Some((*rs1, *rs2, label.as_str(), 0b111)), + Instruction::Beqz { rs, label } => Some((*rs, 0, label.as_str(), 0b000)), + Instruction::Bnez { rs, label } => Some((*rs, 0, label.as_str(), 0b001)), + _ => None, + } +} + +fn inverse_branch_funct3(funct3: u32) -> u32 { + match funct3 { + 0b000 => 0b001, + 0b001 => 0b000, + 0b100 => 0b101, + 0b101 => 0b100, + 0b110 => 0b111, + 0b111 => 0b110, + _ => unreachable!("unsupported branch funct3"), + } +} + +fn encode_instruction( + out: &mut Vec, + inst: &Instruction, + pc: u64, + parsed: &ParsedAssembly, + layout: &SectionLayout, + relaxed_branch: bool, +) -> Result<()> { + match inst { + Instruction::Addi { rd, rs1, imm } => out.extend_from_slice(&encode_i_type(0x13, *rd, 0b000, *rs1, *imm)?.to_le_bytes()), + Instruction::Add { rd, rs1, rs2 } => { + out.extend_from_slice(&encode_r_type(0x33, *rd, 0b000, *rs1, *rs2, 0b0000000).to_le_bytes()) + } + Instruction::Sub { rd, rs1, rs2 } => { + out.extend_from_slice(&encode_r_type(0x33, *rd, 0b000, *rs1, *rs2, 0b0100000).to_le_bytes()) + } + Instruction::And { rd, rs1, rs2 } => { + out.extend_from_slice(&encode_r_type(0x33, *rd, 0b111, *rs1, *rs2, 0b0000000).to_le_bytes()) + } + Instruction::Or { rd, rs1, rs2 } => { + out.extend_from_slice(&encode_r_type(0x33, *rd, 0b110, *rs1, *rs2, 0b0000000).to_le_bytes()) + } + Instruction::Xor { rd, rs1, rs2 } => { + out.extend_from_slice(&encode_r_type(0x33, *rd, 0b100, *rs1, *rs2, 0b0000000).to_le_bytes()) + } + Instruction::Mul { rd, rs1, rs2 } => { + out.extend_from_slice(&encode_r_type(0x33, *rd, 0b000, *rs1, *rs2, 0b0000001).to_le_bytes()) + } + Instruction::Mulhu { rd, rs1, rs2 } => { + out.extend_from_slice(&encode_r_type(0x33, *rd, 0b011, *rs1, *rs2, 0b0000001).to_le_bytes()) + } + Instruction::Div { rd, rs1, rs2 } => { + out.extend_from_slice(&encode_r_type(0x33, *rd, 0b100, *rs1, *rs2, 0b0000001).to_le_bytes()) + } + Instruction::Divu { rd, rs1, rs2 } => { + out.extend_from_slice(&encode_r_type(0x33, *rd, 0b101, *rs1, *rs2, 0b0000001).to_le_bytes()) + } + Instruction::Rem { rd, rs1, rs2 } => { + out.extend_from_slice(&encode_r_type(0x33, *rd, 0b110, *rs1, *rs2, 0b0000001).to_le_bytes()) + } + Instruction::Remu { rd, rs1, rs2 } => { + out.extend_from_slice(&encode_r_type(0x33, *rd, 0b111, *rs1, *rs2, 0b0000001).to_le_bytes()) + } + Instruction::Slt { rd, rs1, rs2 } => { + out.extend_from_slice(&encode_r_type(0x33, *rd, 0b010, *rs1, *rs2, 0b0000000).to_le_bytes()) + } + Instruction::Sltu { rd, rs1, rs2 } => { + out.extend_from_slice(&encode_r_type(0x33, *rd, 0b011, *rs1, *rs2, 0b0000000).to_le_bytes()) + } + Instruction::Sgt { rd, rs1, rs2 } => { + out.extend_from_slice(&encode_r_type(0x33, *rd, 0b010, *rs2, *rs1, 0b0000000).to_le_bytes()) + } + Instruction::Xori { rd, rs1, imm } => out.extend_from_slice(&encode_i_type(0x13, *rd, 0b100, *rs1, *imm)?.to_le_bytes()), + Instruction::Seqz { rd, rs } => out.extend_from_slice(&encode_i_type(0x13, *rd, 0b011, *rs, 1)?.to_le_bytes()), + Instruction::Snez { rd, rs } => out.extend_from_slice(&encode_r_type(0x33, *rd, 0b011, 0, *rs, 0b0000000).to_le_bytes()), + Instruction::Neg { rd, rs } => out.extend_from_slice(&encode_r_type(0x33, *rd, 0b000, 0, *rs, 0b0100000).to_le_bytes()), + Instruction::Ld { rd, rs1, imm } => out.extend_from_slice(&encode_i_type(0x03, *rd, 0b011, *rs1, *imm)?.to_le_bytes()), + Instruction::Lbu { rd, rs1, imm } => out.extend_from_slice(&encode_i_type(0x03, *rd, 0b100, *rs1, *imm)?.to_le_bytes()), + Instruction::Sb { rs2, rs1, imm } => out.extend_from_slice(&encode_s_type(0x23, 0b000, *rs1, *rs2, *imm)?.to_le_bytes()), + Instruction::Sh { rs2, rs1, imm } => out.extend_from_slice(&encode_s_type(0x23, 0b001, *rs1, *rs2, *imm)?.to_le_bytes()), + Instruction::Sw { rs2, rs1, imm } => out.extend_from_slice(&encode_s_type(0x23, 0b010, *rs1, *rs2, *imm)?.to_le_bytes()), + Instruction::Sd { rs2, rs1, imm } => out.extend_from_slice(&encode_s_type(0x23, 0b011, *rs1, *rs2, *imm)?.to_le_bytes()), + Instruction::Slli { rd, rs1, shamt } => { + if !(0..=63).contains(shamt) { + return Err(CompileError::new("slli shift amount must be in 0..=63", crate::error::Span::default())); + } + out.extend_from_slice(&encode_i_type(0x13, *rd, 0b001, *rs1, *shamt)?.to_le_bytes()); + } + Instruction::Srai { rd, rs1, shamt } => { + if !(0..=63).contains(shamt) { + return Err(CompileError::new("srai shift amount must be in 0..=63", crate::error::Span::default())); + } + let imm = (0b0100000_i64 << 5) | *shamt; + out.extend_from_slice(&encode_i_type(0x13, *rd, 0b101, *rs1, imm)?.to_le_bytes()); + } + Instruction::Srli { rd, rs1, shamt } => { + if !(0..=63).contains(shamt) { + return Err(CompileError::new("srli shift amount must be in 0..=63", crate::error::Span::default())); + } + out.extend_from_slice(&encode_i_type(0x13, *rd, 0b101, *rs1, *shamt)?.to_le_bytes()); + } + Instruction::Li { rd, imm } => encode_li_sequence(out, *rd, *imm)?, + Instruction::La { rd, label } => encode_address_sequence(out, *rd, pc, parsed.symbol_address(label, layout)?)?, + Instruction::Call { label } => { + let target = parsed.symbol_address(label, layout)?; + encode_call_sequence(out, pc, target)?; + } + Instruction::Jump { label } => { + let target = parsed.symbol_address(label, layout)?; + out.extend_from_slice(&encode_j_type(0x6f, 0, relative_offset(pc, target)?)?.to_le_bytes()); + } + Instruction::Beq { .. } + | Instruction::Bne { .. } + | Instruction::Blt { .. } + | Instruction::Bge { .. } + | Instruction::Bltu { .. } + | Instruction::Bgeu { .. } + | Instruction::Beqz { .. } + | Instruction::Bnez { .. } => { + let (rs1, rs2, label, funct3) = conditional_branch_parts(inst).expect("conditional branch parts"); + let target = parsed.symbol_address(label, layout)?; + if relaxed_branch { + out.extend_from_slice(&encode_b_type(0x63, inverse_branch_funct3(funct3), rs1, rs2, 8)?.to_le_bytes()); + out.extend_from_slice(&encode_j_type(0x6f, 0, relative_offset(pc + 4, target)?)?.to_le_bytes()); + } else { + out.extend_from_slice(&encode_b_type(0x63, funct3, rs1, rs2, relative_offset(pc, target)?)?.to_le_bytes()); + } + } + Instruction::Ret => out.extend_from_slice(&encode_i_type(0x67, 0, 0b000, 1, 0)?.to_le_bytes()), + Instruction::Ecall => out.extend_from_slice(&encode_ecall().to_le_bytes()), + } + Ok(()) +} + +fn encode_li_sequence(out: &mut Vec, rd: u8, imm: i128) -> Result<()> { + if let Some(signed) = li_signed_i64(imm) + && li_fits_lui_addi_rv64(signed) + { + let (hi, lo) = split_hi_lo(signed)?; + out.extend_from_slice(&encode_u_type(0x37, rd, hi).to_le_bytes()); + out.extend_from_slice(&encode_i_type(0x13, rd, 0b000, rd, lo)?.to_le_bytes()); + return Ok(()); + } + encode_large_li_sequence(out, rd, li_bits(imm)?) +} + +fn encode_large_li_sequence(out: &mut Vec, rd: u8, bits: u64) -> Result<()> { + let bytes = bits.to_be_bytes(); + out.extend_from_slice(&encode_i_type(0x13, rd, 0b000, 0, i64::from(bytes[0]))?.to_le_bytes()); + for byte in bytes.iter().skip(1) { + out.extend_from_slice(&encode_i_type(0x13, rd, 0b001, rd, 8)?.to_le_bytes()); + out.extend_from_slice(&encode_i_type(0x13, rd, 0b000, rd, i64::from(*byte))?.to_le_bytes()); + } + Ok(()) +} + +fn li_signed_i64(imm: i128) -> Option { + i64::try_from(imm).ok() +} + +fn li_bits(imm: i128) -> Result { + if imm < i128::from(i64::MIN) || imm > i128::from(u64::MAX) { + return Err(CompileError::new(format!("li immediate '{}' does not fit 64 bits", imm), crate::error::Span::default())); + } + if imm < 0 { + Ok((imm as i64) as u64) + } else { + Ok(imm as u64) + } +} + +fn encode_address_sequence(out: &mut Vec, rd: u8, pc: u64, target: u64) -> Result<()> { + let (hi, lo) = split_hi_lo(relative_offset(pc, target)?)?; + out.extend_from_slice(&encode_u_type(0x17, rd, hi).to_le_bytes()); + out.extend_from_slice(&encode_i_type(0x13, rd, 0b000, rd, lo)?.to_le_bytes()); + Ok(()) +} + +fn encode_call_sequence(out: &mut Vec, pc: u64, target: u64) -> Result<()> { + let (hi, lo) = split_hi_lo(relative_offset(pc, target)?)?; + out.extend_from_slice(&encode_u_type(0x17, 1, hi).to_le_bytes()); + out.extend_from_slice(&encode_i_type(0x67, 1, 0b000, 1, lo)?.to_le_bytes()); + Ok(()) +} + +fn op_size(op: &AsmOp, current_offset: usize, section: SectionKind, op_index: usize, branch_size_mode: BranchSizeMode<'_>) -> usize { + match op { + AsmOp::Label(_) => 0, + AsmOp::Instruction(Instruction::Li { imm, .. }) => li_sequence_size(*imm), + AsmOp::Instruction(Instruction::La { .. }) => 8, + AsmOp::Instruction(Instruction::Call { .. }) => 8, + AsmOp::Instruction( + Instruction::Beq { .. } + | Instruction::Bne { .. } + | Instruction::Blt { .. } + | Instruction::Bge { .. } + | Instruction::Bltu { .. } + | Instruction::Bgeu { .. } + | Instruction::Beqz { .. } + | Instruction::Bnez { .. }, + ) => match branch_size_mode { + BranchSizeMode::Conservative => 8, + BranchSizeMode::Exact(relaxed) if section == SectionKind::Text && relaxed.contains(&op_index) => 8, + BranchSizeMode::Exact(_) => 4, + }, + AsmOp::Instruction(_) => 4, + AsmOp::Word(_) => 4, + AsmOp::Byte(_) => 1, + AsmOp::Ascii(bytes) => bytes.len(), + AsmOp::Align(bytes) => padding_for(current_offset, *bytes), + } +} + +fn li_sequence_size(imm: i128) -> usize { + if li_signed_i64(imm).is_some_and(li_fits_lui_addi_rv64) { + 8 + } else { + 60 + } +} + +fn write_elf_header( + out: &mut [u8], + entry: u64, + program_header_count: u16, + section_header_offset: u64, + section_header_count: u16, + section_name_index: u16, +) -> Result<()> { + if out.len() != ELF_HEADER_SIZE { + return Err(CompileError::new("invalid ELF header buffer size", crate::error::Span::default())); + } + out.fill(0); + out[0..4].copy_from_slice(b"\x7fELF"); + out[4] = 2; + out[5] = 1; + out[6] = 1; + out[16..18].copy_from_slice(&2u16.to_le_bytes()); + out[18..20].copy_from_slice(&243u16.to_le_bytes()); + out[20..24].copy_from_slice(&1u32.to_le_bytes()); + out[24..32].copy_from_slice(&entry.to_le_bytes()); + out[32..40].copy_from_slice(&(ELF_HEADER_SIZE as u64).to_le_bytes()); + out[40..48].copy_from_slice(§ion_header_offset.to_le_bytes()); + out[48..52].copy_from_slice(&0u32.to_le_bytes()); + out[52..54].copy_from_slice(&(ELF_HEADER_SIZE as u16).to_le_bytes()); + out[54..56].copy_from_slice(&(ELF_PROGRAM_HEADER_SIZE as u16).to_le_bytes()); + out[56..58].copy_from_slice(&program_header_count.to_le_bytes()); + out[58..60].copy_from_slice(&(ELF_SECTION_HEADER_SIZE as u16).to_le_bytes()); + out[60..62].copy_from_slice(§ion_header_count.to_le_bytes()); + out[62..64].copy_from_slice(§ion_name_index.to_le_bytes()); + Ok(()) +} + +fn write_section_header( + out: &mut [u8], + name_offset: u32, + section_type: u32, + flags: u64, + address: u64, + offset: u64, + size: u64, + alignment: u64, +) -> Result<()> { + if out.len() != ELF_SECTION_HEADER_SIZE { + return Err(CompileError::new("invalid ELF section header buffer size", crate::error::Span::default())); + } + out.fill(0); + out[0..4].copy_from_slice(&name_offset.to_le_bytes()); + out[4..8].copy_from_slice(§ion_type.to_le_bytes()); + out[8..16].copy_from_slice(&flags.to_le_bytes()); + out[16..24].copy_from_slice(&address.to_le_bytes()); + out[24..32].copy_from_slice(&offset.to_le_bytes()); + out[32..40].copy_from_slice(&size.to_le_bytes()); + out[48..56].copy_from_slice(&alignment.to_le_bytes()); + Ok(()) +} + +fn write_program_header(out: &mut [u8], flags: u32, offset: u64, vaddr: u64, file_size: u64, memory_size: u64) -> Result<()> { + if out.len() != ELF_PROGRAM_HEADER_SIZE { + return Err(CompileError::new("invalid ELF program header buffer size", crate::error::Span::default())); + } + out.fill(0); + out[0..4].copy_from_slice(&1u32.to_le_bytes()); + out[4..8].copy_from_slice(&flags.to_le_bytes()); + out[8..16].copy_from_slice(&offset.to_le_bytes()); + out[16..24].copy_from_slice(&vaddr.to_le_bytes()); + out[24..32].copy_from_slice(&vaddr.to_le_bytes()); + out[32..40].copy_from_slice(&file_size.to_le_bytes()); + out[40..48].copy_from_slice(&memory_size.to_le_bytes()); + out[48..56].copy_from_slice(&(ELF_SEGMENT_ALIGN as u64).to_le_bytes()); + Ok(()) +} + +pub(super) fn strip_comment(line: &str) -> Option<&str> { + let mut in_string = false; + let mut escape = false; + for (idx, ch) in line.char_indices() { + match ch { + '"' if !escape => in_string = !in_string, + '#' if !in_string => return Some(line[..idx].trim()), + '\\' if in_string => { + escape = !escape; + continue; + } + _ => {} + } + escape = false; + } + let trimmed = line.trim(); + (!trimmed.is_empty()).then_some(trimmed) +} + +fn parse_ascii_literal(value: &str) -> Result> { + let Some(inner) = value.strip_prefix('"').and_then(|value| value.strip_suffix('"')) else { + return Err(CompileError::new(format!("invalid .ascii literal '{}'", value), crate::error::Span::default())); + }; + + let mut out = Vec::new(); + let mut chars = inner.chars(); + while let Some(ch) = chars.next() { + if ch != '\\' { + out.extend_from_slice(ch.to_string().as_bytes()); + continue; + } + + let escaped = chars + .next() + .ok_or_else(|| CompileError::new("unterminated escape sequence in .ascii literal", crate::error::Span::default()))?; + match escaped { + 'n' => out.push(b'\n'), + 'r' => out.push(b'\r'), + 't' => out.push(b'\t'), + '\\' => out.push(b'\\'), + '"' => out.push(b'"'), + 'x' => { + let hi = chars + .next() + .ok_or_else(|| CompileError::new("incomplete hex escape in .ascii literal", crate::error::Span::default()))?; + let lo = chars + .next() + .ok_or_else(|| CompileError::new("incomplete hex escape in .ascii literal", crate::error::Span::default()))?; + let hex = format!("{}{}", hi, lo); + let byte = u8::from_str_radix(&hex, 16) + .map_err(|_| CompileError::new(format!("invalid hex escape '\\x{}'", hex), crate::error::Span::default()))?; + out.push(byte); + } + other => { + return Err(CompileError::new( + format!("unsupported escape sequence '\\{}' in .ascii literal", other), + crate::error::Span::default(), + )); + } + } + } + + Ok(out) +} + +fn parse_memory_operand(value: &str) -> Result<(i64, u8)> { + let open = value + .find('(') + .ok_or_else(|| CompileError::new(format!("invalid memory operand '{}'", value), crate::error::Span::default()))?; + let close = value + .rfind(')') + .ok_or_else(|| CompileError::new(format!("invalid memory operand '{}'", value), crate::error::Span::default()))?; + let imm = parse_immediate(value[..open].trim())?; + let rs1 = parse_register(value[open + 1..close].trim())?; + Ok((imm, rs1)) +} + +pub(super) fn memory_operand_offset_and_base(value: &str) -> Option<(i64, &str)> { + let open = value.find('(')?; + let close = value.rfind(')')?; + let offset = parse_immediate(value[..open].trim()).ok()?; + let base = value[open + 1..close].trim(); + (!base.is_empty()).then_some((offset, base)) +} + +pub(super) fn small_signed_immediate(value: i64) -> bool { + (-2048..=2047).contains(&value) +} + +pub(super) fn scratch_register_avoiding(registers: &[&str]) -> &'static str { + for candidate in ["t6", "t5", "t3", "t2", "t1", "t0"] { + let candidate_id = parse_register(candidate).expect("scratch register name should be valid"); + if registers.iter().all(|register| parse_register(register).ok() != Some(candidate_id)) { + return candidate; + } + } + "t6" +} + +pub(super) fn parse_register(name: &str) -> Result { + let reg = match name { + "zero" | "x0" => 0, + "ra" | "x1" => 1, + "sp" | "x2" => 2, + "gp" | "x3" => 3, + "tp" | "x4" => 4, + "t0" | "x5" => 5, + "t1" | "x6" => 6, + "t2" | "x7" => 7, + "s0" | "fp" | "x8" => 8, + "s1" | "x9" => 9, + "a0" | "x10" => 10, + "a1" | "x11" => 11, + "a2" | "x12" => 12, + "a3" | "x13" => 13, + "a4" | "x14" => 14, + "a5" | "x15" => 15, + "a6" | "x16" => 16, + "a7" | "x17" => 17, + "s2" | "x18" => 18, + "s3" | "x19" => 19, + "s4" | "x20" => 20, + "s5" | "x21" => 21, + "s6" | "x22" => 22, + "s7" | "x23" => 23, + "s8" | "x24" => 24, + "s9" | "x25" => 25, + "s10" | "x26" => 26, + "s11" | "x27" => 27, + "t3" | "x28" => 28, + "t4" | "x29" => 29, + "t5" | "x30" => 30, + "t6" | "x31" => 31, + other => return Err(CompileError::new(format!("unknown register '{}'", other), crate::error::Span::default())), + }; + Ok(reg) +} + +pub(super) fn parse_immediate(value: &str) -> Result { + if let Some(hex) = value.strip_prefix("-0x") { + return i64::from_str_radix(hex, 16) + .map(|value| -value) + .map_err(|_| CompileError::new(format!("invalid immediate '{}'", value), crate::error::Span::default())); + } + if let Some(hex) = value.strip_prefix("0x").or_else(|| value.strip_prefix("+0x")) { + return i64::from_str_radix(hex, 16) + .map_err(|_| CompileError::new(format!("invalid immediate '{}'", value), crate::error::Span::default())); + } + value.parse::().map_err(|_| CompileError::new(format!("invalid immediate '{}'", value), crate::error::Span::default())) +} + +fn parse_li_immediate(value: &str) -> Result { + if let Some(hex) = value.strip_prefix("-0x") { + let parsed = i128::from_str_radix(hex, 16) + .map_err(|_| CompileError::new(format!("invalid immediate '{}'", value), crate::error::Span::default()))?; + return validate_li_immediate(-parsed, value); + } + if let Some(hex) = value.strip_prefix("0x").or_else(|| value.strip_prefix("+0x")) { + let parsed = u128::from_str_radix(hex, 16) + .map_err(|_| CompileError::new(format!("invalid immediate '{}'", value), crate::error::Span::default()))?; + if parsed <= u128::from(u64::MAX) { + return Ok(parsed as i128); + } + return Err(CompileError::new(format!("li immediate '{}' does not fit 64 bits", value), crate::error::Span::default())); + } + if value.starts_with('-') { + let parsed = value + .parse::() + .map_err(|_| CompileError::new(format!("invalid immediate '{}'", value), crate::error::Span::default()))?; + validate_li_immediate(parsed, value) + } else { + value + .parse::() + .map_err(|_| CompileError::new(format!("invalid immediate '{}'", value), crate::error::Span::default())) + .and_then(|parsed| { + if parsed <= u128::from(u64::MAX) { + Ok(parsed as i128) + } else { + Err(CompileError::new(format!("li immediate '{}' does not fit 64 bits", value), crate::error::Span::default())) + } + }) + } +} + +fn validate_li_immediate(parsed: i128, source: &str) -> Result { + if (i64::MIN as i128..=u64::MAX as i128).contains(&parsed) { + Ok(parsed) + } else { + Err(CompileError::new(format!("li immediate '{}' does not fit 64 bits", source), crate::error::Span::default())) + } +} + +fn arg(args: &[String], index: usize) -> Result<&str> { + args.get(index) + .map(|value| value.as_str()) + .ok_or_else(|| CompileError::new("malformed assembly instruction", crate::error::Span::default())) +} + +fn encode_r_type(opcode: u32, rd: u8, funct3: u32, rs1: u8, rs2: u8, funct7: u32) -> u32 { + (funct7 << 25) | ((rs2 as u32) << 20) | ((rs1 as u32) << 15) | (funct3 << 12) | ((rd as u32) << 7) | opcode +} + +fn encode_i_type(opcode: u32, rd: u8, funct3: u32, rs1: u8, imm: i64) -> Result { + let imm = encode_signed_bits(imm, 12)?; + Ok((imm << 20) | ((rs1 as u32) << 15) | (funct3 << 12) | ((rd as u32) << 7) | opcode) +} + +fn encode_s_type(opcode: u32, funct3: u32, rs1: u8, rs2: u8, imm: i64) -> Result { + let imm = encode_signed_bits(imm, 12)?; + let imm_lo = imm & 0x1f; + let imm_hi = (imm >> 5) & 0x7f; + Ok((imm_hi << 25) | ((rs2 as u32) << 20) | ((rs1 as u32) << 15) | (funct3 << 12) | (imm_lo << 7) | opcode) +} + +fn encode_b_type(opcode: u32, funct3: u32, rs1: u8, rs2: u8, imm: i64) -> Result { + if imm % 2 != 0 { + return Err(CompileError::new("branch target is not 2-byte aligned", crate::error::Span::default())); + } + let imm = encode_signed_bits(imm, 13)?; + let bit12 = (imm >> 12) & 0x1; + let bits10_5 = (imm >> 5) & 0x3f; + let bits4_1 = (imm >> 1) & 0xf; + let bit11 = (imm >> 11) & 0x1; + Ok((bit12 << 31) + | (bits10_5 << 25) + | ((rs2 as u32) << 20) + | ((rs1 as u32) << 15) + | (funct3 << 12) + | (bits4_1 << 8) + | (bit11 << 7) + | opcode) +} + +fn encode_u_type(opcode: u32, rd: u8, imm: i64) -> u32 { + (((imm as i32 as u32) & 0x000f_ffff) << 12) | ((rd as u32) << 7) | opcode +} + +fn encode_j_type(opcode: u32, rd: u8, imm: i64) -> Result { + if imm % 2 != 0 { + return Err(CompileError::new("jump target is not 2-byte aligned", crate::error::Span::default())); + } + let imm = encode_signed_bits(imm, 21)?; + let bit20 = (imm >> 20) & 0x1; + let bits10_1 = (imm >> 1) & 0x3ff; + let bit11 = (imm >> 11) & 0x1; + let bits19_12 = (imm >> 12) & 0xff; + Ok((bit20 << 31) | (bits10_1 << 21) | (bit11 << 20) | (bits19_12 << 12) | ((rd as u32) << 7) | opcode) +} + +fn encode_ecall() -> u32 { + 0x0000_0073 +} + +fn encode_signed_bits(value: i64, bits: u32) -> Result { + if !signed_bits_fit(value, bits) { + return Err(CompileError::new( + format!("immediate '{}' does not fit {}-bit signed field", value, bits), + crate::error::Span::default(), + )); + } + Ok((value as i32 as u32) & ((1u32 << bits) - 1)) +} + +fn signed_bits_fit(value: i64, bits: u32) -> bool { + let min = -(1i64 << (bits - 1)); + let max = (1i64 << (bits - 1)) - 1; + value >= min && value <= max +} + +fn split_hi_lo(value: i64) -> Result<(i64, i64)> { + if !li_fits_lui_addi_rv64(value) { + return Err(CompileError::new( + format!("value '{}' is outside the supported RV64 LUI/ADDI immediate range", value), + crate::error::Span::default(), + )); + } + let adjusted = value.checked_add(0x800).ok_or_else(|| { + CompileError::new(format!("value '{}' overflowed while splitting its immediate", value), crate::error::Span::default()) + })?; + let hi = adjusted >> 12; + let lo = value - (hi << 12); + if !(-2048..=2047).contains(&lo) { + return Err(CompileError::new(format!("low immediate '{}' is out of range after split", lo), crate::error::Span::default())); + } + Ok((hi, lo)) +} + +fn li_fits_lui_addi_rv64(value: i64) -> bool { + if !(i32::MIN as i64..=i32::MAX as i64).contains(&value) { + return false; + } + let hi = (value + 0x800) >> 12; + (-0x80000..=0x7ffff).contains(&hi) +} + +fn relative_offset(pc: u64, target: u64) -> Result { + i64::try_from(target as i128 - pc as i128) + .map_err(|_| CompileError::new("relative offset overflowed i64", crate::error::Span::default())) +} + +pub(super) fn align_up(value: usize, align: usize) -> usize { + if align <= 1 { + return value; + } + (value + align - 1) & !(align - 1) +} + +pub(super) fn align_frame(value: usize) -> usize { + align_up(value.max(16), 16) +} + +pub(super) fn is_min_call(func: &str) -> bool { + matches!(func, "min" | "math_min" | "__math_min") +} + +pub(super) fn is_void_runtime_requirement_call(func: &str) -> bool { + matches!( + func, + "__ckb_require_maturity" + | "__ckb_require_time" + | "__ckb_require_epoch_after" + | "__ckb_require_epoch_relative" + | "__ckb_require_cell_lock_hash" + | "__ckb_require_cell_type_hash" + | "__ckb_require_current_script_args_empty" + | "__ckb_require_cell_lock_args_empty" + | "__ckb_require_cell_type_args_empty" + | "__ckb_require_cell_lock_args_hash" + | "__ckb_require_cell_type_args_hash" + | "__ckb_require_cell_lock_args_prefix_hash" + | "__ckb_require_cell_type_args_prefix_hash" + | "__ckb_require_cell_lock_args_suffix_hash" + | "__ckb_require_cell_type_args_suffix_hash" + | "__ckb_require_cell_lock_script_hash_type" + | "__ckb_require_cell_type_script_hash_type" + | "__ckb_require_input_out_point_tx_hash" + | "__ckb_require_input_out_point" + | "__ckb_require_metapoint_relative" + | "__ckb_require_lock_type_metapoint_pairs" + | "__ckb_require_type_lock_metapoint_pairs" + | "__ckb_require_lock_type_metapoint_pairs_from_i32_data" + | "__ckb_require_type_lock_metapoint_pairs_from_i32_data" + | "__ckb_require_lock_type_metapoint_pairs_from_i32_data_filtered" + | "__ckb_require_type_lock_metapoint_pairs_from_i32_data_filtered" + | "__ckb_require_lock_match_master_out_point_pairs_from_data" + | "__dao_require_header_dep_for_input" + | "__dao_require_input_since_at_least" + | "__dao_require_input_relative_epoch_since_at_least" + | "__xudt_require_owner_mode_input_type" + | "__xudt_require_owner_mode_type_args" + | "__xudt_require_owner_mode_type_args_current_script" + | "__cellscript_require_fungible_type_group_v1" + | "__xudt_require_group_amount_conserved" + | "__xudt_require_group_amount_minted" + | "__xudt_require_group_amount_burned" + | "__c256_require_u128_product_lte" + | "__c256_require_u128_product_eq" + | "__c256_require_u128_sum2_products_lte" + | "__c256_require_u128_sum2_products_eq" + | "__ckb_require_witness_size_at_least" + ) +} + +pub(super) fn is_runtime_scalar_failclosed_call(func: &str) -> bool { + matches!( + func, + "__ckb_source_input" + | "__ckb_source_output" + | "__ckb_source_cell_dep" + | "__ckb_source_header_dep" + | "__ckb_source_group_input" + | "__ckb_source_group_output" + | "__ckb_since_epoch_absolute" + | "__ckb_since_epoch_relative" + | "__ckb_current_role" + | "__ckb_cell_capacity" + | "__ckb_cell_occupied_capacity" + | "__ckb_cell_unoccupied_capacity" + | "__ckb_cell_output_index" + | "__ckb_cell_data_size" + | "__ckb_cell_data_u32_le" + | "__ckb_cell_data_u64_le" + | "__ckb_cell_lock_hash_type" + | "__ckb_cell_type_hash_type" + | "__ckb_cell_lock_args_empty" + | "__ckb_cell_type_args_empty" + | "__dao_accumulated_rate" + | "__dao_input_accumulated_rate" + | "__dao_has_dao_type" + | "__dao_is_deposit_data" + | "__dao_is_withdrawal_request_data" + | "__xudt_amount_low" + | "__xudt_amount_high" + | "__xudt_owner_mode_input_type_hash" + | "__ckb_witness_size" + ) +} + +pub(super) fn is_runtime_header_u64_call(func: &str) -> bool { + matches!( + func, + "__env_current_timepoint" + | "__ckb_header_epoch_number" + | "__ckb_header_epoch_start_block_number" + | "__ckb_header_epoch_length" + | "__ckb_input_since" + ) +} + +pub(super) fn ckb_source_name(source: u64) -> &'static str { + match source { + CKB_SOURCE_INPUT => "Input", + CKB_SOURCE_OUTPUT => "Output", + CKB_SOURCE_CELL_DEP => "CellDep", + CKB_SOURCE_HEADER_DEP => "HeaderDep", + CKB_SOURCE_GROUP_INPUT => "GroupInput", + CKB_SOURCE_GROUP_OUTPUT => "GroupOutput", + source if source == (CKB_SOURCE_GROUP_FLAG | CKB_SOURCE_INPUT) => "GroupInput", + source if source == (CKB_SOURCE_GROUP_FLAG | CKB_SOURCE_OUTPUT) => "GroupOutput", + source if source == (CKB_SOURCE_GROUP_FLAG | CKB_SOURCE_CELL_DEP) => "GroupCellDep", + source if source == (CKB_SOURCE_GROUP_FLAG | CKB_SOURCE_HEADER_DEP) => "GroupHeaderDep", + _ => "Unknown", + } +} + +fn padding_for(offset: usize, align: usize) -> usize { + align_up(offset, align) - offset +} + +fn pad_to_alignment(out: &mut Vec, align: usize) { + let pad = padding_for(out.len(), align); + out.resize(out.len() + pad, 0); +} + +#[cfg(test)] +mod tests { + use super::*; + + const SUPPORTED_INTERNAL_ASSEMBLER_MNEMONICS: &[(&str, &str)] = &[ + ("add", "add t0, a0, a1"), + ("addi", "addi t0, t0, -1"), + ("and", "and t2, a0, a1"), + ("beq", "beq a0, a1, branch_target"), + ("bge", "bge a0, a1, branch_target"), + ("bgeu", "bgeu a0, a1, branch_target"), + ("bgez", "bgez a0, branch_target"), + ("bgt", "bgt a0, a1, branch_target"), + ("blt", "blt a1, a0, branch_target"), + ("bltu", "bltu a1, a0, branch_target"), + ("bne", "bne a0, a1, branch_target"), + ("bnez", "bnez a0, branch_target"), + ("beqz", "beqz a0, branch_target"), + ("call", "call helper"), + ("div", "div t5, a0, a1"), + ("divu", "divu t5, a0, a1"), + ("ecall", "ecall"), + ("j", "j done"), + ("la", "la t3, data_label"), + ("lbu", "lbu t2, 8(sp)"), + ("ld", "ld t1, 0(sp)"), + ("li", "li a0, 8"), + ("mul", "mul t4, a0, a1"), + ("mv", "mv s9, a0"), + ("neg", "neg s6, a0"), + ("or", "or t3, a0, a1"), + ("rem", "rem t6, a0, a1"), + ("remu", "remu t6, a0, a1"), + ("ret", "ret"), + ("sb", "sb t1, 8(sp)"), + ("sd", "sd t0, 0(sp)"), + ("seqz", "seqz s4, a0"), + ("sgt", "sgt s2, a0, a1"), + ("sh", "sh t1, 10(sp)"), + ("slli", "slli s7, a0, 3"), + ("slt", "slt s0, a1, a0"), + ("sltu", "sltu s1, a1, a0"), + ("snez", "snez s5, a0"), + ("srai", "srai a0, a0, 1"), + ("srli", "srli s8, a0, 1"), + ("sub", "sub t1, a0, a1"), + ("sw", "sw t1, 12(sp)"), + ("xor", "xor a0, a0, a1"), + ("xori", "xori s3, a0, 1"), + ]; + + const INTENTIONALLY_UNSUPPORTED_INTERNAL_ASSEMBLER_MNEMONICS: &[(&str, &str)] = &[ + ("addiw", "addiw a0, a0, 1"), + ("addw", "addw a0, a0, a1"), + ("andi", "andi a0, a0, 1"), + ("amoadd.w", "amoadd.w a0, a1, (a2)"), + ("auipc", "auipc a0, 0"), + ("ble", "ble a0, a1, target"), + ("bleu", "bleu a0, a1, target"), + ("blez", "blez a0, target"), + ("bgtu", "bgtu a0, a1, target"), + ("bgtz", "bgtz a0, target"), + ("bltz", "bltz a0, target"), + ("c.nop", "c.nop"), + ("csrr", "csrr a0, cycle"), + ("fence", "fence"), + ("flw", "flw fa0, 0(sp)"), + ("jal", "jal ra, target"), + ("jalr", "jalr zero, 0(ra)"), + ("jr", "jr ra"), + ("lb", "lb a0, 0(sp)"), + ("lh", "lh a0, 0(sp)"), + ("lhu", "lhu a0, 0(sp)"), + ("lui", "lui a0, 1"), + ("lw", "lw a0, 0(sp)"), + ("lwu", "lwu a0, 0(sp)"), + ("nop", "nop"), + ("not", "not a0, a1"), + ("ori", "ori a0, a0, 1"), + ("sll", "sll a0, a0, a1"), + ("slti", "slti a0, a0, 1"), + ("sltiu", "sltiu a0, a0, 1"), + ("sra", "sra a0, a0, a1"), + ("srl", "srl a0, a0, a1"), + ("subw", "subw a0, a0, a1"), + ("tail", "tail target"), + ]; + + #[derive(Debug)] + struct TestProgramHeader { + p_type: u32, + flags: u32, + offset: u64, + vaddr: u64, + file_size: u64, + memory_size: u64, + } + + fn read_u16_le(bytes: &[u8], offset: usize) -> u16 { + let mut raw = [0u8; 2]; + raw.copy_from_slice(&bytes[offset..offset + 2]); + u16::from_le_bytes(raw) + } + + fn read_u32_le(bytes: &[u8], offset: usize) -> u32 { + let mut raw = [0u8; 4]; + raw.copy_from_slice(&bytes[offset..offset + 4]); + u32::from_le_bytes(raw) + } + + fn read_u64_le(bytes: &[u8], offset: usize) -> u64 { + let mut raw = [0u8; 8]; + raw.copy_from_slice(&bytes[offset..offset + 8]); + u64::from_le_bytes(raw) + } + + fn elf_program_headers(elf: &[u8]) -> Vec { + assert!(elf.starts_with(b"\x7fELF"), "expected ELF magic"); + let phoff = usize::try_from(read_u64_le(elf, 32)).expect("program header offset should fit usize"); + let phentsize = usize::from(read_u16_le(elf, 54)); + let phnum = usize::from(read_u16_le(elf, 56)); + assert_eq!(phentsize, ELF_PROGRAM_HEADER_SIZE); + + (0..phnum) + .map(|index| { + let offset = phoff + index * phentsize; + TestProgramHeader { + p_type: read_u32_le(elf, offset), + flags: read_u32_le(elf, offset + 4), + offset: read_u64_le(elf, offset + 8), + vaddr: read_u64_le(elf, offset + 16), + file_size: read_u64_le(elf, offset + 32), + memory_size: read_u64_le(elf, offset + 40), + } + }) + .collect() + } + + fn elf_text_file_offset(elf: &[u8]) -> usize { + let header = elf_program_headers(elf) + .into_iter() + .find(|header| header.p_type == 1 && header.flags & ELF_PF_X != 0) + .expect("ELF should contain an executable load segment"); + let offset_into_segment = ELF_BASE_ADDR.checked_sub(header.vaddr).expect("text base should be inside load segment"); + usize::try_from(header.offset + offset_into_segment).expect("text file offset should fit usize") + } + + #[test] + fn strict_audit_internal_elf_entry_preserves_ckb_stack_pointer() { + let lines = vec![".section .text".to_string(), ".global entry".to_string(), "entry:".to_string(), "ret".to_string()]; + + let elf = assemble_elf_internal(&lines).expect("internal assembler should emit a CKB-loadable ELF"); + let headers = elf_program_headers(&elf); + assert_eq!(headers.len(), 1, "internal CKB ELF should expose one load segment"); + assert_eq!(headers[0].flags, ELF_PF_R | ELF_PF_X, "code segment should be readable and executable only"); + assert_eq!(headers[0].flags & ELF_PF_W, 0, "code segment must not be writable"); + assert_eq!(headers[0].file_size, headers[0].memory_size, "code segment should not fake stack memory in PT_LOAD"); + + let text_offset = elf_text_file_offset(&elf); + let trampoline = (0..START_TRAMPOLINE_SIZE / 4).map(|index| read_u32_le(&elf, text_offset + index * 4)).collect::>(); + assert_eq!(trampoline, vec![0x0000_0097, 0x0140_80e7, 0x0000_08b7, 0x05d8_8893, 0x0000_0073]); + assert!(trampoline[..4].iter().all(|instruction| (instruction >> 7) & 0x1f != 2), "trampoline must not write sp"); + + let entry_instruction = read_u32_le(&elf, text_offset + START_TRAMPOLINE_SIZE); + assert_eq!(entry_instruction, 0x0000_8067, "entry body should start after the 20-byte trampoline"); + } + + #[test] + fn internal_assembler_relaxes_out_of_range_conditional_branch() { + let mut lines = vec![ + ".section .text".to_string(), + ".global entry".to_string(), + "entry:".to_string(), + "li a0, 0".to_string(), + "beqz a0, far_target".to_string(), + ]; + for _ in 0..1500 { + lines.push("addi t0, t0, 0".to_string()); + } + lines.push("far_target:".to_string()); + lines.push("ret".to_string()); + + let elf = assemble_elf_internal(&lines).expect("internal assembler should relax long conditional branches"); + assert!(elf.starts_with(b"\x7fELF")); + } + + #[test] + fn internal_assembler_encodes_register_conditional_branches() { + for mnemonic in ["beq", "bne", "blt", "bge", "bltu", "bgeu"] { + let lines = vec![ + ".section .text".to_string(), + ".global entry".to_string(), + "entry:".to_string(), + "li a0, 1".to_string(), + "li a1, 1".to_string(), + format!("{} a0, a1, target", mnemonic), + "li a0, 2".to_string(), + "target:".to_string(), + "ret".to_string(), + ]; + + let elf = assemble_elf_internal(&lines).unwrap_or_else(|err| panic!("internal assembler should encode {mnemonic}: {err}")); + assert!(elf.starts_with(b"\x7fELF"), "expected ELF output for {mnemonic}"); + } + } + + #[test] + fn internal_assembler_encodes_emitted_instruction_surface() { + let lines = supported_instruction_surface_lines(); + + let elf = assemble_elf_internal(&lines).expect("internal assembler should encode the emitted instruction surface"); + assert!(elf.starts_with(b"\x7fELF")); + } + + #[test] + fn internal_assembler_rejects_intentionally_unsupported_mnemonics() { + for (mnemonic, instruction) in INTENTIONALLY_UNSUPPORTED_INTERNAL_ASSEMBLER_MNEMONICS { + let lines = vec![ + ".section .text".to_string(), + ".global entry".to_string(), + "entry:".to_string(), + (*instruction).to_string(), + "target:".to_string(), + "ret".to_string(), + ]; + let err = match assemble_elf_internal(&lines) { + Ok(_) => panic!("internal assembler unexpectedly accepted unsupported mnemonic {mnemonic}"), + Err(err) => err, + }; + assert!( + err.message.contains("unsupported assembly instruction"), + "unexpected error for unsupported mnemonic {mnemonic}: {err}" + ); + } + } + + #[test] + fn generated_public_assembly_mnemonics_are_declared() { + let surfaces = [ + ("stdlib", crate::stdlib::StdLib::generate_assembly()), + ("collections", crate::stdlib::collections::Collections::generate_assembly()), + ]; + let supported = SUPPORTED_INTERNAL_ASSEMBLER_MNEMONICS.iter().map(|(mnemonic, _)| *mnemonic).collect::>(); + let mut undeclared = Vec::new(); + + for (surface, assembly) in surfaces { + for (line_number, mnemonic) in emitted_mnemonics(&assembly).into_iter() { + if !supported.contains(mnemonic.as_str()) { + undeclared.push(format!("{surface}:{line_number}: {mnemonic}")); + } + } + } + + assert!( + undeclared.is_empty(), + "generated public assembly used mnemonics outside the declared internal assembler surface:\n{}", + undeclared.join("\n") + ); + } + + #[test] + fn bundled_example_codegen_mnemonics_are_declared() { + let examples = ["amm_pool.cell", "launch.cell", "multisig.cell", "nft.cell", "timelock.cell", "token.cell", "vesting.cell"]; + let supported = SUPPORTED_INTERNAL_ASSEMBLER_MNEMONICS.iter().map(|(mnemonic, _)| *mnemonic).collect::>(); + let mut undeclared = Vec::new(); + + for example in examples { + let path = camino::Utf8PathBuf::from(format!("{}/examples/{}", env!("CARGO_MANIFEST_DIR"), example)); + let result = crate::compile_file( + path, + crate::CompileOptions { target: Some("riscv64-asm".to_string()), ..crate::CompileOptions::default() }, + ) + .unwrap_or_else(|err| panic!("{example} should compile to assembly: {}", err.message)); + let assembly = std::str::from_utf8(&result.artifact_bytes) + .unwrap_or_else(|err| panic!("{example} emitted invalid utf-8 assembly: {err}")); + + for (line_number, mnemonic) in emitted_mnemonics(assembly).into_iter() { + if !supported.contains(mnemonic.as_str()) { + undeclared.push(format!("{example}:{line_number}: {mnemonic}")); + } + } + } + + assert!( + undeclared.is_empty(), + "bundled examples used mnemonics outside the declared internal assembler surface:\n{}", + undeclared.join("\n") + ); + } + + fn supported_instruction_surface_lines() -> Vec { + let mut lines = vec![".section .text".to_string(), ".global entry".to_string(), "entry:".to_string(), "li a1, 4".to_string()]; + for (mnemonic, instruction) in SUPPORTED_INTERNAL_ASSEMBLER_MNEMONICS { + if !matches!(*mnemonic, "ecall" | "ret") { + lines.push((*instruction).to_string()); + } + } + lines.extend([ + "branch_target:".to_string(), + "ecall".to_string(), + "helper:".to_string(), + "ret".to_string(), + "done:".to_string(), + "ret".to_string(), + ".section .rodata".to_string(), + "data_label:".to_string(), + ".word 7".to_string(), + ".byte 1".to_string(), + ".ascii \"x\"".to_string(), + ".align 3".to_string(), + ]); + lines + } + + fn emitted_mnemonics(assembly: &str) -> Vec<(usize, String)> { + assembly + .lines() + .enumerate() + .filter_map(|(index, line)| { + let clean = strip_comment(line)?; + if clean.is_empty() || clean.starts_with('.') || clean.ends_with(':') { + return None; + } + let mnemonic = clean.split_whitespace().next()?.trim_end_matches(','); + Some((index + 1, mnemonic.to_string())) + }) + .collect() + } + + #[test] + fn internal_assembler_encodes_full_width_li_literals() { + let lines = vec![ + ".section .text".to_string(), + ".global entry".to_string(), + "entry:".to_string(), + "li a0, 9223372036854775808".to_string(), + "li a1, 18446744073709551615".to_string(), + "ret".to_string(), + ]; + + let elf = assemble_elf_internal(&lines).expect("internal assembler should encode u64-width li literals"); + assert!(elf.starts_with(b"\x7fELF")); + } + + #[test] + fn li_parser_enforces_the_complete_64_bit_domain() { + assert_eq!(parse_li_immediate("-0x8000000000000000").unwrap(), i64::MIN as i128); + assert_eq!(parse_li_immediate("+0xffffffffffffffff").unwrap(), u64::MAX as i128); + for value in ["-0x8000000000000001", "-9223372036854775809", "0x10000000000000000", "18446744073709551616"] { + let error = parse_li_immediate(value).expect_err("out-of-domain li literal must fail during parsing"); + assert!(error.message.contains("does not fit 64 bits"), "unexpected error for {value}: {}", error.message); + } + } + + #[test] + fn signed_immediate_parser_accepts_explicit_plus() { + assert_eq!(parse_immediate("+12").unwrap(), 12); + assert_eq!(parse_immediate("+0x7ff").unwrap(), 0x7ff); + } + + #[test] + fn split_hi_lo_rejects_extreme_values_before_arithmetic() { + assert!(split_hi_lo(i64::MIN).is_err()); + assert!(split_hi_lo(i64::MAX).is_err()); + assert_eq!(split_hi_lo(i32::MIN as i64).unwrap(), (-0x80000, 0)); + assert_eq!(split_hi_lo(0x7fff_f7ff).unwrap(), (0x7ffff, 0x7ff)); + assert!(split_hi_lo(0x7fff_f800).is_err()); + } + + #[test] + fn runtime_expression_temp_offsets_are_explicitly_bounded() { + let mut generator = CodeGenerator::new(CodegenOptions::default()); + generator.frame_size = RUNTIME_EXPR_TEMP_SIZE + RUNTIME_SCRATCH_SIZE + 16; + assert!(generator.checked_runtime_expr_temp_offset(0).is_some()); + assert!(generator.checked_runtime_expr_temp_offset(RUNTIME_EXPR_TEMP_SLOTS - 1).is_some()); + assert_eq!(generator.checked_runtime_expr_temp_offset(RUNTIME_EXPR_TEMP_SLOTS), None); + assert_eq!(generator.checked_runtime_expr_temp_offset(usize::MAX), None); + } + + #[test] + fn rv64_li_boundary_values_materialize_correct_bits() { + let cases = [(0x7fff_f7ffi128, 8usize), (0x7fff_f800i128, 60usize), (0x7fff_ffffi128, 60usize), (0x8000_0000i128, 60usize)]; + + for (value, expected_size) in cases { + let mut bytes = Vec::new(); + encode_li_sequence(&mut bytes, 10, value).expect("li should encode"); + assert_eq!(bytes.len(), expected_size, "unexpected li size for {value:#x}"); + assert_eq!(simulate_li_sequence(&bytes, 10), value as u64, "li materialized wrong bits for {value:#x}"); + } + } + + fn simulate_li_sequence(bytes: &[u8], register: usize) -> u64 { + let mut regs = [0u64; 32]; + for chunk in bytes.chunks_exact(4) { + let inst = u32::from_le_bytes(chunk.try_into().expect("instruction chunk should be four bytes")); + let opcode = inst & 0x7f; + let rd = ((inst >> 7) & 0x1f) as usize; + let funct3 = (inst >> 12) & 0x7; + let rs1 = ((inst >> 15) & 0x1f) as usize; + match (opcode, funct3) { + (0x37, _) => { + regs[rd] = ((inst & 0xffff_f000) as i32 as i64) as u64; + } + (0x13, 0b000) => { + let imm = sign_extend(inst >> 20, 12); + regs[rd] = regs[rs1].wrapping_add(imm as u64); + } + (0x13, 0b001) => { + let shamt = (inst >> 20) & 0x3f; + regs[rd] = regs[rs1] << shamt; + } + _ => panic!("unexpected instruction in li sequence: 0x{inst:08x}"), + } + regs[0] = 0; + } + regs[register] + } + + fn sign_extend(value: u32, bits: u32) -> i64 { + let shift = 64 - bits; + ((u64::from(value) << shift) as i64) >> shift + } + + #[test] + fn stack_pointer_offsets_are_emitted_through_helpers() { + let implementation = include_str!("mod.rs") + .split("\n fn emit_runtime_ckb_v014_surface_helpers") + .next() + .expect("source should contain runtime helper boundary"); + let offenders = implementation + .lines() + .enumerate() + .filter_map(|(index, line)| { + let emits_stack_memory = + (line.contains("self.emit(format!(") || line.contains("self.emit(\"")) && line.contains("(sp)"); + let emits_stack_addi = + (line.contains("self.emit(\"addi ") || line.contains("self.emit(format!(\"addi ")) && line.contains(", sp,"); + let allowed_stack_memory = line.contains("self.emit(format!(\"{} {}, {}(sp)\", opcode, register, offset))"); + let allowed_outgoing_stack_memory = line.contains("self.emit(format!(\"sd {}, {}(sp)\", register, offset))"); + let allowed_stack_addi = line.contains("self.emit(format!(\"addi {}, sp, {}\", rd, offset))"); + ((emits_stack_memory && !allowed_stack_memory && !allowed_outgoing_stack_memory) + || (emits_stack_addi && !allowed_stack_addi)) + .then(|| format!("{}: {}", index + 1, line.trim())) + }) + .collect::>(); + + assert!(offenders.is_empty(), "stack pointer accesses must go through stack helpers:\n{}", offenders.join("\n")); + } + + #[test] + fn large_addi_avoids_clobbering_source_register() { + let mut generator = CodeGenerator::new(CodegenOptions::default()); + generator.emit_large_addi("t0", "t6", 2048); + generator.emit_large_addi("t6", "t6", 4096); + + assert_eq!(generator.assembly, vec![" li t5, 2048", " add t0, t6, t5", " li t5, 4096", " add t6, t6, t5",]); + } + + #[test] + fn sp_addi_large_offsets_clobber_only_destination_register() { + let mut generator = CodeGenerator::new(CodegenOptions::default()); + generator.emit_sp_addi("t4", 4096); + generator.emit_sp_addi("t6", 8192); + + assert_eq!(generator.assembly, vec![" li t4, 4096", " add t4, sp, t4", " li t6, 8192", " add t6, sp, t6",]); + } + + #[test] + fn state_transition_edges_use_explicit_consumed_binding() { + let mut generator = CodeGenerator::new(CodegenOptions::default()); + generator.consume_order = vec![1, 2]; + generator.consume_type_names.insert(1, "Offer".to_string()); + generator.consume_type_names.insert(2, "Offer".to_string()); + generator.consume_binding_ids.insert("left".to_string(), 1); + generator.consume_binding_ids.insert("right".to_string(), 2); + + let state_edge = IrStateTransitionEdge { + input_binding: Some("right".to_string()), + output_binding: None, + type_name: "Offer".to_string(), + field_name: "state".to_string(), + from: "Live".to_string(), + to: "Filled".to_string(), + from_index: 1, + to_index: 2, + }; + + assert_eq!(generator.consumed_var_for_state_transition("Offer", &[state_edge]), Some(2)); + } + + #[test] + fn consumed_schema_params_use_loaded_cell_size_for_field_checks() { + let mut generator = CodeGenerator::new(CodegenOptions::default()); + let binding = IrVar { id: 0, name: "auth".to_string(), ty: IrType::Named("MintAuthority".to_string()) }; + let params = vec![IrParam { + name: "auth".to_string(), + ty: binding.ty.clone(), + is_mut: false, + is_ref: false, + is_read_ref: false, + source: ParamSource::Default, + binding: binding.clone(), + }]; + let body = IrBody { + consume_set: vec![CellPattern { + operation: "input".to_string(), + type_hash: None, + binding: "auth".to_string(), + fields: Vec::new(), + }], + read_refs: Vec::new(), + create_set: Vec::new(), + mutate_set: Vec::new(), + write_intents: Vec::new(), + bounded_collection_ops: Vec::new(), + borrow_regions: Vec::new(), + blocks: Vec::new(), + }; + + generator.prepare_function_layout(&body, ¶ms); + + let loaded_size_offset = + generator.cell_buffer_size_offsets.get(&binding.id).copied().expect("consumed input should have size slot"); + assert_eq!(generator.schema_pointer_size_offsets.get(&binding.id), Some(&loaded_size_offset)); + } + + #[test] + fn unaligned_scalar_load_large_offsets_preserve_live_accumulator() { + let mut generator = CodeGenerator::new(CodegenOptions::default()); + generator.emit_unaligned_scalar_load("t4", "t6", "t2", 2048, 2); + + assert_eq!( + generator.assembly, + vec![ + " li t6, 0", + " li t5, 2048", + " add t5, t4, t5", + " lbu t2, 0(t5)", + " or t6, t6, t2", + " li t5, 2049", + " add t5, t4, t5", + " lbu t2, 0(t5)", + " slli t2, t2, 8", + " or t6, t6, t2", + ] + ); + } + + #[test] + fn generated_large_offsets_are_normalized_before_assembly() { + let mut generator = CodeGenerator::new(CodegenOptions::default()); + generator.emit("sd t0, 2048(sp)"); + generator.emit("ld t6, 2056(sp)"); + generator.emit("lbu t2, 2048(t4)"); + generator.emit("addi t0, t4, 2048"); + generator.emit("sb t0, 4096(t6)"); + + assert_eq!( + generator.assembly, + vec![ + " li t6, 2048", + " add t6, sp, t6", + " sd t0, 0(t6)", + " li t5, 2056", + " add t5, sp, t5", + " ld t6, 0(t5)", + " li t6, 2048", + " add t6, t4, t6", + " lbu t2, 0(t6)", + " li t6, 2048", + " add t0, t4, t6", + " li t5, 4096", + " add t5, t6, t5", + " sb t0, 0(t5)", + ] + ); + } + + #[test] + fn read_ref_runtime_fallback_records_cell_buffer_state() { + let mut generator = CodeGenerator::new(CodegenOptions::default()); + generator.frame_size = align_frame(RUNTIME_EXPR_TEMP_SIZE + RUNTIME_SCRATCH_SIZE + 16); + let dest = IrVar { id: 42, name: "cfg".to_string(), ty: IrType::Named("Config".to_string()) }; + generator.read_ref_indices.insert(dest.id, 0); + + generator.emit_read_ref(&dest, "Config").expect("read_ref fallback should lower"); + + let size_offset = generator.runtime_scratch_size_offset(); + let buffer_offset = generator.runtime_scratch_buffer_offset(); + assert_eq!(generator.schema_pointer_size_offsets.get(&dest.id), Some(&size_offset)); + assert_eq!(generator.cell_buffer_size_offsets.get(&dest.id), Some(&size_offset)); + assert_eq!(generator.cell_buffer_offsets.get(&dest.id), Some(&buffer_offset)); + } + + #[test] + fn generated_stdlib_assembly_is_internal_assembler_clean() { + let lines = crate::stdlib::StdLib::generate_assembly().lines().map(|line| line.to_string()).collect::>(); + + let elf = assemble_elf_internal(&lines).expect("generated stdlib assembly should assemble internally"); + assert!(elf.starts_with(b"\x7fELF")); + } + + #[test] + fn generated_collection_assembly_is_internal_assembler_clean() { + let lines = + crate::stdlib::collections::Collections::generate_assembly().lines().map(|line| line.to_string()).collect::>(); + + let elf = assemble_elf_internal(&lines).expect("generated collection assembly should assemble internally"); + assert!(elf.starts_with(b"\x7fELF")); + } + + #[test] + fn internal_assembler_rejects_unresolved_call_targets() { + let lines = vec![".section .text".to_string(), ".global main".to_string(), "main:".to_string(), "call missing".to_string()]; + let err = assemble_elf_internal(&lines).unwrap_err(); + + assert!(err.message.contains("unknown assembly label 'missing'"), "unexpected error: {}", err.message); + } + + #[test] + fn elf_assembly_classifies_unresolved_symbols() { + let lines = vec![".section .text".to_string(), ".global main".to_string(), "main:".to_string(), "call missing".to_string()]; + let err = assemble_generated_elf(&lines).unwrap_err(); + + assert_eq!(err.code.as_deref(), Some("E2200")); + assert!(err.message.contains("unresolved call target"), "unexpected error: {}", err.message); + } + + #[test] + fn internal_assembler_relaxes_out_of_range_register_conditional_branch() { + for mnemonic in ["beq", "bne", "blt", "bge", "bltu", "bgeu"] { + let mut lines = vec![ + ".section .text".to_string(), + ".global entry".to_string(), + "entry:".to_string(), + "li a0, 0".to_string(), + "li a1, 0".to_string(), + format!("{} a0, a1, far_target", mnemonic), + ]; + for _ in 0..1500 { + lines.push("addi t0, t0, 0".to_string()); + } + lines.push("far_target:".to_string()); + lines.push("ret".to_string()); + + let plan = MachineLayoutPlan::build(&lines).unwrap_or_else(|err| panic!("machine layout should relax {mnemonic}: {err}")); + assert_eq!(plan.metrics.relaxed_branch_count, 1, "expected one relaxed branch for {mnemonic}"); + let elf = assemble_elf_internal(&lines).unwrap_or_else(|err| panic!("internal assembler should relax {mnemonic}: {err}")); + assert!(elf.starts_with(b"\x7fELF"), "expected ELF output for relaxed {mnemonic}"); + } + } + + #[test] + fn machine_layout_plan_reports_branch_relaxation_metrics() { + let mut lines = vec![ + ".section .text".to_string(), + ".global entry".to_string(), + "entry:".to_string(), + "li a0, 0".to_string(), + "beqz a0, far_target".to_string(), + ]; + for _ in 0..1500 { + lines.push("addi t0, t0, 0".to_string()); + } + lines.push("far_target:".to_string()); + lines.push("ret".to_string()); + + let plan = MachineLayoutPlan::build(&lines).expect("machine layout plan"); + assert_eq!(plan.metrics.relaxed_branch_count, 1); + assert!( + plan.metrics.max_cond_branch_abs_distance > 4096, + "synthetic branch should exceed RV64 B-type range: {:?}", + plan.metrics + ); + assert_eq!(plan.metrics.text_size, plan.parsed.section_size(SectionKind::Text)); + assert_eq!(plan.metrics.covered_text_op_count, plan.metrics.executable_text_op_count); + assert!(plan.metrics.executable_text_op_count > 1500, "synthetic text ops should be visible: {:?}", plan.metrics); + assert_eq!(plan.metrics.layout_order_block_count, plan.metrics.machine_block_count); + assert_eq!(plan.metrics.layout_order_text_size, plan.metrics.text_size); + assert_eq!(plan.metrics.conditional_branch_block_count, 1); + assert!(plan.metrics.machine_cfg_edge_count >= 2, "far branch CFG edges should be visible: {:?}", plan.metrics); + assert_eq!(plan.metrics.machine_call_edge_count, 0); + assert_eq!(plan.metrics.unreachable_machine_block_count, 0); + assert!(plan.metrics.machine_block_count >= 2, "far branch should produce multiple machine blocks: {:?}", plan.metrics); + assert!( + plan.metrics.max_machine_block_size > 4096, + "large fallthrough block should be visible in layout metrics: {:?}", + plan.metrics + ); + } + + #[test] + fn machine_layout_plan_builds_explicit_machine_blocks() { + let lines = vec![ + ".section .text".to_string(), + ".global entry".to_string(), + "entry:".to_string(), + "li a0, 0".to_string(), + "beqz a0, done".to_string(), + "li a0, 1".to_string(), + "j done".to_string(), + "done:".to_string(), + "ret".to_string(), + ]; + + let plan = MachineLayoutPlan::build(&lines).expect("machine layout plan"); + let cfg = &plan.cfg; + let blocks = &cfg.blocks; + assert_eq!(blocks.len(), 3, "expected entry, fallthrough, and done blocks: {:?}", blocks); + assert_eq!(blocks[0].label.as_deref(), Some("entry")); + assert_eq!(blocks[0].terminator, MachineTerminator::ConditionalBranch { target: "done".to_string() }); + assert_eq!(blocks[1].terminator, MachineTerminator::Jump { target: "done".to_string() }); + assert_eq!(blocks[2].label.as_deref(), Some("done")); + assert_eq!(blocks[2].terminator, MachineTerminator::Return); + + assert_eq!(cfg.blocks.len(), 3); + assert_eq!(plan.order.block_order, vec![0, 1, 2]); + assert_eq!(plan.order.placed_blocks.len(), 3); + assert_eq!( + plan.order.placed_blocks, + vec![ + MachinePlacedBlock { block_index: 0, byte_start: 0, byte_size: cfg.blocks[0].byte_size }, + MachinePlacedBlock { block_index: 1, byte_start: cfg.blocks[0].byte_size, byte_size: cfg.blocks[1].byte_size }, + MachinePlacedBlock { + block_index: 2, + byte_start: cfg.blocks[0].byte_size + cfg.blocks[1].byte_size, + byte_size: cfg.blocks[2].byte_size + }, + ] + ); + assert_eq!(plan.order.text_size, plan.metrics.text_size); + assert_eq!(plan.metrics.executable_text_op_count, 5); + assert_eq!(plan.metrics.covered_text_op_count, 5); + assert_eq!(plan.metrics.layout_order_block_count, 3); + assert_eq!( + cfg.edges, + vec![ + MachineCfgEdge { from: 0, to: 2, kind: MachineCfgEdgeKind::ConditionalTaken }, + MachineCfgEdge { from: 0, to: 1, kind: MachineCfgEdgeKind::ConditionalFallthrough }, + MachineCfgEdge { from: 1, to: 2, kind: MachineCfgEdgeKind::Jump }, + ] + ); + assert_eq!(unreachable_machine_block_count(&plan.parsed, cfg), 0); + } + + #[test] + fn machine_layout_plan_builds_register_conditional_branch_blocks() { + let lines = vec![ + ".section .text".to_string(), + ".global entry".to_string(), + "entry:".to_string(), + "li a0, 0".to_string(), + "li a1, 0".to_string(), + "bgeu a0, a1, done".to_string(), + "li a0, 1".to_string(), + "j done".to_string(), + "done:".to_string(), + "ret".to_string(), + ]; + + let plan = MachineLayoutPlan::build(&lines).expect("machine layout plan"); + let cfg = &plan.cfg; + assert_eq!(cfg.blocks.len(), 3, "expected entry, fallthrough, and done blocks: {:?}", cfg.blocks); + assert_eq!(cfg.blocks[0].label.as_deref(), Some("entry")); + assert_eq!(cfg.blocks[0].terminator, MachineTerminator::ConditionalBranch { target: "done".to_string() }); + assert_eq!( + cfg.edges, + vec![ + MachineCfgEdge { from: 0, to: 2, kind: MachineCfgEdgeKind::ConditionalTaken }, + MachineCfgEdge { from: 0, to: 1, kind: MachineCfgEdgeKind::ConditionalFallthrough }, + MachineCfgEdge { from: 1, to: 2, kind: MachineCfgEdgeKind::Jump }, + ] + ); + } + + #[test] + fn machine_cfg_tracks_call_edges_to_local_helpers() { + let lines = vec![ + ".section .text".to_string(), + ".global entry".to_string(), + "entry:".to_string(), + "call local_helper".to_string(), + "ret".to_string(), + "local_helper:".to_string(), + "li a0, 0".to_string(), + "ret".to_string(), + ]; + + let plan = MachineLayoutPlan::build(&lines).expect("machine layout plan"); + let cfg = &plan.cfg; + assert_eq!(cfg.blocks.len(), 2, "expected entry and local helper blocks: {:?}", cfg.blocks); + assert_eq!(cfg.blocks[0].label.as_deref(), Some("entry")); + assert_eq!(cfg.blocks[1].label.as_deref(), Some("local_helper")); + assert!( + cfg.edges.contains(&MachineCfgEdge { from: 0, to: 1, kind: MachineCfgEdgeKind::Call }), + "call edge to local helper should be explicit: {:?}", + cfg.edges + ); + assert_eq!(plan.metrics.machine_call_edge_count, 1); + assert_eq!(unreachable_machine_block_count(&plan.parsed, cfg), 0); + } + + #[test] + fn machine_reachability_uses_entry_label_not_every_global() { + let lines = vec![ + ".section .text".to_string(), + ".global entry".to_string(), + "entry:".to_string(), + "li a0, 0".to_string(), + "ret".to_string(), + ".global unused_export".to_string(), + "unused_export:".to_string(), + "li a0, 1".to_string(), + "ret".to_string(), + ]; + + let plan = MachineLayoutPlan::build(&lines).expect("machine layout plan"); + assert_eq!(plan.parsed.entry_label.as_deref(), Some("entry")); + assert_eq!(plan.cfg.blocks.len(), 2, "expected entry and unused export blocks: {:?}", plan.cfg.blocks); + assert_eq!(plan.metrics.unreachable_machine_block_count, 1); + assert_eq!(unreachable_machine_block_count(&plan.parsed, &plan.cfg), 1); + } + + #[test] + fn machine_layout_order_rejects_missing_duplicate_or_unknown_blocks() { + let lines = vec![ + ".section .text".to_string(), + ".global entry".to_string(), + "entry:".to_string(), + "li a0, 0".to_string(), + "beqz a0, done".to_string(), + "li a0, 1".to_string(), + "j done".to_string(), + "done:".to_string(), + "ret".to_string(), + ]; + + let plan = MachineLayoutPlan::build(&lines).expect("machine layout plan"); + assert!(validate_machine_layout_order(&plan.cfg, &[0, 1]).is_err()); + assert!(validate_machine_layout_order(&plan.cfg, &[0, 1, 1]).is_err()); + assert!(validate_machine_layout_order(&plan.cfg, &[0, 1, 3]).is_err()); + let permuted = build_machine_layout_order(&plan.cfg, vec![2, 0, 1]).expect("permuted layout order should be valid"); + assert_eq!(permuted.block_order, vec![2, 0, 1]); + assert_eq!(permuted.placed_blocks[0].block_index, 2); + assert_eq!(permuted.placed_blocks[0].byte_start, 0); + assert_eq!(permuted.placed_blocks[1].byte_start, plan.cfg.blocks[2].byte_size); + assert_eq!(permuted.text_size, plan.order.text_size); + } + + #[test] + fn machine_layout_plan_rejects_branch_target_outside_text() { + let lines = vec![ + ".section .text".to_string(), + ".global entry".to_string(), + "entry:".to_string(), + "li a0, 0".to_string(), + "beqz a0, data_label".to_string(), + "ret".to_string(), + ".section .rodata".to_string(), + "data_label:".to_string(), + ".word 1".to_string(), + ]; + + let err = MachineLayoutPlan::build(&lines).expect_err("branch targets outside text blocks should be rejected"); + assert!(err.message.contains("does not start a machine block"), "unexpected error for invalid CFG target: {}", err.message); + } + + #[test] + fn generated_functions_use_shared_epilogue_tail() { + let ir = IrModule { + name: "shape_test".to_string(), + items: vec![IrItem::Action(IrAction { + name: "shape".to_string(), + params: vec![], + return_type: Some(IrType::U64), + state_transition_edges: vec![], + protocol_role_candidates: vec![], + effect_class: EffectClass::Pure, + scheduler_hints: SchedulerHints::default(), + body: IrBody { + consume_set: vec![], + read_refs: vec![], + create_set: vec![], + mutate_set: vec![], + write_intents: vec![], + bounded_collection_ops: vec![], + borrow_regions: vec![], + blocks: vec![IrBlock { + id: BlockId(0), + instructions: vec![], + terminator: IrTerminator::Return(Some(IrOperand::Const(IrConst::U64(7)))), + runtime_error: None, + }], + }, + })], + external_type_defs: vec![], + external_callable_abis: vec![], + enum_fixed_sizes: HashMap::new(), + enum_layouts: HashMap::new(), + }; + let assembly = CodeGenerator::new(CodegenOptions::default()).generate(&ir, ArtifactFormat::RiscvAssembly).unwrap(); + let assembly = String::from_utf8(assembly).unwrap(); + let shape_start = assembly.find("shape:\n").expect("shape function label"); + let runtime_start = + assembly[shape_start..].find(".section .text").map(|offset| shape_start + offset).unwrap_or(assembly.len()); + let shape_assembly = &assembly[shape_start..runtime_start]; + + assert!(shape_assembly.contains("j .Lshape_epilogue"), "return sites should jump to the shared epilogue:\n{}", shape_assembly); + assert_eq!( + shape_assembly.matches(".Lshape_epilogue:").count(), + 1, + "a function should emit one shared epilogue label:\n{}", + shape_assembly + ); + assert_eq!( + shape_assembly.matches("ret").count(), + 1, + "a function should emit one physical return in its shared epilogue:\n{}", + shape_assembly + ); + } +} diff --git a/src/codegen/calls.rs b/src/codegen/calls.rs new file mode 100644 index 00000000..80d8325b --- /dev/null +++ b/src/codegen/calls.rs @@ -0,0 +1,1427 @@ +use super::*; + +impl CodeGenerator { + fn emit_ckb_fixed_hash_call(&mut self, dest: Option<&IrVar>, func: &str, args: &[IrOperand]) -> Result { + if !is_ckb_fixed_hash_helper(func) { + return Ok(false); + } + self.emit(format!("# call {}", func)); + let Some(dest) = dest else { + self.emit("# cellscript abi: fail closed because hash helper result has no destination"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + let Some(dest_offset) = self.fixed_byte_local_offsets.get(&dest.id).copied() else { + self.emit("# cellscript abi: fail closed because hash helper output buffer was not allocated"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + if matches!(func, "__ckb_hash_pair" | "__ckb_hash_sha256_pair" | "__ckb_hash_sha256d_pair") { + if args.len() != 2 { + self.emit("# cellscript abi: fail closed because hash_pair needs two inputs"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + } + let Some(left) = self.expected_fixed_byte_source(&args[0], 32) else { + self.emit("# cellscript abi: fail closed because hash_pair left input is not a 32-byte value"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + let Some(right) = self.expected_fixed_byte_source(&args[1], 32) else { + self.emit("# cellscript abi: fail closed because hash_pair right input is not a 32-byte value"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + self.emit_prepare_fixed_byte_source(&left, 32, "pair hash left input"); + self.emit_prepare_fixed_byte_source(&right, 32, "pair hash right input"); + if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &left) { + self.emit("# cellscript abi: fail closed because hash_pair left pointer is not materializable"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + } + if !self.emit_fixed_byte_source_pointer_or_const_to("a1", &right) { + self.emit("# cellscript abi: fail closed because hash_pair right pointer is not materializable"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + } + self.emit_sp_addi("a2", dest_offset); + self.emit(format!("call {}", func)); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_sp_addi("t0", dest_offset); + self.emit_stack_store("t0", dest.id * 8); + return Ok(true); + } + if func == "__ckb_hash_blake2b_packed" { + let Some(arg) = args.first() else { + self.emit("# cellscript abi: fail closed because hash_blake2b_packed is missing input"); + self.emit_fail(CellScriptRuntimeError::PackedHashPreimageMaterializationUnresolved); + return Ok(true); + }; + let Some(width) = operand_fixed_byte_width(arg).or_else(|| match arg { + IrOperand::Var(var) => self.fixed_byte_like_width(&var.ty), + _ => None, + }) else { + self.emit("# cellscript abi: fail closed because hash_blake2b_packed input has no static packed width"); + self.emit_fail(CellScriptRuntimeError::PackedHashPreimageMaterializationUnresolved); + return Ok(true); + }; + let Some(source) = self.expected_fixed_byte_source(arg, width) else { + self.emit("# cellscript abi: fail closed because hash_blake2b_packed input is not materializable"); + self.emit_fail(CellScriptRuntimeError::PackedHashPreimageMaterializationUnresolved); + return Ok(true); + }; + let type_name = match arg { + IrOperand::Var(var) => named_type_name(&var.ty).map(str::to_string).unwrap_or_else(|| aggregate_type_label(&var.ty)), + IrOperand::Const(_) => "const".to_string(), + }; + let mut header = b"CellScriptPackedHashV0\0".to_vec(); + header.extend_from_slice(type_name.as_bytes()); + header.push(0); + header.extend_from_slice(&(width as u32).to_le_bytes()); + let total_width = header.len() + width; + if total_width > RUNTIME_SCRATCH_BUFFER_SIZE { + self.emit("# cellscript abi: fail closed because hash_blake2b_packed preimage exceeds scratch buffer"); + self.emit_fail(CellScriptRuntimeError::PackedHashPreimageMaterializationUnresolved); + return Ok(true); + } + let buffer_offset = self.runtime_scratch_buffer_offset(); + for (index, byte) in header.iter().enumerate() { + self.emit(format!("li t0, {}", byte)); + self.emit_stack_store_byte("t0", buffer_offset + index); + } + self.emit_prepare_fixed_byte_source(&source, width, "hash_blake2b_packed input"); + if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &source) { + self.emit("# cellscript abi: fail closed because hash_blake2b_packed input pointer is not materializable"); + self.emit_fail(CellScriptRuntimeError::PackedHashPreimageMaterializationUnresolved); + return Ok(true); + } + self.emit_sp_addi("a1", buffer_offset + header.len()); + self.emit(format!("li a2, {}", width)); + self.emit("call __cellscript_memcpy_fixed"); + self.emit_sp_addi("a0", buffer_offset); + self.emit(format!("li a1, {}", total_width)); + self.emit_sp_addi("a2", dest_offset); + self.emit("call __ckb_hash_blake2b_var"); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_sp_addi("t0", dest_offset); + self.emit_stack_store("t0", dest.id * 8); + return Ok(true); + } + if func == "__ckb_hash_data_packed" { + let Some(arg) = args.first() else { + self.emit("# cellscript abi: fail closed because hash_data_packed is missing input"); + self.emit_fail(CellScriptRuntimeError::PackedHashPreimageMaterializationUnresolved); + return Ok(true); + }; + let Some(width) = operand_fixed_byte_width(arg).or_else(|| match arg { + IrOperand::Var(var) => self.fixed_byte_like_width(&var.ty), + _ => None, + }) else { + self.emit("# cellscript abi: fail closed because hash_data_packed input has no static packed width"); + self.emit_fail(CellScriptRuntimeError::PackedHashPreimageMaterializationUnresolved); + return Ok(true); + }; + let Some(source) = self.expected_fixed_byte_source(arg, width) else { + self.emit("# cellscript abi: fail closed because hash_data_packed input is not materializable"); + self.emit_fail(CellScriptRuntimeError::PackedHashPreimageMaterializationUnresolved); + return Ok(true); + }; + self.emit_prepare_fixed_byte_source(&source, width, "hash_data_packed input"); + if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &source) { + self.emit("# cellscript abi: fail closed because hash_data_packed input pointer is not materializable"); + self.emit_fail(CellScriptRuntimeError::PackedHashPreimageMaterializationUnresolved); + return Ok(true); + } + self.emit(format!("li a1, {}", width)); + self.emit_sp_addi("a2", dest_offset); + self.emit("call __ckb_hash_blake2b_var"); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_sp_addi("t0", dest_offset); + self.emit_stack_store("t0", dest.id * 8); + return Ok(true); + } + let Some(arg) = args.first() else { + self.emit("# cellscript abi: fail closed because hash helper is missing input"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + let Some(source) = self.expected_fixed_byte_source(arg, 32) else { + self.emit("# cellscript abi: fail closed because hash helper input is not a 32-byte value"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + self.emit_prepare_fixed_byte_source(&source, 32, "fixed hash input"); + if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &source) { + self.emit("# cellscript abi: fail closed because hash helper input pointer is not materializable"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + } + self.emit_sp_addi("a1", dest_offset); + self.emit(format!("call {}", func)); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_sp_addi("t0", dest_offset); + self.emit_stack_store("t0", dest.id * 8); + Ok(true) + } + + pub(super) fn emit_call(&mut self, dest: Option<&IrVar>, func: &str, args: &[IrOperand]) -> Result<()> { + if self.emit_ckb_fixed_hash_call(dest, func, args)? { + return Ok(()); + } + if matches!(func, "__novaseal_bip340_require_signature" | "__novaseal_bip340_require_signature_from_cell_dep") { + self.emit(format!("# call {} args={}", func, args.len())); + let explicit_dep = func == "__novaseal_bip340_require_signature_from_cell_dep"; + let value_offset = usize::from(explicit_dep); + if args.len() != 3 + value_offset { + self.emit("# cellscript abi: fail closed because BIP340 verifier requires message, pubkey, signature"); + self.emit_fail(CellScriptRuntimeError::Bip340MessageMaterializationUnresolved); + return Ok(()); + } + let Some(message) = self.expected_fixed_byte_source(&args[value_offset], 32) else { + self.emit("# cellscript abi: fail closed because BIP340 message is not a 32-byte value"); + self.emit_fail(CellScriptRuntimeError::Bip340MessageMaterializationUnresolved); + return Ok(()); + }; + let Some(pubkey) = self.expected_fixed_byte_source(&args[value_offset + 1], 32) else { + self.emit("# cellscript abi: fail closed because BIP340 pubkey is not a 32-byte value"); + self.emit_fail(CellScriptRuntimeError::Bip340PubkeyMaterializationUnresolved); + return Ok(()); + }; + let Some(signature) = self.expected_fixed_byte_source(&args[value_offset + 2], 64) else { + self.emit("# cellscript abi: fail closed because BIP340 signature is not a 64-byte value"); + self.emit_fail(CellScriptRuntimeError::Bip340SignatureMaterializationUnresolved); + return Ok(()); + }; + self.emit_prepare_fixed_byte_source(&message, 32, "novaseal bip340 message"); + self.emit_prepare_fixed_byte_source(&pubkey, 32, "novaseal bip340 pubkey"); + self.emit_prepare_fixed_byte_source(&signature, 64, "novaseal bip340 signature"); + let Some(read_fd_offset) = self.checked_runtime_expr_temp_offset(0) else { + self.emit_fail(CellScriptRuntimeError::Bip340MessageMaterializationUnresolved); + return Ok(()); + }; + let Some(write_fd_offset) = self.checked_runtime_expr_temp_offset(1) else { + self.emit_fail(CellScriptRuntimeError::Bip340MessageMaterializationUnresolved); + return Ok(()); + }; + let Some(child_pid_offset) = self.checked_runtime_expr_temp_offset(2) else { + self.emit_fail(CellScriptRuntimeError::Bip340MessageMaterializationUnresolved); + return Ok(()); + }; + let ipc_buffer_offset = self.runtime_scratch_buffer_offset(); + self.emit("# cellscript abi: NovaSeal BIP340 verifier IPC envelope via VM2 pipe/spawn/wait"); + let pipe_ok = self.fresh_label("novaseal_bip340_pipe_ok"); + self.emit("call __ckb_pipe"); + self.emit(format!("beqz a0, {}", pipe_ok)); + self.emit_fail(CellScriptRuntimeError::Bip340PipeCreateFailed); + self.emit_label(&pipe_ok); + self.emit_stack_store("a1", read_fd_offset); + self.emit_stack_store("a2", write_fd_offset); + self.emit("# cellscript abi: materialize cellscript-btc-bip340-ipc-v0 envelope in scratch"); + for (index, byte) in b"NSBV0IPC".iter().enumerate() { + self.emit(format!("li t0, {}", byte)); + self.emit_stack_store_byte("t0", ipc_buffer_offset + index); + } + for (index, byte) in [0u8, 0, 1, 0, 0, 0, 0, 0].iter().enumerate() { + self.emit(format!("li t0, {}", byte)); + self.emit_stack_store_byte("t0", ipc_buffer_offset + 8 + index); + } + if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &message) { + self.emit_fail(CellScriptRuntimeError::Bip340MessageMaterializationUnresolved); + return Ok(()); + } + self.emit_sp_addi("a1", ipc_buffer_offset + 16); + self.emit("li a2, 32"); + self.emit("call __cellscript_memcpy_fixed"); + if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &pubkey) { + self.emit_fail(CellScriptRuntimeError::Bip340PubkeyMaterializationUnresolved); + return Ok(()); + } + self.emit_sp_addi("a1", ipc_buffer_offset + 48); + self.emit("li a2, 32"); + self.emit("call __cellscript_memcpy_fixed"); + if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &signature) { + self.emit_fail(CellScriptRuntimeError::Bip340SignatureMaterializationUnresolved); + return Ok(()); + } + self.emit_sp_addi("a1", ipc_buffer_offset + 80); + self.emit("li a2, 64"); + self.emit("call __cellscript_memcpy_fixed"); + self.emit("# cellscript abi: spawn manifest-bound verifier CellDep with prepared read fd inherited"); + if explicit_dep { + self.emit_operand_to_register("a0", &args[0]); + } else { + self.emit("li a0, 0"); + } + self.emit_stack_load("a1", read_fd_offset); + self.emit("call __ckb_spawn_with_fd1"); + let spawn_ok = self.fresh_label("novaseal_bip340_spawn_ok"); + self.emit(format!("beqz a0, {}", spawn_ok)); + self.emit_fail(CellScriptRuntimeError::Bip340SpawnFailed); + self.emit_label(&spawn_ok); + self.emit_stack_store("a1", child_pid_offset); + self.emit("# cellscript abi: BIP340 IPC write canonical 18-word little-endian envelope"); + for word_index in 0..18 { + self.emit(format!("# cellscript abi: novaseal bip340 ipc word {}", word_index)); + self.emit_stack_load("a0", write_fd_offset); + self.emit_stack_load("a1", ipc_buffer_offset + word_index * 8); + self.emit("call __ckb_pipe_write"); + let write_ok = self.fresh_label("novaseal_bip340_write_ok"); + self.emit(format!("beqz a0, {}", write_ok)); + self.emit_fail(CellScriptRuntimeError::Bip340MessageWriteFailed); + self.emit_label(&write_ok); + } + self.emit_stack_load("a0", write_fd_offset); + self.emit("call __ckb_close"); + let close_ok = self.fresh_label("novaseal_bip340_close_ok"); + self.emit(format!("beqz a0, {}", close_ok)); + self.emit_fail(CellScriptRuntimeError::Bip340VerifierReadFailed); + self.emit_label(&close_ok); + self.emit_stack_load("a0", child_pid_offset); + self.emit("call __ckb_wait"); + let wait_ok = self.fresh_label("novaseal_bip340_wait_ok"); + self.emit(format!("beqz a0, {}", wait_ok)); + self.emit_fail(CellScriptRuntimeError::Bip340ChildRejected); + self.emit_label(&wait_ok); + return Ok(()); + } + if func.contains("::") { + return Err(CompileError::new( + format!("qualified function call '{}' reached codegen without IR label normalization; this is a compiler bug", func), + crate::error::Span::default(), + )); + } + self.emit(format!("# call {}", func)); + + if self.emit_runtime_fixed_hash_requirement_call(func, args)? { + return Ok(()); + } + if self.emit_runtime_bounded_cell_dep_requirement_call(func, args)? { + return Ok(()); + } + if self.emit_runtime_sha256d_merkle_requirement_call(func, args)? { + return Ok(()); + } + if self.emit_runtime_cell_script_args_exact_requirement_call(func, args)? { + return Ok(()); + } + if self.emit_runtime_cell_script_hash_type_requirement_call(func, args)? { + return Ok(()); + } + if self.emit_runtime_input_out_point_requirement_call(func, args)? { + return Ok(()); + } + if self.emit_runtime_xudt_type_args_requirement_call(func, args)? { + return Ok(()); + } + if self.emit_runtime_xudt_group_amount_delta_call(func, args)? { + return Ok(()); + } + if self.emit_runtime_metapoint_filtered_pair_call(func, args)? { + return Ok(()); + } + if self.emit_runtime_c256_product_requirement_call(func, args)? { + return Ok(()); + } + if self.emit_runtime_c256_sum2_product_requirement_call(func, args)? { + return Ok(()); + } + if self.emit_runtime_current_script_hash_call(dest, func, args)? { + return Ok(()); + } + if self.emit_runtime_input_out_point_tx_hash_call(dest, func, args)? { + return Ok(()); + } + if self.emit_runtime_cell_data_hash_at_call(dest, func, args)? { + return Ok(()); + } + if self.emit_runtime_cell_script_hash_field_call(dest, func, args)? { + return Ok(()); + } + if self.emit_runtime_witness_hash_call(dest, func, args)? { + return Ok(()); + } + + let abi = self.callable_abis.get(func).cloned(); + let outgoing_stack_arg_bytes = align_stack_arg_bytes(call_abi_arg_count(abi.as_ref(), args).saturating_sub(8) * 8); + let mut abi_index = 0usize; + for (arg_index, arg) in args.iter().enumerate() { + if let Some(abi) = &abi + && let Some(param) = abi.params.get(arg_index) + { + let needs_type_hash = abi.type_hash_param_indices.contains(&arg_index); + if !self.emit_call_param_arg(func, param, needs_type_hash, &mut abi_index, arg, outgoing_stack_arg_bytes) { + return Ok(()); + } + continue; + } + if !self.emit_call_scalar_arg(func, &format!("arg{}", arg_index), &mut abi_index, arg, outgoing_stack_arg_bytes) { + return Ok(()); + } + } + + if outgoing_stack_arg_bytes > 0 { + self.emit(format!("# cellscript abi: reserve {} bytes for outgoing stack call arguments", outgoing_stack_arg_bytes)); + self.emit_large_addi("sp", "sp", -(outgoing_stack_arg_bytes as i64)); + } + self.emit(format!("call {}", func)); + if outgoing_stack_arg_bytes > 0 { + self.emit_large_addi("sp", "sp", outgoing_stack_arg_bytes as i64); + } + + if is_runtime_scalar_failclosed_call(func) { + let ok_label = self.fresh_label("runtime_scalar_ok"); + self.emit("# cellscript abi: scalar runtime helper status check (a1 == 0)"); + self.emit(format!("beqz a1, {}", ok_label)); + self.emit("addi a0, a1, 0"); + self.emit_epilogue(); + self.emit_label(&ok_label); + } + + if dest.is_none() && is_void_runtime_requirement_call(func) { + let ok_label = self.fresh_label("runtime_requirement_ok"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_epilogue(); + self.emit_label(&ok_label); + } + + if let Some(d) = dest { + let payload_enum = match &d.ty { + IrType::Named(name) => { + self.enum_layouts.get(name).filter(|layout| layout.has_payload()).map(|layout| (name.clone(), layout.clone())) + } + _ => None, + }; + if let Some((name, layout)) = payload_enum { + if let Some(offset) = self.fixed_byte_local_offsets.get(&d.id).copied() { + self.emit(format!( + "# cellscript abi: receive payload enum {} size={} from a0/a1 register pair", + name, layout.encoded_size + )); + let low_width = layout.encoded_size.min(8); + for byte_index in 0..low_width { + self.emit_stack_store_byte("a0", offset + byte_index); + if byte_index + 1 < low_width { + self.emit("srli a0, a0, 8"); + } + } + if layout.encoded_size > 8 { + let high_width = layout.encoded_size - 8; + for byte_index in 0..high_width { + self.emit_stack_store_byte("a1", offset + 8 + byte_index); + if byte_index + 1 < high_width { + self.emit("srli a1, a1, 8"); + } + } + } + self.emit_sp_addi("t0", offset); + self.emit_stack_store("t0", d.id * 8); + } else { + self.emit("# cellscript abi: payload enum call destination has no storage; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + } + } else if d.ty == IrType::U128 { + if let Some(offset) = self.u128_value_offsets.get(&d.id).copied() { + self.emit("# cellscript abi: receive u128 return from a0(low)/a1(high)"); + self.emit_stack_store("a0", offset); + self.emit_stack_store("a1", offset + 8); + self.emit_store_u128_pointer_for_var(d.id, offset); + } else { + self.emit("# cellscript abi: u128 call destination has no storage; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + } + } else if let IrType::Tuple(items) = &d.ty { + self.emit_stack_store("a0", d.id * 8); + for index in 0..items.len().min(8) { + let field = index.to_string(); + if let Some(field_var_id) = self.tuple_call_return_field_slots.get(&(d.id, field)).copied() { + self.emit_stack_store(&format!("a{}", index), field_var_id * 8); + } + } + } else { + self.emit_stack_store("a0", d.id * 8); + } + } + + Ok(()) + } + + fn emit_runtime_current_script_hash_call(&mut self, dest: Option<&IrVar>, func: &str, args: &[IrOperand]) -> Result { + if func != "__ckb_current_script_hash" { + return Ok(false); + } + let Some(dest) = dest else { + return Ok(false); + }; + if !args.is_empty() || dest.ty != IrType::Hash { + return Ok(false); + } + let Some(size_offset) = self.cell_buffer_size_offsets.get(&dest.id).copied() else { + self.emit("# cellscript abi: current script hash destination has no 32-byte storage; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + let Some(buffer_offset) = self.cell_buffer_offsets.get(&dest.id).copied() else { + self.emit("# cellscript abi: current script hash destination has no buffer storage; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + + self.emit("# cellscript abi: load current script hash into addressable Hash"); + self.emit("li t0, 32"); + self.emit_stack_store("t0", size_offset); + self.emit_sp_addi("a0", buffer_offset); + self.emit_sp_addi("a1", size_offset); + self.emit("call __ckb_current_script_hash"); + let ok_label = self.fresh_label("current_script_hash_ok"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_epilogue(); + self.emit_label(&ok_label); + self.emit_sp_addi("t0", buffer_offset); + self.emit_stack_store("t0", dest.id * 8); + Ok(true) + } + + fn emit_runtime_input_out_point_tx_hash_call(&mut self, dest: Option<&IrVar>, func: &str, args: &[IrOperand]) -> Result { + if func != "__ckb_input_out_point_tx_hash" { + return Ok(false); + } + let Some(dest) = dest else { + return Ok(false); + }; + if args.len() != 1 || dest.ty != IrType::Hash { + return Ok(false); + } + let Some(size_offset) = self.cell_buffer_size_offsets.get(&dest.id).copied() else { + self.emit("# cellscript abi: input OutPoint tx hash destination has no 32-byte storage; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + let Some(buffer_offset) = self.cell_buffer_offsets.get(&dest.id).copied() else { + self.emit("# cellscript abi: input OutPoint tx hash destination has no buffer storage; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + + self.emit("# cellscript abi: load SourceView input OutPoint tx hash into addressable Hash"); + self.emit("li t0, 32"); + self.emit_stack_store("t0", size_offset); + self.emit_operand_to_register("a0", &args[0]); + self.emit_sp_addi("a1", buffer_offset); + self.emit_sp_addi("a2", size_offset); + self.emit("call __ckb_input_out_point_tx_hash"); + let ok_label = self.fresh_label("input_out_point_tx_hash_ok"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_epilogue(); + self.emit_label(&ok_label); + self.emit_sp_addi("t0", buffer_offset); + self.emit_stack_store("t0", dest.id * 8); + Ok(true) + } + + fn emit_runtime_cell_script_hash_field_call(&mut self, dest: Option<&IrVar>, func: &str, args: &[IrOperand]) -> Result { + if !matches!( + func, + "__ckb_cell_lock_hash" + | "__ckb_cell_type_hash" + | "__ckb_cell_data_hash" + | "__ckb_cell_lock_code_hash" + | "__ckb_cell_type_code_hash" + | "__ckb_cell_lock_args_hash" + | "__ckb_cell_type_args_hash" + ) { + return Ok(false); + } + let Some(dest) = dest else { + return Ok(false); + }; + if args.len() != 1 || dest.ty != IrType::Hash { + return Ok(false); + } + let Some(size_offset) = self.cell_buffer_size_offsets.get(&dest.id).copied() else { + self.emit("# cellscript abi: ScriptRef hash destination has no 32-byte storage; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + let Some(buffer_offset) = self.cell_buffer_offsets.get(&dest.id).copied() else { + self.emit("# cellscript abi: ScriptRef hash destination has no buffer storage; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + + self.emit("# cellscript abi: load SourceView ScriptRef hash field into addressable Hash"); + self.emit("li t0, 32"); + self.emit_stack_store("t0", size_offset); + self.emit_operand_to_register("a0", &args[0]); + self.emit_sp_addi("a1", buffer_offset); + self.emit_sp_addi("a2", size_offset); + self.emit(format!("call {}", func)); + let ok_label = self.fresh_label("script_ref_hash_ok"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_epilogue(); + self.emit_label(&ok_label); + self.emit_sp_addi("t0", buffer_offset); + self.emit_stack_store("t0", dest.id * 8); + Ok(true) + } + + fn emit_runtime_cell_data_hash_at_call(&mut self, dest: Option<&IrVar>, func: &str, args: &[IrOperand]) -> Result { + if func != "__ckb_cell_data_hash_at" { + return Ok(false); + } + let Some(dest) = dest else { + return Ok(false); + }; + if args.len() != 2 || dest.ty != IrType::Hash { + return Ok(false); + } + let Some(size_offset) = self.cell_buffer_size_offsets.get(&dest.id).copied() else { + self.emit("# cellscript abi: cell data hash-at destination has no 32-byte storage; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + let Some(buffer_offset) = self.cell_buffer_offsets.get(&dest.id).copied() else { + self.emit("# cellscript abi: cell data hash-at destination has no buffer storage; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + + self.emit("# cellscript abi: load 32 bytes from SourceView cell data into addressable Hash"); + self.emit("li t0, 32"); + self.emit_stack_store("t0", size_offset); + self.emit_operand_to_register("a0", &args[0]); + self.emit_operand_to_register("a1", &args[1]); + self.emit_sp_addi("a2", buffer_offset); + self.emit_sp_addi("a3", size_offset); + self.emit("call __ckb_cell_data_hash_at"); + let ok_label = self.fresh_label("cell_data_hash_at_ok"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_epilogue(); + self.emit_label(&ok_label); + self.emit_sp_addi("t0", buffer_offset); + self.emit_stack_store("t0", dest.id * 8); + Ok(true) + } + + fn emit_runtime_witness_hash_call(&mut self, dest: Option<&IrVar>, func: &str, args: &[IrOperand]) -> Result { + if !matches!(func, "__ckb_witness_raw" | "__ckb_witness_lock" | "__ckb_witness_input_type" | "__ckb_witness_output_type") { + return Ok(false); + } + let Some(dest) = dest else { + return Ok(false); + }; + if args.len() != 1 || dest.ty != IrType::Hash { + return Ok(false); + } + let Some(size_offset) = self.cell_buffer_size_offsets.get(&dest.id).copied() else { + self.emit("# cellscript abi: witness hash destination has no 32-byte storage; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + let Some(buffer_offset) = self.cell_buffer_offsets.get(&dest.id).copied() else { + self.emit("# cellscript abi: witness hash destination has no buffer storage; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + + self.emit("# cellscript abi: load witness hash into addressable Hash"); + self.emit("li t0, 32"); + self.emit_stack_store("t0", size_offset); + self.emit_operand_to_register("a0", &args[0]); + self.emit_sp_addi("a1", buffer_offset); + self.emit(format!("call {}", func)); + let ok_label = self.fresh_label("witness_hash_ok"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_epilogue(); + self.emit_label(&ok_label); + self.emit_sp_addi("t0", buffer_offset); + self.emit_stack_store("t0", dest.id * 8); + Ok(true) + } + + fn emit_runtime_fixed_hash_requirement_call(&mut self, func: &str, args: &[IrOperand]) -> Result { + if !matches!( + func, + "__ckb_require_cell_lock_hash" + | "__ckb_require_cell_type_hash" + | "__ckb_require_cell_data_hash" + | "__ckb_require_cell_lock_args_hash" + | "__ckb_require_cell_type_args_hash" + | "__ckb_require_cell_lock_args_prefix_hash" + | "__ckb_require_cell_type_args_prefix_hash" + | "__ckb_require_cell_lock_args_suffix_hash" + | "__ckb_require_cell_type_args_suffix_hash" + | "__ckb_require_input_out_point_tx_hash" + | "__xudt_require_owner_mode_input_type" + ) { + return Ok(false); + } + if args.len() != 2 { + return Ok(false); + } + + let expected = self.expected_fixed_byte_source(&args[1], 32); + match expected { + Some(ExpectedFixedByteSource::Const(bytes)) => { + let size_offset = self.runtime_scratch_size_offset(); + let buffer_offset = self.runtime_scratch_buffer_offset(); + let hash: [u8; 32] = bytes.as_slice().try_into().expect("expected fixed hash width"); + self.emit_store_fixed_byte_const_to_scratch(&IrOperand::Const(IrConst::Hash(hash)), size_offset, buffer_offset, 32); + self.emit_sp_addi("a1", buffer_offset); + self.emit("li a2, 32"); + } + Some(source) => { + self.emit_prepare_fixed_byte_source(&source, 32, "runtime expected hash"); + if self.emit_fixed_byte_source_pointer_to("a1", &source) { + self.emit("li a2, 32"); + } else { + self.emit("# cellscript abi: runtime expected hash source is not addressable; pass null to fail closed"); + self.emit("li a1, 0"); + self.emit("li a2, 0"); + } + } + None => { + self.emit("# cellscript abi: runtime expected hash source is unavailable; pass null to fail closed"); + self.emit("li a1, 0"); + self.emit("li a2, 0"); + } + } + + self.emit_operand_to_register("a0", &args[0]); + self.emit("call ".to_string() + func); + let ok_label = self.fresh_label("runtime_hash_requirement_ok"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_epilogue(); + self.emit_label(&ok_label); + Ok(true) + } + + fn emit_runtime_bounded_cell_dep_requirement_call(&mut self, func: &str, args: &[IrOperand]) -> Result { + if func != "__ckb_require_bounded_cell_dep_data_hash" { + return Ok(false); + } + if args.len() != 2 { + return Ok(false); + } + + let expected = self.expected_fixed_byte_source(&args[1], 32); + match expected { + Some(ExpectedFixedByteSource::Const(bytes)) => { + let size_offset = self.runtime_scratch_size_offset(); + let buffer_offset = self.runtime_scratch_buffer_offset(); + let hash: [u8; 32] = bytes.as_slice().try_into().expect("expected fixed hash width"); + self.emit_store_fixed_byte_const_to_scratch(&IrOperand::Const(IrConst::Hash(hash)), size_offset, buffer_offset, 32); + self.emit_sp_addi("a1", buffer_offset); + } + Some(source) => { + self.emit_prepare_fixed_byte_source(&source, 32, "bounded CellDep expected data hash"); + if !self.emit_fixed_byte_source_pointer_to("a1", &source) { + self.emit("# cellscript abi: bounded CellDep expected hash is not addressable; pass null to fail closed"); + self.emit("li a1, 0"); + } + } + None => { + self.emit("# cellscript abi: bounded CellDep expected hash is unavailable; pass null to fail closed"); + self.emit("li a1, 0"); + } + } + self.emit_operand_to_register("a0", &args[0]); + self.emit("call __ckb_require_bounded_cell_dep_data_hash"); + let ok_label = self.fresh_label("bounded_cell_dep_requirement_ok"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_epilogue(); + self.emit_label(&ok_label); + Ok(true) + } + + fn emit_runtime_sha256d_merkle_requirement_call(&mut self, func: &str, args: &[IrOperand]) -> Result { + if func != "__ckb_require_sha256d_merkle_root" { + return Ok(false); + } + if args.len() != 5 { + return Ok(false); + } + let Some(leaf) = self.expected_fixed_byte_source(&args[0], 32) else { + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + let Some(siblings) = self.expected_fixed_byte_source(&args[1], 16 * 32) else { + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + let Some(expected_root) = self.expected_fixed_byte_source(&args[4], 32) else { + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + self.emit_prepare_fixed_byte_source(&leaf, 32, "SHA256d Merkle leaf"); + self.emit_prepare_fixed_byte_source(&siblings, 16 * 32, "SHA256d Merkle siblings"); + self.emit_prepare_fixed_byte_source(&expected_root, 32, "SHA256d Merkle expected root"); + if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &leaf) + || !self.emit_fixed_byte_source_pointer_or_const_to("a1", &siblings) + || !self.emit_fixed_byte_source_pointer_or_const_to("a4", &expected_root) + { + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + } + self.emit_operand_to_register("a2", &args[2]); + self.emit_operand_to_register("a3", &args[3]); + self.emit("call __ckb_require_sha256d_merkle_root"); + let ok = self.fresh_label("sha256d_merkle_requirement_ok"); + self.emit(format!("beqz a0, {}", ok)); + self.emit_epilogue(); + self.emit_label(&ok); + Ok(true) + } + + fn emit_runtime_cell_script_hash_type_requirement_call(&mut self, func: &str, args: &[IrOperand]) -> Result { + if !matches!(func, "__ckb_require_cell_lock_script_hash_type" | "__ckb_require_cell_type_script_hash_type") { + return Ok(false); + } + if args.len() != 3 { + return Ok(false); + } + + let expected = self.expected_fixed_byte_source(&args[1], 32); + match expected { + Some(ExpectedFixedByteSource::Const(bytes)) => { + let size_offset = self.runtime_scratch_size_offset(); + let buffer_offset = self.runtime_scratch_buffer_offset(); + let hash: [u8; 32] = bytes.as_slice().try_into().expect("expected fixed hash width"); + self.emit_store_fixed_byte_const_to_scratch(&IrOperand::Const(IrConst::Hash(hash)), size_offset, buffer_offset, 32); + self.emit_sp_addi("a1", buffer_offset); + self.emit("li a2, 32"); + } + Some(source) => { + self.emit_prepare_fixed_byte_source(&source, 32, "runtime expected Script code hash"); + if self.emit_fixed_byte_source_pointer_to("a1", &source) { + self.emit("li a2, 32"); + } else { + self.emit( + "# cellscript abi: runtime expected Script code hash source is not addressable; pass null to fail closed", + ); + self.emit("li a1, 0"); + self.emit("li a2, 0"); + } + } + None => { + self.emit("# cellscript abi: runtime expected Script code hash is unavailable; pass null to fail closed"); + self.emit("li a1, 0"); + self.emit("li a2, 0"); + } + } + + self.emit_operand_to_register("a0", &args[0]); + self.emit_operand_to_register("a3", &args[2]); + self.emit("call ".to_string() + func); + let ok_label = self.fresh_label("runtime_script_identity_ok"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_epilogue(); + self.emit_label(&ok_label); + Ok(true) + } + + fn emit_runtime_cell_script_args_exact_requirement_call(&mut self, func: &str, args: &[IrOperand]) -> Result { + if !matches!(func, "__ckb_require_cell_lock_args_exact" | "__ckb_require_cell_type_args_exact") { + return Ok(false); + } + if args.len() != 2 { + return Ok(false); + } + let Some(width) = operand_fixed_byte_width(&args[1]) else { + self.emit("# cellscript abi: runtime expected Script args source has no fixed byte width; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + let Some(expected) = self.expected_fixed_byte_source(&args[1], width) else { + self.emit("# cellscript abi: runtime expected Script args source is unavailable; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + }; + + match expected { + ExpectedFixedByteSource::Const(bytes) => { + let size_offset = self.runtime_scratch_size_offset(); + let buffer_offset = self.runtime_scratch_buffer_offset(); + self.emit_store_fixed_byte_const_to_scratch( + &IrOperand::Const(IrConst::Array(bytes.into_iter().map(IrConst::U8).collect())), + size_offset, + buffer_offset, + width, + ); + self.emit_sp_addi("a1", buffer_offset); + } + source => { + self.emit_prepare_fixed_byte_source(&source, width, "runtime expected Script args"); + if !self.emit_fixed_byte_source_pointer_to("a1", &source) { + self.emit("# cellscript abi: runtime expected Script args source is not addressable; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(true); + } + } + } + + self.emit_operand_to_register("a0", &args[0]); + self.emit(format!("li a2, {}", width)); + self.emit("call ".to_string() + func); + let ok_label = self.fresh_label("runtime_script_args_exact_ok"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_epilogue(); + self.emit_label(&ok_label); + Ok(true) + } + + fn emit_runtime_input_out_point_requirement_call(&mut self, func: &str, args: &[IrOperand]) -> Result { + if func != "__ckb_require_input_out_point" { + return Ok(false); + } + if args.len() != 3 { + return Ok(false); + } + + let expected = self.expected_fixed_byte_source(&args[1], 32); + match expected { + Some(ExpectedFixedByteSource::Const(bytes)) => { + let size_offset = self.runtime_scratch_size_offset(); + let buffer_offset = self.runtime_scratch_buffer_offset(); + let hash: [u8; 32] = bytes.as_slice().try_into().expect("expected fixed hash width"); + self.emit_store_fixed_byte_const_to_scratch(&IrOperand::Const(IrConst::Hash(hash)), size_offset, buffer_offset, 32); + self.emit_sp_addi("a1", buffer_offset); + self.emit("li a2, 32"); + } + Some(source) => { + self.emit_prepare_fixed_byte_source(&source, 32, "runtime expected input out point tx hash"); + if self.emit_fixed_byte_source_pointer_to("a1", &source) { + self.emit("li a2, 32"); + } else { + self.emit( + "# cellscript abi: runtime expected input out point hash source is not addressable; pass null to fail closed", + ); + self.emit("li a1, 0"); + self.emit("li a2, 0"); + } + } + None => { + self.emit("# cellscript abi: runtime expected input out point hash source is unavailable; pass null to fail closed"); + self.emit("li a1, 0"); + self.emit("li a2, 0"); + } + } + + self.emit_operand_to_register("a3", &args[2]); + self.emit_operand_to_register("a0", &args[0]); + self.emit("call __ckb_require_input_out_point"); + let ok_label = self.fresh_label("runtime_input_out_point_requirement_ok"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_epilogue(); + self.emit_label(&ok_label); + Ok(true) + } + + fn emit_runtime_xudt_type_args_requirement_call(&mut self, func: &str, args: &[IrOperand]) -> Result { + if func != "__xudt_require_owner_mode_type_args" { + return Ok(false); + } + if args.len() != 3 { + return Ok(false); + } + + let expected = self.expected_fixed_byte_source(&args[1], 32); + match expected { + Some(ExpectedFixedByteSource::Const(bytes)) => { + let size_offset = self.runtime_scratch_size_offset(); + let buffer_offset = self.runtime_scratch_buffer_offset(); + let hash: [u8; 32] = bytes.as_slice().try_into().expect("expected fixed hash width"); + self.emit_store_fixed_byte_const_to_scratch(&IrOperand::Const(IrConst::Hash(hash)), size_offset, buffer_offset, 32); + self.emit_sp_addi("a1", buffer_offset); + self.emit("li a2, 32"); + } + Some(source) => { + self.emit_prepare_fixed_byte_source(&source, 32, "runtime expected xUDT owner hash"); + if self.emit_fixed_byte_source_pointer_to("a1", &source) { + self.emit("li a2, 32"); + } else { + self.emit("# cellscript abi: runtime xUDT owner hash source is not addressable; pass null to fail closed"); + self.emit("li a1, 0"); + self.emit("li a2, 0"); + } + } + None => { + self.emit("# cellscript abi: runtime xUDT owner hash source is unavailable; pass null to fail closed"); + self.emit("li a1, 0"); + self.emit("li a2, 0"); + } + } + + self.emit_operand_to_register("a0", &args[0]); + self.emit_operand_to_register("a3", &args[2]); + self.emit("call __xudt_require_owner_mode_type_args"); + let ok_label = self.fresh_label("runtime_xudt_args_requirement_ok"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_epilogue(); + self.emit_label(&ok_label); + Ok(true) + } + + fn emit_runtime_xudt_group_amount_delta_call(&mut self, func: &str, args: &[IrOperand]) -> Result { + if !matches!(func, "__xudt_require_group_amount_minted" | "__xudt_require_group_amount_burned") { + return Ok(false); + } + if args.len() != 1 { + return Ok(false); + } + + let source = self.expected_fixed_byte_source(&args[0], 16); + match source { + Some(ExpectedFixedByteSource::Const(bytes)) => { + let value = u128::from_le_bytes(bytes.as_slice().try_into().expect("expected fixed u128 width")); + let buffer_offset = self.runtime_scratch_buffer_offset(); + self.emit_store_fixed_byte_const_to_scratch( + &IrOperand::Const(IrConst::U128(value)), + self.runtime_scratch_size_offset(), + buffer_offset, + 16, + ); + self.emit_sp_addi("a0", buffer_offset); + } + Some(source) => { + self.emit_prepare_fixed_byte_source(&source, 16, "runtime xUDT group amount delta"); + if !self.emit_fixed_byte_source_pointer_to("a0", &source) { + self.emit("# cellscript abi: xUDT group amount delta is not addressable; pass null to fail closed"); + self.emit("li a0, 0"); + } + } + None => { + self.emit("# cellscript abi: xUDT group amount delta is unavailable; pass null to fail closed"); + self.emit("li a0, 0"); + } + } + + self.emit("call ".to_string() + func); + let ok_label = self.fresh_label("runtime_xudt_delta_requirement_ok"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_epilogue(); + self.emit_label(&ok_label); + Ok(true) + } + + fn emit_runtime_metapoint_filtered_pair_call(&mut self, func: &str, args: &[IrOperand]) -> Result { + if !matches!( + func, + "__ckb_require_lock_type_metapoint_pairs_from_i32_data_filtered" + | "__ckb_require_type_lock_metapoint_pairs_from_i32_data_filtered" + ) { + return Ok(false); + } + if args.len() != 4 { + return Ok(false); + } + + let expected = self.expected_fixed_byte_source(&args[2], 32); + match expected { + Some(ExpectedFixedByteSource::Const(bytes)) => { + let size_offset = self.runtime_scratch_size_offset(); + let buffer_offset = self.runtime_scratch_buffer_offset(); + let hash: [u8; 32] = bytes.as_slice().try_into().expect("expected fixed hash width"); + self.emit_store_fixed_byte_const_to_scratch(&IrOperand::Const(IrConst::Hash(hash)), size_offset, buffer_offset, 32); + self.emit_sp_addi("a2", buffer_offset); + self.emit("li a3, 32"); + } + Some(source) => { + self.emit_prepare_fixed_byte_source(&source, 32, "runtime filtered MetaPoint related type hash"); + if self.emit_fixed_byte_source_pointer_to("a2", &source) { + self.emit("li a3, 32"); + } else { + self.emit("# cellscript abi: filtered MetaPoint expected type hash is not addressable; pass null to fail closed"); + self.emit("li a2, 0"); + self.emit("li a3, 0"); + } + } + None => { + self.emit("# cellscript abi: filtered MetaPoint expected type hash is unavailable; pass null to fail closed"); + self.emit("li a2, 0"); + self.emit("li a3, 0"); + } + } + + self.emit_operand_to_register("a0", &args[0]); + self.emit_operand_to_register("a1", &args[1]); + self.emit_operand_to_register("a4", &args[3]); + self.emit("call ".to_string() + func); + let ok_label = self.fresh_label("runtime_metapoint_filtered_pair_ok"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_epilogue(); + self.emit_label(&ok_label); + Ok(true) + } + + fn emit_runtime_c256_product_requirement_call(&mut self, func: &str, args: &[IrOperand]) -> Result { + if !matches!(func, "__c256_require_u128_product_lte" | "__c256_require_u128_product_eq") { + return Ok(false); + } + if args.len() != 4 { + return Ok(false); + } + + let scratch_base = self.runtime_scratch_buffer_offset(); + for (index, (register, arg)) in ["a0", "a1", "a2", "a3"].into_iter().zip(args.iter()).enumerate() { + let source = self.expected_fixed_byte_source(arg, 16); + match source { + Some(ExpectedFixedByteSource::Const(bytes)) => { + let value = u128::from_le_bytes(bytes.as_slice().try_into().expect("expected fixed u128 width")); + let buffer_offset = scratch_base + index * 16; + self.emit_store_fixed_byte_const_to_scratch( + &IrOperand::Const(IrConst::U128(value)), + self.runtime_scratch_size_offset(), + buffer_offset, + 16, + ); + self.emit_sp_addi(register, buffer_offset); + } + Some(source) => { + self.emit_prepare_fixed_byte_source(&source, 16, "runtime c256 u128 product operand"); + if !self.emit_fixed_byte_source_pointer_to(register, &source) { + self.emit(format!( + "# cellscript abi: c256 product operand {} is not addressable; pass null to fail closed", + index + )); + self.emit(format!("li {}, 0", register)); + } + } + None => { + self.emit(format!("# cellscript abi: c256 product operand {} is unavailable; pass null to fail closed", index)); + self.emit(format!("li {}, 0", register)); + } + } + } + + self.emit("call ".to_string() + func); + let ok_label = self.fresh_label("runtime_c256_requirement_ok"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_epilogue(); + self.emit_label(&ok_label); + Ok(true) + } + + fn emit_runtime_c256_sum2_product_requirement_call(&mut self, func: &str, args: &[IrOperand]) -> Result { + if !matches!(func, "__c256_require_u128_sum2_products_lte" | "__c256_require_u128_sum2_products_eq") { + return Ok(false); + } + if args.len() != 8 { + return Ok(false); + } + + let scratch_base = self.runtime_scratch_buffer_offset(); + for (index, (register, arg)) in ["a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7"].into_iter().zip(args.iter()).enumerate() { + let source = self.expected_fixed_byte_source(arg, 16); + match source { + Some(ExpectedFixedByteSource::Const(bytes)) => { + let value = u128::from_le_bytes(bytes.as_slice().try_into().expect("expected fixed u128 width")); + let buffer_offset = scratch_base + index * 16; + self.emit_store_fixed_byte_const_to_scratch( + &IrOperand::Const(IrConst::U128(value)), + self.runtime_scratch_size_offset(), + buffer_offset, + 16, + ); + self.emit_sp_addi(register, buffer_offset); + } + Some(source) => { + self.emit_prepare_fixed_byte_source(&source, 16, "runtime c256 sum-product operand"); + if !self.emit_fixed_byte_source_pointer_to(register, &source) { + self.emit(format!( + "# cellscript abi: c256 sum-product operand {} is not addressable; pass null to fail closed", + index + )); + self.emit(format!("li {}, 0", register)); + } + } + None => { + self.emit(format!( + "# cellscript abi: c256 sum-product operand {} is unavailable; pass null to fail closed", + index + )); + self.emit(format!("li {}, 0", register)); + } + } + } + + self.emit("call ".to_string() + func); + let ok_label = self.fresh_label("runtime_c256_sum_requirement_ok"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_epilogue(); + self.emit_label(&ok_label); + Ok(true) + } + + fn emit_call_param_arg( + &mut self, + func: &str, + param: &IrParam, + needs_type_hash: bool, + abi_index: &mut usize, + arg: &IrOperand, + outgoing_stack_arg_bytes: usize, + ) -> bool { + if let IrType::Named(name) = ¶m.ty + && let Some(layout) = self.enum_layouts.get(name).filter(|layout| layout.has_payload()) + { + let width = layout.encoded_size; + self.emit(format!( + "# cellscript abi: call {} payload enum param {} pointer={} length={} size={}", + func, + param.name, + abi_arg_label(*abi_index), + abi_arg_label(*abi_index + 1), + width + )); + if !self.emit_call_pointer_arg(func, ¶m.name, abi_index, arg, Some(width), outgoing_stack_arg_bytes) { + return false; + } + if !self.emit_call_length_arg(func, ¶m.name, abi_index, arg, CallLengthKind::FixedBytes, outgoing_stack_arg_bytes) { + return false; + } + return true; + } + if named_type_name(¶m.ty).is_some() { + self.emit(format!( + "# cellscript abi: call {} schema param {} pointer={} length={}", + func, + param.name, + abi_arg_label(*abi_index), + abi_arg_label(*abi_index + 1) + )); + if !self.emit_call_pointer_arg(func, ¶m.name, abi_index, arg, None, outgoing_stack_arg_bytes) { + return false; + } + if !self.emit_call_length_arg(func, ¶m.name, abi_index, arg, CallLengthKind::Schema, outgoing_stack_arg_bytes) { + return false; + } + if needs_type_hash { + self.emit(format!( + "# cellscript abi: call {} schema param {} type_hash pointer={} length={} size=32", + func, + param.name, + abi_arg_label(*abi_index), + abi_arg_label(*abi_index + 1) + )); + if !self.emit_call_type_hash_pointer_arg(func, ¶m.name, abi_index, arg, outgoing_stack_arg_bytes) { + return false; + } + if !self.emit_call_type_hash_length_arg(func, ¶m.name, abi_index, arg, outgoing_stack_arg_bytes) { + return false; + } + } + return true; + } + + let fixed_pointer_width = fixed_byte_pointer_param_width(¶m.ty).or_else(|| fixed_aggregate_pointer_param_width(¶m.ty)); + if let Some(width) = fixed_pointer_width { + self.emit(format!( + "# cellscript abi: call {} fixed-byte param {} pointer={} length={} size={}", + func, + param.name, + abi_arg_label(*abi_index), + abi_arg_label(*abi_index + 1), + width + )); + if !self.emit_call_pointer_arg(func, ¶m.name, abi_index, arg, Some(width), outgoing_stack_arg_bytes) { + return false; + } + if !self.emit_call_length_arg(func, ¶m.name, abi_index, arg, CallLengthKind::FixedBytes, outgoing_stack_arg_bytes) { + return false; + } + return true; + } + + self.emit_call_scalar_arg(func, ¶m.name, abi_index, arg, outgoing_stack_arg_bytes) + } + + fn emit_call_scalar_arg( + &mut self, + func: &str, + label: &str, + abi_index: &mut usize, + arg: &IrOperand, + outgoing_stack_arg_bytes: usize, + ) -> bool { + let register = self.call_abi_register(*abi_index); + self.emit(format!("# cellscript abi: call {} scalar {} -> {}", func, label, register)); + self.emit_operand_to_register(®ister, arg); + self.emit_outgoing_call_stack_arg_store(®ister, *abi_index, outgoing_stack_arg_bytes); + *abi_index += 1; + true + } + + fn emit_call_pointer_arg( + &mut self, + func: &str, + label: &str, + abi_index: &mut usize, + arg: &IrOperand, + const_width: Option, + outgoing_stack_arg_bytes: usize, + ) -> bool { + let register = self.call_abi_register(*abi_index); + if const_width.is_some() && matches!(arg, IrOperand::Const(_)) { + self.emit(format!( + "# cellscript abi: call {} pointer param {} uses a constant unsupported by the call ABI; pass null pointer", + func, label + )); + self.emit(format!("li {}, 0", register)); + } else { + self.emit_operand_to_register(®ister, arg); + } + self.emit_outgoing_call_stack_arg_store(®ister, *abi_index, outgoing_stack_arg_bytes); + *abi_index += 1; + true + } + + fn emit_call_length_arg( + &mut self, + func: &str, + label: &str, + abi_index: &mut usize, + arg: &IrOperand, + kind: CallLengthKind, + outgoing_stack_arg_bytes: usize, + ) -> bool { + let register = self.call_abi_register(*abi_index); + let size_offset = match (arg, kind) { + (IrOperand::Var(var), CallLengthKind::Schema) => self.schema_pointer_size_offsets.get(&var.id).copied(), + (IrOperand::Var(var), CallLengthKind::FixedBytes) => self.fixed_byte_param_size_offsets.get(&var.id).copied(), + _ => None, + }; + if let Some(size_offset) = size_offset { + self.emit_stack_load(®ister, size_offset); + } else if let (IrOperand::Var(var), CallLengthKind::FixedBytes) = (arg, kind) { + if let Some(width) = self.fixed_named_type_width(&var.ty) { + self.emit(format!("li {}, {}", register, width)); + } else { + self.emit(format!( + "# cellscript abi: call {} fixed-byte param {} has no tracked ABI length; pass zero length to fail closed", + func, label + )); + self.emit(format!("li {}, 0", register)); + } + } else if let CallLengthKind::FixedBytes = kind { + if matches!(arg, IrOperand::Const(_)) { + self.emit(format!( + "# cellscript abi: call {} fixed-byte const param {} has no materialized pointer; pass zero length to fail closed", + func, label + )); + self.emit(format!("li {}, 0", register)); + } else { + self.emit(format!( + "# cellscript abi: call {} fixed-byte param {} has no tracked ABI length; pass zero length to fail closed", + func, label + )); + self.emit(format!("li {}, 0", register)); + } + } else { + self.emit(format!( + "# cellscript abi: call {} schema param {} has no tracked ABI length; pass zero length to fail closed", + func, label + )); + self.emit(format!("li {}, 0", register)); + } + self.emit_outgoing_call_stack_arg_store(®ister, *abi_index, outgoing_stack_arg_bytes); + *abi_index += 1; + true + } + + fn emit_call_type_hash_pointer_arg( + &mut self, + func: &str, + label: &str, + abi_index: &mut usize, + arg: &IrOperand, + outgoing_stack_arg_bytes: usize, + ) -> bool { + let register = self.call_abi_register(*abi_index); + if let IrOperand::Var(var) = arg { + if let Some(pointer_offset) = self.param_type_hash_pointer_offsets.get(&var.id).copied() { + self.emit_stack_load(®ister, pointer_offset); + } else { + self.emit(format!( + "# cellscript abi: call {} schema param {} has no tracked TypeHash pointer; pass null pointer", + func, label + )); + self.emit(format!("li {}, 0", register)); + } + } else { + self.emit(format!( + "# cellscript abi: call {} schema param {} TypeHash source is not a variable; pass null pointer", + func, label + )); + self.emit(format!("li {}, 0", register)); + } + self.emit_outgoing_call_stack_arg_store(®ister, *abi_index, outgoing_stack_arg_bytes); + *abi_index += 1; + true + } + + fn emit_call_type_hash_length_arg( + &mut self, + func: &str, + label: &str, + abi_index: &mut usize, + arg: &IrOperand, + outgoing_stack_arg_bytes: usize, + ) -> bool { + let register = self.call_abi_register(*abi_index); + if let IrOperand::Var(var) = arg { + if let Some(size_offset) = self.param_type_hash_size_offsets.get(&var.id).copied() { + self.emit_stack_load(®ister, size_offset); + } else { + self.emit(format!( + "# cellscript abi: call {} schema param {} has no tracked TypeHash length; pass zero length to fail closed", + func, label + )); + self.emit(format!("li {}, 0", register)); + } + } else { + self.emit(format!( + "# cellscript abi: call {} schema param {} TypeHash length source is not a variable; pass zero length", + func, label + )); + self.emit(format!("li {}, 0", register)); + } + self.emit_outgoing_call_stack_arg_store(®ister, *abi_index, outgoing_stack_arg_bytes); + *abi_index += 1; + true + } + + fn emit_outgoing_call_stack_arg_store(&mut self, register: &str, abi_index: usize, outgoing_stack_arg_bytes: usize) { + if abi_index < 8 { + return; + } + let stack_slot_offset = (abi_index - 8) * 8; + let offset = i64::try_from(stack_slot_offset).expect("call stack slot should fit in i64") + - i64::try_from(outgoing_stack_arg_bytes).expect("call stack argument area should fit in i64"); + self.emit(format!( + "# cellscript abi: stage outgoing stack arg{} at pre-call sp{}{}", + abi_index, + if offset < 0 { "" } else { "+" }, + offset + )); + self.emit_sp_store_signed(register, offset); + } + + pub(super) fn emit_sp_store_signed(&mut self, register: &str, offset: i64) { + if small_signed_immediate(offset) { + self.emit(format!("sd {}, {}(sp)", register, offset)); + } else { + let scratch = scratch_register_avoiding(&[register]); + self.emit(format!("li {}, {}", scratch, offset)); + self.emit(format!("add {}, sp, {}", scratch, scratch)); + self.emit(format!("sd {}, 0({})", register, scratch)); + } + } + + fn call_abi_register(&self, abi_index: usize) -> String { + if abi_index < 8 { + format!("a{}", abi_index) + } else { + "t0".to_string() + } + } +} diff --git a/src/codegen/cell_ops.rs b/src/codegen/cell_ops.rs new file mode 100644 index 00000000..be2476c1 --- /dev/null +++ b/src/codegen/cell_ops.rs @@ -0,0 +1,2075 @@ +use super::*; + +pub(super) struct CellFieldHashLocation<'a> { + pub(super) reason: &'a str, + pub(super) source: u64, + pub(super) index: usize, +} + +pub(super) struct CellFieldHashCheck<'a> { + pub(super) left: CellFieldHashLocation<'a>, + pub(super) right: CellFieldHashLocation<'a>, + pub(super) cell_field: u64, + pub(super) field_name: &'a str, + pub(super) detail: &'a str, + pub(super) error: CellScriptRuntimeError, +} + +impl CodeGenerator { + pub(super) fn emit_mutate_replacement_field_hash_check( + &mut self, + pattern: &MutatePattern, + cell_field: u64, + field_name: &str, + error: CellScriptRuntimeError, + ) { + let input_size_offset = self.runtime_scratch_size_offset(); + let input_buffer_offset = self.runtime_scratch_buffer_offset(); + let output_size_offset = self.runtime_scratch2_size_offset(); + let output_buffer_offset = self.runtime_scratch2_buffer_offset(); + + self.emit_load_cell_by_field_syscall_to_offsets( + &format!("mutate_input_{}", field_name), + CKB_SOURCE_INPUT, + pattern.input_index, + cell_field, + input_size_offset, + input_buffer_offset, + 32, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_load_cell_by_field_syscall_to_offsets( + &format!("mutate_output_{}", field_name), + CKB_SOURCE_OUTPUT, + pattern.output_index, + cell_field, + output_size_offset, + output_buffer_offset, + 32, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_loaded_schema_exact_size_check(input_size_offset, 32, &format!("mutate input {}", field_name)); + self.emit_loaded_schema_exact_size_check(output_size_offset, 32, &format!("mutate output {}", field_name)); + self.emit(format!( + "# cellscript abi: verify mutate output {} {} Input#{} == Output#{} size=32", + pattern.ty, field_name, pattern.input_index, pattern.output_index + )); + self.emit_sp_addi("t4", input_buffer_offset); + self.emit_sp_addi("t5", output_buffer_offset); + for byte_index in 0..32 { + self.emit(format!("lbu t0, {}(t4)", byte_index)); + self.emit(format!("lbu t1, {}(t5)", byte_index)); + self.emit("sub t2, t0, t1"); + let ok_label = self.fresh_label("mutate_identity_byte_ok"); + self.emit(format!("beqz t2, {}", ok_label)); + self.emit_runtime_error_comment(error); + self.emit(format!("li a0, {}", error.code())); + self.emit_epilogue(); + self.emit_label(&ok_label); + } + } + + pub(super) fn emit_cell_metadata_equality(&mut self, left: &IrOperand, right: &IrOperand, field: CellMetadataField) -> Result<()> { + let Some((left_source, left_index)) = self.operand_cell_location(left) else { + self.emit("# cellscript abi: fail closed because left cell metadata source cannot be determined"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(()); + }; + let Some((right_source, right_index)) = self.operand_cell_location(right) else { + self.emit("# cellscript abi: fail closed because right cell metadata source cannot be determined"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(()); + }; + let (cell_field, field_name, width, mismatch_error) = match field { + CellMetadataField::LockHash => { + (CKB_CELL_FIELD_LOCK_HASH, "lock_hash", 32usize, CellScriptRuntimeError::LockHashPreservationMismatch) + } + CellMetadataField::Capacity => { + (CKB_CELL_FIELD_CAPACITY, "capacity", 8usize, CellScriptRuntimeError::CapacityPreservationMismatch) + } + }; + + let left_size_offset = self.runtime_scratch_size_offset(); + let left_buffer_offset = self.runtime_scratch_buffer_offset(); + let right_size_offset = self.runtime_scratch2_size_offset(); + let right_buffer_offset = self.runtime_scratch2_buffer_offset(); + + self.emit_load_cell_by_field_syscall_to_offsets( + &format!("cell_metadata_left_{}", field_name), + left_source, + left_index, + cell_field, + left_size_offset, + left_buffer_offset, + RUNTIME_SCRATCH_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_load_cell_by_field_syscall_to_offsets( + &format!("cell_metadata_right_{}", field_name), + right_source, + right_index, + cell_field, + right_size_offset, + right_buffer_offset, + RUNTIME_SCRATCH_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_loaded_schema_exact_size_check(left_size_offset, width, &format!("cell metadata left {}", field_name)); + self.emit_loaded_schema_exact_size_check(right_size_offset, width, &format!("cell metadata right {}", field_name)); + self.emit(format!( + "# cellscript abi: verify cell metadata {} equality {}#{} == {}#{} size={}", + field_name, + ckb_source_name(left_source), + left_index, + ckb_source_name(right_source), + right_index, + width + )); + self.emit_sp_addi("t4", left_buffer_offset); + self.emit_sp_addi("t5", right_buffer_offset); + for byte_index in 0..width { + self.emit(format!("lbu t0, {}(t4)", byte_index)); + self.emit(format!("lbu t1, {}(t5)", byte_index)); + self.emit("sub t2, t0, t1"); + let ok_label = self.fresh_label("cell_metadata_byte_ok"); + self.emit(format!("beqz t2, {}", ok_label)); + self.emit_runtime_error_comment(mismatch_error); + self.emit(format!("li a0, {}", mismatch_error.code())); + self.emit_epilogue(); + self.emit_label(&ok_label); + } + Ok(()) + } + + pub(super) fn emit_cell_field_hash_equality(&mut self, check: CellFieldHashCheck<'_>) { + let CellFieldHashCheck { left, right, cell_field, field_name, detail, error } = check; + let left_size_offset = self.runtime_scratch_size_offset(); + let left_buffer_offset = self.runtime_scratch_buffer_offset(); + let right_size_offset = self.runtime_scratch2_size_offset(); + let right_buffer_offset = self.runtime_scratch2_buffer_offset(); + + self.emit_load_cell_by_field_syscall_to_offsets( + left.reason, + left.source, + left.index, + cell_field, + left_size_offset, + left_buffer_offset, + 32, + ); + self.emit_return_on_syscall_error(error); + self.emit_load_cell_by_field_syscall_to_offsets( + right.reason, + right.source, + right.index, + cell_field, + right_size_offset, + right_buffer_offset, + 32, + ); + self.emit_return_on_syscall_error(error); + self.emit_loaded_schema_exact_size_check(left_size_offset, 32, &format!("{} {}", left.reason, field_name)); + self.emit_loaded_schema_exact_size_check(right_size_offset, 32, &format!("{} {}", right.reason, field_name)); + self.emit(format!( + "# cellscript abi: verify {} {} {}#{} == {}#{} size=32", + detail, + field_name, + ckb_source_name(left.source), + left.index, + ckb_source_name(right.source), + right.index + )); + self.emit_sp_addi("a0", left_buffer_offset); + self.emit_sp_addi("a1", right_buffer_offset); + self.emit("li a2, 32"); + self.emit("call __cellscript_memcmp_fixed"); + let ok_label = self.fresh_label("identity_hash_ok"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_runtime_error_comment(error); + self.emit(format!("li a0, {}", error.code())); + self.emit_epilogue(); + self.emit_label(&ok_label); + } + + pub(super) fn emit_output_type_hash_present_check(&mut self, output_index: usize, context: &str) { + let size_offset = self.runtime_scratch2_size_offset(); + let buffer_offset = self.runtime_scratch2_buffer_offset(); + self.emit_load_cell_by_field_syscall_to_offsets( + context, + CKB_SOURCE_OUTPUT, + output_index, + CKB_CELL_FIELD_TYPE_HASH, + size_offset, + buffer_offset, + 32, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::TypeHashMismatch); + self.emit_loaded_schema_exact_size_check(size_offset, 32, context); + self.emit(format!("# cellscript abi: verify {} Output#{} TypeHash is present size=32", context, output_index)); + } + + pub(super) fn emit_loaded_fixed_field_pointer_to_stack( + &mut self, + size_offset: usize, + buffer_offset: usize, + layout: &SchemaFieldLayout, + width: usize, + context: &str, + pointer_stack_offset: usize, + ) { + self.emit_loaded_schema_bounds_check(size_offset, layout.offset + width, context); + self.emit_sp_addi("t5", buffer_offset + layout.offset); + self.emit_stack_store("t5", pointer_stack_offset); + } + + pub(super) fn emit_dynamic_fixed_field_pointer_to_stack( + &mut self, + size_offset: usize, + buffer_offset: usize, + layout: &SchemaFieldLayout, + field_count: usize, + width: usize, + context: &str, + pointer_stack_offset: usize, + len_stack_offset: usize, + ) { + self.emit_dynamic_table_field_span_to_stack( + size_offset, + buffer_offset, + layout.index, + field_count, + context, + pointer_stack_offset, + len_stack_offset, + ); + self.emit_stack_load("t0", len_stack_offset); + self.emit(format!("li t1, {}", width)); + self.emit("sub t2, t0, t1"); + let ok_label = self.fresh_label("identity_field_len_ok"); + self.emit(format!("beqz t2, {}", ok_label)); + self.emit_runtime_error_comment(CellScriptRuntimeError::DynamicFieldValueMismatch); + self.emit(format!("li a0, {}", CellScriptRuntimeError::DynamicFieldValueMismatch.code())); + self.emit_epilogue(); + self.emit_label(&ok_label); + } + + pub(super) fn emit_fixed_pointer_equality( + &mut self, + left_pointer_stack_offset: usize, + right_pointer_stack_offset: usize, + width: usize, + context: &str, + error: CellScriptRuntimeError, + ) { + self.emit(format!("# cellscript abi: verify {} size={}", context, width)); + self.emit_stack_load("a0", left_pointer_stack_offset); + self.emit_stack_load("a1", right_pointer_stack_offset); + self.emit(format!("li a2, {}", width)); + self.emit("call __cellscript_memcmp_fixed"); + let ok_label = self.fresh_label("identity_field_ok"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_runtime_error_comment(error); + self.emit(format!("li a0, {}", error.code())); + self.emit_epilogue(); + self.emit_label(&ok_label); + } + + pub(super) fn operand_cell_location(&self, operand: &IrOperand) -> Option<(u64, usize)> { + let IrOperand::Var(var) = operand else { + return None; + }; + if let Some(input_index) = self.consume_indices.get(&var.id).copied() { + Some((CKB_SOURCE_INPUT, input_index)) + } else if let Some(output_index) = self.operation_output_indices.get(&var.id).copied() { + Some((CKB_SOURCE_OUTPUT, output_index)) + } else if let Some(dep_index) = self.read_ref_indices.get(&var.id).copied() { + Some((CKB_SOURCE_CELL_DEP, dep_index)) + } else if let Some(input_index) = self.read_ref_param_input_indices.get(&var.id).copied() { + Some((CKB_SOURCE_INPUT, input_index)) + } else { + self.read_ref_param_dep_indices.get(&var.id).copied().map(|dep_index| (CKB_SOURCE_CELL_DEP, dep_index)) + } + } + + pub(super) fn emit_destroy_group_output_absence_scan(&mut self, pattern: &CellPattern, input_index: usize) { + let input_size_offset = self.runtime_scratch_size_offset(); + let input_buffer_offset = self.runtime_scratch_buffer_offset(); + let output_size_offset = self.runtime_scratch2_size_offset(); + let output_buffer_offset = self.runtime_scratch2_buffer_offset(); + let loop_label = self.fresh_label("destroy_output_scan"); + let type_hash_label = self.fresh_label("destroy_output_type_hash"); + let next_label = self.fresh_label("destroy_output_next"); + let done_label = self.fresh_label("destroy_output_done"); + + self.emit(format!("# cellscript abi: destroy output type-hash absence scan binding={} size=32", pattern.binding)); + self.emit_load_cell_by_field_syscall_to_offsets( + "destroy_input_type_hash", + CKB_SOURCE_INPUT, + input_index, + CKB_CELL_FIELD_TYPE_HASH, + input_size_offset, + input_buffer_offset, + 32, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_loaded_schema_exact_size_check(input_size_offset, 32, "destroy input type hash"); + self.emit("li t6, 0"); + self.emit_label(&loop_label); + self.emit_load_cell_by_field_syscall_to_offsets_dynamic_index( + "destroy_output_type_hash", + CKB_SOURCE_OUTPUT, + "t6", + CKB_CELL_FIELD_TYPE_HASH, + output_size_offset, + output_buffer_offset, + 32, + ); + self.emit(format!("beqz a0, {}", type_hash_label)); + self.emit(format!("li t0, {}", CKB_INDEX_OUT_OF_BOUND)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", done_label)); + self.emit(format!("li t0, {}", CKB_ITEM_MISSING)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", next_label)); + self.emit_fail(CellScriptRuntimeError::DynamicFieldBoundsInvalid); + + self.emit_label(&type_hash_label); + self.emit_loaded_schema_exact_size_check(output_size_offset, 32, "destroy output type hash"); + self.emit(format!("# cellscript abi: reject destroy successor when Output#t6 TypeHash matches consumed {}", pattern.binding)); + self.emit_sp_addi("t4", output_buffer_offset); + self.emit_sp_addi("t5", input_buffer_offset); + for byte_index in 0..32 { + self.emit(format!("lbu t0, {}(t4)", byte_index)); + self.emit(format!("lbu t1, {}(t5)", byte_index)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", next_label)); + } + self.emit_fail(CellScriptRuntimeError::DynamicFieldBoundsInvalid); + + self.emit_label(&next_label); + self.emit("addi t6, t6, 1"); + self.emit(format!("j {}", loop_label)); + self.emit_label(&done_label); + self.emit("li a0, 0"); + } + + pub(super) fn mutate_preserved_field_layouts(&self, pattern: &MutatePattern) -> Vec<(String, SchemaFieldLayout, usize)> { + let Some(type_size) = self.type_fixed_sizes.get(&pattern.ty).copied() else { + return Vec::new(); + }; + if type_size > RUNTIME_SCRATCH_BUFFER_SIZE { + return Vec::new(); + } + pattern + .preserved_fields + .iter() + .filter_map(|field| { + let layout = self.type_layouts.get(&pattern.ty).and_then(|fields| fields.get(field)).cloned()?; + let width = layout_fixed_byte_width(&layout)?; + (layout.offset + width <= RUNTIME_SCRATCH_BUFFER_SIZE).then(|| (field.clone(), layout, width)) + }) + .collect() + } + + pub(super) fn mutate_transition_exclusion_ranges(&self, pattern: &MutatePattern) -> Option> { + if pattern.transitions.len() != pattern.fields.len() { + return None; + } + let type_size = self.type_fixed_sizes.get(&pattern.ty).copied()?; + if type_size > RUNTIME_SCRATCH_BUFFER_SIZE { + return None; + } + let mut ranges = Vec::new(); + for transition in &pattern.transitions { + let layout = self.type_layouts.get(&pattern.ty).and_then(|fields| fields.get(&transition.field))?; + let width = layout_fixed_byte_width(layout)?; + if layout.offset + width > RUNTIME_SCRATCH_BUFFER_SIZE { + return None; + } + ranges.push((layout.offset, layout.offset + width)); + } + ranges.sort_unstable(); + let mut merged: Vec<(usize, usize)> = Vec::new(); + for (start, end) in ranges { + if start >= end { + continue; + } + if let Some(last) = merged.last_mut() + && start <= last.1 + { + last.1 = last.1.max(end); + continue; + } + merged.push((start, end)); + } + Some(merged) + } + + pub(super) fn emit_mutate_replacement_preserved_field_checks(&mut self, pattern: &MutatePattern) { + let preserved_fields = self.mutate_preserved_field_layouts(pattern); + if !pattern.preserved_fields.is_empty() && preserved_fields.len() != pattern.preserved_fields.len() { + if self.emit_mutate_replacement_dynamic_table_preserved_field_checks(pattern) { + return; + } + if self.emit_mutate_replacement_data_except_transition_checks(pattern) { + return; + } + self.emit("# cellscript abi: fail closed because not all preserved fields are verifier-addressable"); + self.emit_fail(CellScriptRuntimeError::FieldPreservationMismatch); + return; + } + if preserved_fields.is_empty() { + return; + } + let input_size_offset = self.runtime_scratch_size_offset(); + let input_buffer_offset = self.runtime_scratch_buffer_offset(); + let output_size_offset = self.runtime_scratch2_size_offset(); + let output_buffer_offset = self.runtime_scratch2_buffer_offset(); + self.emit_load_cell_data_syscall_to_offsets( + "mutate_input_data", + CKB_SOURCE_INPUT, + pattern.input_index, + input_size_offset, + input_buffer_offset, + RUNTIME_SCRATCH_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_load_cell_data_syscall_to_offsets( + "mutate_output_data", + CKB_SOURCE_OUTPUT, + pattern.output_index, + output_size_offset, + output_buffer_offset, + RUNTIME_SCRATCH_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + if let Some(expected_size) = self.type_fixed_sizes.get(&pattern.ty).copied() { + self.emit_loaded_schema_exact_size_check(input_size_offset, expected_size, &format!("{} mutate input", pattern.ty)); + self.emit_loaded_schema_exact_size_check(output_size_offset, expected_size, &format!("{} mutate output", pattern.ty)); + } + self.emit(format!( + "# cellscript abi: verify mutate preserved fields {} Input#{} == Output#{}", + pattern.ty, pattern.input_index, pattern.output_index + )); + self.emit_sp_addi("t4", input_buffer_offset); + self.emit_sp_addi("t5", output_buffer_offset); + for (field, layout, width) in preserved_fields { + self.emit_loaded_schema_bounds_check(input_size_offset, layout.offset + width, &format!("{} input.{}", pattern.ty, field)); + self.emit_loaded_schema_bounds_check( + output_size_offset, + layout.offset + width, + &format!("{} output.{}", pattern.ty, field), + ); + self.emit(format!( + "# cellscript abi: verify mutate preserved field {}.{} Input#{} == Output#{} offset={} size={}", + pattern.ty, field, pattern.input_index, pattern.output_index, layout.offset, width + )); + let mismatch_label = self.fresh_label("mutate_preserved_byte_mismatch"); + for byte_index in 0..width { + self.emit(format!("lbu t0, {}(t4)", layout.offset + byte_index)); + self.emit(format!("lbu t1, {}(t5)", layout.offset + byte_index)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", mismatch_label)); + } + self.emit_fixed_byte_mismatch_fail(&mismatch_label, CellScriptRuntimeError::FieldPreservationMismatch); + } + } + + pub(super) fn emit_mutate_replacement_dynamic_table_preserved_field_checks(&mut self, pattern: &MutatePattern) -> bool { + if self.type_fixed_sizes.contains_key(&pattern.ty) || pattern.preserved_fields.is_empty() { + return false; + } + let Some(layouts) = self.type_layouts.get(&pattern.ty).cloned() else { + return false; + }; + let field_count = layouts.len(); + if field_count == 0 || !pattern.preserved_fields.iter().all(|field| layouts.contains_key(field)) { + return false; + } + + let input_size_offset = self.runtime_scratch_size_offset(); + let input_buffer_offset = self.runtime_scratch_buffer_offset(); + let output_size_offset = self.runtime_scratch2_size_offset(); + let output_buffer_offset = self.runtime_scratch2_buffer_offset(); + self.emit_load_cell_data_syscall_to_offsets( + "mutate_input_table_preserved", + CKB_SOURCE_INPUT, + pattern.input_index, + input_size_offset, + input_buffer_offset, + RUNTIME_SCRATCH_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_load_cell_data_syscall_to_offsets( + "mutate_output_table_preserved", + CKB_SOURCE_OUTPUT, + pattern.output_index, + output_size_offset, + output_buffer_offset, + RUNTIME_SCRATCH_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit(format!( + "# cellscript abi: verify mutate preserved Molecule table fields {} Input#{} == Output#{}", + pattern.ty, pattern.input_index, pattern.output_index + )); + for field in &pattern.preserved_fields { + let Some(layout) = layouts.get(field).cloned() else { + return false; + }; + self.emit_dynamic_table_field_equality_check( + &pattern.ty, + field, + &layout, + field_count, + input_size_offset, + input_buffer_offset, + output_size_offset, + output_buffer_offset, + CellScriptRuntimeError::FieldPreservationMismatch, + ); + } + true + } + + #[allow(clippy::too_many_arguments)] + pub(super) fn emit_dynamic_table_field_equality_check( + &mut self, + type_name: &str, + field: &str, + layout: &SchemaFieldLayout, + field_count: usize, + input_size_offset: usize, + input_buffer_offset: usize, + output_size_offset: usize, + output_buffer_offset: usize, + fail_code: CellScriptRuntimeError, + ) { + let start_offset = self.runtime_expr_temp_offset(0); + let len_offset = self.runtime_expr_temp_offset(1); + let output_start_offset = self.runtime_expr_temp_offset(2); + if let Some(width) = layout_fixed_byte_width(layout) { + self.emit_dynamic_table_fixed_field_pointer_to_stack( + input_size_offset, + input_buffer_offset, + layout, + width, + &format!("{} input.{}", type_name, field), + start_offset, + ); + self.emit_dynamic_table_fixed_field_pointer_to_stack( + output_size_offset, + output_buffer_offset, + layout, + width, + &format!("{} output.{}", type_name, field), + output_start_offset, + ); + self.emit(format!("li t0, {}", width)); + self.emit_stack_store("t0", len_offset); + } else { + self.emit_dynamic_table_field_span_to_stack( + input_size_offset, + input_buffer_offset, + layout.index, + field_count, + &format!("{} input.{}", type_name, field), + start_offset, + len_offset, + ); + self.emit_dynamic_table_field_span_to_stack( + output_size_offset, + output_buffer_offset, + layout.index, + field_count, + &format!("{} output.{}", type_name, field), + output_start_offset, + self.runtime_expr_temp_offset(3), + ); + self.emit_stack_load("t0", len_offset); + self.emit_stack_load("t1", self.runtime_expr_temp_offset(3)); + self.emit("sub t2, t0, t1"); + let len_ok = self.fresh_label("mutate_table_field_len_ok"); + self.emit(format!("beqz t2, {}", len_ok)); + self.emit_fail(fail_code); + self.emit_label(&len_ok); + } + + self.emit(format!( + "# cellscript abi: verify mutate preserved Molecule table field {}.{} Input#{} == Output#{}", + type_name, field, 0, 1 + )); + let mismatch_label = self.fresh_label("mutate_table_field_mismatch"); + self.emit_stack_load("a0", start_offset); + self.emit_stack_load("a1", output_start_offset); + self.emit_stack_load("a2", len_offset); + self.emit("call __cellscript_memcmp_fixed"); + self.emit(format!("bnez a0, {}", mismatch_label)); + self.emit_fixed_byte_mismatch_fail(&mismatch_label, fail_code); + } + + pub(super) fn emit_dynamic_table_field_span_to_stack( + &mut self, + size_offset: usize, + buffer_offset: usize, + field_index: usize, + field_count: usize, + context: &str, + start_stack_offset: usize, + len_stack_offset: usize, + ) { + self.emit_sp_addi("t4", buffer_offset); + self.emit_molecule_table_field_span_to_t5_t6("t4", size_offset, field_index, field_count, context); + self.emit_sp_addi("t4", buffer_offset); + self.emit("add t5, t4, t5"); + self.emit("add t6, t4, t6"); + self.emit("sub t0, t6, t5"); + self.emit_stack_store("t5", start_stack_offset); + self.emit_stack_store("t0", len_stack_offset); + } + + pub(super) fn emit_dynamic_table_fixed_field_pointer_to_stack( + &mut self, + size_offset: usize, + buffer_offset: usize, + layout: &SchemaFieldLayout, + width: usize, + context: &str, + start_stack_offset: usize, + ) { + self.emit_sp_addi("t4", buffer_offset); + self.emit_molecule_table_field_bounds_to_t5("t4", size_offset, layout.index, width, context); + self.emit_sp_addi("t4", buffer_offset); + self.emit("add t5, t4, t5"); + self.emit_stack_store("t5", start_stack_offset); + } + + pub(super) fn emit_mutate_replacement_dynamic_table_append_checks(&mut self, pattern: &MutatePattern) -> bool { + if self.type_fixed_sizes.contains_key(&pattern.ty) || pattern.transitions.is_empty() { + return false; + } + let Some(layouts) = self.type_layouts.get(&pattern.ty).cloned() else { + return false; + }; + let field_count = layouts.len(); + let appends = pattern + .transitions + .iter() + .filter_map(|transition| { + if transition.op != MutateTransitionOp::Append { + return None; + } + let layout = layouts.get(&transition.field).cloned()?; + let element_width = molecule_vector_element_fixed_width(&layout.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes)?; + self.fixed_append_fields(&transition.operand, element_width) + .map(|fields| (transition.clone(), layout, element_width, fields)) + }) + .collect::>(); + if appends.len() != pattern.transitions.len() { + return false; + } + + let input_size_offset = self.runtime_scratch_size_offset(); + let input_buffer_offset = self.runtime_scratch_buffer_offset(); + let output_size_offset = self.runtime_scratch2_size_offset(); + let output_buffer_offset = self.runtime_scratch2_buffer_offset(); + self.emit_load_cell_data_syscall_to_offsets( + "mutate_input_table_append", + CKB_SOURCE_INPUT, + pattern.input_index, + input_size_offset, + input_buffer_offset, + RUNTIME_SCRATCH_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_load_cell_data_syscall_to_offsets( + "mutate_output_table_append", + CKB_SOURCE_OUTPUT, + pattern.output_index, + output_size_offset, + output_buffer_offset, + RUNTIME_SCRATCH_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit(format!( + "# cellscript abi: verify mutate Molecule table append fields {} Input#{} -> Output#{}", + pattern.ty, pattern.input_index, pattern.output_index + )); + for (transition, layout, element_width, fields) in appends { + self.emit_dynamic_table_vector_append_check( + &pattern.ty, + &transition.field, + &layout, + field_count, + element_width, + &fields, + input_size_offset, + input_buffer_offset, + output_size_offset, + output_buffer_offset, + ); + } + true + } + + pub(super) fn fixed_append_fields( + &self, + operand: &IrOperand, + expected_width: usize, + ) -> Option> { + if self.expected_fixed_byte_source(operand, expected_width).is_some() { + let ty = match operand { + IrOperand::Var(var) => var.ty.clone(), + IrOperand::Const(IrConst::Address(_)) => IrType::Address, + IrOperand::Const(IrConst::Hash(_)) => IrType::Hash, + IrOperand::Const(IrConst::Array(items)) => IrType::Array(Box::new(IrType::U8), items.len()), + IrOperand::Const(_) => return None, + }; + return Some(vec![( + operand.clone(), + SchemaFieldLayout { index: 0, offset: 0, ty, fixed_size: Some(expected_width), fixed_enum_size: None }, + expected_width, + )]); + } + let IrOperand::Var(var) = operand else { + return None; + }; + let fields = self.tuple_aggregate_fields.get(&var.id)?; + let type_name = named_type_name(&var.ty)?; + let mut layouts = self.type_layouts.get(type_name)?.values().cloned().collect::>(); + layouts.sort_by_key(|layout| layout.offset); + if layouts.len() != fields.len() { + return None; + } + let total_width = self.type_fixed_sizes.get(type_name).copied()?; + if total_width != expected_width { + return None; + } + fields + .iter() + .cloned() + .zip(layouts) + .map(|(field_operand, layout)| { + let width = layout_fixed_byte_width(&layout)?; + self.expected_fixed_byte_source(&field_operand, width)?; + Some((field_operand, layout, width)) + }) + .collect() + } + + #[allow(clippy::too_many_arguments)] + pub(super) fn emit_dynamic_table_vector_append_check( + &mut self, + type_name: &str, + field: &str, + layout: &SchemaFieldLayout, + field_count: usize, + element_width: usize, + fields: &[(IrOperand, SchemaFieldLayout, usize)], + input_size_offset: usize, + input_buffer_offset: usize, + output_size_offset: usize, + output_buffer_offset: usize, + ) { + let input_start_offset = self.runtime_expr_temp_offset(0); + let input_len_offset = self.runtime_expr_temp_offset(1); + let output_start_offset = self.runtime_expr_temp_offset(2); + let output_len_offset = self.runtime_expr_temp_offset(3); + self.emit_dynamic_table_field_span_to_stack( + input_size_offset, + input_buffer_offset, + layout.index, + field_count, + &format!("{} input.{}", type_name, field), + input_start_offset, + input_len_offset, + ); + self.emit_dynamic_table_field_span_to_stack( + output_size_offset, + output_buffer_offset, + layout.index, + field_count, + &format!("{} output.{}", type_name, field), + output_start_offset, + output_len_offset, + ); + self.emit(format!( + "# cellscript abi: verify mutate Molecule vector append {}.{} element_size={}", + type_name, field, element_width + )); + self.emit_loaded_schema_bounds_check(input_len_offset, 4, &format!("{} input.{} vector", type_name, field)); + self.emit_loaded_schema_bounds_check(output_len_offset, 4 + element_width, &format!("{} output.{} vector", type_name, field)); + + self.emit_stack_load("t4", input_start_offset); + self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, 4); + self.emit_stack_load("t1", input_len_offset); + self.emit(format!("li t2, {}", element_width)); + self.emit("mul t3, t0, t2"); + self.emit("addi t3, t3, 4"); + self.emit("sub t2, t1, t3"); + let input_size_ok = self.fresh_label("molecule_append_input_size_ok"); + self.emit(format!("beqz t2, {}", input_size_ok)); + self.emit_fail(CellScriptRuntimeError::MutateTransitionMismatch); + self.emit_label(&input_size_ok); + + self.emit_stack_load("t4", output_start_offset); + self.emit_unaligned_scalar_load("t4", "t1", "t2", 0, 4); + self.emit("addi t0, t0, 1"); + self.emit("sub t2, t1, t0"); + let count_ok = self.fresh_label("molecule_append_count_ok"); + self.emit(format!("beqz t2, {}", count_ok)); + self.emit_fail(CellScriptRuntimeError::MutateTransitionMismatch); + self.emit_label(&count_ok); + + self.emit_stack_load("t0", input_len_offset); + self.emit(format!("li t1, {}", element_width)); + self.emit("add t0, t0, t1"); + self.emit_stack_load("t1", output_len_offset); + self.emit("sub t2, t1, t0"); + let len_ok = self.fresh_label("molecule_append_len_ok"); + self.emit(format!("beqz t2, {}", len_ok)); + self.emit_fail(CellScriptRuntimeError::MutateTransitionMismatch); + self.emit_label(&len_ok); + + let prefix_ok = self.fresh_label("molecule_append_prefix_ok"); + self.emit_stack_load("a0", input_start_offset); + self.emit("addi a0, a0, 4"); + self.emit_stack_load("a1", output_start_offset); + self.emit("addi a1, a1, 4"); + self.emit_stack_load("a2", input_len_offset); + self.emit("addi a2, a2, -4"); + self.emit("call __cellscript_memcmp_fixed"); + self.emit(format!("beqz a0, {}", prefix_ok)); + self.emit_fail(CellScriptRuntimeError::MutateTransitionMismatch); + self.emit_label(&prefix_ok); + + self.emit_stack_load("t0", output_start_offset); + self.emit_stack_load("t1", input_len_offset); + self.emit("add t0, t0, t1"); + self.emit_stack_store("t0", output_start_offset); + for (operand, field_layout, width) in fields { + let Some(source) = self.expected_fixed_byte_source(operand, *width) else { + self.emit_fail(CellScriptRuntimeError::MutateTransitionMismatch); + continue; + }; + self.emit_prepare_fixed_byte_source(&source, *width, &format!("append {}.{}", type_name, field)); + self.emit_pointer_fixed_bytes_against_source( + output_start_offset, + field_layout.offset, + &source, + *width, + CellScriptRuntimeError::MutateTransitionMismatch, + ); + } + } + + pub(super) fn emit_pointer_fixed_bytes_against_source( + &mut self, + output_pointer_stack_offset: usize, + output_field_offset: usize, + source: &ExpectedFixedByteSource, + width: usize, + fail_code: CellScriptRuntimeError, + ) { + let mismatch_label = self.fresh_label("fixed_byte_mismatch"); + match source { + ExpectedFixedByteSource::Const(bytes) => { + self.emit_stack_load("t4", output_pointer_stack_offset); + for (byte_index, byte) in bytes.iter().take(width).enumerate() { + self.emit(format!("lbu t0, {}(t4)", output_field_offset + byte_index)); + self.emit(format!("li t1, {}", byte)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", mismatch_label)); + } + } + ExpectedFixedByteSource::SchemaField(source) => { + if self.emit_schema_field_source_pointer_to("a1", source, width) { + self.emit_stack_load("a0", output_pointer_stack_offset); + if output_field_offset != 0 { + self.emit_large_addi("a0", "a0", output_field_offset as i64); + } + self.emit(format!("li a2, {}", width)); + self.emit("call __cellscript_memcmp_fixed"); + self.emit(format!("bnez a0, {}", mismatch_label)); + } else { + self.emit_fail(CellScriptRuntimeError::DynamicFieldBoundsInvalid); + } + } + ExpectedFixedByteSource::StackSlot { var_id, .. } => { + self.emit_stack_load("a0", output_pointer_stack_offset); + if output_field_offset != 0 { + self.emit_large_addi("a0", "a0", output_field_offset as i64); + } + self.emit_sp_addi("a1", var_id * 8); + self.emit(format!("li a2, {}", width)); + self.emit("call __cellscript_memcmp_fixed"); + self.emit(format!("bnez a0, {}", mismatch_label)); + } + ExpectedFixedByteSource::PointerBytes { var_id, .. } + | ExpectedFixedByteSource::ParamBytes { var_id, .. } + | ExpectedFixedByteSource::LoadedBytes { var_id, .. } => { + self.emit_stack_load("a0", output_pointer_stack_offset); + if output_field_offset != 0 { + self.emit_large_addi("a0", "a0", output_field_offset as i64); + } + self.emit_stack_load("a1", var_id * 8); + self.emit(format!("li a2, {}", width)); + self.emit("call __cellscript_memcmp_fixed"); + self.emit(format!("bnez a0, {}", mismatch_label)); + } + } + self.emit_fixed_byte_mismatch_fail(&mismatch_label, fail_code); + } + + pub(super) fn emit_mutate_replacement_data_except_transition_checks(&mut self, pattern: &MutatePattern) -> bool { + let Some(exclusion_ranges) = self.mutate_transition_exclusion_ranges(pattern) else { + return false; + }; + if exclusion_ranges.is_empty() { + return false; + } + let input_size_offset = self.runtime_scratch_size_offset(); + let input_buffer_offset = self.runtime_scratch_buffer_offset(); + let output_size_offset = self.runtime_scratch2_size_offset(); + let output_buffer_offset = self.runtime_scratch2_buffer_offset(); + self.emit_load_cell_data_syscall_to_offsets( + "mutate_input_preserved_data", + CKB_SOURCE_INPUT, + pattern.input_index, + input_size_offset, + input_buffer_offset, + RUNTIME_SCRATCH_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_load_cell_data_syscall_to_offsets( + "mutate_output_preserved_data", + CKB_SOURCE_OUTPUT, + pattern.output_index, + output_size_offset, + output_buffer_offset, + RUNTIME_SCRATCH_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + if let Some(expected_size) = self.type_fixed_sizes.get(&pattern.ty).copied() { + self.emit_loaded_schema_exact_size_check( + input_size_offset, + expected_size, + &format!("{} preserved-data input", pattern.ty), + ); + self.emit_loaded_schema_exact_size_check( + output_size_offset, + expected_size, + &format!("{} preserved-data output", pattern.ty), + ); + } + let size_ok_label = self.fresh_label("mutate_preserved_data_size_ok"); + self.emit_stack_load("t0", input_size_offset); + self.emit_stack_load("t1", output_size_offset); + self.emit("sub t2, t0, t1"); + self.emit(format!("beqz t2, {}", size_ok_label)); + self.emit_fail(CellScriptRuntimeError::FieldPreservationMismatch); + self.emit_label(&size_ok_label); + + self.emit(format!( + "# cellscript abi: verify mutate preserved data {} Input#{} == Output#{} except transition ranges {:?}", + pattern.ty, pattern.input_index, pattern.output_index, exclusion_ranges + )); + let loop_label = self.fresh_label("mutate_preserved_data_loop"); + let compare_label = self.fresh_label("mutate_preserved_data_compare"); + let skip_label = self.fresh_label("mutate_preserved_data_skip"); + let done_label = self.fresh_label("mutate_preserved_data_done"); + let mismatch_label = self.fresh_label("mutate_preserved_data_mismatch"); + self.emit_sp_addi("a3", input_buffer_offset); + self.emit_sp_addi("a4", output_buffer_offset); + self.emit("li t6, 0"); + self.emit_label(&loop_label); + self.emit("sltu t2, t6, t0"); + self.emit(format!("beqz t2, {}", done_label)); + for (range_index, (start, end)) in exclusion_ranges.iter().enumerate() { + let next_range_label = self.fresh_label(&format!("mutate_preserved_data_next_range_{}", range_index)); + self.emit(format!("li t3, {}", start)); + self.emit("sltu t2, t6, t3"); + self.emit(format!("bnez t2, {}", compare_label)); + self.emit(format!("li t3, {}", end)); + self.emit("sltu t2, t6, t3"); + self.emit(format!("beqz t2, {}", next_range_label)); + self.emit(format!("j {}", skip_label)); + self.emit_label(&next_range_label); + } + self.emit_label(&compare_label); + self.emit("add t3, a3, t6"); + self.emit("lbu t4, 0(t3)"); + self.emit("add t3, a4, t6"); + self.emit("lbu t5, 0(t3)"); + self.emit("sub t2, t4, t5"); + self.emit(format!("bnez t2, {}", mismatch_label)); + self.emit_label(&skip_label); + self.emit("addi t6, t6, 1"); + self.emit(format!("j {}", loop_label)); + self.emit_label(&mismatch_label); + self.emit_fail(CellScriptRuntimeError::FieldPreservationMismatch); + self.emit_label(&done_label); + true + } + + pub(super) fn mutate_u128_transition_layouts(&self, pattern: &MutatePattern) -> Vec<(MutateFieldTransition, SchemaFieldLayout)> { + let Some(type_size) = self.type_fixed_sizes.get(&pattern.ty).copied() else { + return Vec::new(); + }; + if type_size > RUNTIME_SCRATCH_BUFFER_SIZE { + return Vec::new(); + } + pattern + .transitions + .iter() + .filter_map(|transition| { + if transition.op == MutateTransitionOp::Set { + return None; + } + let layout = self.type_layouts.get(&pattern.ty).and_then(|fields| fields.get(&transition.field)).cloned()?; + // Only u128 fields (16 bytes) that don't fit in a single register. + if layout.ty != IrType::U128 || layout.fixed_size != Some(16) { + return None; + } + if layout.offset + 16 > RUNTIME_SCRATCH_BUFFER_SIZE { + return None; + } + // u128 transition: the operand must be a u64 value (delta always fits in 64 bits). + self.prelude_u64_operand_source(&transition.operand)?; + Some((transition.clone(), layout)) + }) + .collect() + } + + pub(super) fn mutate_transition_layouts(&self, pattern: &MutatePattern) -> Vec<(MutateFieldTransition, SchemaFieldLayout, usize)> { + let Some(type_size) = self.type_fixed_sizes.get(&pattern.ty).copied() else { + return Vec::new(); + }; + if type_size > RUNTIME_SCRATCH_BUFFER_SIZE { + return Vec::new(); + } + pattern + .transitions + .iter() + .filter_map(|transition| { + if transition.op == MutateTransitionOp::Set { + return None; + } + let layout = self.type_layouts.get(&pattern.ty).and_then(|fields| fields.get(&transition.field)).cloned()?; + let width = fixed_register_width(&layout.ty, layout.fixed_size)?; + if layout.offset + width > RUNTIME_SCRATCH_BUFFER_SIZE { + return None; + } + self.prelude_u64_operand_source(&transition.operand)?; + Some((transition.clone(), layout, width)) + }) + .collect() + } + + pub(super) fn mutate_set_transition_layouts( + &self, + pattern: &MutatePattern, + ) -> Vec<(MutateFieldTransition, SchemaFieldLayout, usize)> { + let Some(type_size) = self.type_fixed_sizes.get(&pattern.ty).copied() else { + return Vec::new(); + }; + if type_size > RUNTIME_SCRATCH_BUFFER_SIZE { + return Vec::new(); + } + pattern + .transitions + .iter() + .filter_map(|transition| { + if transition.op != MutateTransitionOp::Set { + return None; + } + let layout = self.type_layouts.get(&pattern.ty).and_then(|fields| fields.get(&transition.field)).cloned()?; + let width = layout_fixed_byte_width(&layout)?; + if layout.offset + width > RUNTIME_SCRATCH_BUFFER_SIZE { + return None; + } + if layout_fixed_scalar_width(&layout).is_none() + && self.expected_fixed_byte_source(&transition.operand, width).is_none() + { + return None; + } + Some((transition.clone(), layout, width)) + }) + .collect() + } + + pub(super) fn emit_mutate_replacement_transition_checks(&mut self, pattern: &MutatePattern) { + if self.emit_mutate_replacement_dynamic_table_append_checks(pattern) { + return; + } + if self.emit_mutate_replacement_dynamic_table_transition_checks(pattern) { + return; + } + let transitions = self.mutate_transition_layouts(pattern); + if transitions.is_empty() { + return; + } + let input_size_offset = self.runtime_scratch_size_offset(); + let input_buffer_offset = self.runtime_scratch_buffer_offset(); + let output_size_offset = self.runtime_scratch2_size_offset(); + let output_buffer_offset = self.runtime_scratch2_buffer_offset(); + self.emit_load_cell_data_syscall_to_offsets( + "mutate_input_transition", + CKB_SOURCE_INPUT, + pattern.input_index, + input_size_offset, + input_buffer_offset, + RUNTIME_SCRATCH_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_load_cell_data_syscall_to_offsets( + "mutate_output_transition", + CKB_SOURCE_OUTPUT, + pattern.output_index, + output_size_offset, + output_buffer_offset, + RUNTIME_SCRATCH_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + if let Some(expected_size) = self.type_fixed_sizes.get(&pattern.ty).copied() { + self.emit_loaded_schema_exact_size_check( + input_size_offset, + expected_size, + &format!("{} mutate transition input", pattern.ty), + ); + self.emit_loaded_schema_exact_size_check( + output_size_offset, + expected_size, + &format!("{} mutate transition output", pattern.ty), + ); + } + self.emit(format!( + "# cellscript abi: verify mutate transition fields {} Input#{} -> Output#{}", + pattern.ty, pattern.input_index, pattern.output_index + )); + for (transition, layout, width) in transitions { + let Some(delta) = self.prelude_u64_operand_source(&transition.operand) else { + continue; + }; + self.emit_loaded_schema_bounds_check( + input_size_offset, + layout.offset + width, + &format!("{} input.{}", pattern.ty, transition.field), + ); + self.emit_loaded_schema_bounds_check( + output_size_offset, + layout.offset + width, + &format!("{} output.{}", pattern.ty, transition.field), + ); + self.emit(format!( + "# cellscript abi: verify mutate transition field {}.{} {:?} Input#{} -> Output#{} offset={} size={}", + pattern.ty, transition.field, transition.op, pattern.input_index, pattern.output_index, layout.offset, width + )); + self.emit_sp_addi("t4", input_buffer_offset); + self.emit_unaligned_scalar_load("t4", "t0", "t2", layout.offset, width); + let input_value_offset = self.runtime_expr_temp_offset(RUNTIME_EXPR_TEMP_SLOTS - 2); + self.emit("# cellscript abi: preserve mutate input scalar before transition expression"); + self.emit_stack_store("t0", input_value_offset); + self.emit_prelude_u64_operand_source_to_t1(&delta); + self.emit_stack_load("t0", input_value_offset); + match transition.op { + MutateTransitionOp::Add => self.emit("add t1, t0, t1"), + MutateTransitionOp::Sub => self.emit("sub t1, t0, t1"), + MutateTransitionOp::Set => { + unreachable!("set transitions are verified by emit_mutate_replacement_set_transition_checks") + } + MutateTransitionOp::Append => { + unreachable!("append transitions are verified by emit_mutate_replacement_dynamic_table_append_checks") + } + } + let expected_value_offset = self.runtime_expr_temp_offset(RUNTIME_EXPR_TEMP_SLOTS - 1); + self.emit("# cellscript abi: preserve mutate expected scalar across output field load"); + self.emit_stack_store("t1", expected_value_offset); + self.emit_sp_addi("t4", output_buffer_offset); + self.emit_unaligned_scalar_load("t4", "t0", "t2", layout.offset, width); + self.emit_stack_load("t1", expected_value_offset); + self.emit("sub t2, t0, t1"); + let ok_label = self.fresh_label("mutate_transition_ok"); + self.emit(format!("beqz t2, {}", ok_label)); + self.emit_fail(CellScriptRuntimeError::MutateTransitionMismatch); + self.emit_label(&ok_label); + } + } + + pub(super) fn emit_mutate_replacement_dynamic_table_transition_checks(&mut self, pattern: &MutatePattern) -> bool { + if self.type_fixed_sizes.contains_key(&pattern.ty) || pattern.transitions.is_empty() { + return false; + } + let Some(layouts) = self.type_layouts.get(&pattern.ty).cloned() else { + return false; + }; + let field_count = layouts.len(); + let transitions = pattern + .transitions + .iter() + .filter_map(|transition| { + let layout = layouts.get(&transition.field).cloned()?; + let width = layout_fixed_scalar_width(&layout)?; + (width <= 8 && self.prelude_u64_operand_source(&transition.operand).is_some()) + .then(|| (transition.clone(), layout, width)) + }) + .collect::>(); + if transitions.len() != pattern.transitions.len() { + return false; + } + + let input_size_offset = self.runtime_scratch_size_offset(); + let input_buffer_offset = self.runtime_scratch_buffer_offset(); + let output_size_offset = self.runtime_scratch2_size_offset(); + let output_buffer_offset = self.runtime_scratch2_buffer_offset(); + self.emit_load_cell_data_syscall_to_offsets( + "mutate_input_table_transition", + CKB_SOURCE_INPUT, + pattern.input_index, + input_size_offset, + input_buffer_offset, + RUNTIME_SCRATCH_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_load_cell_data_syscall_to_offsets( + "mutate_output_table_transition", + CKB_SOURCE_OUTPUT, + pattern.output_index, + output_size_offset, + output_buffer_offset, + RUNTIME_SCRATCH_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit(format!( + "# cellscript abi: verify mutate Molecule table transition fields {} Input#{} -> Output#{}", + pattern.ty, pattern.input_index, pattern.output_index + )); + for (transition, layout, width) in transitions { + let Some(delta) = self.prelude_u64_operand_source(&transition.operand) else { + continue; + }; + self.emit_sp_addi("t4", input_buffer_offset); + self.emit_molecule_table_field_bounds_to_t5( + "t4", + input_size_offset, + layout.index, + width, + &format!("{} input.{}", pattern.ty, transition.field), + ); + self.emit("add t4, t4, t5"); + self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, width); + let input_value_offset = self.runtime_expr_temp_offset(RUNTIME_EXPR_TEMP_SLOTS - 2); + self.emit("# cellscript abi: preserve mutate table input scalar before transition expression"); + self.emit_stack_store("t0", input_value_offset); + self.emit_prelude_u64_operand_source_to_t1(&delta); + self.emit_stack_load("t0", input_value_offset); + match transition.op { + MutateTransitionOp::Add => self.emit("add t1, t0, t1"), + MutateTransitionOp::Sub => self.emit("sub t1, t0, t1"), + MutateTransitionOp::Set => {} + MutateTransitionOp::Append => {} + } + let expected_value_offset = self.runtime_expr_temp_offset(RUNTIME_EXPR_TEMP_SLOTS - 1); + self.emit("# cellscript abi: preserve mutate table expected scalar across output field load"); + self.emit_stack_store("t1", expected_value_offset); + self.emit_sp_addi("t4", output_buffer_offset); + self.emit_molecule_table_field_bounds_to_t5( + "t4", + output_size_offset, + layout.index, + width, + &format!("{} output.{}", pattern.ty, transition.field), + ); + self.emit("add t4, t4, t5"); + self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, width); + self.emit_stack_load("t1", expected_value_offset); + self.emit("sub t2, t0, t1"); + let ok_label = self.fresh_label("mutate_table_transition_ok"); + self.emit(format!("beqz t2, {}", ok_label)); + self.emit_fail(CellScriptRuntimeError::MutateTransitionMismatch); + self.emit_label(&ok_label); + } + let _ = field_count; + true + } + + pub(super) fn emit_mutate_replacement_set_transition_checks(&mut self, pattern: &MutatePattern) { + let transitions = self.mutate_set_transition_layouts(pattern); + if transitions.is_empty() { + return; + } + let output_size_offset = self.runtime_scratch2_size_offset(); + let output_buffer_offset = self.runtime_scratch2_buffer_offset(); + self.emit_load_cell_data_syscall_to_offsets( + "mutate_output_set_transition", + CKB_SOURCE_OUTPUT, + pattern.output_index, + output_size_offset, + output_buffer_offset, + RUNTIME_SCRATCH_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + if let Some(expected_size) = self.type_fixed_sizes.get(&pattern.ty).copied() { + self.emit_loaded_schema_exact_size_check( + output_size_offset, + expected_size, + &format!("{} mutate set transition output", pattern.ty), + ); + } + self.emit(format!("# cellscript abi: verify mutate set transition fields {} Output#{}", pattern.ty, pattern.output_index)); + for (transition, layout, width) in transitions { + self.emit(format!( + "# cellscript abi: verify mutate set transition field {}.{} Output#{} offset={} size={}", + pattern.ty, transition.field, pattern.output_index, layout.offset, width + )); + if !self.emit_loaded_field_bytes_equals_expected( + output_size_offset, + output_buffer_offset, + &layout, + &transition.operand, + &format!("{} set.{}", pattern.ty, transition.field), + ) { + self.emit_fail(CellScriptRuntimeError::MutateTransitionMismatch); + } + } + } + + /// u128 transition verification using 128-bit add/sub with carry. + /// Layout: field is 16 bytes (low 8 + high 8, little-endian). + /// Delta is always u64 (fits in a single register). + /// Verification: output == input +/- delta, with carry propagation. + pub(super) fn emit_mutate_replacement_u128_transition_checks(&mut self, pattern: &MutatePattern) { + let transitions = self.mutate_u128_transition_layouts(pattern); + if transitions.is_empty() { + return; + } + let input_size_offset = self.runtime_scratch_size_offset(); + let input_buffer_offset = self.runtime_scratch_buffer_offset(); + let output_size_offset = self.runtime_scratch2_size_offset(); + let output_buffer_offset = self.runtime_scratch2_buffer_offset(); + // Load Input and Output cell data (already done by the caller for + // preserved field checks, but we need it for transition checks too). + // If the scratch buffers were already loaded by the preserved-field + // path, the syscall results are cached in the buffer; we only need + // to reload if this function is called independently. + self.emit_load_cell_data_syscall_to_offsets( + "mutate_input_u128_transition", + CKB_SOURCE_INPUT, + pattern.input_index, + input_size_offset, + input_buffer_offset, + RUNTIME_SCRATCH_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_load_cell_data_syscall_to_offsets( + "mutate_output_u128_transition", + CKB_SOURCE_OUTPUT, + pattern.output_index, + output_size_offset, + output_buffer_offset, + RUNTIME_SCRATCH_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + if let Some(expected_size) = self.type_fixed_sizes.get(&pattern.ty).copied() { + self.emit_loaded_schema_exact_size_check( + input_size_offset, + expected_size, + &format!("{} mutate u128 transition input", pattern.ty), + ); + self.emit_loaded_schema_exact_size_check( + output_size_offset, + expected_size, + &format!("{} mutate u128 transition output", pattern.ty), + ); + } + for (transition, layout) in transitions { + let Some(delta) = self.prelude_u64_operand_source(&transition.operand) else { + continue; + }; + self.emit_loaded_schema_bounds_check( + input_size_offset, + layout.offset + 16, + &format!("{} input.{}", pattern.ty, transition.field), + ); + self.emit_loaded_schema_bounds_check( + output_size_offset, + layout.offset + 16, + &format!("{} output.{}", pattern.ty, transition.field), + ); + self.emit(format!( + "# cellscript abi: verify mutate u128 transition field {}.{} {:?} Input#{} -> Output#{} offset={} size=16", + pattern.ty, transition.field, transition.op, pattern.input_index, pattern.output_index, layout.offset + )); + + // Load input low 64 bits (little-endian bytes 0..8) into t0 + // Load input high 64 bits (little-endian bytes 8..16) into t3 + self.emit_sp_addi("t4", input_buffer_offset); + self.emit_unaligned_scalar_load("t4", "t0", "t2", layout.offset, 8); + self.emit_unaligned_scalar_load("t4", "t3", "t2", layout.offset + 8, 8); + + // Load delta into t1 + self.emit_prelude_u64_operand_source_to_t1(&delta); + + // Compute expected output = input +/- delta with carry + match transition.op { + MutateTransitionOp::Add => { + // expected_lo = input_lo + delta + // expected_hi = input_hi + carry + // where carry = (input_lo + delta < input_lo) ? 1 : 0 + self.emit("add t5, t0, t1"); // expected_lo = input_lo + delta + self.emit("sltu t2, t5, t0"); // carry = 1 if addition overflowed + self.emit("add t6, t3, t2"); // expected_hi = input_hi + carry + } + MutateTransitionOp::Sub => { + // expected_lo = input_lo - delta + // expected_hi = input_hi - borrow + // where borrow = (input_lo < delta) ? 1 : 0 + self.emit("sub t5, t0, t1"); // expected_lo = input_lo - delta + self.emit("sltu t2, t0, t1"); // borrow = 1 if subtraction underflowed + self.emit("sub t6, t3, t2"); // expected_hi = input_hi - borrow + } + MutateTransitionOp::Set => { + unreachable!("set transitions are verified by emit_mutate_replacement_set_transition_checks") + } + MutateTransitionOp::Append => { + unreachable!("append transitions are verified by emit_mutate_replacement_dynamic_table_append_checks") + } + } + + // Load actual output low 64 bits into t0, high 64 bits into t3 + self.emit_sp_addi("t4", output_buffer_offset); + self.emit_unaligned_scalar_load("t4", "t0", "t2", layout.offset, 8); + self.emit_unaligned_scalar_load("t4", "t3", "t2", layout.offset + 8, 8); + + // Compare: expected (t5, t6) == actual (t0, t3) + let ok_label = self.fresh_label("mutate_u128_transition_ok"); + self.emit("sub t2, t0, t5"); // diff_lo = actual_lo - expected_lo + self.emit("sub t1, t3, t6"); // diff_hi = actual_hi - expected_hi + self.emit("or t2, t2, t1"); // combined diff = diff_lo | diff_hi + self.emit(format!("beqz t2, {}", ok_label)); + self.emit_fail(CellScriptRuntimeError::MutateTransitionMismatch); + self.emit_label(&ok_label); + } + } + + pub(super) fn can_verify_create_output_fields(&self, pattern: &CreatePattern) -> bool { + if pattern.fields.is_empty() { + return false; + } + if !self.create_output_fields_cover_type(pattern) { + return false; + } + pattern.fields.iter().all(|(field, value)| { + self.type_layouts.get(&pattern.ty).and_then(|layouts| layouts.get(field)).is_some_and(|layout| { + if let Some(width) = layout_fixed_byte_width(layout).or_else(|| self.fixed_named_type_width(&layout.ty)) { + self.is_prelude_available_fixed_value(value, width) + } else { + self.can_verify_dynamic_create_output_field_value(value, layout) + } + }) + }) + } + + pub(super) fn create_output_fields_cover_type(&self, pattern: &CreatePattern) -> bool { + let Some(layouts) = self.type_layouts.get(&pattern.ty) else { + return false; + }; + let covered_fields = pattern.fields.iter().map(|(field, _)| field.as_str()).collect::>(); + layouts.keys().all(|field| covered_fields.contains(field.as_str())) + } + + pub(super) fn can_verify_dynamic_create_output_field_value(&self, value: &IrOperand, layout: &SchemaFieldLayout) -> bool { + let IrOperand::Var(var) = value else { + return false; + }; + (self.schema_pointer_vars.contains(&var.id) && self.schema_pointer_size_offsets.contains_key(&var.id)) + || self.constructed_byte_vectors.contains_key(&var.id) + || (self.empty_molecule_vector_vars.contains(&var.id) + && molecule_vector_element_fixed_width(&layout.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes).is_some()) + } + + pub(super) fn can_verify_output_lock(&self, pattern: &CreatePattern) -> bool { + match &pattern.lock { + Some(lock) => self.expected_fixed_byte_source(lock, 32).is_some(), + None => true, + } + } + + pub(super) fn emit_create_output_checks(&mut self, pattern: &CreatePattern) { + let size_offset = self.runtime_scratch_size_offset(); + let buffer_offset = self.runtime_scratch_buffer_offset(); + self.emit_create_output_checks_at(pattern, size_offset, buffer_offset); + } + + pub(super) fn emit_create_output_checks_at(&mut self, pattern: &CreatePattern, size_offset: usize, buffer_offset: usize) { + let is_fixed_type = self.type_fixed_sizes.contains_key(&pattern.ty); + if let Some(expected_size) = self.type_fixed_sizes.get(&pattern.ty).copied() { + self.emit_loaded_schema_exact_size_check(size_offset, expected_size, &pattern.ty); + } + for (field, value) in &pattern.fields { + let Some(layout) = self.type_layouts.get(&pattern.ty).and_then(|fields| fields.get(field)).cloned() else { + continue; + }; + if layout_fixed_byte_width(&layout).or_else(|| self.fixed_named_type_width(&layout.ty)).is_some() { + if is_fixed_type { + self.emit_loaded_field_bytes_equals_expected( + size_offset, + buffer_offset, + &layout, + value, + &format!("{}.{}", pattern.ty, field), + ); + } else { + let Some(field_count) = self.type_layouts.get(&pattern.ty).map(|fields| fields.len()) else { + self.emit_fail(CellScriptRuntimeError::AssertionFailed); + continue; + }; + if !self.emit_dynamic_create_output_fixed_field_equals_expected( + size_offset, + buffer_offset, + &pattern.ty, + field, + &layout, + field_count, + value, + ) { + self.emit_fail(CellScriptRuntimeError::AssertionFailed); + } + } + } else { + let Some(field_count) = self.type_layouts.get(&pattern.ty).map(|fields| fields.len()) else { + self.emit_fail(CellScriptRuntimeError::AssertionFailed); + continue; + }; + if !self.emit_dynamic_create_output_field_equals_expected( + size_offset, + buffer_offset, + &pattern.ty, + field, + &layout, + field_count, + value, + ) { + self.emit_fail(CellScriptRuntimeError::AssertionFailed); + } + } + } + if pattern.operation == "settle" { + self.emit_settle_final_state_check(pattern, size_offset, buffer_offset); + } else { + self.emit_state_transition_check(pattern, size_offset, buffer_offset); + } + } + + pub(super) fn emit_dynamic_create_output_fixed_field_equals_expected( + &mut self, + output_size_offset: usize, + output_buffer_offset: usize, + type_name: &str, + field: &str, + layout: &SchemaFieldLayout, + field_count: usize, + expected: &IrOperand, + ) -> bool { + let Some(width) = layout_fixed_byte_width(layout).or_else(|| self.fixed_named_type_width(&layout.ty)) else { + return false; + }; + let output_start_offset = self.runtime_expr_temp_offset(0); + let output_len_offset = self.runtime_expr_temp_offset(1); + self.emit_dynamic_table_field_span_to_stack( + output_size_offset, + output_buffer_offset, + layout.index, + field_count, + &format!("{}.{}", type_name, field), + output_start_offset, + output_len_offset, + ); + self.emit_stack_load("t0", output_len_offset); + self.emit(format!("li t1, {}", width)); + self.emit("sub t2, t0, t1"); + let len_ok = self.fresh_label("create_fixed_table_field_len_ok"); + self.emit(format!("beqz t2, {}", len_ok)); + self.emit_fail(CellScriptRuntimeError::CellLoadFailed); + self.emit_label(&len_ok); + + if layout_fixed_scalar_width(layout).is_some() { + self.emit(format!( + "# cellscript abi: verify output Molecule table scalar field {}.{} index={} size={}", + type_name, field, layout.index, width + )); + self.emit_stack_load("t4", output_start_offset); + self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, width); + let actual_value_offset = self.runtime_expr_temp_offset(RUNTIME_EXPR_TEMP_SLOTS - 1); + self.emit("# cellscript abi: preserve output table scalar before expected expression"); + self.emit_stack_store("t0", actual_value_offset); + self.emit_expected_operand_to_t1(expected); + self.emit_stack_load("t0", actual_value_offset); + self.emit("sub t2, t0, t1"); + let ok_label = self.fresh_label("output_table_field_ok"); + self.emit(format!("beqz t2, {}", ok_label)); + self.emit_fail(CellScriptRuntimeError::CellLoadFailed); + self.emit_label(&ok_label); + return true; + } + + let Some(source) = self.expected_fixed_byte_source(expected, width) else { + return false; + }; + self.emit(format!( + "# cellscript abi: verify output Molecule table bytes field {}.{} index={} size={}", + type_name, field, layout.index, width + )); + self.emit_prepare_fixed_byte_source(&source, width, &format!("{}.{}", type_name, field)); + self.emit_pointer_fixed_bytes_against_source( + output_start_offset, + 0, + &source, + width, + CellScriptRuntimeError::DynamicFieldValueMismatch, + ); + true + } + + pub(super) fn emit_dynamic_create_output_field_equals_expected( + &mut self, + output_size_offset: usize, + output_buffer_offset: usize, + type_name: &str, + field: &str, + layout: &SchemaFieldLayout, + field_count: usize, + expected: &IrOperand, + ) -> bool { + let IrOperand::Var(var) = expected else { + return false; + }; + let output_start_offset = self.runtime_expr_temp_offset(0); + let output_len_offset = self.runtime_expr_temp_offset(1); + self.emit_dynamic_table_field_span_to_stack( + output_size_offset, + output_buffer_offset, + layout.index, + field_count, + &format!("{}.{}", type_name, field), + output_start_offset, + output_len_offset, + ); + if let Some(parts) = self.constructed_byte_vectors.get(&var.id).cloned() + && let Some(element_width) = + molecule_vector_element_fixed_width(&layout.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) + { + if parts.is_empty() && element_width != 1 { + self.emit_empty_molecule_vector_field_check(type_name, field, output_start_offset, output_len_offset); + return true; + } + self.emit_constructed_molecule_vector_field_check( + type_name, + field, + output_start_offset, + output_len_offset, + &parts, + element_width, + ); + return true; + } + if self.empty_molecule_vector_vars.contains(&var.id) + && molecule_vector_element_fixed_width(&layout.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes).is_some() + { + self.emit_empty_molecule_vector_field_check(type_name, field, output_start_offset, output_len_offset); + return true; + } + if !self.schema_pointer_vars.contains(&var.id) { + return false; + } + let Some(expected_size_offset) = self.schema_pointer_size_offsets.get(&var.id).copied() else { + return false; + }; + self.emit_stack_load("t0", output_len_offset); + self.emit_stack_load("t1", expected_size_offset); + self.emit("sub t2, t0, t1"); + let len_ok = self.fresh_label("create_dynamic_field_len_ok"); + self.emit(format!("beqz t2, {}", len_ok)); + self.emit_fail(CellScriptRuntimeError::CellLoadFailed); + self.emit_label(&len_ok); + + self.emit(format!("# cellscript abi: verify output dynamic field {}.{} as Molecule bytes", type_name, field)); + let mismatch_label = self.fresh_label("create_dynamic_field_mismatch"); + self.emit_stack_load("a0", output_start_offset); + self.emit_stack_load("a1", var.id * 8); + self.emit_stack_load("a2", output_len_offset); + self.emit("call __cellscript_memcmp_fixed"); + self.emit(format!("bnez a0, {}", mismatch_label)); + self.emit_fixed_byte_mismatch_fail(&mismatch_label, CellScriptRuntimeError::CellLoadFailed); + true + } + + pub(super) fn emit_empty_molecule_vector_field_check( + &mut self, + type_name: &str, + field: &str, + output_start_offset: usize, + output_len_offset: usize, + ) { + self.emit(format!("# cellscript abi: verify output dynamic field {}.{} as empty Molecule vector", type_name, field)); + self.emit_stack_load("t0", output_len_offset); + self.emit("li t1, 4"); + self.emit("sub t2, t0, t1"); + let len_ok = self.fresh_label("create_empty_vector_len_ok"); + self.emit(format!("beqz t2, {}", len_ok)); + self.emit_fail(CellScriptRuntimeError::CellLoadFailed); + self.emit_label(&len_ok); + self.emit_stack_load("t0", output_start_offset); + for offset in 0..4 { + self.emit(format!("lbu t1, {}(t0)", offset)); + let byte_ok = self.fresh_label("create_empty_vector_byte_ok"); + self.emit(format!("beqz t1, {}", byte_ok)); + self.emit_fail(CellScriptRuntimeError::CellLoadFailed); + self.emit_label(&byte_ok); + } + } + + pub(super) fn emit_constructed_molecule_vector_field_check( + &mut self, + type_name: &str, + field: &str, + output_start_offset: usize, + output_len_offset: usize, + parts: &[IrOperand], + element_width: usize, + ) { + let Some(expected_bytes) = + parts.iter().try_fold(0usize, |acc, part| self.constructed_byte_vector_part_width(part).map(|width| acc + width)) + else { + self.emit_fail(CellScriptRuntimeError::CellLoadFailed); + return; + }; + if element_width == 0 || expected_bytes % element_width != 0 { + self.emit_fail(CellScriptRuntimeError::CellLoadFailed); + return; + } + let expected_elements = expected_bytes / element_width; + let expected_len = 4 + expected_bytes; + if element_width == 1 { + self.emit(format!( + "# cellscript abi: verify output dynamic field {}.{} as constructed Molecule byte vector len={}", + type_name, field, expected_bytes + )); + } else { + self.emit(format!( + "# cellscript abi: verify output dynamic field {}.{} as constructed Molecule vector elements={} bytes={} element_size={}", + type_name, field, expected_elements, expected_bytes, element_width + )); + } + self.emit_stack_load("t0", output_len_offset); + self.emit(format!("li t1, {}", expected_len)); + self.emit("sub t2, t0, t1"); + let len_ok = self.fresh_label("create_constructed_vector_len_ok"); + self.emit(format!("beqz t2, {}", len_ok)); + self.emit_fail(CellScriptRuntimeError::CellLoadFailed); + self.emit_label(&len_ok); + + self.emit_stack_load("t4", output_start_offset); + for (offset, byte) in (expected_elements as u32).to_le_bytes().iter().enumerate() { + self.emit(format!("lbu t0, {}(t4)", offset)); + self.emit(format!("li t1, {}", byte)); + self.emit("sub t2, t0, t1"); + let byte_ok = self.fresh_label("create_constructed_vector_count_ok"); + self.emit(format!("beqz t2, {}", byte_ok)); + self.emit_fail(CellScriptRuntimeError::CellLoadFailed); + self.emit_label(&byte_ok); + } + + let mut cursor = 4usize; + for part in parts { + let Some(width) = self.constructed_byte_vector_part_width(part) else { + self.emit_fail(CellScriptRuntimeError::CellLoadFailed); + continue; + }; + let Some(source) = self.expected_fixed_byte_source(part, width) else { + self.emit_fail(CellScriptRuntimeError::CellLoadFailed); + continue; + }; + self.emit_prepare_fixed_byte_source(&source, width, &format!("constructed {}.{}", type_name, field)); + self.emit_pointer_fixed_bytes_against_source( + output_start_offset, + cursor, + &source, + width, + CellScriptRuntimeError::CellLoadFailed, + ); + cursor += width; + } + } + + pub(super) fn emit_output_lock_hash_check(&mut self, output_index: usize, expected: &IrOperand) -> bool { + if self.expected_fixed_byte_source(expected, 32).is_none() { + return false; + } + let size_offset = self.runtime_scratch_size_offset(); + let buffer_offset = self.runtime_scratch_buffer_offset(); + self.emit_load_cell_by_field_syscall_to_offsets( + "output_lock_hash", + CKB_SOURCE_OUTPUT, + output_index, + CKB_CELL_FIELD_LOCK_HASH, + size_offset, + buffer_offset, + RUNTIME_SCRATCH_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_loaded_schema_exact_size_check(size_offset, 32, "output lock hash"); + self.emit("# cellscript abi: verify output lock hash offset=0 size=32"); + let layout = SchemaFieldLayout { index: 0, offset: 0, ty: IrType::Hash, fixed_size: Some(32), fixed_enum_size: None }; + self.emit_loaded_field_bytes_equals_expected(size_offset, buffer_offset, &layout, expected, "output lock hash") + } + + pub(super) fn emit_state_transition_check( + &mut self, + pattern: &CreatePattern, + output_size_offset: usize, + output_buffer_offset: usize, + ) { + let Some(states) = self.flow_states.get(&pattern.ty) else { + return; + }; + let state_count = states.len(); + let action_edges = self.state_transition_edges_for_pattern(pattern); + let Some(consumed_var_id) = self.consumed_var_for_state_transition(&pattern.ty, &action_edges) else { + if !action_edges.is_empty() { + self.emit_fail(CellScriptRuntimeError::FlowTransitionMismatch); + } + return; + }; + let Some(input_size_offset) = self.cell_buffer_size_offsets.get(&consumed_var_id).copied() else { + return; + }; + let Some(input_buffer_offset) = self.cell_buffer_offsets.get(&consumed_var_id).copied() else { + return; + }; + let state_field = self.flow_state_fields.get(&pattern.ty).cloned().unwrap_or_else(|| FLOW_STATE_FIELD_NAME.to_string()); + let Some(state_layout) = self.type_layouts.get(&pattern.ty).and_then(|fields| fields.get(&state_field)).cloned() else { + return; + }; + let Some(width) = layout_flow_state_width(&state_layout) else { + return; + }; + let Some(expected_size) = self.type_fixed_sizes.get(&pattern.ty).copied() else { + return; + }; + + self.emit(format!("# cellscript abi: state transition {}.{} state_count={}", pattern.ty, state_field, state_count)); + self.emit_loaded_schema_exact_size_check(input_size_offset, expected_size, &format!("{} input", pattern.ty)); + self.emit_loaded_schema_bounds_check( + input_size_offset, + state_layout.offset + width, + &format!("{} input.{}", pattern.ty, state_field), + ); + self.emit_loaded_schema_bounds_check( + output_size_offset, + state_layout.offset + width, + &format!("{} output.{}", pattern.ty, state_field), + ); + self.emit_sp_addi("t4", input_buffer_offset); + self.emit_unaligned_scalar_load("t4", "t0", "t2", state_layout.offset, width); + let old_range_ok_label = self.fresh_label("flow_old_state_range_ok"); + self.emit(format!("li t3, {}", state_count)); + self.emit("sltu t2, t0, t3"); + self.emit(format!("bnez t2, {}", old_range_ok_label)); + self.emit_fail(CellScriptRuntimeError::FlowOldStateInvalid); + self.emit_label(&old_range_ok_label); + + self.emit_sp_addi("t4", output_buffer_offset); + self.emit_unaligned_scalar_load("t4", "t1", "t2", state_layout.offset, width); + let ok_label = self.fresh_label("flow_transition_ok"); + let rules = self.state_transition_rules_for_pattern(pattern, &action_edges); + if rules.is_empty() { + self.emit("addi t0, t0, 1"); + self.emit("sub t2, t1, t0"); + self.emit(format!("beqz t2, {}", ok_label)); + } else { + for rule in rules { + let next_rule_label = self.fresh_label("flow_transition_next_rule"); + self.emit(format!("li t3, {}", rule.from_index)); + self.emit("sub t2, t0, t3"); + self.emit(format!("bnez t2, {}", next_rule_label)); + self.emit(format!("li t3, {}", rule.to_index)); + self.emit("sub t2, t1, t3"); + self.emit(format!("beqz t2, {}", ok_label)); + self.emit_label(&next_rule_label); + } + } + self.emit_fail(CellScriptRuntimeError::FlowTransitionMismatch); + self.emit_label(&ok_label); + + let range_ok_label = self.fresh_label("flow_state_range_ok"); + self.emit(format!("li t3, {}", state_count)); + self.emit("sltu t2, t1, t3"); + self.emit(format!("bnez t2, {}", range_ok_label)); + self.emit_fail(CellScriptRuntimeError::FlowNewStateInvalid); + self.emit_label(&range_ok_label); + } + + pub(super) fn state_transition_edges_for_pattern(&self, pattern: &CreatePattern) -> Vec { + self.current_state_transition_edges + .iter() + .filter(|state_edge| { + state_edge.type_name == pattern.ty + && state_edge.output_binding.as_ref().is_none_or(|binding| binding == &pattern.binding) + }) + .cloned() + .collect() + } + + pub(super) fn state_transition_rules_for_pattern( + &self, + pattern: &CreatePattern, + action_edges: &[IrStateTransitionEdge], + ) -> Vec { + if !action_edges.is_empty() { + return action_edges + .iter() + .map(|state_edge| IrFlowRule { + from: state_edge.from.clone(), + to: state_edge.to.clone(), + from_index: state_edge.from_index, + to_index: state_edge.to_index, + }) + .collect(); + } + self.flow_rules.get(&pattern.ty).cloned().unwrap_or_default() + } + + pub(super) fn consumed_var_for_state_transition(&self, type_name: &str, action_edges: &[IrStateTransitionEdge]) -> Option { + if let Some(binding) = action_edges.iter().filter_map(|state_edge| state_edge.input_binding.as_ref()).next() { + let var_id = self.consume_binding_ids.get(binding).copied()?; + if self.consume_type_names.get(&var_id).is_some_and(|consumed_type| consumed_type == type_name) { + return Some(var_id); + } + return None; + } + self.consumed_var_for_type(type_name) + } + + pub(super) fn emit_settle_final_state_check( + &mut self, + pattern: &CreatePattern, + output_size_offset: usize, + output_buffer_offset: usize, + ) { + let Some(states) = self.flow_states.get(&pattern.ty) else { + return; + }; + if states.len() < 2 { + return; + } + let final_state = states.len() - 1; + let Some(consumed_var_id) = self.consumed_var_for_type(&pattern.ty) else { + return; + }; + let Some(input_size_offset) = self.cell_buffer_size_offsets.get(&consumed_var_id).copied() else { + return; + }; + let Some(input_buffer_offset) = self.cell_buffer_offsets.get(&consumed_var_id).copied() else { + return; + }; + let state_field = self.flow_state_fields.get(&pattern.ty).cloned().unwrap_or_else(|| FLOW_STATE_FIELD_NAME.to_string()); + let Some(state_layout) = self.type_layouts.get(&pattern.ty).and_then(|fields| fields.get(&state_field)).cloned() else { + return; + }; + let Some(width) = layout_flow_state_width(&state_layout) else { + return; + }; + let Some(expected_size) = self.type_fixed_sizes.get(&pattern.ty).copied() else { + return; + }; + + self.emit(format!( + "# cellscript abi: settle final-state {}.{} final_state={} state_count={}", + pattern.ty, + state_field, + final_state, + states.len() + )); + self.emit_loaded_schema_exact_size_check(input_size_offset, expected_size, &format!("{} input", pattern.ty)); + self.emit_loaded_schema_bounds_check( + input_size_offset, + state_layout.offset + width, + &format!("{} input.{}", pattern.ty, state_field), + ); + self.emit_loaded_schema_bounds_check( + output_size_offset, + state_layout.offset + width, + &format!("{} output.{}", pattern.ty, state_field), + ); + + self.emit_sp_addi("t4", input_buffer_offset); + self.emit_unaligned_scalar_load("t4", "t0", "t2", state_layout.offset, width); + self.emit(format!("li t3, {}", final_state)); + self.emit("sub t2, t0, t3"); + let input_ok_label = self.fresh_label("settle_input_final_state_ok"); + self.emit(format!("beqz t2, {}", input_ok_label)); + self.emit_fail(CellScriptRuntimeError::NumericOrDiscriminantInvalid); + self.emit_label(&input_ok_label); + + self.emit_sp_addi("t4", output_buffer_offset); + self.emit_unaligned_scalar_load("t4", "t1", "t2", state_layout.offset, width); + self.emit("sub t2, t1, t3"); + let output_ok_label = self.fresh_label("settle_output_final_state_ok"); + self.emit(format!("beqz t2, {}", output_ok_label)); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + self.emit_label(&output_ok_label); + } + + pub(super) fn consumed_var_for_type(&self, type_name: &str) -> Option { + self.consume_order + .iter() + .copied() + .find(|var_id| self.consume_type_names.get(var_id).is_some_and(|consumed_type| consumed_type == type_name)) + } + + pub(super) fn is_prelude_available_scalar(&self, operand: &IrOperand) -> bool { + match operand { + IrOperand::Const(IrConst::Bool(_) | IrConst::U8(_) | IrConst::U16(_) | IrConst::U32(_) | IrConst::U64(_)) => true, + IrOperand::Var(var) => matches!(var.ty, IrType::Bool | IrType::U8 | IrType::U16 | IrType::U32 | IrType::I32 | IrType::U64), + _ => false, + } + } + + pub(super) fn is_prelude_available_fixed_value(&self, operand: &IrOperand, expected_width: usize) -> bool { + if self.is_prelude_available_scalar(operand) { + return true; + } + self.expected_fixed_byte_source(operand, expected_width).is_some() + } + + pub(super) fn emit_unaligned_scalar_load( + &mut self, + base_reg: &str, + dest_reg: &str, + scratch_reg: &str, + offset: usize, + width: usize, + ) { + self.emit(format!("li {}, 0", dest_reg)); + for byte_index in 0..width { + self.emit_memory_load_with_avoid("lbu", scratch_reg, base_reg, offset + byte_index, &[dest_reg, scratch_reg, base_reg]); + if byte_index != 0 { + self.emit(format!("slli {}, {}, {}", scratch_reg, scratch_reg, byte_index * 8)); + } + self.emit(format!("or {}, {}, {}", dest_reg, dest_reg, scratch_reg)); + } + } + + pub(super) fn emit_sign_extend_i32(&mut self, register: &str) { + self.emit(format!("# cellscript abi: sign-extend i32 in {}", register)); + self.emit(format!("slli {}, {}, 32", register, register)); + self.emit(format!("srai {}, {}, 32", register, register)); + } + + pub(super) fn fresh_label(&mut self, prefix: &str) -> String { + let label = format!(".L{}_{}", prefix, self.next_runtime_label); + self.next_runtime_label += 1; + label + } +} diff --git a/src/codegen/collections.rs b/src/codegen/collections.rs new file mode 100644 index 00000000..891717c2 --- /dev/null +++ b/src/codegen/collections.rs @@ -0,0 +1,1329 @@ +use super::*; + +impl CodeGenerator { + pub(super) fn emit_index(&mut self, dest: &IrVar, arr: &IrOperand, idx: &IrOperand) -> Result<()> { + if self.emit_fixed_aggregate_index(dest, arr, idx) { + return Ok(()); + } + if self.emit_dynamic_molecule_vector_index(dest, arr, idx) { + return Ok(()); + } + if self.emit_stack_collection_index(dest, arr, idx) { + return Ok(()); + } + if self.emit_dynamic_index_access(dest, arr, idx) { + return Ok(()); + } + + self.emit("# index access (unresolved)"); + self.emit("# cellscript abi: fail closed because element layout is not statically computable"); + self.emit_fail(CellScriptRuntimeError::TypeHashMismatch); + Ok(()) + } + + fn emit_fixed_aggregate_index(&mut self, dest: &IrVar, arr: &IrOperand, idx: &IrOperand) -> bool { + let (IrOperand::Var(arr_var), Some(index)) = (arr, const_usize_operand(idx)) else { + return false; + }; + if !self.aggregate_pointer_sources.contains_key(&arr_var.id) { + return false; + } + let IrType::Array(inner, len) = &arr_var.ty else { + return false; + }; + if index >= *len { + return false; + } + let Some(element_width) = type_static_length(inner) else { + return false; + }; + let Some(total_width) = type_static_length(&arr_var.ty) else { + return false; + }; + let offset = index * element_width; + self.emit(format!("# index access [{}]", index)); + self.emit(format!("# cellscript abi: fixed aggregate index element_offset={} element_size={}", offset, element_width)); + if let Some(size_offset) = self.fixed_byte_param_size_offsets.get(&arr_var.id).copied() { + self.emit_loaded_schema_exact_size_check(size_offset, total_width, "fixed aggregate param"); + self.emit_loaded_schema_bounds_check(size_offset, offset + element_width, "fixed aggregate index"); + } + self.emit_stack_load("t4", arr_var.id * 8); + if let Some(width) = fixed_scalar_width(inner, Some(element_width)) { + self.emit_unaligned_scalar_load("t4", "t0", "t2", offset, width); + } else { + self.emit(format!("addi t0, t4, {}", offset)); + } + self.emit_stack_store("t0", dest.id * 8); + true + } + + fn emit_dynamic_molecule_vector_index(&mut self, dest: &IrVar, arr: &IrOperand, idx: &IrOperand) -> bool { + let IrOperand::Var(arr_var) = arr else { + return false; + }; + let Some(size_offset) = self + .dynamic_value_size_offsets + .get(&arr_var.id) + .copied() + .or_else(|| self.schema_pointer_size_offsets.get(&arr_var.id).copied()) + else { + return false; + }; + let Some(element_width) = molecule_vector_element_fixed_width(&arr_var.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) + else { + return false; + }; + + self.emit("# index access"); + self.emit(format!( + "# cellscript abi: dynamic Molecule vector index element_size={} size_offset={}", + element_width, size_offset + )); + self.emit_loaded_schema_bounds_check(size_offset, 4, "dynamic Molecule vector index"); + self.emit_stack_load("t4", arr_var.id * 8); + self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, 4); + + self.emit_stack_load("t3", size_offset); + self.emit(format!("li t2, {}", element_width)); + self.emit("mul t5, t0, t2"); + self.emit("addi t5, t5, 4"); + self.emit("sub t2, t3, t5"); + let size_ok = self.fresh_label("molecule_vector_index_size_ok"); + self.emit(format!("beqz t2, {}", size_ok)); + self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); + self.emit_label(&size_ok); + + match idx { + IrOperand::Var(v) => self.emit_stack_load("t1", v.id * 8), + IrOperand::Const(IrConst::U8(n)) => self.emit(format!("li t1, {}", n)), + IrOperand::Const(IrConst::U16(n)) => self.emit(format!("li t1, {}", n)), + IrOperand::Const(IrConst::U32(n)) => self.emit(format!("li t1, {}", n)), + IrOperand::Const(IrConst::U64(n)) => self.emit(format!("li t1, {}", n)), + _ => self.emit("li t1, 0"), + } + + let bounds_ok = self.fresh_label("molecule_vector_index_bounds_ok"); + self.emit("sltu t2, t1, t0"); + self.emit(format!("bnez t2, {}", bounds_ok)); + self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); + self.emit_label(&bounds_ok); + + self.emit(format!("li t2, {}", element_width)); + self.emit("mul t1, t1, t2"); + self.emit("addi t1, t1, 4"); + self.emit("add t4, t4, t1"); + if fixed_scalar_width(&dest.ty, Some(element_width)).is_some() { + self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, element_width.min(8)); + } else { + self.emit("addi t0, t4, 0"); + } + self.emit_stack_store("t0", dest.id * 8); + true + } + + fn emit_stack_collection_index(&mut self, dest: &IrVar, arr: &IrOperand, idx: &IrOperand) -> bool { + let IrOperand::Var(arr_var) = arr else { + return false; + }; + if !self.stack_collection_vars.contains(&arr_var.id) { + return false; + } + let Some(element_width) = molecule_vector_element_fixed_width(&arr_var.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) + else { + return false; + }; + let dest_scalar = fixed_scalar_width(&dest.ty, Some(element_width)).is_some(); + let dest_fixed_bytes = self.fixed_byte_like_width(&dest.ty).is_some_and(|width| width == element_width); + if !dest_scalar && !dest_fixed_bytes { + return false; + } + + self.emit("# index access"); + self.emit(format!("# cellscript abi: stack collection index element_size={}", element_width)); + self.emit_stack_load("t4", arr_var.id * 8); + self.emit("ld t0, -8(t4)"); + self.emit_operand_to_register("t1", idx); + + let bounds_ok = self.fresh_label("stack_collection_index_bounds_ok"); + self.emit("sltu t2, t1, t0"); + self.emit(format!("bnez t2, {}", bounds_ok)); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + self.emit_label(&bounds_ok); + + self.emit(format!("li t2, {}", element_width)); + self.emit("mul t1, t1, t2"); + self.emit("add t4, t4, t1"); + if dest_scalar { + self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, element_width); + } else { + self.emit("addi t0, t4, 0"); + } + self.emit_stack_store("t0", dest.id * 8); + true + } + + /// Dynamic index access: compute element offset from array type layout. + /// Handles cases where the index is not a constant or the array is not in + /// aggregate_pointer_sources, but the element size is still statically known. + fn emit_dynamic_index_access(&mut self, dest: &IrVar, arr: &IrOperand, idx: &IrOperand) -> bool { + let IrOperand::Var(arr_var) = arr else { + return false; + }; + let IrType::Array(inner, len) = &arr_var.ty else { + return false; + }; + let Some(element_width) = type_static_length(inner) else { + return false; + }; + let Some(total_width) = type_static_length(&arr_var.ty) else { + return false; + }; + + self.emit("# index access"); + self.emit(format!("# cellscript abi: dynamic index element_size={}", element_width)); + + // Bounds check: if we have a size offset, verify total data is large enough + if let Some(size_offset) = self.fixed_byte_param_size_offsets.get(&arr_var.id).copied() { + self.emit_loaded_schema_exact_size_check(size_offset, total_width, "dynamic index aggregate"); + } + + // Load array base pointer + self.emit_stack_load("t4", arr_var.id * 8); + + // Load index value into t1 + match idx { + IrOperand::Var(v) => self.emit_stack_load("t1", v.id * 8), + IrOperand::Const(IrConst::U8(n)) => self.emit(format!("li t1, {}", n)), + IrOperand::Const(IrConst::U16(n)) => self.emit(format!("li t1, {}", n)), + IrOperand::Const(IrConst::U32(n)) => self.emit(format!("li t1, {}", n)), + IrOperand::Const(IrConst::U64(n)) => self.emit(format!("li t1, {}", n)), + _ => self.emit("li t1, 0"), + } + + // Bounds check: index < len + let bounds_ok = self.fresh_label("idx_bounds_ok"); + self.emit(format!("li t2, {}", len)); + self.emit("slt t3, t1, t2"); + self.emit(format!("bnez t3, {}", bounds_ok)); + self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); + self.emit_label(&bounds_ok); + + // Compute offset = index * element_width + self.emit(format!("li t2, {}", element_width)); + self.emit("mul t1, t1, t2"); + + if fixed_scalar_width(inner, Some(element_width)).is_some() { + // Scalar element: load from base + offset + self.emit("add t4, t4, t1"); + self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, element_width.min(8)); + } else { + // Pointer-sized element: compute base + offset + self.emit("add t0, t4, t1"); + } + self.emit_stack_store("t0", dest.id * 8); + true + } + + pub(super) fn emit_length(&mut self, dest: &IrVar, operand: &IrOperand) -> Result<()> { + self.emit("# length"); + if let Some(static_len) = self.static_length(operand) { + self.emit(format!("li t0, {}", static_len)); + } else if self.emit_stack_collection_length(operand) || self.emit_dynamic_molecule_vector_length(operand) { + } else if let Some(size_offset) = self.dynamic_length_from_size_offset(operand) { + // For schema-backed or fixed-byte params, the actual size word is already + // stored at the size offset; load it directly. + self.emit(format!("# cellscript abi: dynamic length from size word at offset={}", size_offset)); + self.emit_stack_load("t0", size_offset); + } else { + self.emit("# cellscript abi: fail closed because dynamic length is not available"); + self.emit_fail(CellScriptRuntimeError::CollectionRuntimeUnsupported); + return Ok(()); + } + self.emit_stack_store("t0", dest.id * 8); + Ok(()) + } + + fn emit_stack_collection_length(&mut self, operand: &IrOperand) -> bool { + let IrOperand::Var(var) = operand else { + return false; + }; + if !self.stack_collection_vars.contains(&var.id) { + return false; + } + self.emit("# cellscript abi: stack collection length"); + self.emit_stack_load("t4", var.id * 8); + self.emit("ld t0, -8(t4)"); + true + } + + fn emit_dynamic_molecule_vector_length(&mut self, operand: &IrOperand) -> bool { + let IrOperand::Var(var) = operand else { + return false; + }; + let Some(size_offset) = + self.dynamic_value_size_offsets.get(&var.id).copied().or_else(|| self.schema_pointer_size_offsets.get(&var.id).copied()) + else { + return false; + }; + let Some(element_width) = molecule_vector_element_fixed_width(&var.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) else { + return false; + }; + + self.emit(format!( + "# cellscript abi: dynamic Molecule vector length element_size={} size_offset={}", + element_width, size_offset + )); + self.emit_loaded_schema_bounds_check(size_offset, 4, "dynamic Molecule vector length"); + self.emit_stack_load("t4", var.id * 8); + self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, 4); + + self.emit_stack_load("t1", size_offset); + self.emit(format!("li t2, {}", element_width)); + self.emit("mul t3, t0, t2"); + self.emit("addi t3, t3, 4"); + self.emit("sub t2, t1, t3"); + let size_ok = self.fresh_label("molecule_vector_size_ok"); + self.emit(format!("beqz t2, {}", size_ok)); + self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); + self.emit_label(&size_ok); + true + } + + /// Try to obtain the size offset for a dynamically-sized operand. + pub(super) fn dynamic_length_from_size_offset(&self, operand: &IrOperand) -> Option { + let IrOperand::Var(var) = operand else { + return None; + }; + // Check schema pointer size offsets (named-type params, consumed inputs, read_refs) + if let Some(size_offset) = self.schema_pointer_size_offsets.get(&var.id).copied() { + return Some(size_offset); + } + // Check fixed-byte param size offsets + if let Some(size_offset) = self.fixed_byte_param_size_offsets.get(&var.id).copied() { + return Some(size_offset); + } + // Check cell buffer size offsets (consumed inputs, read_refs, type_hash) + if let Some(size_offset) = self.cell_buffer_size_offsets.get(&var.id).copied() { + return Some(size_offset); + } + None + } + + pub(super) fn emit_type_hash(&mut self, dest: &IrVar, operand: &IrOperand) -> Result<()> { + if let Some(output_index) = self.output_type_hash_sources.get(&dest.id).copied() { + let Some(size_offset) = self.cell_buffer_size_offsets.get(&dest.id).copied() else { + return Ok(()); + }; + let Some(buffer_offset) = self.cell_buffer_offsets.get(&dest.id).copied() else { + return Ok(()); + }; + self.emit("# type_hash"); + self.emit_operand_comment("type_hash source", operand); + self.emit_load_cell_by_field_syscall_to_offsets( + "output_type_hash", + CKB_SOURCE_OUTPUT, + output_index, + CKB_CELL_FIELD_TYPE_HASH, + size_offset, + buffer_offset, + 32, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_loaded_schema_exact_size_check(size_offset, 32, "output type hash"); + self.emit_sp_addi("t0", buffer_offset); + self.emit_stack_store("t0", dest.id * 8); + return Ok(()); + } + if self.emit_runtime_type_hash(dest, operand) { + return Ok(()); + } + if let Some(param_id) = self.param_type_hash_sources.get(&dest.id).copied() { + let Some(pointer_offset) = self.param_type_hash_pointer_offsets.get(¶m_id).copied() else { + return Ok(()); + }; + let Some(size_offset) = self.param_type_hash_size_offsets.get(¶m_id).copied() else { + return Ok(()); + }; + self.emit("# type_hash"); + self.emit_operand_comment("type_hash source", operand); + self.emit_loaded_schema_exact_size_check(size_offset, 32, "param type hash"); + self.emit_stack_load("t0", pointer_offset); + self.emit_stack_store("t0", dest.id * 8); + return Ok(()); + } + + self.emit("# type_hash (unresolved)"); + self.emit("# cellscript abi: fail closed because type_hash source cell cannot be determined"); + self.emit_fail(CellScriptRuntimeError::NumericOrDiscriminantInvalid); + Ok(()) + } + + /// Runtime type_hash: try to load the type hash from a cell identified by the operand's + /// association with a consumed input, created output, or read_ref cell dep. + fn emit_runtime_type_hash(&mut self, dest: &IrVar, operand: &IrOperand) -> bool { + let IrOperand::Var(var) = operand else { + return false; + }; + + // Try to find which cell this var is associated with + let (source, index) = if let Some(input_index) = self.consume_indices.get(&var.id).copied() { + (CKB_SOURCE_INPUT, input_index) + } else if let Some(output_index) = self.operation_output_indices.get(&var.id).copied() { + (CKB_SOURCE_OUTPUT, output_index) + } else if let Some(dep_index) = self.read_ref_indices.get(&var.id).copied() { + (CKB_SOURCE_CELL_DEP, dep_index) + } else { + return false; + }; + + let size_offset = self.cell_buffer_size_offsets.get(&dest.id).copied().unwrap_or_else(|| self.runtime_scratch_size_offset()); + let buffer_offset = self.cell_buffer_offsets.get(&dest.id).copied().unwrap_or_else(|| self.runtime_scratch_buffer_offset()); + + self.emit("# type_hash"); + self.emit_operand_comment("type_hash source", operand); + self.emit_load_cell_by_field_syscall_to_offsets( + "runtime_type_hash", + source, + index, + CKB_CELL_FIELD_TYPE_HASH, + size_offset, + buffer_offset, + 32, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_loaded_schema_exact_size_check(size_offset, 32, "runtime type hash"); + self.emit_sp_addi("t0", buffer_offset); + self.emit_stack_store("t0", dest.id * 8); + true + } + + pub(super) fn emit_collection_new(&mut self, dest: &IrVar, ty: &str, capacity: Option<&IrOperand>) -> Result<()> { + // Stack-allocated collection: the stack slot stores a pointer to the + // collection buffer area, with the length word immediately before the buffer. + // Layout: [length: u64][buffer: RUNTIME_COLLECTION_BUFFER_SIZE bytes] + // We allocate space in the stack frame and initialize length to 0. + let collection_slot_size = 8 + RUNTIME_COLLECTION_BUFFER_SIZE; + let length_offset = self.collection_region_start + collection_slot_size * self.next_collection_slot; + let buffer_offset = length_offset + 8; + + self.emit(format!("# collection new {}", ty)); + self.emit(format!( + "# cellscript abi: stack collection buffer_offset={} max_size={}", + buffer_offset, RUNTIME_COLLECTION_BUFFER_SIZE + )); + if let Some(capacity) = capacity { + self.emit("# cellscript abi: stack collection with_capacity uses fixed backing buffer"); + self.emit_operand_comment("capacity", capacity); + } + + // Initialize length to 0 + self.emit_stack_store("zero", length_offset); + self.emit_sp_addi("t0", buffer_offset); + self.emit_stack_store("t0", dest.id * 8); + self.empty_molecule_vector_vars.insert(dest.id); + self.stack_collection_vars.insert(dest.id); + self.next_collection_slot += 1; + Ok(()) + } + + pub(super) fn emit_collection_capacity(&mut self, dest: &IrVar, collection: &IrOperand) -> Result<()> { + self.emit("# collection capacity"); + self.emit_operand_comment("collection", collection); + if self.emit_stack_collection_capacity(dest, collection) { + return Ok(()); + } + self.emit("# cellscript abi: collection capacity is not available for this collection"); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + Ok(()) + } + + fn emit_stack_collection_capacity(&mut self, dest: &IrVar, collection: &IrOperand) -> bool { + if dest.ty != IrType::U64 { + return false; + } + let IrOperand::Var(collection) = collection else { + return false; + }; + if !self.stack_collection_vars.contains(&collection.id) { + return false; + } + let Some(element_width) = molecule_vector_element_fixed_width(&collection.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) + else { + return false; + }; + if element_width == 0 { + return false; + } + + self.emit(format!("# cellscript abi: stack collection capacity element_size={}", element_width)); + self.emit(format!("li t0, {}", RUNTIME_COLLECTION_BUFFER_SIZE / element_width)); + self.emit_stack_store("t0", dest.id * 8); + true + } + + pub(super) fn emit_collection_push(&mut self, collection: &IrOperand, value: &IrOperand) -> Result<()> { + self.emit("# collection push"); + self.emit_operand_comment("collection", collection); + self.emit_operand_comment("value", value); + if matches!(value, IrOperand::Var(var) if self.verified_collection_push_values.contains(&var.id)) { + self.emit("# cellscript abi: collection push is covered by mutate append verifier"); + return Ok(()); + } + if matches!(collection, IrOperand::Var(var) if self.verified_collection_construction_vectors.contains(&var.id)) { + self.emit("# cellscript abi: collection push is covered by create-output vector verifier"); + return Ok(()); + } + if self.emit_stack_collection_push(collection, value) { + return Ok(()); + } + // In the verifier context, collection push is used for building output data. + // The verifier doesn't need to actually build the data; it needs to verify + // that the output cell data matches expectations. The collection operations + // in the verifier body are vestigial from the source-level specification. + // For now, emit a fail-closed trap because runtime collection mutation is not + // needed in the verifier path – the prelude already verified the output. + self.emit("# cellscript abi: collection push is not needed for verifier execution"); + self.emit("# cellscript abi: if this path is reached, the source program uses dynamic collections"); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + Ok(()) + } + + fn emit_stack_collection_push(&mut self, collection: &IrOperand, value: &IrOperand) -> bool { + let IrOperand::Var(collection) = collection else { + return false; + }; + if !self.stack_collection_vars.contains(&collection.id) { + return false; + } + let Some(width) = self.constructed_byte_vector_part_width(value) else { + return false; + }; + if width > RUNTIME_COLLECTION_BUFFER_SIZE { + return false; + } + + self.emit(format!("# cellscript abi: stack collection push element_size={}", width)); + self.emit_stack_load("t4", collection.id * 8); + self.emit("ld t0, -8(t4)"); + self.emit(format!("li t1, {}", width)); + self.emit("mul t2, t0, t1"); + self.emit(format!("li t3, {}", RUNTIME_COLLECTION_BUFFER_SIZE)); + self.emit("sub t5, t3, t2"); + self.emit("sltu t5, t5, t1"); + let capacity_ok = self.fresh_label("stack_collection_push_capacity_ok"); + self.emit(format!("beqz t5, {}", capacity_ok)); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + self.emit_label(&capacity_ok); + + self.emit("add t5, t4, t2"); + if width <= 8 && fixed_scalar_operand_width(value).is_some() { + self.emit_operand_to_register("t1", value); + match width { + 1 => self.emit("sb t1, 0(t5)"), + 2 => self.emit("sh t1, 0(t5)"), + 4 => self.emit("sw t1, 0(t5)"), + 8 => self.emit("sd t1, 0(t5)"), + _ => return false, + } + } else { + let Some(source) = self.expected_fixed_byte_source(value, width) else { + return false; + }; + self.emit_prepare_fixed_byte_source(&source, width, "stack collection push"); + self.emit(format!("# cellscript abi: stack collection copy fixed bytes size={}", width)); + for byte_index in 0..width { + self.emit_fixed_byte_source_byte_to("t1", "t6", &source, byte_index); + self.emit_stack_load("t4", collection.id * 8); + self.emit("ld t0, -8(t4)"); + self.emit(format!("li t2, {}", width)); + self.emit("mul t2, t0, t2"); + self.emit("add t4, t4, t2"); + if byte_index <= 2047 { + self.emit(format!("sb t1, {}(t4)", byte_index)); + } else { + self.emit_large_addi("t0", "t4", byte_index as i64); + self.emit("sb t1, 0(t0)"); + } + } + } + self.emit_stack_load("t4", collection.id * 8); + self.emit("ld t0, -8(t4)"); + self.emit("addi t0, t0, 1"); + self.emit("sd t0, -8(t4)"); + true + } + + pub(super) fn emit_collection_extend(&mut self, collection: &IrOperand, slice: &IrOperand) -> Result<()> { + self.emit("# collection extend_from_slice"); + self.emit_operand_comment("collection", collection); + self.emit_operand_comment("slice", slice); + if matches!(collection, IrOperand::Var(var) if self.verified_collection_construction_vectors.contains(&var.id)) { + self.emit("# cellscript abi: collection extend is covered by create-output vector verifier"); + return Ok(()); + } + if self.emit_stack_collection_extend(collection, slice) { + return Ok(()); + } + self.emit("# cellscript abi: collection extend is not needed for verifier execution"); + self.emit("# cellscript abi: if this path is reached, the source program uses dynamic collections"); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + Ok(()) + } + + fn emit_stack_collection_extend(&mut self, collection: &IrOperand, slice: &IrOperand) -> bool { + let IrOperand::Var(collection) = collection else { + return false; + }; + if !self.stack_collection_vars.contains(&collection.id) { + return false; + } + let Some(width) = operand_fixed_byte_width(slice) else { + return false; + }; + let element_width = + molecule_vector_element_fixed_width(&collection.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes).unwrap_or(1); + if element_width == 0 || width % element_width != 0 { + return false; + } + let element_count = width / element_width; + if width > RUNTIME_COLLECTION_BUFFER_SIZE { + return false; + } + let Some(source) = self.expected_fixed_byte_source(slice, width) else { + return false; + }; + + self.emit(format!( + "# cellscript abi: stack collection extend bytes={} elements={} element_size={}", + width, element_count, element_width + )); + self.emit_stack_load("t4", collection.id * 8); + self.emit("ld t0, -8(t4)"); + self.emit(format!("li t1, {}", element_width)); + self.emit("mul t2, t0, t1"); + self.emit(format!("li t3, {}", RUNTIME_COLLECTION_BUFFER_SIZE)); + self.emit("sub t5, t3, t2"); + self.emit(format!("li t1, {}", width)); + self.emit("sltu t5, t5, t1"); + let capacity_ok = self.fresh_label("stack_collection_extend_capacity_ok"); + self.emit(format!("beqz t5, {}", capacity_ok)); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + self.emit_label(&capacity_ok); + + self.emit_prepare_fixed_byte_source(&source, width, "stack collection extend"); + self.emit(format!("# cellscript abi: stack collection extend copy fixed bytes size={}", width)); + for byte_index in 0..width { + self.emit_fixed_byte_source_byte_to("t1", "t6", &source, byte_index); + self.emit_stack_load("t4", collection.id * 8); + self.emit("ld t0, -8(t4)"); + self.emit(format!("li t2, {}", element_width)); + self.emit("mul t2, t0, t2"); + self.emit("add t4, t4, t2"); + if byte_index <= 2047 { + self.emit(format!("sb t1, {}(t4)", byte_index)); + } else { + self.emit_large_addi("t0", "t4", byte_index as i64); + self.emit("sb t1, 0(t0)"); + } + } + self.emit_stack_load("t4", collection.id * 8); + self.emit("ld t0, -8(t4)"); + self.emit(format!("addi t0, t0, {}", element_count)); + self.emit("sd t0, -8(t4)"); + true + } + + pub(super) fn emit_collection_clear(&mut self, collection: &IrOperand) -> Result<()> { + self.emit("# collection clear"); + self.emit_operand_comment("collection", collection); + if matches!(collection, IrOperand::Var(var) if self.verified_collection_construction_vectors.contains(&var.id)) { + self.emit("# cellscript abi: collection clear is covered by create-output vector verifier"); + return Ok(()); + } + if self.emit_stack_collection_clear(collection) { + return Ok(()); + } + self.emit("# cellscript abi: collection clear is not needed for verifier execution"); + self.emit("# cellscript abi: if this path is reached, the source program uses dynamic collections"); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + Ok(()) + } + + fn emit_stack_collection_clear(&mut self, collection: &IrOperand) -> bool { + let IrOperand::Var(collection) = collection else { + return false; + }; + if !self.stack_collection_vars.contains(&collection.id) { + return false; + } + self.emit("# cellscript abi: stack collection clear"); + self.emit_stack_load("t4", collection.id * 8); + self.emit("sd zero, -8(t4)"); + true + } + + pub(super) fn emit_collection_reverse(&mut self, collection: &IrOperand) -> Result<()> { + self.emit("# collection reverse"); + self.emit_operand_comment("collection", collection); + if self.emit_stack_collection_reverse(collection) { + return Ok(()); + } + self.emit("# cellscript abi: collection reverse is not available for this collection"); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + Ok(()) + } + + fn emit_stack_collection_reverse(&mut self, collection: &IrOperand) -> bool { + let IrOperand::Var(collection) = collection else { + return false; + }; + if !self.stack_collection_vars.contains(&collection.id) { + return false; + } + let Some(element_width) = molecule_vector_element_fixed_width(&collection.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) + else { + return false; + }; + if element_width == 0 || element_width > RUNTIME_COLLECTION_BUFFER_SIZE { + return false; + } + + self.emit(format!("# cellscript abi: stack collection reverse element_size={}", element_width)); + self.emit_stack_load("t4", collection.id * 8); + self.emit("ld t0, -8(t4)"); + let done_label = self.fresh_label("stack_collection_reverse_done"); + self.emit("li t1, 2"); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", done_label)); + + let left_offset = self.runtime_expr_temp_offset(0); + let right_offset = self.runtime_expr_temp_offset(1); + self.emit_stack_store("zero", left_offset); + self.emit("addi t0, t0, -1"); + self.emit_stack_store("t0", right_offset); + + let loop_label = self.fresh_label("stack_collection_reverse_loop"); + self.emit_label(&loop_label); + self.emit_stack_load("t0", left_offset); + self.emit_stack_load("t1", right_offset); + self.emit("sltu t2, t0, t1"); + self.emit(format!("beqz t2, {}", done_label)); + + self.emit_stack_load("t4", collection.id * 8); + self.emit(format!("li t3, {}", element_width)); + self.emit("mul t5, t0, t3"); + self.emit("add t5, t4, t5"); + self.emit("mul t6, t1, t3"); + self.emit("add t6, t4, t6"); + self.emit(format!("# cellscript abi: stack collection reverse swap element_size={}", element_width)); + for byte_index in 0..element_width { + if byte_index <= 2047 { + self.emit(format!("lbu t0, {}(t5)", byte_index)); + self.emit(format!("lbu t1, {}(t6)", byte_index)); + self.emit(format!("sb t1, {}(t5)", byte_index)); + self.emit(format!("sb t0, {}(t6)", byte_index)); + } else { + self.emit_large_addi("t2", "t5", byte_index as i64); + self.emit_large_addi("t3", "t6", byte_index as i64); + self.emit("lbu t0, 0(t2)"); + self.emit("lbu t1, 0(t3)"); + self.emit("sb t1, 0(t2)"); + self.emit("sb t0, 0(t3)"); + } + } + self.emit_stack_load("t0", left_offset); + self.emit("addi t0, t0, 1"); + self.emit_stack_store("t0", left_offset); + self.emit_stack_load("t1", right_offset); + self.emit("addi t1, t1, -1"); + self.emit_stack_store("t1", right_offset); + self.emit(format!("j {}", loop_label)); + self.emit_label(&done_label); + true + } + + pub(super) fn emit_collection_truncate(&mut self, collection: &IrOperand, len: &IrOperand) -> Result<()> { + self.emit("# collection truncate"); + self.emit_operand_comment("collection", collection); + self.emit_operand_comment("len", len); + if self.emit_stack_collection_truncate(collection, len) { + return Ok(()); + } + self.emit("# cellscript abi: collection truncate is not available for this collection"); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + Ok(()) + } + + fn emit_stack_collection_truncate(&mut self, collection: &IrOperand, len: &IrOperand) -> bool { + let IrOperand::Var(collection) = collection else { + return false; + }; + if !self.stack_collection_vars.contains(&collection.id) { + return false; + } + + self.emit("# cellscript abi: stack collection truncate"); + self.emit_stack_load("t4", collection.id * 8); + self.emit("ld t0, -8(t4)"); + self.emit_operand_to_register("t1", len); + let done_label = self.fresh_label("stack_collection_truncate_done"); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", done_label)); + self.emit("sd t1, -8(t4)"); + self.emit_label(&done_label); + true + } + + pub(super) fn emit_collection_swap(&mut self, collection: &IrOperand, left: &IrOperand, right: &IrOperand) -> Result<()> { + self.emit("# collection swap"); + self.emit_operand_comment("collection", collection); + self.emit_operand_comment("left", left); + self.emit_operand_comment("right", right); + if self.emit_stack_collection_swap(collection, left, right) { + return Ok(()); + } + self.emit("# cellscript abi: collection swap is not available for this collection"); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + Ok(()) + } + + fn emit_stack_collection_swap(&mut self, collection: &IrOperand, left: &IrOperand, right: &IrOperand) -> bool { + let IrOperand::Var(collection) = collection else { + return false; + }; + if !self.stack_collection_vars.contains(&collection.id) { + return false; + } + let Some(element_width) = molecule_vector_element_fixed_width(&collection.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) + else { + return false; + }; + if element_width == 0 || element_width > RUNTIME_COLLECTION_BUFFER_SIZE { + return false; + } + + self.emit(format!("# cellscript abi: stack collection swap element_size={}", element_width)); + self.emit_stack_load("t4", collection.id * 8); + self.emit("ld t0, -8(t4)"); + self.emit_operand_to_register("t1", left); + self.emit_operand_to_register("t2", right); + + let left_ok = self.fresh_label("stack_collection_swap_left_ok"); + self.emit("sltu t3, t1, t0"); + self.emit(format!("bnez t3, {}", left_ok)); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + self.emit_label(&left_ok); + + let right_ok = self.fresh_label("stack_collection_swap_right_ok"); + self.emit("sltu t3, t2, t0"); + self.emit(format!("bnez t3, {}", right_ok)); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + self.emit_label(&right_ok); + + self.emit(format!("li t3, {}", element_width)); + self.emit("mul t5, t1, t3"); + self.emit("add t5, t4, t5"); + self.emit("mul t6, t2, t3"); + self.emit("add t6, t4, t6"); + self.emit(format!("# cellscript abi: stack collection swap bytes element_size={}", element_width)); + for byte_index in 0..element_width { + if byte_index <= 2047 { + self.emit(format!("lbu t0, {}(t5)", byte_index)); + self.emit(format!("lbu t1, {}(t6)", byte_index)); + self.emit(format!("sb t1, {}(t5)", byte_index)); + self.emit(format!("sb t0, {}(t6)", byte_index)); + } else { + self.emit_large_addi("t2", "t5", byte_index as i64); + self.emit_large_addi("t3", "t6", byte_index as i64); + self.emit("lbu t0, 0(t2)"); + self.emit("lbu t1, 0(t3)"); + self.emit("sb t1, 0(t2)"); + self.emit("sb t0, 0(t3)"); + } + } + true + } + + pub(super) fn emit_collection_contains(&mut self, dest: &IrVar, collection: &IrOperand, value: &IrOperand) -> Result<()> { + self.emit("# collection contains"); + self.emit_operand_comment("collection", collection); + self.emit_operand_comment("value", value); + if self.emit_stack_collection_contains(dest, collection, value) { + return Ok(()); + } + self.emit("# cellscript abi: collection contains is not available for this collection"); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + Ok(()) + } + + fn emit_stack_collection_contains(&mut self, dest: &IrVar, collection: &IrOperand, value: &IrOperand) -> bool { + let IrOperand::Var(collection) = collection else { + return false; + }; + if !self.stack_collection_vars.contains(&collection.id) { + return false; + } + let Some(value_width) = self.constructed_byte_vector_part_width(value) else { + return false; + }; + let element_width = + molecule_vector_element_fixed_width(&collection.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes).unwrap_or(value_width); + if element_width == 0 || element_width != value_width { + return false; + } + + self.emit(format!("# cellscript abi: stack collection contains element_size={}", element_width)); + let index_offset = self.runtime_expr_temp_offset(0); + self.emit_stack_store("zero", index_offset); + self.emit_stack_store("zero", dest.id * 8); + let loop_label = self.fresh_label("stack_collection_contains_loop"); + let next_label = self.fresh_label("stack_collection_contains_next"); + let found_label = self.fresh_label("stack_collection_contains_found"); + let done_label = self.fresh_label("stack_collection_contains_done"); + self.emit_label(&loop_label); + self.emit_stack_load("t1", index_offset); + self.emit_stack_load("t4", collection.id * 8); + self.emit("ld t2, -8(t4)"); + self.emit(format!("beq t1, t2, {}", done_label)); + + if element_width <= 8 && fixed_scalar_operand_width(value).is_some() { + self.emit(format!("li t2, {}", element_width)); + self.emit("mul t3, t1, t2"); + self.emit("add t4, t4, t3"); + self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, element_width); + self.emit_operand_to_register("t5", value); + self.emit("sub t6, t0, t5"); + self.emit(format!("beqz t6, {}", found_label)); + } else { + let Some(source) = self.expected_fixed_byte_source(value, element_width) else { + return false; + }; + self.emit_prepare_fixed_byte_source(&source, element_width, "stack collection contains"); + for byte_index in 0..element_width { + self.emit_stack_load("t1", index_offset); + self.emit_stack_load("t4", collection.id * 8); + self.emit(format!("li t2, {}", element_width)); + self.emit("mul t3, t1, t2"); + self.emit("add t4, t4, t3"); + if byte_index <= 2047 { + self.emit(format!("lbu t0, {}(t4)", byte_index)); + } else { + self.emit_large_addi("t2", "t4", byte_index as i64); + self.emit("lbu t0, 0(t2)"); + } + self.emit_fixed_byte_source_byte_to("t5", "t6", &source, byte_index); + self.emit("sub t0, t0, t5"); + self.emit(format!("bnez t0, {}", next_label)); + } + self.emit(format!("j {}", found_label)); + } + + self.emit_label(&next_label); + self.emit_stack_load("t1", index_offset); + self.emit("addi t1, t1, 1"); + self.emit_stack_store("t1", index_offset); + self.emit(format!("j {}", loop_label)); + self.emit_label(&found_label); + self.emit("li t0, 1"); + self.emit_stack_store("t0", dest.id * 8); + self.emit_label(&done_label); + true + } + + pub(super) fn emit_collection_remove(&mut self, dest: &IrVar, collection: &IrOperand, index: &IrOperand) -> Result<()> { + self.emit("# collection remove"); + self.emit_operand_comment("collection", collection); + self.emit_operand_comment("index", index); + if self.emit_stack_collection_remove(dest, collection, index) { + return Ok(()); + } + self.emit("# cellscript abi: collection remove is not available for this collection"); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + Ok(()) + } + + fn emit_stack_collection_remove(&mut self, dest: &IrVar, collection: &IrOperand, index: &IrOperand) -> bool { + let IrOperand::Var(collection) = collection else { + return false; + }; + if !self.stack_collection_vars.contains(&collection.id) { + return false; + } + let Some(element_width) = molecule_vector_element_fixed_width(&collection.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) + else { + return false; + }; + let dest_scalar = fixed_scalar_width(&dest.ty, Some(element_width)).is_some(); + let dest_fixed_bytes = self.fixed_byte_like_width(&dest.ty).is_some_and(|width| width == element_width); + if !dest_scalar && !dest_fixed_bytes { + return false; + } + let removed_value_slots = if dest_fixed_bytes { element_width.div_ceil(8) } else { 0 }; + if dest_fixed_bytes && removed_value_slots + 1 > RUNTIME_EXPR_TEMP_SLOTS { + return false; + } + let Some(index_offset) = self.checked_runtime_expr_temp_offset(removed_value_slots) else { + return false; + }; + + self.emit(format!("# cellscript abi: stack collection remove element_size={}", element_width)); + self.emit_stack_load("t4", collection.id * 8); + self.emit("ld t0, -8(t4)"); + self.emit_operand_to_register("t1", index); + + let bounds_ok = self.fresh_label("stack_collection_remove_bounds_ok"); + self.emit("sltu t2, t1, t0"); + self.emit(format!("bnez t2, {}", bounds_ok)); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + self.emit_label(&bounds_ok); + + self.emit(format!("li t2, {}", element_width)); + self.emit("mul t3, t1, t2"); + self.emit("add t5, t4, t3"); + if dest_scalar { + self.emit_unaligned_scalar_load("t5", "t6", "t2", 0, element_width); + self.emit_stack_store("t6", dest.id * 8); + } else { + let removed_offset = self.runtime_expr_temp_offset(0); + self.emit(format!("# cellscript abi: stack collection remove snapshot fixed bytes size={}", element_width)); + for byte_index in 0..element_width { + if byte_index <= 2047 { + self.emit(format!("lbu t6, {}(t5)", byte_index)); + } else { + self.emit_large_addi("t2", "t5", byte_index as i64); + self.emit("lbu t6, 0(t2)"); + } + self.emit_sp_addi("t2", removed_offset + byte_index); + self.emit("sb t6, 0(t2)"); + } + self.emit_sp_addi("t6", removed_offset); + self.emit_stack_store("t6", dest.id * 8); + } + + self.emit_stack_store("t1", index_offset); + let shift_loop = self.fresh_label("stack_collection_remove_shift_loop"); + let shift_done = self.fresh_label("stack_collection_remove_shift_done"); + self.emit(format!("# cellscript abi: stack collection remove shift element_size={}", element_width)); + self.emit_label(&shift_loop); + self.emit_stack_load("t1", index_offset); + self.emit("addi t2, t1, 1"); + self.emit_stack_load("t4", collection.id * 8); + self.emit("ld t0, -8(t4)"); + self.emit("sltu t3, t2, t0"); + self.emit(format!("beqz t3, {}", shift_done)); + self.emit(format!("li t3, {}", element_width)); + self.emit("mul t5, t1, t3"); + self.emit("add t5, t4, t5"); + self.emit("mul t6, t2, t3"); + self.emit("add t6, t4, t6"); + for byte_index in 0..element_width { + if byte_index <= 2047 { + self.emit(format!("lbu t0, {}(t6)", byte_index)); + self.emit(format!("sb t0, {}(t5)", byte_index)); + } else { + self.emit_large_addi("t0", "t6", byte_index as i64); + self.emit("lbu t0, 0(t0)"); + self.emit_large_addi("t2", "t5", byte_index as i64); + self.emit("sb t0, 0(t2)"); + } + } + self.emit_stack_load("t1", index_offset); + self.emit("addi t1, t1, 1"); + self.emit_stack_store("t1", index_offset); + self.emit(format!("j {}", shift_loop)); + self.emit_label(&shift_done); + self.emit_stack_load("t4", collection.id * 8); + self.emit("ld t0, -8(t4)"); + self.emit("addi t0, t0, -1"); + self.emit("sd t0, -8(t4)"); + true + } + + pub(super) fn emit_collection_pop(&mut self, dest: &IrVar, collection: &IrOperand) -> Result<()> { + self.emit("# collection pop"); + self.emit_operand_comment("collection", collection); + if self.emit_stack_collection_pop(dest, collection) { + return Ok(()); + } + self.emit("# cellscript abi: collection pop is not available for this collection"); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + Ok(()) + } + + fn emit_stack_collection_pop(&mut self, dest: &IrVar, collection: &IrOperand) -> bool { + let IrOperand::Var(collection) = collection else { + return false; + }; + if !self.stack_collection_vars.contains(&collection.id) { + return false; + } + let Some(element_width) = molecule_vector_element_fixed_width(&collection.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) + else { + return false; + }; + let dest_scalar = fixed_scalar_width(&dest.ty, Some(element_width)).is_some(); + let dest_fixed_bytes = self.fixed_byte_like_width(&dest.ty).is_some_and(|width| width == element_width); + if !dest_scalar && !dest_fixed_bytes { + return false; + } + + self.emit(format!("# cellscript abi: stack collection pop element_size={}", element_width)); + self.emit_stack_load("t4", collection.id * 8); + self.emit("ld t0, -8(t4)"); + let bounds_ok = self.fresh_label("stack_collection_pop_bounds_ok"); + self.emit(format!("bnez t0, {}", bounds_ok)); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + self.emit_label(&bounds_ok); + + self.emit("addi t1, t0, -1"); + self.emit(format!("li t2, {}", element_width)); + self.emit("mul t3, t1, t2"); + self.emit("add t5, t4, t3"); + if dest_scalar { + self.emit_unaligned_scalar_load("t5", "t6", "t2", 0, element_width); + self.emit_stack_store("t6", dest.id * 8); + } else { + self.emit("# cellscript abi: stack collection pop fixed bytes"); + self.emit_stack_store("t5", dest.id * 8); + } + self.emit("sd t1, -8(t4)"); + true + } + + pub(super) fn emit_collection_insert(&mut self, collection: &IrOperand, index: &IrOperand, value: &IrOperand) -> Result<()> { + self.emit("# collection insert"); + self.emit_operand_comment("collection", collection); + self.emit_operand_comment("index", index); + self.emit_operand_comment("value", value); + if self.emit_stack_collection_insert(collection, index, value) { + return Ok(()); + } + self.emit("# cellscript abi: collection insert is not available for this collection"); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + Ok(()) + } + + fn emit_stack_collection_insert(&mut self, collection: &IrOperand, index: &IrOperand, value: &IrOperand) -> bool { + let IrOperand::Var(collection) = collection else { + return false; + }; + if !self.stack_collection_vars.contains(&collection.id) { + return false; + } + let Some(value_width) = self.constructed_byte_vector_part_width(value) else { + return false; + }; + let Some(element_width) = molecule_vector_element_fixed_width(&collection.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) + else { + return false; + }; + if element_width != value_width { + return false; + } + let value_scalar = element_width <= 8 && fixed_scalar_operand_width(value).is_some(); + let fixed_byte_source = if value_scalar { + None + } else { + if element_width > (RUNTIME_EXPR_TEMP_SLOTS - 2) * 8 { + return false; + } + let Some(source) = self.expected_fixed_byte_source(value, element_width) else { + return false; + }; + Some(source) + }; + + self.emit(format!("# cellscript abi: stack collection insert element_size={}", element_width)); + self.emit_stack_load("t4", collection.id * 8); + self.emit("ld t0, -8(t4)"); + self.emit_operand_to_register("t1", index); + + let bounds_ok = self.fresh_label("stack_collection_insert_bounds_ok"); + self.emit("sltu t2, t0, t1"); + self.emit(format!("beqz t2, {}", bounds_ok)); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + self.emit_label(&bounds_ok); + + self.emit(format!("li t2, {}", element_width)); + self.emit("mul t3, t0, t2"); + self.emit(format!("li t5, {}", RUNTIME_COLLECTION_BUFFER_SIZE)); + self.emit("sub t6, t5, t3"); + self.emit("sltu t6, t6, t2"); + let capacity_ok = self.fresh_label("stack_collection_insert_capacity_ok"); + self.emit(format!("beqz t6, {}", capacity_ok)); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + self.emit_label(&capacity_ok); + + let index_offset = self.runtime_expr_temp_offset(0); + let current_offset = self.runtime_expr_temp_offset(1); + self.emit_stack_store("t1", index_offset); + self.emit_stack_store("t0", current_offset); + if let Some(source) = fixed_byte_source.as_ref() { + self.emit_prepare_fixed_byte_source(source, element_width, "stack collection insert"); + let value_offset = self.runtime_expr_temp_offset(2); + self.emit(format!("# cellscript abi: stack collection insert snapshot fixed bytes size={}", element_width)); + for byte_index in 0..element_width { + self.emit_fixed_byte_source_byte_to("t1", "t6", source, byte_index); + self.emit_sp_addi("t6", value_offset + byte_index); + self.emit("sb t1, 0(t6)"); + } + } + let shift_loop = self.fresh_label("stack_collection_insert_shift_loop"); + let shift_done = self.fresh_label("stack_collection_insert_shift_done"); + self.emit(format!("# cellscript abi: stack collection insert shift element_size={}", element_width)); + self.emit_label(&shift_loop); + self.emit_stack_load("t0", current_offset); + self.emit_stack_load("t1", index_offset); + self.emit(format!("beq t0, t1, {}", shift_done)); + self.emit("addi t2, t0, -1"); + self.emit_stack_load("t4", collection.id * 8); + self.emit(format!("li t3, {}", element_width)); + self.emit("mul t5, t0, t3"); + self.emit("add t5, t4, t5"); + self.emit("mul t6, t2, t3"); + self.emit("add t6, t4, t6"); + if element_width <= 8 { + self.emit_unaligned_scalar_load("t6", "t0", "t2", 0, element_width); + match element_width { + 1 => self.emit("sb t0, 0(t5)"), + 2 => self.emit("sh t0, 0(t5)"), + 4 => self.emit("sw t0, 0(t5)"), + 8 => self.emit("sd t0, 0(t5)"), + _ => return false, + } + } else { + for byte_index in 0..element_width { + if byte_index <= 2047 { + self.emit(format!("lbu t0, {}(t6)", byte_index)); + self.emit(format!("sb t0, {}(t5)", byte_index)); + } else { + self.emit_large_addi("t0", "t6", byte_index as i64); + self.emit("lbu t0, 0(t0)"); + self.emit_large_addi("t2", "t5", byte_index as i64); + self.emit("sb t0, 0(t2)"); + } + } + } + self.emit_stack_load("t0", current_offset); + self.emit("addi t0, t0, -1"); + self.emit_stack_store("t0", current_offset); + self.emit(format!("j {}", shift_loop)); + self.emit_label(&shift_done); + + self.emit_stack_load("t4", collection.id * 8); + self.emit_stack_load("t0", index_offset); + self.emit(format!("li t2, {}", element_width)); + self.emit("mul t3, t0, t2"); + self.emit("add t5, t4, t3"); + if value_scalar { + self.emit_operand_to_register("t1", value); + match element_width { + 1 => self.emit("sb t1, 0(t5)"), + 2 => self.emit("sh t1, 0(t5)"), + 4 => self.emit("sw t1, 0(t5)"), + 8 => self.emit("sd t1, 0(t5)"), + _ => return false, + } + } else { + let value_offset = self.runtime_expr_temp_offset(2); + self.emit(format!("# cellscript abi: stack collection insert copy fixed bytes size={}", element_width)); + for byte_index in 0..element_width { + self.emit_sp_addi("t6", value_offset + byte_index); + self.emit("lbu t1, 0(t6)"); + if byte_index <= 2047 { + self.emit(format!("sb t1, {}(t5)", byte_index)); + } else { + self.emit_large_addi("t0", "t5", byte_index as i64); + self.emit("sb t1, 0(t0)"); + } + } + } + self.emit_stack_load("t4", collection.id * 8); + self.emit("ld t0, -8(t4)"); + self.emit("addi t0, t0, 1"); + self.emit("sd t0, -8(t4)"); + true + } + + pub(super) fn emit_collection_set(&mut self, collection: &IrOperand, index: &IrOperand, value: &IrOperand) -> Result<()> { + self.emit("# collection set"); + self.emit_operand_comment("collection", collection); + self.emit_operand_comment("index", index); + self.emit_operand_comment("value", value); + if self.emit_stack_collection_set(collection, index, value) { + return Ok(()); + } + self.emit("# cellscript abi: collection set is not available for this collection"); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + Ok(()) + } + + fn emit_stack_collection_set(&mut self, collection: &IrOperand, index: &IrOperand, value: &IrOperand) -> bool { + let IrOperand::Var(collection) = collection else { + return false; + }; + if !self.stack_collection_vars.contains(&collection.id) { + return false; + } + let Some(value_width) = self.constructed_byte_vector_part_width(value) else { + return false; + }; + let Some(element_width) = molecule_vector_element_fixed_width(&collection.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) + else { + return false; + }; + if element_width == 0 || element_width > RUNTIME_COLLECTION_BUFFER_SIZE || element_width != value_width { + return false; + } + let value_scalar = element_width <= 8 && fixed_scalar_operand_width(value).is_some(); + let fixed_byte_source = if value_scalar { + None + } else { + let Some(source) = self.expected_fixed_byte_source(value, element_width) else { + return false; + }; + Some(source) + }; + + self.emit(format!("# cellscript abi: stack collection set element_size={}", element_width)); + if let Some(source) = fixed_byte_source.as_ref() { + self.emit_prepare_fixed_byte_source(source, element_width, "stack collection set"); + } + self.emit_stack_load("t4", collection.id * 8); + self.emit("ld t0, -8(t4)"); + self.emit_operand_to_register("t1", index); + + let bounds_ok = self.fresh_label("stack_collection_set_bounds_ok"); + self.emit("sltu t2, t1, t0"); + self.emit(format!("bnez t2, {}", bounds_ok)); + self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); + self.emit_label(&bounds_ok); + + self.emit(format!("li t2, {}", element_width)); + self.emit("mul t3, t1, t2"); + self.emit("add t5, t4, t3"); + if value_scalar { + self.emit_operand_to_register("t1", value); + match element_width { + 1 => self.emit("sb t1, 0(t5)"), + 2 => self.emit("sh t1, 0(t5)"), + 4 => self.emit("sw t1, 0(t5)"), + 8 => self.emit("sd t1, 0(t5)"), + _ => return false, + } + } else { + let source = fixed_byte_source.as_ref().expect("fixed byte source"); + self.emit(format!("# cellscript abi: stack collection set copy fixed bytes size={}", element_width)); + for byte_index in 0..element_width { + self.emit_fixed_byte_source_byte_to("t1", "t6", source, byte_index); + if byte_index <= 2047 { + self.emit(format!("sb t1, {}(t5)", byte_index)); + } else { + self.emit_large_addi("t0", "t5", byte_index as i64); + self.emit("sb t1, 0(t0)"); + } + } + } + true + } +} diff --git a/src/codegen/expr.rs b/src/codegen/expr.rs new file mode 100644 index 00000000..f010d334 --- /dev/null +++ b/src/codegen/expr.rs @@ -0,0 +1,768 @@ +use super::*; + +impl CodeGenerator { + pub(super) fn emit_load_const(&mut self, dest: &IrVar, value: &IrConst) -> Result<()> { + if dest.ty == IrType::U128 { + self.emit_materialize_u128_operand_to_var(dest, &IrOperand::Const(value.clone())); + return Ok(()); + } + match value { + IrConst::Unit => self.emit("li t0, 0"), + IrConst::U8(n) => self.emit(format!("li t0, {}", n)), + IrConst::U16(n) => self.emit(format!("li t0, {}", n)), + IrConst::U32(n) => self.emit(format!("li t0, {}", n)), + IrConst::U64(n) => self.emit(format!("li t0, {}", n)), + IrConst::U128(value) => { + if let Some(offset) = self.fixed_byte_local_offsets.get(&dest.id).copied() { + self.emit_store_const_bytes_to_stack(&value.to_le_bytes(), offset); + self.emit_sp_addi("t0", offset); + self.emit_stack_store("t0", dest.id * 8); + return Ok(()); + } + let label = self.const_data_label_for_bytes(value.to_le_bytes().to_vec()); + self.emit(format!("la t0, {}", label)); + } + IrConst::Bool(b) => self.emit(format!("li t0, {}", if *b { 1 } else { 0 })), + IrConst::Address(_) | IrConst::Hash(_) | IrConst::Array(_) => { + let Some(bytes) = fixed_byte_const_bytes(value) else { + self.emit("# cellscript abi: fail closed because fixed-byte constant bytes are not materializable"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + self.emit("li t0, 0"); + self.emit_stack_store("t0", dest.id * 8); + return Ok(()); + }; + let label = self.const_data_label_for_bytes(bytes); + self.emit(format!("la t0, {}", label)); + } + } + self.emit_stack_store("t0", dest.id * 8); + Ok(()) + } + + pub(super) fn emit_load_var(&mut self, dest: &IrVar, name: &str) -> Result<()> { + self.emit(format!("# load var {}", name)); + let Some(offset) = self.named_var_offsets.get(name).copied() else { + self.emit("# cellscript abi: fail closed because named variable slot was not allocated"); + self.emit_fail(CellScriptRuntimeError::ConsumeInvalidOperand); + return Ok(()); + }; + self.emit_stack_load("t0", offset); + self.emit_stack_store("t0", dest.id * 8); + Ok(()) + } + + pub(super) fn emit_store_var(&mut self, name: &str, src: &IrOperand) -> Result<()> { + self.emit(format!("# store var {}", name)); + let Some(offset) = self.named_var_offsets.get(name).copied() else { + self.emit("# cellscript abi: fail closed because named variable slot was not allocated"); + self.emit_fail(CellScriptRuntimeError::ConsumeInvalidOperand); + return Ok(()); + }; + self.emit_operand_to_register("t0", src); + self.emit_stack_store("t0", offset); + Ok(()) + } + + pub(super) fn emit_binary(&mut self, dest: &IrVar, op: BinaryOp, left: &IrOperand, right: &IrOperand) -> Result<()> { + if self.emit_u128_add_sub_with_u64(dest, op, left, right) { + return Ok(()); + } + if self.emit_u128_binary(dest, op, left, right) { + return Ok(()); + } + if matches!(op, BinaryOp::Eq | BinaryOp::Ne) && self.emit_dynamic_byte_comparison(dest, op, left, right) { + return Ok(()); + } + if matches!(op, BinaryOp::Eq | BinaryOp::Ne) + && (operand_fixed_byte_width(left).is_some() || operand_fixed_byte_width(right).is_some()) + { + if self.emit_fixed_byte_comparison(dest, op, left, right) { + return Ok(()); + } + if self.emit_generic_fixed_byte_comparison(dest, op, left, right) { + return Ok(()); + } + // Final fallback: emit a fail-closed trap with specific error code + self.emit(format!("# binary {:?} over fixed-byte operands (unresolved)", op)); + self.emit("# cellscript abi: fail closed because fixed-byte operand sources are not available"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonMaterializationUnresolved); + return Ok(()); + } + + if dest.ty == IrType::U128 || self.operand_is_u128(left) || self.operand_is_u128(right) { + self.emit(format!("# binary {:?} over unsupported u128 operand shape", op)); + self.emit("# cellscript abi: fail closed because generic u128 arithmetic/comparison shape is not lowered"); + self.emit_fail(CellScriptRuntimeError::NumericOrDiscriminantInvalid); + return Ok(()); + } + + self.emit_expected_operand_to_t1(left); + self.emit_stack_store("t1", dest.id * 8); + self.emit_expected_operand_to_t1(right); + self.emit_stack_load("t0", dest.id * 8); + + match op { + BinaryOp::Add => self.emit("add t0, t0, t1"), + BinaryOp::Sub => self.emit("sub t0, t0, t1"), + BinaryOp::Mul => self.emit("mul t0, t0, t1"), + BinaryOp::Div if binary_operands_signed_i32(left, right) => self.emit("div t0, t0, t1"), + BinaryOp::Div => self.emit("divu t0, t0, t1"), + BinaryOp::Mod if binary_operands_signed_i32(left, right) => self.emit("rem t0, t0, t1"), + BinaryOp::Mod => self.emit("remu t0, t0, t1"), + BinaryOp::Eq => { + self.emit("sub t0, t0, t1"); + self.emit("seqz t0, t0"); + } + BinaryOp::Ne => { + self.emit("sub t0, t0, t1"); + self.emit("snez t0, t0"); + } + BinaryOp::Lt if binary_operands_signed_i32(left, right) => self.emit("slt t0, t0, t1"), + BinaryOp::Lt => self.emit("sltu t0, t0, t1"), + BinaryOp::Le if binary_operands_signed_i32(left, right) => { + self.emit("slt t0, t1, t0"); + self.emit("xori t0, t0, 1"); + } + BinaryOp::Le => { + self.emit("sltu t0, t1, t0"); + self.emit("xori t0, t0, 1"); + } + BinaryOp::Gt if binary_operands_signed_i32(left, right) => self.emit("slt t0, t1, t0"), + BinaryOp::Gt => self.emit("sltu t0, t1, t0"), + BinaryOp::Ge if binary_operands_signed_i32(left, right) => { + self.emit("slt t0, t0, t1"); + self.emit("xori t0, t0, 1"); + } + BinaryOp::Ge => { + self.emit("sltu t0, t0, t1"); + self.emit("xori t0, t0, 1"); + } + BinaryOp::And => self.emit("and t0, t0, t1"), + BinaryOp::Or => self.emit("or t0, t0, t1"), + } + + self.emit_stack_store("t0", dest.id * 8); + Ok(()) + } + + fn emit_u128_binary(&mut self, dest: &IrVar, op: BinaryOp, left: &IrOperand, right: &IrOperand) -> bool { + let arithmetic_u128 = dest.ty == IrType::U128 || self.operand_is_u128_like(left) || self.operand_is_u128_like(right); + let comparison_u128 = matches!(op, BinaryOp::Eq | BinaryOp::Ne | BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge) + && (self.operand_is_u128_like(left) || self.operand_is_u128_like(right)); + if !arithmetic_u128 && !comparison_u128 { + return false; + } + + match op { + BinaryOp::Add | BinaryOp::Sub if dest.ty == IrType::U128 => { + self.emit_u128_add_sub(dest, op, left, right); + true + } + BinaryOp::Eq | BinaryOp::Ne | BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge => { + self.emit_u128_compare(dest, op, left, right); + true + } + BinaryOp::Mul if dest.ty == IrType::U128 => { + self.emit_u128_mul(dest, left, right); + true + } + BinaryOp::Div if dest.ty == IrType::U128 => { + self.emit_u128_div(dest, left, right); + true + } + BinaryOp::Mod if arithmetic_u128 => { + self.emit("# cellscript abi: u128 Mod requires full-width lowering; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + true + } + BinaryOp::Add | BinaryOp::Sub if arithmetic_u128 => { + self.emit(format!("# cellscript abi: u128 {:?} result is not materialized as u128; fail closed", op)); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + true + } + _ => false, + } + } + + fn emit_u128_add_sub(&mut self, dest: &IrVar, op: BinaryOp, left: &IrOperand, right: &IrOperand) { + let Some(dest_offset) = self.u128_value_offsets.get(&dest.id).copied() else { + self.emit("# cellscript abi: u128 arithmetic destination has no storage; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return; + }; + if !self.emit_u128_binary_operand_limbs(left, right, "u128 arithmetic") { + return; + } + let ok_label = self.fresh_label("u128_arithmetic_ok"); + let overflow_label = self.fresh_label("u128_arithmetic_overflow"); + match op { + BinaryOp::Add => { + self.emit("# cellscript abi: u128 add with carry"); + self.emit("add t4, t0, t2"); + self.emit("sltu t6, t4, t0"); + self.emit("add t5, t1, t3"); + self.emit("sltu a6, t5, t1"); + self.emit(format!("bnez a6, {}", overflow_label)); + self.emit("add t5, t5, t6"); + self.emit("sltu a6, t5, t6"); + self.emit(format!("bnez a6, {}", overflow_label)); + } + BinaryOp::Sub => { + self.emit("# cellscript abi: u128 sub with borrow"); + self.emit("sltu t6, t0, t2"); + self.emit("sltu a6, t1, t3"); + self.emit(format!("bnez a6, {}", overflow_label)); + self.emit("sub t4, t0, t2"); + self.emit("sub t5, t1, t3"); + self.emit(format!("beqz t6, {}", ok_label)); + self.emit(format!("beqz t5, {}", overflow_label)); + self.emit("addi t5, t5, -1"); + } + _ => unreachable!("u128 add/sub only"), + } + self.emit_label(&ok_label); + self.emit_stack_store("t4", dest_offset); + self.emit_stack_store("t5", dest_offset + 8); + self.emit_store_u128_pointer_for_var(dest.id, dest_offset); + let done_label = self.fresh_label("u128_arithmetic_done"); + self.emit(format!("j {}", done_label)); + self.emit_label(&overflow_label); + self.emit_runtime_error_comment(CellScriptRuntimeError::AggregateAmountMismatch); + self.emit(format!("li a0, {}", CellScriptRuntimeError::AggregateAmountMismatch.code())); + self.emit_epilogue(); + self.emit_label(&done_label); + } + + fn emit_u128_compare(&mut self, dest: &IrVar, op: BinaryOp, left: &IrOperand, right: &IrOperand) { + if !self.emit_u128_binary_operand_limbs(left, right, "u128 compare") { + return; + } + self.emit("# cellscript abi: u128 compare high limb first"); + let high_lt = self.fresh_label("u128_compare_high_lt"); + let high_gt = self.fresh_label("u128_compare_high_gt"); + let same_high = self.fresh_label("u128_compare_same_high"); + let done = self.fresh_label("u128_compare_done"); + self.emit("sltu t4, t1, t3"); + self.emit(format!("bnez t4, {}", high_lt)); + self.emit("sltu t4, t3, t1"); + self.emit(format!("bnez t4, {}", high_gt)); + self.emit_label(&same_high); + match op { + BinaryOp::Eq => { + self.emit("sub t4, t0, t2"); + self.emit("seqz t0, t4"); + } + BinaryOp::Ne => { + self.emit("sub t4, t0, t2"); + self.emit("snez t0, t4"); + } + BinaryOp::Lt => self.emit("sltu t0, t0, t2"), + BinaryOp::Le => { + self.emit("sltu t0, t2, t0"); + self.emit("xori t0, t0, 1"); + } + BinaryOp::Gt => self.emit("sltu t0, t2, t0"), + BinaryOp::Ge => { + self.emit("sltu t0, t0, t2"); + self.emit("xori t0, t0, 1"); + } + _ => unreachable!("u128 compare only"), + } + self.emit(format!("j {}", done)); + self.emit_label(&high_lt); + let high_lt_value = matches!(op, BinaryOp::Ne | BinaryOp::Lt | BinaryOp::Le); + self.emit(format!("li t0, {}", u8::from(high_lt_value))); + self.emit(format!("j {}", done)); + self.emit_label(&high_gt); + let high_gt_value = matches!(op, BinaryOp::Ne | BinaryOp::Gt | BinaryOp::Ge); + self.emit(format!("li t0, {}", u8::from(high_gt_value))); + self.emit_label(&done); + self.emit_stack_store("t0", dest.id * 8); + } + + fn emit_u128_mul(&mut self, dest: &IrVar, left: &IrOperand, right: &IrOperand) { + let Some(dest_offset) = self.u128_value_offsets.get(&dest.id).copied() else { + self.emit("# cellscript abi: u128 multiplication destination has no storage; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return; + }; + if !self.emit_u128_binary_operand_limbs(left, right, "u128 multiplication") { + return; + } + self.emit("# cellscript abi: checked u128 multiplication"); + let overflow_label = self.fresh_label("u128_mul_overflow"); + let high_left_zero = self.fresh_label("u128_mul_high_left_zero"); + let high_pair_ok = self.fresh_label("u128_mul_high_pair_ok"); + let done_label = self.fresh_label("u128_mul_done"); + + self.emit(format!("beqz t1, {}", high_left_zero)); + self.emit(format!("bnez t3, {}", overflow_label)); + self.emit_label(&high_left_zero); + self.emit(format!("beqz t3, {}", high_pair_ok)); + self.emit(format!("bnez t1, {}", overflow_label)); + self.emit_label(&high_pair_ok); + + self.emit("mul t4, t0, t2"); + self.emit("mulhu a2, t0, t2"); + + self.emit("mul a3, t0, t3"); + self.emit("mulhu a4, t0, t3"); + self.emit(format!("bnez a4, {}", overflow_label)); + + self.emit("mul a5, t1, t2"); + self.emit("mulhu a6, t1, t2"); + self.emit(format!("bnez a6, {}", overflow_label)); + + self.emit("add t5, a2, a3"); + self.emit("sltu a7, t5, a2"); + self.emit(format!("bnez a7, {}", overflow_label)); + self.emit("add t5, t5, a5"); + self.emit("sltu a7, t5, a5"); + self.emit(format!("bnez a7, {}", overflow_label)); + + self.emit_stack_store("t4", dest_offset); + self.emit_stack_store("t5", dest_offset + 8); + self.emit_store_u128_pointer_for_var(dest.id, dest_offset); + self.emit(format!("j {}", done_label)); + + self.emit_label(&overflow_label); + self.emit_runtime_error_comment(CellScriptRuntimeError::AggregateAmountMismatch); + self.emit(format!("li a0, {}", CellScriptRuntimeError::AggregateAmountMismatch.code())); + self.emit_epilogue(); + self.emit_label(&done_label); + } + + fn emit_u128_div(&mut self, dest: &IrVar, left: &IrOperand, right: &IrOperand) { + let Some(dest_offset) = self.u128_value_offsets.get(&dest.id).copied() else { + self.emit("# cellscript abi: u128 division destination has no storage; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return; + }; + if !self.emit_u128_binary_operand_limbs(left, right, "u128 division") { + return; + } + self.emit("# cellscript abi: checked u128 division by restoring long division"); + let ok_label = self.fresh_label("u128_div_denominator_ok"); + let loop_label = self.fresh_label("u128_div_loop"); + let skip_sub_label = self.fresh_label("u128_div_skip_subtract"); + let subtract_label = self.fresh_label("u128_div_subtract"); + let done_label = self.fresh_label("u128_div_done"); + let fail_label = self.fresh_label("u128_div_zero_denominator"); + + self.emit("or t4, t2, t3"); + self.emit(format!("bnez t4, {}", ok_label)); + self.emit(format!("j {}", fail_label)); + self.emit_label(&ok_label); + self.emit("li t4, 0"); // remainder low + self.emit("li t5, 0"); // remainder high + self.emit("li a2, 0"); // quotient low + self.emit("li a3, 0"); // quotient high + self.emit("li a4, 128"); + self.emit_label(&loop_label); + + self.emit("slt a5, t1, zero"); // next numerator bit + self.emit("slt a6, t4, zero"); // carry from remainder low + self.emit("slli t4, t4, 1"); + self.emit("or t4, t4, a5"); + self.emit("slli t5, t5, 1"); + self.emit("or t5, t5, a6"); + + self.emit("slt a5, t0, zero"); // carry from numerator low + self.emit("slli t0, t0, 1"); + self.emit("slli t1, t1, 1"); + self.emit("or t1, t1, a5"); + + self.emit("slt a5, a2, zero"); // carry from quotient low + self.emit("slli a2, a2, 1"); + self.emit("slli a3, a3, 1"); + self.emit("or a3, a3, a5"); + + self.emit("sltu a5, t5, t3"); + self.emit(format!("bnez a5, {}", skip_sub_label)); + self.emit("sltu a5, t3, t5"); + self.emit(format!("bnez a5, {}", subtract_label)); + self.emit("sltu a5, t4, t2"); + self.emit(format!("bnez a5, {}", skip_sub_label)); + + self.emit_label(&subtract_label); + self.emit("sltu a5, t4, t2"); + self.emit("sub t4, t4, t2"); + self.emit("sub t5, t5, t3"); + self.emit("sub t5, t5, a5"); + self.emit("addi a2, a2, 1"); + + self.emit_label(&skip_sub_label); + self.emit("addi a4, a4, -1"); + self.emit(format!("bnez a4, {}", loop_label)); + self.emit_stack_store("a2", dest_offset); + self.emit_stack_store("a3", dest_offset + 8); + self.emit_store_u128_pointer_for_var(dest.id, dest_offset); + self.emit(format!("j {}", done_label)); + + self.emit_label(&fail_label); + self.emit_runtime_error_comment(CellScriptRuntimeError::NumericOrDiscriminantInvalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::NumericOrDiscriminantInvalid.code())); + self.emit_epilogue(); + self.emit_label(&done_label); + } + + fn emit_dynamic_byte_comparison(&mut self, dest: &IrVar, op: BinaryOp, left: &IrOperand, right: &IrOperand) -> bool { + let (IrOperand::Var(left_var), IrOperand::Var(right_var)) = (left, right) else { + return false; + }; + let Some(left_len_offset) = self.dynamic_value_size_offsets.get(&left_var.id).copied() else { + return false; + }; + let Some(right_len_offset) = self.dynamic_value_size_offsets.get(&right_var.id).copied() else { + return false; + }; + + let equal_value = if matches!(op, BinaryOp::Eq) { 1 } else { 0 }; + let mismatch_value = if matches!(op, BinaryOp::Eq) { 0 } else { 1 }; + let len_equal_label = self.fresh_label("dynamic_bytes_len_equal"); + let bytes_equal_label = self.fresh_label("dynamic_bytes_equal"); + let done_label = self.fresh_label("dynamic_bytes_cmp_done"); + + self.emit(format!("# binary {:?} over dynamic byte operands", op)); + self.emit_stack_load("t0", left_len_offset); + self.emit_stack_load("t1", right_len_offset); + self.emit("sub t2, t0, t1"); + self.emit(format!("beqz t2, {}", len_equal_label)); + self.emit(format!("li t0, {}", mismatch_value)); + self.emit_stack_store("t0", dest.id * 8); + self.emit(format!("j {}", done_label)); + + self.emit_label(&len_equal_label); + self.emit_stack_load("a0", left_var.id * 8); + self.emit_stack_load("a1", right_var.id * 8); + self.emit_stack_load("a2", left_len_offset); + self.emit("call __cellscript_memcmp_fixed"); + self.emit(format!("beqz a0, {}", bytes_equal_label)); + self.emit(format!("li t0, {}", mismatch_value)); + self.emit_stack_store("t0", dest.id * 8); + self.emit(format!("j {}", done_label)); + + self.emit_label(&bytes_equal_label); + self.emit(format!("li t0, {}", equal_value)); + self.emit_stack_store("t0", dest.id * 8); + self.emit_label(&done_label); + true + } + + pub(super) fn emit_unary(&mut self, dest: &IrVar, op: UnaryOp, operand: &IrOperand) -> Result<()> { + match operand { + IrOperand::Const(IrConst::U64(n)) => self.emit(format!("li t0, {}", n)), + IrOperand::Var(v) => self.emit_stack_load("t0", v.id * 8), + _ => self.emit("li t0, 0"), + } + + match op { + UnaryOp::Neg => self.emit("neg t0, t0"), + UnaryOp::Not => self.emit("xori t0, t0, 1"), + UnaryOp::Ref | UnaryOp::Deref => self.emit("# reference conversion (no-op in asm backend)"), + } + + self.emit_stack_store("t0", dest.id * 8); + Ok(()) + } + + pub(super) fn emit_field_access(&mut self, dest: &IrVar, obj: &IrOperand, field: &str) -> Result<()> { + if self.emit_fixed_byte_field_access(dest, obj, field) { + return Ok(()); + } + if self.emit_schema_field_access(dest, obj, field) { + return Ok(()); + } + if self.emit_aggregate_field_access(dest, obj, field) { + return Ok(()); + } + if self.emit_tuple_call_return_field_access(dest, obj, field) { + return Ok(()); + } + if self.emit_generic_field_access(dest, obj, field) { + return Ok(()); + } + + self.emit(format!("# field access .{} (unresolved)", field)); + self.emit("# cellscript abi: fail closed because field offset is not computable from available type layout"); + self.emit_fail(CellScriptRuntimeError::DynamicFieldBoundsInvalid); + Ok(()) + } + + fn emit_fixed_byte_field_access(&mut self, dest: &IrVar, obj: &IrOperand, field: &str) -> bool { + let IrOperand::Var(var) = obj else { + return false; + }; + let layout = aggregate_field_layout(&var.ty, field).or_else(|| { + named_type_name(&var.ty) + .and_then(|type_name| self.type_layouts.get(type_name).and_then(|fields| fields.get(field)).cloned()) + }); + let Some(layout) = layout else { + return false; + }; + let Some(parent_width) = self.fixed_byte_like_width(&var.ty) else { + return false; + }; + let Some(source) = self.expected_fixed_byte_source(obj, parent_width) else { + return false; + }; + if is_fixed_scalar_ir_type(&dest.ty) { + let Some(width) = layout_fixed_scalar_width(&layout) else { + return false; + }; + self.emit(format!( + "# cellscript abi: fixed-byte scalar field {}.{} offset={} size={}", + aggregate_type_label(&var.ty), + field, + layout.offset, + width + )); + self.emit_prepare_fixed_byte_source(&source, parent_width, "fixed-byte scalar field access"); + if !self.emit_fixed_byte_source_pointer_or_const_to("t4", &source) { + return false; + } + self.emit_unaligned_scalar_load("t4", "t0", "t2", layout.offset, width); + if layout.ty == IrType::I32 { + self.emit_sign_extend_i32("t0"); + } + self.emit_stack_store("t0", dest.id * 8); + if let ExpectedFixedByteSource::SchemaField(parent) = &source { + let mut nested_layout = layout.clone(); + nested_layout.offset += parent.layout.offset; + let nested_source = SchemaFieldValueSource { + obj_var_id: parent.obj_var_id, + type_name: parent.type_name.clone(), + field: format!("{}.{}", parent.field, field), + layout: nested_layout, + }; + self.schema_field_value_sources.insert(dest.id, nested_source.clone()); + if dest.ty == IrType::U64 { + self.prelude_u64_value_sources.insert(dest.id, PreludeU64ValueSource::Field(nested_source)); + } + } + return true; + } + let Some(width) = layout_fixed_byte_width(&layout).or_else(|| self.fixed_named_type_width(&layout.ty)) else { + return false; + }; + let Some(dest_offset) = self.fixed_byte_local_offsets.get(&dest.id).copied() else { + return false; + }; + + self.emit(format!( + "# cellscript abi: fixed-byte field {}.{} offset={} size={}", + aggregate_type_label(&var.ty), + field, + layout.offset, + width + )); + self.emit_prepare_fixed_byte_source(&source, parent_width, "fixed-byte field access"); + if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &source) { + return false; + } + self.emit(format!("addi a0, a0, {}", layout.offset)); + self.emit_sp_addi("a1", dest_offset); + self.emit(format!("li a2, {}", width)); + self.emit("call __cellscript_memcpy_fixed"); + self.emit_sp_addi("t0", dest_offset); + self.emit_stack_store("t0", dest.id * 8); + if let ExpectedFixedByteSource::SchemaField(parent) = &source { + let mut nested_layout = layout.clone(); + nested_layout.offset += parent.layout.offset; + let nested_source = SchemaFieldValueSource { + obj_var_id: parent.obj_var_id, + type_name: parent.type_name.clone(), + field: format!("{}.{}", parent.field, field), + layout: nested_layout, + }; + self.schema_field_value_sources.insert(dest.id, nested_source); + } + true + } + + fn emit_schema_field_access(&mut self, dest: &IrVar, obj: &IrOperand, field: &str) -> bool { + let IrOperand::Var(var) = obj else { + return false; + }; + if !self.schema_pointer_vars.contains(&var.id) { + return false; + } + let Some(type_name) = named_type_name(&var.ty) else { + return false; + }; + let Some(layout) = self.type_layouts.get(type_name).and_then(|fields| fields.get(field)).cloned() else { + return false; + }; + let Some(width) = layout_fixed_byte_width(&layout) else { + return self.emit_dynamic_schema_field_access(dest, var, type_name, field, &layout); + }; + + self.emit(format!("# field access .{}", field)); + self.emit(format!("# cellscript abi: schema field {}.{} offset={} size={}", type_name, field, layout.offset, width)); + self.emit_stack_load("t4", var.id * 8); + if let Some(size_offset) = self.schema_pointer_size_offsets.get(&var.id).copied() { + if let Some(expected_size) = self.type_fixed_sizes.get(type_name).copied() { + self.emit_loaded_schema_exact_size_check(size_offset, expected_size, type_name); + self.emit_loaded_schema_bounds_check(size_offset, layout.offset + width, &format!("{}.{}", type_name, field)); + if layout_fixed_scalar_width(&layout).is_some() { + self.emit_unaligned_scalar_load("t4", "t0", "t2", layout.offset, width); + } else { + self.emit(format!("addi t0, t4, {}", layout.offset)); + } + } else { + self.emit_molecule_table_field_bounds_to_t5( + "t4", + size_offset, + layout.index, + width, + &format!("{}.{}", type_name, field), + ); + self.emit("add t4, t4, t5"); + if layout_fixed_scalar_width(&layout).is_some() { + self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, width); + } else { + self.emit("addi t0, t4, 0"); + } + } + } else { + if !self.type_fixed_sizes.contains_key(type_name) { + return false; + } + if layout_fixed_scalar_width(&layout).is_some() { + self.emit_unaligned_scalar_load("t4", "t0", "t2", layout.offset, width); + } else { + self.emit(format!("addi t0, t4, {}", layout.offset)); + } + } + self.emit_stack_store("t0", dest.id * 8); + true + } + + fn emit_dynamic_schema_field_access( + &mut self, + dest: &IrVar, + obj: &IrVar, + type_name: &str, + field: &str, + layout: &SchemaFieldLayout, + ) -> bool { + if molecule_vector_element_fixed_width(&layout.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes).is_none() { + return false; + } + let Some(size_offset) = self.schema_pointer_size_offsets.get(&obj.id).copied() else { + return false; + }; + let Some(dest_size_offset) = self.dynamic_value_size_offsets.get(&dest.id).copied() else { + return false; + }; + let Some(field_count) = self.type_layouts.get(type_name).map(|fields| fields.len()) else { + return false; + }; + + let context = format!("{}.{}", type_name, field); + self.emit(format!("# field access .{}", field)); + self.emit(format!("# cellscript abi: dynamic schema field {} index={} as Molecule vector bytes", context, layout.index)); + self.emit_stack_load("t4", obj.id * 8); + self.emit_molecule_table_field_span_to_t5_t6("t4", size_offset, layout.index, field_count, &context); + self.emit("add t0, t4, t5"); + self.emit("sub t1, t6, t5"); + self.emit_stack_store("t0", dest.id * 8); + self.emit_stack_store("t1", dest_size_offset); + true + } + + fn emit_aggregate_field_access(&mut self, dest: &IrVar, obj: &IrOperand, field: &str) -> bool { + let IrOperand::Var(var) = obj else { + return false; + }; + let Some(source) = self.aggregate_pointer_sources.get(&var.id) else { + return false; + }; + let source_ty = source.ty.clone(); + let Some(layout) = aggregate_field_layout(&source_ty, field) else { + return false; + }; + let Some(width) = layout_fixed_byte_width(&layout) else { + return false; + }; + + self.emit(format!("# field access .{}", field)); + self.emit(format!( + "# cellscript abi: fixed aggregate field {}.{} offset={} size={}", + aggregate_type_label(&source_ty), + field, + layout.offset, + width + )); + self.emit_stack_load("t4", var.id * 8); + if layout_fixed_scalar_width(&layout).is_some() { + self.emit_unaligned_scalar_load("t4", "t0", "t2", layout.offset, width); + if layout.ty == IrType::I32 { + self.emit_sign_extend_i32("t0"); + } + } else { + self.emit(format!("addi t0, t4, {}", layout.offset)); + } + self.emit_stack_store("t0", dest.id * 8); + true + } + + fn emit_tuple_call_return_field_access(&mut self, dest: &IrVar, obj: &IrOperand, field: &str) -> bool { + let IrOperand::Var(var) = obj else { + return false; + }; + let Some(slot_var_id) = self.tuple_call_return_field_slots.get(&(var.id, field.to_string())).copied() else { + return false; + }; + if slot_var_id != dest.id { + return false; + } + self.emit(format!("# field access .{}", field)); + self.emit(format!("# cellscript abi: tuple call return field .{} projected from return register", field)); + true + } + + /// Generic field access: when specialized paths don't match, try to compute the + /// field offset from type_layouts and emit an unaligned load from the pointer + /// stored in the object's stack slot. This works for any named-type variable + /// whose type has a registered layout, even if it wasn't classified as a + /// schema_pointer_var or aggregate_pointer_source. + fn emit_generic_field_access(&mut self, dest: &IrVar, obj: &IrOperand, field: &str) -> bool { + let IrOperand::Var(var) = obj else { + return false; + }; + let Some(type_name) = named_type_name(&var.ty) else { + return false; + }; + if !self.type_fixed_sizes.contains_key(type_name) { + return false; + } + let Some(layout) = self.type_layouts.get(type_name).and_then(|fields| fields.get(field)).cloned() else { + return false; + }; + let Some(width) = layout_fixed_byte_width(&layout) else { + return false; + }; + + self.emit(format!("# field access .{}", field)); + self.emit(format!("# cellscript abi: generic field {}.{} offset={} size={}", type_name, field, layout.offset, width)); + + // Bounds check: if the object has a known size offset, verify the data + // is large enough to contain this field. + if let Some(size_offset) = self.schema_pointer_size_offsets.get(&var.id).copied() { + self.emit_loaded_schema_bounds_check(size_offset, layout.offset + width, &format!("{}.{}", type_name, field)); + } else if let Some(size_offset) = self.fixed_byte_param_size_offsets.get(&var.id).copied() { + self.emit_loaded_schema_bounds_check(size_offset, layout.offset + width, &format!("{}.{}", type_name, field)); + } + + // Load the object pointer from the stack slot + self.emit_stack_load("t4", var.id * 8); + if layout_fixed_scalar_width(&layout).is_some() { + self.emit_unaligned_scalar_load("t4", "t0", "t2", layout.offset, width); + } else { + self.emit(format!("addi t0, t4, {}", layout.offset)); + } + self.emit_stack_store("t0", dest.id * 8); + true + } +} diff --git a/src/codegen/frame.rs b/src/codegen/frame.rs new file mode 100644 index 00000000..8247ebdd --- /dev/null +++ b/src/codegen/frame.rs @@ -0,0 +1,1157 @@ +use super::*; + +impl CodeGenerator { + pub(super) fn emit_prologue(&mut self) { + self.emit_large_addi("sp", "sp", -(self.frame_size as i64)); + self.emit_stack_store("ra", self.frame_size - 8); + self.emit_stack_store("fp", self.frame_size - 16); + self.emit_sp_addi("fp", self.frame_size); + } + + pub(super) fn emit_epilogue(&mut self) { + if let Some(function) = &self.current_function { + self.emit(format!("j .L{}_epilogue", function)); + return; + } + self.emit_epilogue_body(); + } + + pub(super) fn emit_fail(&mut self, error: CellScriptRuntimeError) { + if let Some(function) = self.current_function.clone() { + self.fail_handler_codes.insert(error); + self.emit(format!("j .L{}_fail_{}", function, error.code())); + return; + } + self.emit_runtime_error_comment(error); + self.emit(format!("li a0, {}", error.code())); + self.emit_epilogue_body(); + } + + pub(super) fn emit_shared_epilogue(&mut self) { + let Some(function) = self.current_function.clone() else { + return; + }; + let fail_codes = self.fail_handler_codes.iter().copied().collect::>(); + for error in fail_codes { + self.emit_label(&format!(".L{}_fail_{}", function, error.code())); + self.emit_runtime_error_comment(error); + self.emit(format!("li a0, {}", error.code())); + self.emit(format!("j .L{}_epilogue", function)); + } + self.emit_label(&format!(".L{}_epilogue", function)); + self.emit_epilogue_body(); + } + + pub(super) fn emit_runtime_error_comment(&mut self, error: CellScriptRuntimeError) { + self.emit(format!("# cellscript runtime error {} {}", error.code(), error.name())); + } + + pub(super) fn emit_epilogue_body(&mut self) { + self.emit_stack_load("ra", self.frame_size - 8); + self.emit_stack_load("fp", self.frame_size - 16); + self.emit_large_addi("sp", "sp", self.frame_size as i64); + self.emit("ret"); + } + + /// Emit `addi rd, rs1, imm` handling immediates that don't fit in 12 bits. + pub(super) fn emit_large_addi(&mut self, rd: &str, rs1: &str, imm: i64) { + if (-2048..=2047).contains(&imm) { + self.emit(format!("addi {}, {}, {}", rd, rs1, imm)); + } else { + let scratch = scratch_register_avoiding(&[rs1]); + self.emit(format!("li {}, {}", scratch, imm)); + self.emit(format!("add {}, {}, {}", rd, rs1, scratch)); + } + } + + pub(super) fn emit_memory_load_with_avoid(&mut self, opcode: &str, dst: &str, base: &str, offset: usize, avoid: &[&str]) { + let offset = i64::try_from(offset).expect("memory offset should fit in i64"); + if small_signed_immediate(offset) { + self.emit(format!("{} {}, {}({})", opcode, dst, offset, base)); + } else { + let mut registers = Vec::with_capacity(2 + avoid.len()); + registers.push(dst); + registers.push(base); + registers.extend_from_slice(avoid); + let scratch = scratch_register_avoiding(®isters); + self.emit(format!("li {}, {}", scratch, offset)); + self.emit(format!("add {}, {}, {}", scratch, base, scratch)); + self.emit(format!("{} {}, 0({})", opcode, dst, scratch)); + } + } + + pub(super) fn emit_memory_store_with_avoid(&mut self, opcode: &str, src: &str, base: &str, offset: usize, avoid: &[&str]) { + let offset = i64::try_from(offset).expect("memory offset should fit in i64"); + if small_signed_immediate(offset) { + self.emit(format!("{} {}, {}({})", opcode, src, offset, base)); + } else { + let mut registers = Vec::with_capacity(2 + avoid.len()); + registers.push(src); + registers.push(base); + registers.extend_from_slice(avoid); + let scratch = scratch_register_avoiding(®isters); + self.emit(format!("li {}, {}", scratch, offset)); + self.emit(format!("add {}, {}, {}", scratch, base, scratch)); + self.emit(format!("{} {}, 0({})", opcode, src, scratch)); + } + } + + /// Emit `ld rd, offset(sp)` through the centralized stack-offset gate. + pub(super) fn emit_stack_load(&mut self, rd: &str, offset: usize) { + self.emit_stack_access_with_avoid("ld", rd, offset, &[]); + } + + /// Emit `ld rd, offset(sp)` without clobbering explicitly live registers. + pub(super) fn emit_stack_load_with_avoid(&mut self, rd: &str, offset: usize, avoid: &[&str]) { + self.emit_stack_access_with_avoid("ld", rd, offset, avoid); + } + + /// Emit `lbu rd, offset(sp)` through the centralized stack-offset gate. + pub(super) fn emit_stack_load_byte(&mut self, rd: &str, offset: usize) { + self.emit_stack_access("lbu", rd, offset); + } + + /// Emit `sd rs2, offset(sp)` through the centralized stack-offset gate. + pub(super) fn emit_stack_store(&mut self, rs2: &str, offset: usize) { + self.emit_stack_access_with_avoid("sd", rs2, offset, &[]); + } + + /// Emit `sd rs2, offset(sp)` without clobbering explicitly live registers. + pub(super) fn emit_stack_store_with_avoid(&mut self, rs2: &str, offset: usize, avoid: &[&str]) { + self.emit_stack_access_with_avoid("sd", rs2, offset, avoid); + } + + /// Emit `sb rs2, offset(sp)` through the centralized stack-offset gate. + pub(super) fn emit_stack_store_byte(&mut self, rs2: &str, offset: usize) { + self.emit_stack_access("sb", rs2, offset); + } + + pub(super) fn emit_stack_access(&mut self, opcode: &str, register: &str, offset: usize) { + self.emit_stack_access_with_avoid(opcode, register, offset, &[]); + } + + pub(super) fn emit_stack_access_with_avoid(&mut self, opcode: &str, register: &str, offset: usize, avoid: &[&str]) { + let offset = i64::try_from(offset).expect("stack offset should fit in i64"); + if small_signed_immediate(offset) { + self.emit(format!("{} {}, {}(sp)", opcode, register, offset)); + } else { + let mut live_registers = Vec::with_capacity(1 + avoid.len()); + live_registers.push(register); + live_registers.extend_from_slice(avoid); + let scratch = scratch_register_avoiding(&live_registers); + self.emit(format!("li {}, {}", scratch, offset)); + self.emit(format!("add {}, sp, {}", scratch, scratch)); + self.emit(format!("{} {}, 0({})", opcode, register, scratch)); + } + } + + /// Emit `addi rd, sp, offset` handling offsets that don't fit in 12 bits. + pub(super) fn emit_sp_addi(&mut self, rd: &str, offset: usize) { + if offset <= 2047 { + self.emit(format!("addi {}, sp, {}", rd, offset)); + } else if rd == "sp" { + self.emit_large_addi("sp", "sp", offset as i64); + } else { + self.emit(format!("li {}, {}", rd, offset)); + self.emit(format!("add {}, sp, {}", rd, rd)); + } + } + + pub(super) fn prepare_function_layout(&mut self, body: &IrBody, params: &[IrParam]) { + let mut max_var_id = None; + let mut fixed_byte_locals = HashMap::::new(); + let mut named_vars = BTreeSet::::new(); + for param in params { + self.record_var(¶m.binding, &mut max_var_id); + } + for block in &body.blocks { + for instruction in &block.instructions { + self.record_instruction_var(instruction, &mut max_var_id); + self.record_instruction_fixed_byte_local(instruction, &mut fixed_byte_locals); + if let IrInstruction::StoreVar { name, .. } = instruction { + named_vars.insert(name.clone()); + } + } + self.record_terminator_var(&block.terminator, &mut max_var_id); + } + + let locals_size = max_var_id.map(|id| (id + 1) * 8).unwrap_or(0); + self.fixed_byte_local_offsets.clear(); + self.named_var_offsets.clear(); + self.cell_buffer_offsets.clear(); + self.cell_buffer_size_offsets.clear(); + self.dynamic_value_size_offsets.clear(); + self.empty_molecule_vector_vars.clear(); + self.constructed_byte_vectors.clear(); + self.constructed_byte_vector_roots.clear(); + self.verified_collection_construction_vectors.clear(); + self.output_type_hash_sources.clear(); + self.consume_order.clear(); + self.consume_indices.clear(); + self.consume_type_names.clear(); + self.consume_binding_ids.clear(); + self.read_ref_order.clear(); + self.read_ref_indices.clear(); + self.read_ref_param_ids.clear(); + self.read_ref_param_input_indices.clear(); + self.read_ref_param_dep_indices.clear(); + self.output_param_ids.clear(); + self.mutate_param_ids.clear(); + self.schema_pointer_size_offsets.clear(); + self.fixed_byte_param_size_offsets.clear(); + self.param_type_hash_pointer_offsets.clear(); + self.param_type_hash_size_offsets.clear(); + self.param_type_hash_sources.clear(); + self.u128_value_offsets.clear(); + self.collection_region_start = 0; + self.next_collection_slot = 0; + + let schema_param_ids = + params.iter().filter(|param| named_type_name(¶m.ty).is_some()).map(|param| param.binding.id).collect::>(); + let mut param_type_hash_ids = BTreeSet::new(); + for block in &body.blocks { + for instruction in &block.instructions { + if let IrInstruction::TypeHash { dest, operand: IrOperand::Var(var) } = instruction + && schema_param_ids.contains(&var.id) + { + param_type_hash_ids.insert(var.id); + self.param_type_hash_sources.insert(dest.id, var.id); + } + } + } + + let mut next_cell_slot = locals_size; + let mut fixed_byte_locals = fixed_byte_locals.into_iter().collect::>(); + fixed_byte_locals.sort_unstable_by_key(|(var_id, _)| *var_id); + for (var_id, width) in fixed_byte_locals { + next_cell_slot = align_up(next_cell_slot, 8); + self.fixed_byte_local_offsets.insert(var_id, next_cell_slot); + next_cell_slot += align_up(width, 8); + } + for name in named_vars { + next_cell_slot = align_up(next_cell_slot, 8); + self.named_var_offsets.insert(name, next_cell_slot); + next_cell_slot += 8; + } + for param in params { + if param.source == ParamSource::Output { + self.output_param_ids.insert(param.name.clone(), param.binding.id); + self.schema_pointer_size_offsets.insert(param.binding.id, next_cell_slot); + self.cell_buffer_size_offsets.insert(param.binding.id, next_cell_slot); + self.cell_buffer_offsets.insert(param.binding.id, next_cell_slot + 8); + next_cell_slot += RUNTIME_CELL_SLOT_SIZE; + continue; + } + if named_type_name(¶m.ty).is_some() { + self.schema_pointer_size_offsets.insert(param.binding.id, next_cell_slot); + next_cell_slot += 8; + } else if fixed_byte_pointer_param_width(¶m.ty).is_some() || fixed_aggregate_pointer_param_width(¶m.ty).is_some() { + self.fixed_byte_param_size_offsets.insert(param.binding.id, next_cell_slot); + next_cell_slot += 8; + } + } + for param in params { + if param_type_hash_ids.contains(¶m.binding.id) { + self.param_type_hash_pointer_offsets.insert(param.binding.id, next_cell_slot); + next_cell_slot += 8; + self.param_type_hash_size_offsets.insert(param.binding.id, next_cell_slot); + next_cell_slot += 8; + } + } + + if self.bind_readonly_schema_params { + let consumed_param_names = body.consume_set.iter().map(|pattern| pattern.binding.as_str()).collect::>(); + let mutate_param_names = body.mutate_set.iter().map(|pattern| pattern.binding.as_str()).collect::>(); + let read_ref_indices_by_binding = + body.read_refs.iter().enumerate().map(|(index, pattern)| (pattern.binding.as_str(), index)).collect::>(); + let mut read_ref_param_index = 0usize; + for param in params { + if matches!(param.source, ParamSource::Output | ParamSource::LockArgs) { + continue; + } + if !self.param_is_runtime_bound(param) { + continue; + } + if mutate_param_names.contains(param.name.as_str()) || consumed_param_names.contains(param.name.as_str()) { + continue; + } + self.read_ref_param_ids.insert(param.name.clone(), param.binding.id); + if let Some(dep_index) = read_ref_indices_by_binding.get(param.name.as_str()).copied() { + self.read_ref_param_dep_indices.insert(param.binding.id, dep_index); + } else { + let input_index = body.consume_set.len() + body.mutate_set.len() + read_ref_param_index; + self.read_ref_param_input_indices.insert(param.binding.id, input_index); + read_ref_param_index += 1; + } + self.schema_pointer_size_offsets.insert(param.binding.id, next_cell_slot); + self.cell_buffer_size_offsets.insert(param.binding.id, next_cell_slot); + self.cell_buffer_offsets.insert(param.binding.id, next_cell_slot + 8); + next_cell_slot += RUNTIME_CELL_SLOT_SIZE; + } + } + + for pattern in &body.mutate_set { + let Some(param) = params.iter().find(|param| param.name == pattern.binding) else { + continue; + }; + self.mutate_param_ids.insert(pattern.binding.clone(), param.binding.id); + self.consume_type_names.insert(param.binding.id, pattern.ty.clone()); + self.consume_binding_ids.insert(pattern.binding.clone(), param.binding.id); + self.consume_indices.insert(param.binding.id, pattern.input_index); + self.schema_pointer_size_offsets.insert(param.binding.id, next_cell_slot); + self.cell_buffer_size_offsets.insert(param.binding.id, next_cell_slot); + self.cell_buffer_offsets.insert(param.binding.id, next_cell_slot + 8); + next_cell_slot += RUNTIME_CELL_SLOT_SIZE; + } + + let consume_pattern_indices = + body.consume_set.iter().enumerate().map(|(index, pattern)| (pattern.binding.as_str(), index)).collect::>(); + for pattern in &body.consume_set { + let Some(param) = params.iter().find(|param| param.name == pattern.binding) else { + continue; + }; + if self.consume_binding_ids.contains_key(&pattern.binding) { + continue; + } + if let Some(type_name) = named_type_name(¶m.ty) { + self.consume_type_names.insert(param.binding.id, type_name.to_string()); + } + self.consume_binding_ids.insert(pattern.binding.clone(), param.binding.id); + self.schema_pointer_size_offsets.insert(param.binding.id, next_cell_slot); + self.cell_buffer_size_offsets.insert(param.binding.id, next_cell_slot); + self.cell_buffer_offsets.insert(param.binding.id, next_cell_slot + 8); + self.consume_order.push(param.binding.id); + self.consume_indices.insert(param.binding.id, consume_pattern_indices.get(pattern.binding.as_str()).copied().unwrap_or(0)); + next_cell_slot += RUNTIME_CELL_SLOT_SIZE; + } + for block in &body.blocks { + for instruction in &block.instructions { + if let Some(var) = consumed_operand_var(instruction) { + if self.consume_binding_ids.contains_key(&var.name) { + continue; + } + if let Some(type_name) = named_type_name(&var.ty) { + self.consume_type_names.insert(var.id, type_name.to_string()); + } + self.consume_binding_ids.insert(var.name.clone(), var.id); + self.schema_pointer_size_offsets.insert(var.id, next_cell_slot); + self.cell_buffer_size_offsets.insert(var.id, next_cell_slot); + self.cell_buffer_offsets.insert(var.id, next_cell_slot + 8); + self.consume_order.push(var.id); + self.consume_indices.insert( + var.id, + consume_pattern_indices.get(var.name.as_str()).copied().unwrap_or(self.consume_order.len() - 1), + ); + next_cell_slot += RUNTIME_CELL_SLOT_SIZE; + } + } + } + + let mut read_ref_index = 0usize; + for block in &body.blocks { + for instruction in &block.instructions { + if let IrInstruction::ReadRef { dest, .. } = instruction { + self.cell_buffer_size_offsets.insert(dest.id, next_cell_slot); + self.cell_buffer_offsets.insert(dest.id, next_cell_slot + 8); + self.read_ref_order.push(dest.id); + self.read_ref_indices.insert(dest.id, read_ref_index); + next_cell_slot += RUNTIME_CELL_SLOT_SIZE; + read_ref_index += 1; + } + } + } + + let mut create_dest_outputs = HashMap::new(); + let mut next_create_output_index = + body.create_set.iter().position(|pattern| pattern.operation == "create").unwrap_or(body.create_set.len()); + for block in &body.blocks { + for instruction in &block.instructions { + match instruction { + IrInstruction::FieldAccess { dest, obj: IrOperand::Var(obj), field } => { + if named_type_name(&dest.ty).is_some() + && named_type_name(&obj.ty) + .and_then(|type_name| self.type_layouts.get(type_name)) + .and_then(|fields| fields.get(field)) + .is_some_and(|layout| { + layout_fixed_byte_width(layout).is_none() + && molecule_vector_element_fixed_width( + &layout.ty, + &self.type_fixed_sizes, + &self.enum_fixed_sizes, + ) + .is_some() + }) + { + self.dynamic_value_size_offsets.insert(dest.id, next_cell_slot); + next_cell_slot += 8; + } + } + IrInstruction::Create { dest, pattern } => { + let output_index = if pattern.operation == "create" { + let output_index = next_create_output_index; + next_create_output_index += 1; + Some(output_index) + } else { + Self::create_output_index(body, &pattern.operation, &pattern.binding, &pattern.ty) + }; + if let Some(output_index) = output_index { + create_dest_outputs.insert(dest.id, output_index); + } + } + IrInstruction::CreateUnique { dest, pattern, .. } | IrInstruction::ReplaceUnique { dest, pattern, .. } => { + if let Some(output_index) = Self::create_output_index(body, &pattern.operation, &pattern.binding, &pattern.ty) + { + create_dest_outputs.insert(dest.id, output_index); + } + } + IrInstruction::Transfer { dest, .. } => { + if let Some(output_index) = Self::create_output_index_for_dest(body, "transfer", dest) { + create_dest_outputs.insert(dest.id, output_index); + } + } + IrInstruction::Claim { dest, .. } => { + if let Some(output_index) = Self::create_output_index_for_dest(body, "claim", dest) { + create_dest_outputs.insert(dest.id, output_index); + } + } + IrInstruction::Settle { dest, .. } => { + if let Some(output_index) = Self::create_output_index_for_dest(body, "settle", dest) { + create_dest_outputs.insert(dest.id, output_index); + } + } + IrInstruction::TypeHash { dest, operand: IrOperand::Var(var) } => { + if let Some(output_index) = create_dest_outputs.get(&var.id).copied() { + self.output_type_hash_sources.insert(dest.id, output_index); + self.cell_buffer_size_offsets.insert(dest.id, next_cell_slot); + self.cell_buffer_offsets.insert(dest.id, next_cell_slot + 8); + next_cell_slot += RUNTIME_CELL_SLOT_SIZE; + } else if self.consume_indices.contains_key(&var.id) + || self.read_ref_indices.contains_key(&var.id) + || self.read_ref_param_input_indices.contains_key(&var.id) + { + self.cell_buffer_size_offsets.insert(dest.id, next_cell_slot); + self.cell_buffer_offsets.insert(dest.id, next_cell_slot + 8); + next_cell_slot += RUNTIME_CELL_SLOT_SIZE; + } + } + IrInstruction::Call { dest: Some(dest), func, args } + if func == "__ckb_current_script_hash" && args.is_empty() && dest.ty == IrType::Hash => + { + self.cell_buffer_size_offsets.insert(dest.id, next_cell_slot); + self.cell_buffer_offsets.insert(dest.id, next_cell_slot + 8); + next_cell_slot += RUNTIME_CELL_SLOT_SIZE; + } + IrInstruction::Call { dest: Some(dest), func, args } + if matches!( + func.as_str(), + "__ckb_input_out_point_tx_hash" + | "__ckb_cell_lock_hash" + | "__ckb_cell_type_hash" + | "__ckb_cell_data_hash" + | "__ckb_cell_data_hash_at" + | "__ckb_cell_lock_code_hash" + | "__ckb_cell_type_code_hash" + | "__ckb_cell_lock_args_hash" + | "__ckb_cell_type_args_hash" + ) && (args.len() == 1 || (func == "__ckb_cell_data_hash_at" && args.len() == 2)) + && dest.ty == IrType::Hash => + { + self.cell_buffer_size_offsets.insert(dest.id, next_cell_slot); + self.cell_buffer_offsets.insert(dest.id, next_cell_slot + 8); + next_cell_slot += RUNTIME_CELL_SLOT_SIZE; + } + IrInstruction::Call { dest: Some(dest), func, args } + if matches!( + func.as_str(), + "__ckb_witness_raw" | "__ckb_witness_lock" | "__ckb_witness_input_type" | "__ckb_witness_output_type" + ) && args.len() == 1 + && dest.ty == IrType::Hash => + { + self.cell_buffer_size_offsets.insert(dest.id, next_cell_slot); + self.cell_buffer_offsets.insert(dest.id, next_cell_slot + 8); + next_cell_slot += RUNTIME_CELL_SLOT_SIZE; + } + _ => {} + } + } + } + + let mut u128_value_ids = BTreeSet::new(); + for param in params { + if param.ty == IrType::U128 { + u128_value_ids.insert(param.binding.id); + } + } + for block in &body.blocks { + for instruction in &block.instructions { + self.collect_u128_instruction_vars(instruction, &mut u128_value_ids); + } + self.collect_u128_terminator_vars(&block.terminator, &mut u128_value_ids); + } + for var_id in u128_value_ids { + self.u128_value_offsets.insert(var_id, next_cell_slot); + next_cell_slot += 16; + } + + let collection_slot_size = 8 + RUNTIME_COLLECTION_BUFFER_SIZE; + let collection_count = body + .blocks + .iter() + .flat_map(|block| block.instructions.iter()) + .filter(|instruction| matches!(instruction, IrInstruction::CollectionNew { .. })) + .count(); + self.collection_region_start = next_cell_slot; + next_cell_slot += collection_count * collection_slot_size; + + self.frame_size = align_frame(next_cell_slot + RUNTIME_EXPR_TEMP_SIZE + RUNTIME_SCRATCH_SIZE + 16); + } + + pub(super) fn runtime_expr_temp_offset(&self, depth: usize) -> usize { + debug_assert!(depth < RUNTIME_EXPR_TEMP_SLOTS); + self.runtime_scratch_size_offset() - RUNTIME_EXPR_TEMP_SIZE + depth * 8 + } + + pub(super) fn checked_runtime_expr_temp_offset(&self, depth: usize) -> Option { + (depth < RUNTIME_EXPR_TEMP_SLOTS).then(|| self.runtime_expr_temp_offset(depth)) + } + + pub(super) fn runtime_scratch_size_offset(&self) -> usize { + self.frame_size - 16 - RUNTIME_SCRATCH_SIZE + } + + pub(super) fn runtime_scratch_buffer_offset(&self) -> usize { + self.runtime_scratch_size_offset() + 8 + } + + pub(super) fn runtime_scratch2_size_offset(&self) -> usize { + self.runtime_scratch_size_offset() + RUNTIME_SCRATCH_SLOT_SIZE + } + + pub(super) fn runtime_scratch2_buffer_offset(&self) -> usize { + self.runtime_scratch2_size_offset() + 8 + } + + pub(super) fn emit_store_data_args_at(&mut self, max_bytes: usize, size_offset: usize, buffer_offset: usize) { + self.emit(format!("li t0, {}", max_bytes)); + self.emit_stack_store("t0", size_offset); + self.emit_sp_addi("a0", buffer_offset); + self.emit_sp_addi("a1", size_offset); + self.emit("li a2, 0"); + } + + pub(super) fn emit_load_cell_data_syscall(&mut self, reason: &str, source: u64, index: usize) { + let size_offset = self.runtime_scratch_size_offset(); + let buffer_offset = self.runtime_scratch_buffer_offset(); + self.emit_load_cell_data_syscall_to_offsets(reason, source, index, size_offset, buffer_offset, RUNTIME_SCRATCH_BUFFER_SIZE); + } + + pub(super) fn emit_load_cell_data_syscall_to_offsets( + &mut self, + reason: &str, + source: u64, + index: usize, + size_offset: usize, + buffer_offset: usize, + max_bytes: usize, + ) { + self.emit(format!("# cellscript abi: LOAD_CELL_DATA reason={} source={} index={}", reason, ckb_source_name(source), index)); + self.emit_store_data_args_at(max_bytes, size_offset, buffer_offset); + self.emit(format!("li a3, {}", index)); + self.emit(format!("li a4, {}", source)); + self.emit(format!("li a7, {}", self.runtime_abi().load_cell_data)); + self.emit("ecall"); + self.emit("# a0 = CKB syscall return code"); + } + + pub(super) fn emit_load_witness_syscall_to_offsets( + &mut self, + reason: &str, + source: u64, + index: usize, + size_offset: usize, + buffer_offset: usize, + max_bytes: usize, + ) { + self.emit(format!("# cellscript abi: LOAD_WITNESS reason={} source={} index={}", reason, ckb_source_name(source), index)); + self.emit_store_data_args_at(max_bytes, size_offset, buffer_offset); + self.emit(format!("li a3, {}", index)); + self.emit(format!("li a4, {}", source)); + self.emit(format!("li a7, {}", self.runtime_abi().load_witness)); + self.emit("ecall"); + self.emit("# a0 = CKB syscall return code"); + } + + pub(super) fn emit_load_script_syscall_to_offsets( + &mut self, + reason: &str, + size_offset: usize, + buffer_offset: usize, + max_bytes: usize, + ) { + self.emit(format!("# cellscript abi: LOAD_SCRIPT reason={}", reason)); + self.emit_store_data_args_at(max_bytes, size_offset, buffer_offset); + self.emit(format!("li a7, {}", self.runtime_abi().load_script)); + self.emit("ecall"); + self.emit("# a0 = CKB syscall return code"); + } + + pub(super) fn emit_load_cell_by_field_syscall_to_offsets( + &mut self, + reason: &str, + source: u64, + index: usize, + field: u64, + size_offset: usize, + buffer_offset: usize, + max_bytes: usize, + ) { + self.emit(format!( + "# cellscript abi: LOAD_CELL_BY_FIELD reason={} source={} index={} field={}", + reason, + ckb_source_name(source), + index, + field + )); + self.emit_store_data_args_at(max_bytes, size_offset, buffer_offset); + self.emit(format!("li a3, {}", index)); + self.emit(format!("li a4, {}", source)); + self.emit(format!("li a5, {}", field)); + self.emit(format!("li a7, {}", self.runtime_abi().load_cell_by_field)); + self.emit("ecall"); + self.emit("# a0 = CKB syscall return code"); + } + + pub(super) fn emit_load_cell_by_field_syscall_to_offsets_dynamic_index( + &mut self, + reason: &str, + source: u64, + index_reg: &str, + field: u64, + size_offset: usize, + buffer_offset: usize, + max_bytes: usize, + ) { + self.emit(format!( + "# cellscript abi: LOAD_CELL_BY_FIELD reason={} source={} index={} field={}", + reason, + ckb_source_name(source), + index_reg, + field + )); + self.emit_store_data_args_at(max_bytes, size_offset, buffer_offset); + self.emit(format!("addi a3, {}, 0", index_reg)); + self.emit(format!("li a4, {}", source)); + self.emit(format!("li a5, {}", field)); + self.emit(format!("li a7, {}", self.runtime_abi().load_cell_by_field)); + self.emit("ecall"); + self.emit("# a0 = CKB syscall return code"); + } + + pub(super) fn emit_return_on_syscall_error(&mut self, error: CellScriptRuntimeError) { + let ok_label = self.fresh_label("ckb_syscall_ok"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_fail(error); + self.emit_label(&ok_label); + } + + pub(super) fn emit_param_spills(&mut self, params: &[IrParam]) -> Result<()> { + let mut abi_index = 0usize; + for param in params { + if named_type_name(¶m.ty).is_some() { + self.emit(format!( + "# cellscript abi: schema param {} pointer={} length={}", + param.name, + abi_arg_label(abi_index), + abi_arg_label(abi_index + 1) + )); + self.emit_spill_abi_arg(abi_index, param.binding.id * 8); + if let Some(size_offset) = self.schema_pointer_size_offsets.get(¶m.binding.id).copied() { + self.emit_spill_abi_arg(abi_index + 1, size_offset); + } + abi_index += 2; + if let (Some(pointer_offset), Some(size_offset)) = ( + self.param_type_hash_pointer_offsets.get(¶m.binding.id).copied(), + self.param_type_hash_size_offsets.get(¶m.binding.id).copied(), + ) { + self.emit(format!( + "# cellscript abi: schema param {} type_hash pointer={} length={} size=32", + param.name, + abi_arg_label(abi_index), + abi_arg_label(abi_index + 1) + )); + self.emit_spill_abi_arg(abi_index, pointer_offset); + self.emit_spill_abi_arg(abi_index + 1, size_offset); + abi_index += 2; + } + } else if let Some(width) = fixed_byte_pointer_param_width(¶m.ty) { + self.emit(format!( + "# cellscript abi: fixed-byte param {} pointer={} length={} size={}", + param.name, + abi_arg_label(abi_index), + abi_arg_label(abi_index + 1), + width + )); + self.emit_spill_abi_arg(abi_index, param.binding.id * 8); + if let Some(size_offset) = self.fixed_byte_param_size_offsets.get(¶m.binding.id).copied() { + self.emit_spill_abi_arg(abi_index + 1, size_offset); + } + abi_index += 2; + } else if let Some(width) = fixed_aggregate_pointer_param_width(¶m.ty) { + self.emit(format!( + "# cellscript abi: fixed-aggregate param {} pointer={} length={} size={}", + param.name, + abi_arg_label(abi_index), + abi_arg_label(abi_index + 1), + width + )); + self.emit_spill_abi_arg(abi_index, param.binding.id * 8); + if let Some(size_offset) = self.fixed_byte_param_size_offsets.get(¶m.binding.id).copied() { + self.emit_spill_abi_arg(abi_index + 1, size_offset); + } + abi_index += 2; + } else { + self.emit_spill_abi_arg(abi_index, param.binding.id * 8); + abi_index += 1; + } + } + + Ok(()) + } + + pub(super) fn emit_spill_abi_arg(&mut self, abi_index: usize, stack_offset: usize) { + if abi_index < 8 { + self.emit_stack_store(&format!("a{}", abi_index), stack_offset); + } else { + let caller_stack_offset = (abi_index - 8) * 8; + self.emit(format!("# cellscript abi: arg{} loaded from caller stack +{}", abi_index, caller_stack_offset)); + self.emit(format!("ld t0, {}(fp)", caller_stack_offset)); + self.emit_stack_store("t0", stack_offset); + } + } + + pub(super) fn record_instruction_var(&self, instruction: &IrInstruction, max_var_id: &mut Option) { + match instruction { + IrInstruction::LoadConst { dest, .. } + | IrInstruction::LoadVar { dest, .. } + | IrInstruction::Unary { dest, .. } + | IrInstruction::FieldAccess { dest, .. } + | IrInstruction::Index { dest, .. } + | IrInstruction::Length { dest, .. } + | IrInstruction::TypeHash { dest, .. } + | IrInstruction::Create { dest, .. } + | IrInstruction::CreateUnique { dest, .. } + | IrInstruction::ReadRef { dest, .. } => self.record_var(dest, max_var_id), + IrInstruction::CollectionNew { dest, capacity, .. } => { + self.record_var(dest, max_var_id); + if let Some(capacity) = capacity { + self.record_operand(capacity, max_var_id); + } + } + IrInstruction::Move { dest, src } => { + self.record_var(dest, max_var_id); + self.record_operand(src, max_var_id); + } + IrInstruction::Tuple { dest, fields } => { + self.record_var(dest, max_var_id); + for field in fields { + self.record_operand(field, max_var_id); + } + } + IrInstruction::EnumConstruct { dest, fields, .. } => { + self.record_var(dest, max_var_id); + for field in fields { + self.record_operand(field, max_var_id); + } + } + IrInstruction::EnumTag { dest, operand, .. } | IrInstruction::EnumPayload { dest, operand, .. } => { + self.record_var(dest, max_var_id); + self.record_operand(operand, max_var_id); + } + IrInstruction::Binary { dest, left, right, .. } => { + self.record_var(dest, max_var_id); + self.record_operand(left, max_var_id); + self.record_operand(right, max_var_id); + } + IrInstruction::StoreVar { src, .. } => self.record_operand(src, max_var_id), + IrInstruction::Call { dest, args, .. } => { + if let Some(dest) = dest { + self.record_var(dest, max_var_id); + } + for arg in args { + self.record_operand(arg, max_var_id); + } + } + IrInstruction::Consume { operand } | IrInstruction::Destroy { operand, policy: _ } => { + self.record_operand(operand, max_var_id) + } + IrInstruction::Transfer { dest, operand, to } => { + self.record_var(dest, max_var_id); + self.record_operand(operand, max_var_id); + self.record_operand(to, max_var_id); + } + IrInstruction::Claim { dest, receipt } => { + self.record_var(dest, max_var_id); + self.record_operand(receipt, max_var_id); + } + IrInstruction::Settle { dest, operand } => { + self.record_var(dest, max_var_id); + self.record_operand(operand, max_var_id) + } + IrInstruction::ReplaceUnique { dest, operand, .. } => { + self.record_var(dest, max_var_id); + self.record_operand(operand, max_var_id) + } + IrInstruction::CellMetadataEquality { left, right, .. } => { + self.record_operand(left, max_var_id); + self.record_operand(right, max_var_id); + } + IrInstruction::CollectionPush { collection, value } => { + self.record_operand(collection, max_var_id); + self.record_operand(value, max_var_id); + } + IrInstruction::CollectionCapacity { dest, collection } => { + self.record_var(dest, max_var_id); + self.record_operand(collection, max_var_id); + } + IrInstruction::CollectionExtend { collection, slice } => { + self.record_operand(collection, max_var_id); + self.record_operand(slice, max_var_id); + } + IrInstruction::CollectionClear { collection } => { + self.record_operand(collection, max_var_id); + } + IrInstruction::CollectionReverse { collection } => { + self.record_operand(collection, max_var_id); + } + IrInstruction::CollectionTruncate { collection, len } => { + self.record_operand(collection, max_var_id); + self.record_operand(len, max_var_id); + } + IrInstruction::CollectionSwap { collection, left, right } => { + self.record_operand(collection, max_var_id); + self.record_operand(left, max_var_id); + self.record_operand(right, max_var_id); + } + IrInstruction::CollectionContains { dest, collection, value } => { + self.record_var(dest, max_var_id); + self.record_operand(collection, max_var_id); + self.record_operand(value, max_var_id); + } + IrInstruction::CollectionRemove { dest, collection, index } => { + self.record_var(dest, max_var_id); + self.record_operand(collection, max_var_id); + self.record_operand(index, max_var_id); + } + IrInstruction::CollectionInsert { collection, index, value } => { + self.record_operand(collection, max_var_id); + self.record_operand(index, max_var_id); + self.record_operand(value, max_var_id); + } + IrInstruction::CollectionSet { collection, index, value } => { + self.record_operand(collection, max_var_id); + self.record_operand(index, max_var_id); + self.record_operand(value, max_var_id); + } + IrInstruction::CollectionPop { dest, collection } => { + self.record_var(dest, max_var_id); + self.record_operand(collection, max_var_id); + } + } + } + + pub(super) fn record_instruction_fixed_byte_local(&self, instruction: &IrInstruction, offsets: &mut HashMap) { + let record = |offsets: &mut HashMap, var: &IrVar| { + if var.ty == IrType::U128 { + offsets.insert(var.id, 16); + } + if let Some(width) = fixed_byte_width(&var.ty, type_static_length(&var.ty)).filter(|width| *width > 8) { + offsets.insert(var.id, width); + } + if let Some(width) = self.fixed_named_type_width(&var.ty) { + offsets.insert(var.id, width); + } + }; + + match instruction { + IrInstruction::LoadConst { dest, .. } + | IrInstruction::LoadVar { dest, .. } + | IrInstruction::Unary { dest, .. } + | IrInstruction::FieldAccess { dest, .. } + | IrInstruction::Index { dest, .. } + | IrInstruction::Length { dest, .. } + | IrInstruction::TypeHash { dest, .. } + | IrInstruction::Create { dest, .. } + | IrInstruction::CreateUnique { dest, .. } + | IrInstruction::ReplaceUnique { dest, .. } + | IrInstruction::Transfer { dest, .. } + | IrInstruction::Claim { dest, .. } + | IrInstruction::Settle { dest, .. } + | IrInstruction::ReadRef { dest, .. } + | IrInstruction::CollectionCapacity { dest, .. } + | IrInstruction::CollectionContains { dest, .. } + | IrInstruction::CollectionRemove { dest, .. } + | IrInstruction::CollectionPop { dest, .. } + | IrInstruction::CollectionNew { dest, .. } + | IrInstruction::Move { dest, .. } + | IrInstruction::Tuple { dest, .. } + | IrInstruction::EnumConstruct { dest, .. } + | IrInstruction::EnumTag { dest, .. } + | IrInstruction::Binary { dest, .. } => record(offsets, dest), + IrInstruction::EnumPayload { dest, enum_name, variant, field_index, .. } => { + record(offsets, dest); + if let Some(field) = self + .enum_layouts + .get(enum_name) + .and_then(|layout| layout.variants.iter().find(|candidate| candidate.name == *variant)) + .and_then(|variant| variant.fields.get(*field_index)) + && !field.linear + && !is_fixed_scalar_ir_type(&field.ty) + && field.width > 0 + { + offsets.insert(dest.id, field.width); + } + } + IrInstruction::Call { dest, func, .. } => { + if let Some(dest) = dest { + if is_ckb_fixed_hash_helper(func) && dest.ty == IrType::Hash { + offsets.insert(dest.id, 32); + } + record(offsets, dest); + } + } + IrInstruction::StoreVar { .. } + | IrInstruction::Consume { .. } + | IrInstruction::Destroy { .. } + | IrInstruction::CellMetadataEquality { .. } + | IrInstruction::CollectionPush { .. } + | IrInstruction::CollectionExtend { .. } + | IrInstruction::CollectionClear { .. } + | IrInstruction::CollectionReverse { .. } + | IrInstruction::CollectionTruncate { .. } + | IrInstruction::CollectionSwap { .. } + | IrInstruction::CollectionInsert { .. } + | IrInstruction::CollectionSet { .. } => {} + } + } + + pub(super) fn record_terminator_var(&self, terminator: &IrTerminator, max_var_id: &mut Option) { + match terminator { + IrTerminator::Return(Some(operand)) | IrTerminator::Branch { cond: operand, .. } => { + self.record_operand(operand, max_var_id) + } + IrTerminator::Return(None) | IrTerminator::Jump(_) => {} + } + } + + pub(super) fn collect_u128_instruction_vars(&self, instruction: &IrInstruction, out: &mut BTreeSet) { + match instruction { + IrInstruction::LoadConst { dest, .. } + | IrInstruction::LoadVar { dest, .. } + | IrInstruction::Unary { dest, .. } + | IrInstruction::FieldAccess { dest, .. } + | IrInstruction::Index { dest, .. } + | IrInstruction::Length { dest, .. } + | IrInstruction::TypeHash { dest, .. } + | IrInstruction::Create { dest, .. } + | IrInstruction::CreateUnique { dest, .. } + | IrInstruction::ReplaceUnique { dest, .. } + | IrInstruction::Claim { dest, .. } + | IrInstruction::ReadRef { dest, .. } + | IrInstruction::CollectionCapacity { dest, .. } + | IrInstruction::CollectionContains { dest, .. } + | IrInstruction::CollectionRemove { dest, .. } + | IrInstruction::CollectionPop { dest, .. } + | IrInstruction::Settle { dest, .. } + | IrInstruction::Transfer { dest, .. } + | IrInstruction::Move { dest, .. } + | IrInstruction::Tuple { dest, .. } + | IrInstruction::EnumConstruct { dest, .. } + | IrInstruction::EnumTag { dest, .. } + | IrInstruction::EnumPayload { dest, .. } + | IrInstruction::Binary { dest, .. } + | IrInstruction::Call { dest: Some(dest), .. } => { + if dest.ty == IrType::U128 { + out.insert(dest.id); + } + } + IrInstruction::CollectionNew { dest, .. } => { + if dest.ty == IrType::U128 { + out.insert(dest.id); + } + } + IrInstruction::StoreVar { .. } + | IrInstruction::Call { dest: None, .. } + | IrInstruction::Consume { .. } + | IrInstruction::Destroy { .. } + | IrInstruction::CellMetadataEquality { .. } + | IrInstruction::CollectionPush { .. } + | IrInstruction::CollectionExtend { .. } + | IrInstruction::CollectionClear { .. } + | IrInstruction::CollectionReverse { .. } + | IrInstruction::CollectionTruncate { .. } + | IrInstruction::CollectionSwap { .. } + | IrInstruction::CollectionInsert { .. } + | IrInstruction::CollectionSet { .. } => {} + } + } + + pub(super) fn collect_u128_terminator_vars(&self, terminator: &IrTerminator, out: &mut BTreeSet) { + if let IrTerminator::Return(Some(IrOperand::Var(var))) = terminator + && var.ty == IrType::U128 + { + out.insert(var.id); + } + } + + pub(super) fn record_operand(&self, operand: &IrOperand, max_var_id: &mut Option) { + if let IrOperand::Var(var) = operand { + self.record_var(var, max_var_id); + } + } + + pub(super) fn record_var(&self, var: &IrVar, max_var_id: &mut Option) { + *max_var_id = Some(max_var_id.map(|current| current.max(var.id)).unwrap_or(var.id)); + } + + pub(super) fn const_as_u128(value: &IrConst) -> Option { + match value { + IrConst::U8(value) => Some((*value).into()), + IrConst::U16(value) => Some((*value).into()), + IrConst::U32(value) => Some((*value).into()), + IrConst::U64(value) => Some((*value).into()), + IrConst::U128(value) => Some(*value), + _ => None, + } + } + + pub(super) fn expected_u128_source(&self, operand: &IrOperand) -> Option { + match operand { + IrOperand::Const(value) => { + Self::const_as_u128(value).map(|value| ExpectedFixedByteSource::Const(value.to_le_bytes().to_vec())) + } + _ => self.expected_fixed_byte_source(operand, 16), + } + } + + pub(super) fn emit_store_byte_to_stack_offset(&mut self, src_reg: &str, offset: usize) { + self.emit_stack_store_byte(src_reg, offset); + } + + pub(super) fn emit_store_u128_const_to_stack_offset(&mut self, value: u128, offset: usize) { + self.emit(format!("# cellscript abi: materialize u128 const at stack+{}", offset)); + for (index, byte) in value.to_le_bytes().iter().enumerate() { + self.emit(format!("li t0, {}", byte)); + self.emit_store_byte_to_stack_offset("t0", offset + index); + } + } + + pub(super) fn emit_store_u128_pointer_for_var(&mut self, var_id: usize, offset: usize) { + self.emit_sp_addi("t0", offset); + self.emit_stack_store("t0", var_id * 8); + } + + pub(super) fn emit_materialize_u128_operand_to_var(&mut self, dest: &IrVar, src: &IrOperand) -> bool { + let Some(dest_offset) = self.u128_value_offsets.get(&dest.id).copied() else { + self.emit("# cellscript abi: u128 destination has no 16-byte storage; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return true; + }; + if let IrOperand::Const(value) = src + && let Some(value) = Self::const_as_u128(value) + { + self.emit_store_u128_const_to_stack_offset(value, dest_offset); + self.emit_store_u128_pointer_for_var(dest.id, dest_offset); + return true; + } + self.emit(format!("# cellscript abi: materialize u128 operand into var{}", dest.id)); + if !self.emit_u128_operand_limbs("t0", "t1", "t6", "t4", src, "u128 materialize") { + return true; + } + self.emit_stack_store("t0", dest_offset); + self.emit_stack_store("t1", dest_offset + 8); + self.emit_store_u128_pointer_for_var(dest.id, dest_offset); + true + } + + pub(super) fn emit_u64_le_from_fixed_byte_source( + &mut self, + dest_reg: &str, + scratch_reg: &str, + base_reg: &str, + source: &ExpectedFixedByteSource, + start: usize, + ) { + self.emit(format!("li {}, 0", dest_reg)); + for byte_offset in 0..8 { + self.emit_fixed_byte_source_byte_to(scratch_reg, base_reg, source, start + byte_offset); + if byte_offset != 0 { + self.emit(format!("slli {}, {}, {}", scratch_reg, scratch_reg, byte_offset * 8)); + } + self.emit(format!("or {}, {}, {}", dest_reg, dest_reg, scratch_reg)); + } + } + + pub(super) fn emit_u128_operand_limbs( + &mut self, + low_reg: &str, + high_reg: &str, + scratch_reg: &str, + base_reg: &str, + operand: &IrOperand, + context: &str, + ) -> bool { + let Some(source) = self.expected_u128_source(operand) else { + self.emit(format!("# cellscript abi: {} u128 operand is not addressable; fail closed", context)); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return false; + }; + self.emit_prepare_fixed_byte_source(&source, 16, context); + if self.emit_fixed_byte_source_pointer_to(base_reg, &source) { + // Resolve schema-backed pointers before either accumulator is + // live. Dynamic Molecule bounds checks use t0..t5 internally, so + // resolving the pointer for every byte would overwrite the limb + // being assembled. + self.emit_unaligned_scalar_load(base_reg, low_reg, scratch_reg, 0, 8); + self.emit_unaligned_scalar_load(base_reg, high_reg, scratch_reg, 8, 8); + } else { + // Constants intentionally have no addressable storage. + self.emit_u64_le_from_fixed_byte_source(low_reg, scratch_reg, base_reg, &source, 0); + self.emit_u64_le_from_fixed_byte_source(high_reg, scratch_reg, base_reg, &source, 8); + } + true + } + + pub(super) fn emit_u128_binary_operand_limbs(&mut self, left: &IrOperand, right: &IrOperand, context: &str) -> bool { + if !self.emit_u128_operand_limbs("t0", "t1", "t6", "t4", left, &format!("{} left", context)) { + return false; + } + let left_low_offset = self.runtime_expr_temp_offset(0); + let left_high_offset = self.runtime_expr_temp_offset(1); + self.emit_stack_store("t0", left_low_offset); + self.emit_stack_store("t1", left_high_offset); + if !self.emit_u128_operand_limbs("t2", "t3", "t6", "t5", right, &format!("{} right", context)) { + return false; + } + // Loading a dynamic schema field runs Molecule validation that uses + // t0/t1. Restore the left limbs only after the right operand is fully + // materialized. + self.emit_stack_load("t0", left_low_offset); + self.emit_stack_load("t1", left_high_offset); + true + } + + pub(super) fn operand_is_u128_like(&self, operand: &IrOperand) -> bool { + match operand { + IrOperand::Var(var) => var.ty == IrType::U128, + IrOperand::Const(IrConst::U128(_)) => true, + _ => false, + } + } + + pub(super) fn emit_store_const_bytes_to_stack(&mut self, bytes: &[u8], offset: usize) { + for (index, byte) in bytes.iter().enumerate() { + self.emit(format!("li t0, {}", byte)); + self.emit_stack_store_byte("t0", offset + index); + } + } +} diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 90d83840..6b89f675 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -10,12 +10,24 @@ use crate::ir::*; use crate::runtime_errors::CellScriptRuntimeError; use crate::{ArtifactFormat, TargetProfile, ENTRY_WITNESS_ABI_MAGIC}; use serde::Serialize; -use std::collections::{BTreeSet, HashMap}; -use std::env; -use std::fs; -use std::path::PathBuf; -use std::process::Command; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; + +mod abi; +mod assembler; +mod calls; +mod cell_ops; +mod collections; +mod expr; +mod frame; +mod runtime; +mod schema; +pub use assembler::BackendShapeMetrics; +use assembler::*; +use cell_ops::{CellFieldHashCheck, CellFieldHashLocation}; +pub use runtime::{ + generate, generate_with_evidence, GeneratedArtifact, MachineBlockEvidence, MachineEdgeEvidence, MachineEdgeKindEvidence, + MachineLayoutEvidence, MachineTerminatorEvidence, +}; const CKB_LOAD_HEADER_SYSCALL_NUMBER: u64 = ckb_abi::syscall::LOAD_HEADER; const CKB_LOAD_HEADER_BY_FIELD_SYSCALL_NUMBER: u64 = ckb_abi::syscall::LOAD_HEADER_BY_FIELD; @@ -66,8 +78,6 @@ const CKB_CELL_FIELD_OCCUPIED_CAPACITY: u64 = ckb_abi::cell_field::OCCUPIED_CAPA const CKB_INDEX_OUT_OF_BOUND: u64 = ckb_abi::syscall_error::INDEX_OUT_OF_BOUND; const CKB_ITEM_MISSING: u64 = ckb_abi::syscall_error::ITEM_MISSING; const CKB_LENGTH_NOT_ENOUGH: u64 = ckb_abi::syscall_error::LENGTH_NOT_ENOUGH; -#[allow(dead_code)] -const CKB_SIG_HASH_ALL: u64 = 1; const RUNTIME_SCRATCH_BUFFER_SIZE: usize = 512; const RUNTIME_SCRATCH_SLOT_SIZE: usize = 8 + RUNTIME_SCRATCH_BUFFER_SIZE; const RUNTIME_SCRATCH_SIZE: usize = RUNTIME_SCRATCH_SLOT_SIZE * 2; @@ -695,11 +705,11 @@ fn abi_arg_label(index: usize) -> String { fn call_abi_arg_count(abi: Option<&CallableAbi>, args: &[IrOperand]) -> usize { let mut count = 0usize; for (arg_index, _) in args.iter().enumerate() { - if let Some(abi) = abi { - if let Some(param) = abi.params.get(arg_index) { - count += call_param_abi_arg_count(param, abi.type_hash_param_indices.contains(&arg_index)); - continue; - } + if let Some(abi) = abi + && let Some(param) = abi.params.get(arg_index) + { + count += call_param_abi_arg_count(param, abi.type_hash_param_indices.contains(&arg_index)); + continue; } count += 1; } @@ -915,6 +925,8 @@ pub struct CodeGenerator { fail_handler_codes: BTreeSet, /// Unique label counter for runtime checks. next_runtime_label: usize, + /// Final stack-frame size for typed action/lock/helper entries. + entry_frame_sizes: BTreeMap, } impl CodeGenerator { @@ -1043,6 +1055,7 @@ impl CodeGenerator { verified_collection_push_values: BTreeSet::new(), fail_handler_codes: BTreeSet::new(), next_runtime_label: 0, + entry_frame_sizes: BTreeMap::new(), } } @@ -1050,7 +1063,11 @@ impl CodeGenerator { runtime_syscall_abi(self.options.target_profile) } - pub fn generate(mut self, ir: &IrModule, format: ArtifactFormat) -> Result> { + pub fn generate(self, ir: &IrModule, format: ArtifactFormat) -> Result> { + self.generate_with_evidence(ir, format).map(|generated| generated.bytes) + } + + pub fn generate_with_evidence(mut self, ir: &IrModule, format: ArtifactFormat) -> Result { let has_entrypoint = ir.items.iter().any(|item| matches!(item, IrItem::Action(_) | IrItem::Lock(_))); self.enum_fixed_sizes = ir.enum_fixed_sizes.clone(); self.enum_layouts = ir.enum_layouts.clone(); @@ -1104,13 +1121,16 @@ impl CodeGenerator { self.generate_runtime_support(ir); self.emit_const_data_pool(); - self.assemble(format).map_err(|error| { - let fallback = match format { - ArtifactFormat::RiscvAssembly => "E2900", - ArtifactFormat::RiscvElf => "E2300", - }; - with_codegen_code(error, fallback) - }) + let generated = match format { + ArtifactFormat::RiscvAssembly => GeneratedArtifact { bytes: self.assembly.join("\n").into_bytes(), machine_layout: None }, + ArtifactFormat::RiscvElf => { + let machine_layout = machine_layout_evidence(&self.assembly, &self.entry_frame_sizes, ir) + .map_err(|error| with_codegen_code(error, "E2201"))?; + let bytes = assemble_generated_elf(&self.assembly).map_err(|error| with_codegen_code(error, "E2300"))?; + GeneratedArtifact { bytes, machine_layout: Some(machine_layout) } + } + }; + Ok(generated) } fn emit_header(&mut self) { @@ -1216,620 +1236,6 @@ impl CodeGenerator { } } - fn emit_entry_abi_marker(&mut self, name: &str) { - self.assembly.push(format!("# cellscript entry abi: {} requires-explicit-parameter-abi", name)); - } - - fn emit_entry_direct_wrapper(&mut self, target: &str) { - self.emit_global(ENTRY_WITNESS_LABEL); - self.emit_label(ENTRY_WITNESS_LABEL); - self.emit(format!("# cellscript entry abi: {} tail-calls no-arg {}", ENTRY_WITNESS_LABEL, target)); - self.emit(format!("j {}", target)); - } - - fn emit_entry_witness_wrapper(&mut self, target: &str, params: &[IrParam]) -> Result<()> { - let callable_abi = self.callable_abis.get(target).cloned(); - let type_hash_param_indices = callable_abi.as_ref().map(|abi| abi.type_hash_param_indices.clone()).unwrap_or_default(); - let runtime_bound_param_indices = callable_abi.as_ref().map(|abi| abi.runtime_bound_param_indices.clone()).unwrap_or_default(); - let outgoing_stack_arg_bytes = align_stack_arg_bytes(entry_abi_arg_count(params, callable_abi.as_ref()).saturating_sub(8) * 8); - let payload = entry_witness_payload_layout(params, &runtime_bound_param_indices, &self.enum_layouts); - let payload_len = payload.iter().map(|arg| arg.width).sum::(); - let has_witness_payload = payload.iter().any(|arg| arg.width > 0 || arg.unsupported); - let has_lock_args = params.iter().any(|param| param.source == ParamSource::LockArgs); - let has_dynamic_payload = payload.iter().any(|arg| arg.schema_dynamic); - let min_witness_len = ENTRY_WITNESS_HEADER_SIZE + payload_len; - let loaded_label = self.fresh_label("entry_witness_loaded"); - let try_group_input_label = self.fresh_label("entry_witness_try_group_input"); - let try_group_output_label = self.fresh_label("entry_witness_try_group_output"); - let buffer_ok_label = self.fresh_label("entry_witness_buffer_ok"); - let size_ok_label = self.fresh_label("entry_witness_size_ok"); - let fail_label = self.fresh_label("entry_witness_fail"); - let done_label = self.fresh_label("entry_witness_done"); - - self.emit_global(ENTRY_WITNESS_LABEL); - self.emit_label(ENTRY_WITNESS_LABEL); - self.emit(format!( - "# cellscript entry abi: {} loads Input#0 witness args for {} and falls back to GroupInput#0/GroupOutput#0", - ENTRY_WITNESS_LABEL, target - )); - self.emit("# cellscript entry abi: witness magic CSARGv1 followed by positional fixed/scalar payload"); - self.emit_large_addi("sp", "sp", -(ENTRY_WITNESS_FRAME_SIZE as i64)); - self.emit_stack_store("ra", ENTRY_WITNESS_RA_OFFSET); - if has_lock_args { - self.emit_entry_load_script_args(&fail_label); - } - if has_witness_payload { - self.emit_load_witness_syscall_to_offsets( - "entry_args", - CKB_SOURCE_INPUT, - 0, - ENTRY_WITNESS_SIZE_OFFSET, - ENTRY_WITNESS_BUFFER_OFFSET, - ENTRY_WITNESS_BUFFER_SIZE, - ); - self.emit(format!("beqz a0, {}", loaded_label)); - self.emit(format!("j {}", try_group_input_label)); - self.emit_label(&try_group_input_label); - self.emit_load_witness_syscall_to_offsets( - "entry_args_fallback_group_input", - self.runtime_abi().source_group_input, - 0, - ENTRY_WITNESS_SIZE_OFFSET, - ENTRY_WITNESS_BUFFER_OFFSET, - ENTRY_WITNESS_BUFFER_SIZE, - ); - self.emit(format!("beqz a0, {}", loaded_label)); - self.emit(format!("j {}", try_group_output_label)); - self.emit_label(&try_group_output_label); - self.emit_load_witness_syscall_to_offsets( - "entry_args_fallback_group_output", - CKB_SOURCE_GROUP_OUTPUT, - 0, - ENTRY_WITNESS_SIZE_OFFSET, - ENTRY_WITNESS_BUFFER_OFFSET, - ENTRY_WITNESS_BUFFER_SIZE, - ); - self.emit(format!("beqz a0, {}", loaded_label)); - self.emit(format!("j {}", fail_label)); - self.emit_label(&loaded_label); - - self.emit_stack_load("t0", ENTRY_WITNESS_SIZE_OFFSET); - self.emit("# cellscript entry abi: reject witnesses larger than the local entry buffer"); - self.emit(format!("li t1, {}", ENTRY_WITNESS_BUFFER_SIZE + 1)); - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", buffer_ok_label)); - self.emit(format!("j {}", fail_label)); - self.emit_label(&buffer_ok_label); - self.emit(format!("li t1, {}", min_witness_len)); - self.emit("sltu t2, t0, t1"); - self.emit(format!("beqz t2, {}", size_ok_label)); - self.emit(format!("j {}", fail_label)); - self.emit_label(&size_ok_label); - - for (index, byte) in ENTRY_WITNESS_MAGIC.iter().enumerate() { - self.emit_stack_load_byte("t0", ENTRY_WITNESS_BUFFER_OFFSET + index); - self.emit(format!("li t1, {}", byte)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", fail_label)); - } - - if !has_dynamic_payload { - let exact_size_label = self.fresh_label("entry_witness_exact_size_ok"); - self.emit("# cellscript entry abi: reject trailing witness payload bytes"); - self.emit_stack_load("t0", ENTRY_WITNESS_SIZE_OFFSET); - self.emit(format!("li t1, {}", min_witness_len)); - self.emit("sub t2, t0, t1"); - self.emit(format!("beqz t2, {}", exact_size_label)); - self.emit(format!("j {}", fail_label)); - self.emit_label(&exact_size_label); - } - } - - if payload.iter().any(|arg| arg.unsupported) { - self.emit("# cellscript entry abi: unsupported witness parameter shape; fail closed"); - self.emit(format!("j {}", fail_label)); - } else if has_dynamic_payload { - let mut abi_index = 0usize; - self.emit("# cellscript entry abi: witness payload contains schema-backed dynamic segments"); - self.emit_stack_load("t5", ENTRY_WITNESS_SIZE_OFFSET); - self.emit(format!("li t6, {}", ENTRY_WITNESS_HEADER_SIZE)); - for (param_index, param) in params.iter().enumerate() { - let param_is_runtime_bound = - runtime_bound_param_indices.contains(¶m_index) || matches!(param.ty, IrType::Ref(_) | IrType::MutRef(_)); - if param.source == ParamSource::LockArgs { - self.emit_entry_lock_args_param(&mut abi_index, param, outgoing_stack_arg_bytes, &fail_label); - } else if param_is_runtime_bound { - self.emit(format!("# cellscript entry abi: runtime-bound param {} is loaded from transaction cells", param.name)); - self.emit_entry_abi_zero_arg(abi_index, outgoing_stack_arg_bytes); - self.emit_entry_abi_zero_arg(abi_index + 1, outgoing_stack_arg_bytes); - abi_index += 2; - if type_hash_param_indices.contains(¶m_index) { - self.emit(format!( - "# cellscript entry abi: runtime-bound param {} TypeHash witness bytes unavailable; pass null ABI bytes", - param.name - )); - self.emit_entry_abi_zero_arg(abi_index, outgoing_stack_arg_bytes); - self.emit_entry_abi_zero_arg(abi_index + 1, outgoing_stack_arg_bytes); - abi_index += 2; - } - } else if entry_witness_dynamic_schema_param(¶m.ty) && self.payload_enum_width(¶m.ty).is_none() { - let len_ok_label = self.fresh_label("entry_witness_schema_len_ok"); - let bytes_ok_label = self.fresh_label("entry_witness_schema_bytes_ok"); - self.emit(format!( - "# cellscript entry abi: schema param {} -> {}={} {}={} (length-prefixed witness bytes)", - param.name, - abi_arg_label(abi_index), - "ptr", - abi_arg_label(abi_index + 1), - "len" - )); - self.emit("addi t1, t6, 4"); - self.emit("sltu t2, t5, t1"); - self.emit(format!("beqz t2, {}", len_ok_label)); - self.emit(format!("j {}", fail_label)); - self.emit_label(&len_ok_label); - self.emit("add t0, sp, t6"); - self.emit(format!("addi t0, t0, {}", ENTRY_WITNESS_BUFFER_OFFSET)); - self.emit("li t4, 0"); - for byte_index in 0..4 { - self.emit(format!("lbu t1, {}(t0)", byte_index)); - if byte_index != 0 { - self.emit(format!("slli t1, t1, {}", byte_index * 8)); - } - self.emit("or t4, t4, t1"); - } - self.emit("addi t1, t6, 4"); - self.emit("add t1, t1, t4"); - self.emit("sltu t2, t5, t1"); - self.emit(format!("beqz t2, {}", bytes_ok_label)); - self.emit(format!("j {}", fail_label)); - self.emit_label(&bytes_ok_label); - self.emit_entry_abi_pointer_from_dynamic_offset(abi_index, "t6", 4, "t0", outgoing_stack_arg_bytes); - self.emit_entry_abi_reg_arg(abi_index + 1, "t4", outgoing_stack_arg_bytes); - abi_index += 2; - self.emit("addi t6, t6, 4"); - self.emit("add t6, t6, t4"); - if type_hash_param_indices.contains(¶m_index) { - self.emit(format!( - "# cellscript entry abi: schema param {} TypeHash witness bytes unavailable; pass null ABI bytes", - param.name - )); - self.emit_entry_abi_zero_arg(abi_index, outgoing_stack_arg_bytes); - self.emit_entry_abi_zero_arg(abi_index + 1, outgoing_stack_arg_bytes); - abi_index += 2; - } - } else if let Some(width) = self - .payload_enum_width(¶m.ty) - .or_else(|| fixed_byte_pointer_param_width(¶m.ty).or_else(|| fixed_aggregate_pointer_param_width(¶m.ty))) - { - let bytes_ok_label = self.fresh_label("entry_witness_fixed_bytes_ok"); - self.emit(format!( - "# cellscript entry abi: fixed-byte param {} pointer={} length={} size={}", - param.name, - abi_arg_label(abi_index), - abi_arg_label(abi_index + 1), - width - )); - self.emit(format!("addi t1, t6, {}", width)); - self.emit("sltu t2, t5, t1"); - self.emit(format!("beqz t2, {}", bytes_ok_label)); - self.emit(format!("j {}", fail_label)); - self.emit_label(&bytes_ok_label); - self.emit_entry_abi_pointer_from_dynamic_offset(abi_index, "t6", 0, "t0", outgoing_stack_arg_bytes); - self.emit_entry_abi_immediate_arg(abi_index + 1, width as u64, outgoing_stack_arg_bytes); - self.emit(format!("addi t6, t6, {}", width)); - abi_index += 2; - } else if let Some(width) = entry_witness_register_param_width(¶m.ty) { - let bytes_ok_label = self.fresh_label("entry_witness_scalar_bytes_ok"); - self.emit(format!( - "# cellscript entry abi: scalar param {} -> {} size={}", - param.name, - abi_arg_label(abi_index), - width - )); - self.emit(format!("addi t1, t6, {}", width)); - self.emit("sltu t2, t5, t1"); - self.emit(format!("beqz t2, {}", bytes_ok_label)); - self.emit(format!("j {}", fail_label)); - self.emit_label(&bytes_ok_label); - self.emit("add t0, sp, t6"); - self.emit(format!("addi t0, t0, {}", ENTRY_WITNESS_BUFFER_OFFSET)); - if abi_index < 8 { - self.emit_entry_witness_scalar_load_from_reg( - &format!("a{}", abi_index), - "t0", - "t1", - width, - param.ty == IrType::I32, - ); - } else { - let caller_stack_offset = (abi_index - 8) * 8; - self.emit_entry_witness_scalar_load_from_reg("t3", "t0", "t1", width, param.ty == IrType::I32); - self.emit(format!( - "# cellscript entry abi: scalar param {} stored to caller stack +{}", - param.name, caller_stack_offset - )); - self.emit_entry_abi_reg_arg(abi_index, "t3", outgoing_stack_arg_bytes); - } - self.emit(format!("addi t6, t6, {}", width)); - abi_index += 1; - } else { - self.emit(format!("# cellscript entry abi: unsupported param {} shape; fail closed", param.name)); - self.emit(format!("j {}", fail_label)); - } - } - let exact_size_label = self.fresh_label("entry_witness_exact_size_ok"); - self.emit("# cellscript entry abi: reject trailing witness payload bytes"); - self.emit_stack_load("t5", ENTRY_WITNESS_SIZE_OFFSET); - self.emit("sub t2, t5, t6"); - self.emit(format!("beqz t2, {}", exact_size_label)); - self.emit(format!("j {}", fail_label)); - self.emit_label(&exact_size_label); - if has_lock_args { - self.emit_entry_lock_args_exact_size_check(&fail_label); - } - self.emit_entry_call_target(target, outgoing_stack_arg_bytes); - self.emit(format!("j {}", done_label)); - } else { - let mut abi_index = 0usize; - let mut payload_cursor = 0usize; - for (param_index, param) in params.iter().enumerate() { - let param_is_runtime_bound = - runtime_bound_param_indices.contains(¶m_index) || matches!(param.ty, IrType::Ref(_) | IrType::MutRef(_)); - if param.source == ParamSource::LockArgs { - self.emit_entry_lock_args_param(&mut abi_index, param, outgoing_stack_arg_bytes, &fail_label); - } else if param_is_runtime_bound { - self.emit(format!("# cellscript entry abi: runtime-bound param {} is loaded from transaction cells", param.name)); - self.emit_entry_abi_zero_arg(abi_index, outgoing_stack_arg_bytes); - self.emit_entry_abi_zero_arg(abi_index + 1, outgoing_stack_arg_bytes); - abi_index += 2; - if type_hash_param_indices.contains(¶m_index) { - self.emit(format!( - "# cellscript entry abi: runtime-bound param {} TypeHash witness bytes unavailable; pass null ABI bytes", - param.name - )); - self.emit_entry_abi_zero_arg(abi_index, outgoing_stack_arg_bytes); - self.emit_entry_abi_zero_arg(abi_index + 1, outgoing_stack_arg_bytes); - abi_index += 2; - } - } else if entry_witness_dynamic_schema_param(¶m.ty) && self.payload_enum_width(¶m.ty).is_none() { - self.emit(format!("# cellscript entry abi: schema param {} is runtime-loaded; pass null ABI bytes", param.name)); - self.emit_entry_abi_zero_arg(abi_index, outgoing_stack_arg_bytes); - self.emit_entry_abi_zero_arg(abi_index + 1, outgoing_stack_arg_bytes); - abi_index += 2; - if type_hash_param_indices.contains(¶m_index) { - self.emit(format!( - "# cellscript entry abi: schema param {} TypeHash witness bytes unavailable; pass null ABI bytes", - param.name - )); - self.emit_entry_abi_zero_arg(abi_index, outgoing_stack_arg_bytes); - self.emit_entry_abi_zero_arg(abi_index + 1, outgoing_stack_arg_bytes); - abi_index += 2; - } - } else if let Some(width) = self - .payload_enum_width(¶m.ty) - .or_else(|| fixed_byte_pointer_param_width(¶m.ty).or_else(|| fixed_aggregate_pointer_param_width(¶m.ty))) - { - self.emit(format!( - "# cellscript entry abi: fixed-byte param {} pointer={} length={} size={}", - param.name, - abi_arg_label(abi_index), - abi_arg_label(abi_index + 1), - width - )); - self.emit_entry_abi_pointer_arg( - abi_index, - ENTRY_WITNESS_BUFFER_OFFSET + ENTRY_WITNESS_HEADER_SIZE + payload_cursor, - outgoing_stack_arg_bytes, - ); - self.emit_entry_abi_immediate_arg(abi_index + 1, width as u64, outgoing_stack_arg_bytes); - payload_cursor += width; - abi_index += 2; - } else if let Some(width) = entry_witness_register_param_width(¶m.ty) { - self.emit(format!( - "# cellscript entry abi: scalar param {} -> {} size={}", - param.name, - abi_arg_label(abi_index), - width - )); - let stack_offset = ENTRY_WITNESS_BUFFER_OFFSET + ENTRY_WITNESS_HEADER_SIZE + payload_cursor; - if abi_index < 8 { - self.emit_entry_witness_scalar_load(&format!("a{}", abi_index), stack_offset, width, param.ty == IrType::I32); - } else { - let caller_stack_offset = (abi_index - 8) * 8; - self.emit_entry_witness_scalar_load("t3", stack_offset, width, param.ty == IrType::I32); - self.emit(format!( - "# cellscript entry abi: scalar param {} stored to caller stack +{}", - param.name, caller_stack_offset - )); - self.emit_entry_abi_reg_arg(abi_index, "t3", outgoing_stack_arg_bytes); - } - payload_cursor += width; - abi_index += 1; - } else { - self.emit(format!("# cellscript entry abi: unsupported param {} shape; fail closed", param.name)); - self.emit(format!("j {}", fail_label)); - } - } - if has_lock_args { - self.emit_entry_lock_args_exact_size_check(&fail_label); - } - self.emit_entry_call_target(target, outgoing_stack_arg_bytes); - self.emit(format!("j {}", done_label)); - } - - self.emit_label(&fail_label); - self.emit_runtime_error_comment(CellScriptRuntimeError::EntryWitnessAbiInvalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::EntryWitnessAbiInvalid.code())); - self.emit_label(&done_label); - self.emit_stack_load("ra", ENTRY_WITNESS_RA_OFFSET); - self.emit_large_addi("sp", "sp", ENTRY_WITNESS_FRAME_SIZE as i64); - self.emit("ret"); - Ok(()) - } - - fn emit_entry_call_target(&mut self, target: &str, outgoing_stack_arg_bytes: usize) { - if outgoing_stack_arg_bytes > 0 { - self.emit(format!("# cellscript entry abi: reserve {} bytes for outgoing stack call arguments", outgoing_stack_arg_bytes)); - self.emit_large_addi("sp", "sp", -(outgoing_stack_arg_bytes as i64)); - } - self.emit(format!("call {}", target)); - if outgoing_stack_arg_bytes > 0 { - self.emit_large_addi("sp", "sp", outgoing_stack_arg_bytes as i64); - } - } - - fn emit_entry_abi_zero_arg(&mut self, abi_index: usize, outgoing_stack_arg_bytes: usize) { - self.emit_entry_abi_immediate_arg(abi_index, 0, outgoing_stack_arg_bytes); - } - - fn emit_entry_abi_reg_arg(&mut self, abi_index: usize, source_reg: &str, outgoing_stack_arg_bytes: usize) { - if abi_index < 8 { - self.emit(format!("addi a{}, {}, 0", abi_index, source_reg)); - } else { - self.emit_entry_outgoing_stack_arg_store(source_reg, abi_index, outgoing_stack_arg_bytes); - } - } - - fn emit_entry_abi_immediate_arg(&mut self, abi_index: usize, value: u64, outgoing_stack_arg_bytes: usize) { - if abi_index < 8 { - self.emit(format!("li a{}, {}", abi_index, value)); - } else { - self.emit(format!("# cellscript entry abi: stack arg{} <- {}", abi_index, value)); - self.emit(format!("li t0, {}", value)); - self.emit_entry_outgoing_stack_arg_store("t0", abi_index, outgoing_stack_arg_bytes); - } - } - - fn emit_entry_abi_pointer_arg(&mut self, abi_index: usize, stack_offset: usize, outgoing_stack_arg_bytes: usize) { - if abi_index < 8 { - self.emit_sp_addi(&format!("a{}", abi_index), stack_offset); - } else { - self.emit(format!("# cellscript entry abi: stack arg{} <- sp+{}", abi_index, stack_offset)); - self.emit_sp_addi("t0", stack_offset); - self.emit_entry_outgoing_stack_arg_store("t0", abi_index, outgoing_stack_arg_bytes); - } - } - - fn emit_entry_abi_pointer_from_dynamic_offset( - &mut self, - abi_index: usize, - offset_reg: &str, - extra_offset: usize, - temp_reg: &str, - outgoing_stack_arg_bytes: usize, - ) { - self.emit(format!("add {}, sp, {}", temp_reg, offset_reg)); - if ENTRY_WITNESS_BUFFER_OFFSET + extra_offset != 0 { - self.emit(format!("addi {}, {}, {}", temp_reg, temp_reg, ENTRY_WITNESS_BUFFER_OFFSET + extra_offset)); - } - self.emit_entry_abi_reg_arg(abi_index, temp_reg, outgoing_stack_arg_bytes); - } - - fn emit_entry_outgoing_stack_arg_store(&mut self, register: &str, abi_index: usize, outgoing_stack_arg_bytes: usize) { - let stack_slot_offset = (abi_index - 8) * 8; - let offset = i64::try_from(stack_slot_offset).expect("entry call stack slot should fit in i64") - - i64::try_from(outgoing_stack_arg_bytes).expect("entry call stack argument area should fit in i64"); - self.emit(format!( - "# cellscript entry abi: stage stack arg{} at pre-call sp{}{}", - abi_index, - if offset < 0 { "" } else { "+" }, - offset - )); - self.emit_sp_store_signed(register, offset); - } - - fn emit_entry_witness_scalar_load(&mut self, dest_reg: &str, stack_offset: usize, width: usize, signed_i32: bool) { - self.emit(format!("li {}, 0", dest_reg)); - for byte_index in 0..width { - self.emit_stack_load_byte("t0", stack_offset + byte_index); - if byte_index != 0 { - self.emit(format!("slli t0, t0, {}", byte_index * 8)); - } - self.emit(format!("or {}, {}, t0", dest_reg, dest_reg)); - } - if signed_i32 { - self.emit_sign_extend_i32(dest_reg); - } - } - - fn emit_entry_witness_scalar_load_from_reg( - &mut self, - dest_reg: &str, - base_reg: &str, - byte_reg: &str, - width: usize, - signed_i32: bool, - ) { - debug_assert_ne!(dest_reg, base_reg, "entry scalar decoder destination must not alias its base"); - debug_assert_ne!(byte_reg, base_reg, "entry scalar decoder scratch must not alias its base"); - debug_assert_ne!(byte_reg, dest_reg, "entry scalar decoder scratch must not alias its destination"); - self.emit(format!("li {}, 0", dest_reg)); - for byte_index in 0..width { - self.emit(format!("lbu {}, {}({})", byte_reg, byte_index, base_reg)); - if byte_index != 0 { - self.emit(format!("slli {}, {}, {}", byte_reg, byte_reg, byte_index * 8)); - } - self.emit(format!("or {}, {}, {}", dest_reg, dest_reg, byte_reg)); - } - if signed_i32 { - self.emit_sign_extend_i32(dest_reg); - } - } - - fn emit_entry_load_u32_from_stack(&mut self, dest_reg: &str, stack_offset: usize) { - self.emit(format!("li {}, 0", dest_reg)); - for byte_index in 0..4 { - self.emit_stack_load_byte("t0", stack_offset + byte_index); - if byte_index != 0 { - self.emit(format!("slli t0, t0, {}", byte_index * 8)); - } - self.emit(format!("or {}, {}, t0", dest_reg, dest_reg)); - } - } - - fn emit_entry_load_u32_from_reg(&mut self, dest_reg: &str, base_reg: &str, byte_reg: &str) { - debug_assert_ne!(dest_reg, base_reg, "entry u32 decoder destination must not alias its base"); - debug_assert_ne!(byte_reg, base_reg, "entry u32 decoder scratch must not alias its base"); - debug_assert_ne!(byte_reg, dest_reg, "entry u32 decoder scratch must not alias its destination"); - self.emit(format!("li {}, 0", dest_reg)); - for byte_index in 0..4 { - self.emit(format!("lbu {}, {}({})", byte_reg, byte_index, base_reg)); - if byte_index != 0 { - self.emit(format!("slli {}, {}, {}", byte_reg, byte_reg, byte_index * 8)); - } - self.emit(format!("or {}, {}, {}", dest_reg, dest_reg, byte_reg)); - } - } - - fn emit_entry_load_script_args(&mut self, fail_label: &str) { - let loaded_label = self.fresh_label("entry_script_loaded"); - let buffer_ok_label = self.fresh_label("entry_script_buffer_ok"); - let total_ok_label = self.fresh_label("entry_script_total_ok"); - let table_header_ok_label = self.fresh_label("entry_script_table_header_ok"); - let args_offset_min_ok_label = self.fresh_label("entry_script_args_offset_min_ok"); - let args_offset_ok_label = self.fresh_label("entry_script_args_offset_ok"); - let args_span_ok_label = self.fresh_label("entry_script_args_span_ok"); - - self.emit("# cellscript entry abi: lock_args parameters are decoded from the executing Script.args bytes"); - self.emit_load_script_syscall_to_offsets( - "entry_lock_args", - ENTRY_SCRIPT_SIZE_OFFSET, - ENTRY_SCRIPT_BUFFER_OFFSET, - ENTRY_SCRIPT_BUFFER_SIZE, - ); - self.emit(format!("beqz a0, {}", loaded_label)); - self.emit(format!("j {}", fail_label)); - self.emit_label(&loaded_label); - - self.emit_stack_load("t0", ENTRY_SCRIPT_SIZE_OFFSET); - self.emit(format!("li t1, {}", ENTRY_SCRIPT_BUFFER_SIZE + 1)); - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", buffer_ok_label)); - self.emit(format!("j {}", fail_label)); - self.emit_label(&buffer_ok_label); - - self.emit_entry_load_u32_from_stack("t3", ENTRY_SCRIPT_BUFFER_OFFSET); - self.emit_stack_load("t0", ENTRY_SCRIPT_SIZE_OFFSET); - self.emit("sub t2, t0, t3"); - self.emit(format!("beqz t2, {}", total_ok_label)); - self.emit(format!("j {}", fail_label)); - self.emit_label(&total_ok_label); - - self.emit("li t1, 16"); - self.emit("sltu t2, t3, t1"); - self.emit(format!("beqz t2, {}", table_header_ok_label)); - self.emit(format!("j {}", fail_label)); - self.emit_label(&table_header_ok_label); - - self.emit_entry_load_u32_from_stack("t4", ENTRY_SCRIPT_BUFFER_OFFSET + 12); - self.emit("li t1, 16"); - self.emit("sltu t2, t4, t1"); - self.emit(format!("beqz t2, {}", args_offset_min_ok_label)); - self.emit(format!("j {}", fail_label)); - self.emit_label(&args_offset_min_ok_label); - self.emit("addi t1, t4, 4"); - self.emit("sltu t2, t3, t1"); - self.emit(format!("beqz t2, {}", args_offset_ok_label)); - self.emit(format!("j {}", fail_label)); - self.emit_label(&args_offset_ok_label); - - self.emit_sp_addi("t0", ENTRY_SCRIPT_BUFFER_OFFSET); - self.emit("add t0, t0, t4"); - self.emit_entry_load_u32_from_reg("t5", "t0", "t1"); - self.emit("addi t6, t4, 4"); - self.emit("add t1, t6, t5"); - self.emit("sltu t2, t3, t1"); - self.emit(format!("beqz t2, {}", args_span_ok_label)); - self.emit(format!("j {}", fail_label)); - self.emit_label(&args_span_ok_label); - self.emit_stack_store_with_avoid("t6", ENTRY_SCRIPT_ARGS_START_OFFSET, &["t5"]); - self.emit_stack_store("t5", ENTRY_SCRIPT_ARGS_LEN_OFFSET); - self.emit("li t0, 0"); - self.emit_stack_store("t0", ENTRY_SCRIPT_ARGS_CURSOR_OFFSET); - } - - fn emit_entry_lock_args_param( - &mut self, - abi_index: &mut usize, - param: &IrParam, - outgoing_stack_arg_bytes: usize, - fail_label: &str, - ) { - let fixed_byte_width = self - .payload_enum_width(¶m.ty) - .or_else(|| fixed_byte_pointer_param_width(¶m.ty).or_else(|| fixed_aggregate_pointer_param_width(¶m.ty))); - let scalar_width = entry_witness_register_param_width(¶m.ty); - let Some(width) = fixed_byte_width.or(scalar_width) else { - self.emit(format!("# cellscript entry abi: unsupported lock_args param {} shape; fail closed", param.name)); - self.emit(format!("j {}", fail_label)); - return; - }; - let bytes_ok_label = self.fresh_label("entry_lock_args_bytes_ok"); - self.emit(format!("# cellscript entry abi: lock_args param {} consumes {} script arg byte(s)", param.name, width)); - let witness_cursor_live = ["t5", "t6"]; - let witness_and_script_cursor_live = ["t3", "t5", "t6"]; - self.emit_stack_load_with_avoid("t3", ENTRY_SCRIPT_ARGS_CURSOR_OFFSET, &witness_cursor_live); - self.emit_stack_load_with_avoid("t4", ENTRY_SCRIPT_ARGS_LEN_OFFSET, &witness_and_script_cursor_live); - self.emit(format!("addi t1, t3, {}", width)); - self.emit("sltu t2, t4, t1"); - self.emit(format!("beqz t2, {}", bytes_ok_label)); - self.emit(format!("j {}", fail_label)); - self.emit_label(&bytes_ok_label); - self.emit_stack_load_with_avoid("t4", ENTRY_SCRIPT_ARGS_START_OFFSET, &witness_and_script_cursor_live); - self.emit("add t4, t4, t3"); - self.emit_sp_addi("t0", ENTRY_SCRIPT_BUFFER_OFFSET); - self.emit("add t0, t0, t4"); - - if fixed_byte_width.is_some() { - self.emit_entry_abi_reg_arg(*abi_index, "t0", outgoing_stack_arg_bytes); - self.emit_entry_abi_immediate_arg(*abi_index + 1, width as u64, outgoing_stack_arg_bytes); - *abi_index += 2; - } else if *abi_index < 8 { - self.emit_entry_witness_scalar_load_from_reg(&format!("a{}", *abi_index), "t0", "t1", width, param.ty == IrType::I32); - *abi_index += 1; - } else { - self.emit_entry_witness_scalar_load_from_reg("t4", "t0", "t1", width, param.ty == IrType::I32); - self.emit_entry_abi_reg_arg(*abi_index, "t4", outgoing_stack_arg_bytes); - *abi_index += 1; - } - - self.emit_stack_load_with_avoid("t3", ENTRY_SCRIPT_ARGS_CURSOR_OFFSET, &witness_cursor_live); - self.emit(format!("addi t3, t3, {}", width)); - self.emit_stack_store_with_avoid("t3", ENTRY_SCRIPT_ARGS_CURSOR_OFFSET, &witness_cursor_live); - } - - fn emit_entry_lock_args_exact_size_check(&mut self, fail_label: &str) { - let exact_label = self.fresh_label("entry_lock_args_exact_size_ok"); - self.emit("# cellscript entry abi: reject trailing Script.args bytes after typed lock_args"); - self.emit_stack_load("t0", ENTRY_SCRIPT_ARGS_CURSOR_OFFSET); - self.emit_stack_load("t1", ENTRY_SCRIPT_ARGS_LEN_OFFSET); - self.emit("sub t2, t1, t0"); - self.emit(format!("beqz t2, {}", exact_label)); - self.emit(format!("j {}", fail_label)); - self.emit_label(&exact_label); - } - fn generate_type_def(&mut self, type_def: &IrTypeDef) -> Result<()> { self.emit_section(".rodata"); self.emit_label(&format!("__type_desc_{}", type_def.name)); @@ -1917,10 +1323,10 @@ impl CodeGenerator { } for block in &body.blocks { for instruction in &block.instructions { - if let IrInstruction::TypeHash { operand: IrOperand::Var(var), .. } = instruction { - if let Some(index) = param_indices.get(&var.id).copied() { - type_hash_param_indices.insert(index); - } + if let IrInstruction::TypeHash { operand: IrOperand::Var(var), .. } = instruction + && let Some(index) = param_indices.get(&var.id).copied() + { + type_hash_param_indices.insert(index); } } } @@ -1974,6 +1380,10 @@ impl CodeGenerator { self.bind_readonly_schema_params = true; self.fail_handler_codes.clear(); self.prepare_function_layout(&action.body, &action.params); + self.entry_frame_sizes + .entry(action.name.clone()) + .and_modify(|size| *size = (*size).max(self.frame_size as u32)) + .or_insert(self.frame_size as u32); self.next_virtual_output = 0; self.set_schema_pointer_params(&action.params); self.set_consumed_schema_pointers(&action.body); @@ -2052,6 +1462,7 @@ impl CodeGenerator { self.bind_readonly_schema_params = false; self.fail_handler_codes.clear(); self.prepare_function_layout(&function.body, &function.params); + self.entry_frame_sizes.insert(function.name.clone(), self.frame_size as u32); self.next_virtual_output = 0; self.set_schema_pointer_params(&function.params); self.set_consumed_schema_pointers(&function.body); @@ -2105,6 +1516,10 @@ impl CodeGenerator { self.current_lock_entry = true; self.fail_handler_codes.clear(); self.prepare_function_layout(&lock.body, &lock.params); + self.entry_frame_sizes + .entry(lock.name.clone()) + .and_modify(|size| *size = (*size).max(self.frame_size as u32)) + .or_insert(self.frame_size as u32); self.next_virtual_output = 0; self.set_schema_pointer_params(&lock.params); self.set_consumed_schema_pointers(&lock.body); @@ -2215,38 +1630,38 @@ impl CodeGenerator { if self.schema_pointer_vars.contains(&src.id) && self.schema_pointer_vars.insert(dest.id) { changed = true; } - if let Some(size_offset) = self.schema_pointer_size_offsets.get(&src.id).copied() { - if self.schema_pointer_size_offsets.insert(dest.id, size_offset) != Some(size_offset) { - changed = true; - } + if let Some(size_offset) = self.schema_pointer_size_offsets.get(&src.id).copied() + && self.schema_pointer_size_offsets.insert(dest.id, size_offset) != Some(size_offset) + { + changed = true; } - if let Some(size_offset) = self.fixed_byte_param_size_offsets.get(&src.id).copied() { - if self.fixed_byte_param_size_offsets.insert(dest.id, size_offset) != Some(size_offset) { - changed = true; - } + if let Some(size_offset) = self.fixed_byte_param_size_offsets.get(&src.id).copied() + && self.fixed_byte_param_size_offsets.insert(dest.id, size_offset) != Some(size_offset) + { + changed = true; } - if let Some(size_offset) = self.dynamic_value_size_offsets.get(&src.id).copied() { - if self.dynamic_value_size_offsets.insert(dest.id, size_offset) != Some(size_offset) { - changed = true; - } + if let Some(size_offset) = self.dynamic_value_size_offsets.get(&src.id).copied() + && self.dynamic_value_size_offsets.insert(dest.id, size_offset) != Some(size_offset) + { + changed = true; } - if let Some(size_offset) = self.cell_buffer_size_offsets.get(&src.id).copied() { - if self.cell_buffer_size_offsets.insert(dest.id, size_offset) != Some(size_offset) { - changed = true; - } + if let Some(size_offset) = self.cell_buffer_size_offsets.get(&src.id).copied() + && self.cell_buffer_size_offsets.insert(dest.id, size_offset) != Some(size_offset) + { + changed = true; } - if let Some(buffer_offset) = self.cell_buffer_offsets.get(&src.id).copied() { - if self.cell_buffer_offsets.insert(dest.id, buffer_offset) != Some(buffer_offset) { - changed = true; - } + if let Some(buffer_offset) = self.cell_buffer_offsets.get(&src.id).copied() + && self.cell_buffer_offsets.insert(dest.id, buffer_offset) != Some(buffer_offset) + { + changed = true; } if self.empty_molecule_vector_vars.contains(&src.id) && self.empty_molecule_vector_vars.insert(dest.id) { changed = true; } - if let Some(source) = self.aggregate_pointer_sources.get(&src.id).cloned() { - if self.aggregate_pointer_sources.insert(dest.id, source).is_none() { - changed = true; - } + if let Some(source) = self.aggregate_pointer_sources.get(&src.id).cloned() + && self.aggregate_pointer_sources.insert(dest.id, source).is_none() + { + changed = true; } } } @@ -2493,11 +1908,11 @@ impl CodeGenerator { match instruction { IrInstruction::Move { dest, src: IrOperand::Var(src) } | IrInstruction::Unary { dest, op: UnaryOp::Ref | UnaryOp::Deref, operand: IrOperand::Var(src) } => { - if !self.schema_field_value_sources.contains_key(&dest.id) { - if let Some(source) = self.schema_field_value_sources.get(&src.id).cloned() { - self.schema_field_value_sources.insert(dest.id, source); - changed = true; - } + if !self.schema_field_value_sources.contains_key(&dest.id) + && let Some(source) = self.schema_field_value_sources.get(&src.id).cloned() + { + self.schema_field_value_sources.insert(dest.id, source); + changed = true; } } _ => {} @@ -2554,12 +1969,11 @@ impl CodeGenerator { IrInstruction::Create { dest, pattern } | IrInstruction::CreateUnique { dest, pattern, .. } | IrInstruction::ReplaceUnique { dest, pattern, .. } => { - if pattern.operation != "create" { - if let Some(output_index) = + if pattern.operation != "create" + && let Some(output_index) = Self::create_output_index(body, &pattern.operation, &pattern.binding, &pattern.ty) - { - self.operation_output_indices.insert(dest.id, output_index); - } + { + self.operation_output_indices.insert(dest.id, output_index); } } IrInstruction::Transfer { dest, .. } => { @@ -2667,14 +2081,14 @@ impl CodeGenerator { } continue; } - if let Some(source_id) = named_vectors.get(name).copied() { - if let Some(bytes) = self.constructed_byte_vectors.get(&source_id).cloned() { - self.constructed_byte_vectors.insert(dest.id, bytes); - if let Some(root_id) = self.constructed_byte_vector_roots.get(&source_id).copied() { - self.constructed_byte_vector_roots.insert(dest.id, root_id); - } - loaded_vector_names.insert(dest.id, name.clone()); + if let Some(source_id) = named_vectors.get(name).copied() + && let Some(bytes) = self.constructed_byte_vectors.get(&source_id).cloned() + { + self.constructed_byte_vectors.insert(dest.id, bytes); + if let Some(root_id) = self.constructed_byte_vector_roots.get(&source_id).copied() { + self.constructed_byte_vector_roots.insert(dest.id, root_id); } + loaded_vector_names.insert(dest.id, name.clone()); } } IrInstruction::CollectionNew { dest, .. } => { @@ -2941,27 +2355,25 @@ impl CodeGenerator { self.emit(format!("# {} input {}", pattern.operation, pattern.binding)); if let Some(var_id) = self.consume_binding_ids.get(&pattern.binding).copied().or_else(|| self.consume_order.get(index).copied()) - { - if let (Some(size_offset), Some(buffer_offset)) = + && let (Some(size_offset), Some(buffer_offset)) = (self.cell_buffer_size_offsets.get(&var_id).copied(), self.cell_buffer_offsets.get(&var_id).copied()) - { - let input_index = self.consume_indices.get(&var_id).copied().unwrap_or(index); - self.emit_load_cell_data_syscall_to_offsets( - &pattern.operation, - CKB_SOURCE_INPUT, - input_index, - size_offset, - buffer_offset, - RUNTIME_CELL_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_sp_addi("t0", buffer_offset); - self.emit_stack_store("t0", var_id * 8); - if pattern.operation == "destroy" { - self.emit_destroy_group_output_absence_scan(pattern, input_index); - } - return Ok(()); + { + let input_index = self.consume_indices.get(&var_id).copied().unwrap_or(index); + self.emit_load_cell_data_syscall_to_offsets( + &pattern.operation, + CKB_SOURCE_INPUT, + input_index, + size_offset, + buffer_offset, + RUNTIME_CELL_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_sp_addi("t0", buffer_offset); + self.emit_stack_store("t0", var_id * 8); + if pattern.operation == "destroy" { + self.emit_destroy_group_output_absence_scan(pattern, input_index); } + return Ok(()); } self.emit_load_cell_data_syscall(&pattern.operation, CKB_SOURCE_INPUT, index); @@ -2974,24 +2386,23 @@ impl CodeGenerator { fn generate_read_ref(&mut self, pattern: &CellPattern, index: usize) -> Result<()> { self.emit(format!("# read_ref {}", pattern.binding)); - if let Some(var_id) = self.read_ref_order.get(index).copied() { - if let (Some(size_offset), Some(buffer_offset)) = + if let Some(var_id) = self.read_ref_order.get(index).copied() + && let (Some(size_offset), Some(buffer_offset)) = (self.cell_buffer_size_offsets.get(&var_id).copied(), self.cell_buffer_offsets.get(&var_id).copied()) - { - let dep_index = self.read_ref_indices.get(&var_id).copied().unwrap_or(index); - self.emit_load_cell_data_syscall_to_offsets( - "read_ref", - CKB_SOURCE_CELL_DEP, - dep_index, - size_offset, - buffer_offset, - RUNTIME_CELL_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_sp_addi("t0", buffer_offset); - self.emit_stack_store("t0", var_id * 8); - return Ok(()); - } + { + let dep_index = self.read_ref_indices.get(&var_id).copied().unwrap_or(index); + self.emit_load_cell_data_syscall_to_offsets( + "read_ref", + CKB_SOURCE_CELL_DEP, + dep_index, + size_offset, + buffer_offset, + RUNTIME_CELL_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_sp_addi("t0", buffer_offset); + self.emit_stack_store("t0", var_id * 8); + return Ok(()); } self.emit_load_cell_data_syscall("read_ref", CKB_SOURCE_CELL_DEP, index); @@ -3291,47 +2702,46 @@ impl CodeGenerator { } return Ok(()); } - if let IrOperand::Var(var) = operand { - if let IrType::Named(name) = &var.ty { - if let Some(layout) = self.enum_layouts.get(name).filter(|layout| layout.has_payload()).cloned() { - let Some(source) = self.expected_fixed_byte_source(operand, layout.encoded_size) else { - self.emit("# cellscript abi: payload enum return source is unavailable; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(()); - }; - self.emit_prepare_fixed_byte_source(&source, layout.encoded_size, "payload enum return"); - if !self.emit_fixed_byte_source_pointer_or_const_to("t4", &source) { - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(()); - } - self.emit_unaligned_scalar_load("t4", "a0", "t2", 0, layout.encoded_size.min(8)); - if layout.encoded_size > 8 { - self.emit_unaligned_scalar_load("t4", "a1", "t2", 8, layout.encoded_size - 8); - } else { - self.emit("li a1, 0"); - } - self.emit(format!( - "# cellscript abi: return payload enum {} size={} via a0/a1 register pair", - name, layout.encoded_size - )); - self.emit_epilogue(); - return Ok(()); - } + if let IrOperand::Var(var) = operand + && let IrType::Named(name) = &var.ty + && let Some(layout) = self.enum_layouts.get(name).filter(|layout| layout.has_payload()).cloned() + { + let Some(source) = self.expected_fixed_byte_source(operand, layout.encoded_size) else { + self.emit("# cellscript abi: payload enum return source is unavailable; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(()); + }; + self.emit_prepare_fixed_byte_source(&source, layout.encoded_size, "payload enum return"); + if !self.emit_fixed_byte_source_pointer_or_const_to("t4", &source) { + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(()); + } + self.emit_unaligned_scalar_load("t4", "a0", "t2", 0, layout.encoded_size.min(8)); + if layout.encoded_size > 8 { + self.emit_unaligned_scalar_load("t4", "a1", "t2", 8, layout.encoded_size - 8); + } else { + self.emit("li a1, 0"); } + self.emit(format!( + "# cellscript abi: return payload enum {} size={} via a0/a1 register pair", + name, layout.encoded_size + )); + self.emit_epilogue(); + return Ok(()); } - if let IrOperand::Var(v) = operand { - if let Some(fields) = self.tuple_aggregate_fields.get(&v.id).cloned() { - self.emit(format!("# cellscript abi: return tuple aggregate var{} fields={}", v.id, fields.len())); - if fields.is_empty() { - self.emit("li a0, 0"); - } - for (index, field) in fields.iter().take(8).enumerate() { - self.emit(format!("# cellscript abi: return tuple field .{} via a{}", index, index)); - self.emit_operand_to_register(&format!("a{}", index), field); - } - self.emit_epilogue(); - return Ok(()); + if let IrOperand::Var(v) = operand + && let Some(fields) = self.tuple_aggregate_fields.get(&v.id).cloned() + { + self.emit(format!("# cellscript abi: return tuple aggregate var{} fields={}", v.id, fields.len())); + if fields.is_empty() { + self.emit("li a0, 0"); + } + for (index, field) in fields.iter().take(8).enumerate() { + self.emit(format!("# cellscript abi: return tuple field .{} via a{}", index, index)); + self.emit_operand_to_register(&format!("a{}", index), field); } + self.emit_epilogue(); + return Ok(()); } self.emit_operand_to_register("a0", operand); if self.current_lock_entry { @@ -3379,18393 +2789,970 @@ impl CodeGenerator { Ok(()) } - fn emit_prologue(&mut self) { - self.emit_large_addi("sp", "sp", -(self.frame_size as i64)); - self.emit_stack_store("ra", self.frame_size - 8); - self.emit_stack_store("fp", self.frame_size - 16); - self.emit_sp_addi("fp", self.frame_size); - } - - fn emit_epilogue(&mut self) { - if let Some(function) = &self.current_function { - self.emit(format!("j .L{}_epilogue", function)); - return; - } - self.emit_epilogue_body(); - } - - fn emit_fail(&mut self, error: CellScriptRuntimeError) { - if let Some(function) = self.current_function.clone() { - self.fail_handler_codes.insert(error); - self.emit(format!("j .L{}_fail_{}", function, error.code())); - return; + fn emit_read_ref(&mut self, dest: &IrVar, ty: &str) -> Result<()> { + if self.cell_buffer_offsets.contains_key(&dest.id) { + self.emit(format!("# read_ref {} (preloaded from CellDep)", ty)); + return Ok(()); } - self.emit_runtime_error_comment(error); - self.emit(format!("li a0, {}", error.code())); - self.emit_epilogue_body(); - } - fn emit_shared_epilogue(&mut self) { - let Some(function) = self.current_function.clone() else { - return; - }; - let fail_codes = self.fail_handler_codes.iter().copied().collect::>(); - for error in fail_codes { - self.emit_label(&format!(".L{}_fail_{}", function, error.code())); - self.emit_runtime_error_comment(error); - self.emit(format!("li a0, {}", error.code())); - self.emit(format!("j .L{}_epilogue", function)); - } - self.emit_label(&format!(".L{}_epilogue", function)); - self.emit_epilogue_body(); - } + // Runtime fallback: emit LOAD_CELL_DATA syscall to load the cell dep data + // into the scratch buffer and store the pointer. + let Some(dep_index) = self.read_ref_indices.get(&dest.id).copied() else { + self.emit("# cellscript abi: fail closed because read_ref CellDep index was not allocated"); + self.emit_fail(CellScriptRuntimeError::ConsumeInvalidOperand); + return Ok(()); + }; + let size_offset = self.runtime_scratch_size_offset(); + let buffer_offset = self.runtime_scratch_buffer_offset(); - fn emit_runtime_error_comment(&mut self, error: CellScriptRuntimeError) { - self.emit(format!("# cellscript runtime error {} {}", error.code(), error.name())); - } + self.emit(format!("# read_ref {}", ty)); + self.emit(format!("# cellscript abi: runtime read_ref CellDep index={}", dep_index)); + self.emit_load_cell_data_syscall_to_offsets( + "read_ref", + CKB_SOURCE_CELL_DEP, + dep_index, + size_offset, + buffer_offset, + RUNTIME_SCRATCH_BUFFER_SIZE, + ); + self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + self.emit_sp_addi("t0", buffer_offset); + self.emit_stack_store("t0", dest.id * 8); - fn emit_epilogue_body(&mut self) { - self.emit_stack_load("ra", self.frame_size - 8); - self.emit_stack_load("fp", self.frame_size - 16); - self.emit_large_addi("sp", "sp", self.frame_size as i64); - self.emit("ret"); - } + // Also store the size so that subsequent schema operations can use it + self.schema_pointer_size_offsets.insert(dest.id, size_offset); + self.cell_buffer_size_offsets.insert(dest.id, size_offset); + self.cell_buffer_offsets.insert(dest.id, buffer_offset); - /// Emit `addi rd, rs1, imm` handling immediates that don't fit in 12 bits. - fn emit_large_addi(&mut self, rd: &str, rs1: &str, imm: i64) { - if (-2048..=2047).contains(&imm) { - self.emit(format!("addi {}, {}, {}", rd, rs1, imm)); - } else { - let scratch = scratch_register_avoiding(&[rs1]); - self.emit(format!("li {}, {}", scratch, imm)); - self.emit(format!("add {}, {}, {}", rd, rs1, scratch)); - } + Ok(()) } - fn emit_memory_load_with_avoid(&mut self, opcode: &str, dst: &str, base: &str, offset: usize, avoid: &[&str]) { - let offset = i64::try_from(offset).expect("memory offset should fit in i64"); - if small_signed_immediate(offset) { - self.emit(format!("{} {}, {}({})", opcode, dst, offset, base)); - } else { - let mut registers = Vec::with_capacity(2 + avoid.len()); - registers.push(dst); - registers.push(base); - registers.extend_from_slice(avoid); - let scratch = scratch_register_avoiding(®isters); - self.emit(format!("li {}, {}", scratch, offset)); - self.emit(format!("add {}, {}, {}", scratch, base, scratch)); - self.emit(format!("{} {}, 0({})", opcode, dst, scratch)); + fn emit_move(&mut self, dest: &IrVar, src: &IrOperand) -> Result<()> { + if dest.ty == IrType::U128 { + self.emit_materialize_u128_operand_to_var(dest, src); + return Ok(()); } - } - - fn emit_memory_store_with_avoid(&mut self, opcode: &str, src: &str, base: &str, offset: usize, avoid: &[&str]) { - let offset = i64::try_from(offset).expect("memory offset should fit in i64"); - if small_signed_immediate(offset) { - self.emit(format!("{} {}, {}({})", opcode, src, offset, base)); - } else { - let mut registers = Vec::with_capacity(2 + avoid.len()); - registers.push(src); - registers.push(base); - registers.extend_from_slice(avoid); - let scratch = scratch_register_avoiding(®isters); - self.emit(format!("li {}, {}", scratch, offset)); - self.emit(format!("add {}, {}, {}", scratch, base, scratch)); - self.emit(format!("{} {}, 0({})", opcode, src, scratch)); + if let Some(width) = self.fixed_byte_like_width(&dest.ty).filter(|width| *width > 8) + && self.emit_materialize_fixed_byte_operand_to_var(dest, src, width) + { + return Ok(()); } + self.emit_operand_to_register("t0", src); + self.emit_stack_store("t0", dest.id * 8); + Ok(()) } - /// Emit `ld rd, offset(sp)` through the centralized stack-offset gate. - fn emit_stack_load(&mut self, rd: &str, offset: usize) { - self.emit_stack_access_with_avoid("ld", rd, offset, &[]); - } - - /// Emit `ld rd, offset(sp)` without clobbering explicitly live registers. - fn emit_stack_load_with_avoid(&mut self, rd: &str, offset: usize, avoid: &[&str]) { - self.emit_stack_access_with_avoid("ld", rd, offset, avoid); - } - - /// Emit `lbu rd, offset(sp)` through the centralized stack-offset gate. - fn emit_stack_load_byte(&mut self, rd: &str, offset: usize) { - self.emit_stack_access("lbu", rd, offset); - } - - /// Emit `sd rs2, offset(sp)` through the centralized stack-offset gate. - fn emit_stack_store(&mut self, rs2: &str, offset: usize) { - self.emit_stack_access_with_avoid("sd", rs2, offset, &[]); - } - - /// Emit `sd rs2, offset(sp)` without clobbering explicitly live registers. - fn emit_stack_store_with_avoid(&mut self, rs2: &str, offset: usize, avoid: &[&str]) { - self.emit_stack_access_with_avoid("sd", rs2, offset, avoid); - } - - /// Emit `sb rs2, offset(sp)` through the centralized stack-offset gate. - fn emit_stack_store_byte(&mut self, rs2: &str, offset: usize) { - self.emit_stack_access("sb", rs2, offset); - } - - fn emit_stack_access(&mut self, opcode: &str, register: &str, offset: usize) { - self.emit_stack_access_with_avoid(opcode, register, offset, &[]); - } - - fn emit_stack_access_with_avoid(&mut self, opcode: &str, register: &str, offset: usize, avoid: &[&str]) { - let offset = i64::try_from(offset).expect("stack offset should fit in i64"); - if small_signed_immediate(offset) { - self.emit(format!("{} {}, {}(sp)", opcode, register, offset)); - } else { - let mut live_registers = Vec::with_capacity(1 + avoid.len()); - live_registers.push(register); - live_registers.extend_from_slice(avoid); - let scratch = scratch_register_avoiding(&live_registers); - self.emit(format!("li {}, {}", scratch, offset)); - self.emit(format!("add {}, sp, {}", scratch, scratch)); - self.emit(format!("{} {}, 0({})", opcode, register, scratch)); + fn emit_materialize_fixed_byte_operand_to_var(&mut self, dest: &IrVar, src: &IrOperand, width: usize) -> bool { + let Some(dest_offset) = self.fixed_byte_local_offsets.get(&dest.id).copied() else { + return false; + }; + let Some(source) = self.expected_fixed_byte_source(src, width) else { + self.emit("# cellscript abi: fail closed because fixed-byte move source is unavailable"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return true; + }; + self.emit(format!("# cellscript abi: materialize fixed-byte move var{} size={}", dest.id, width)); + self.emit_prepare_fixed_byte_source(&source, width, "fixed-byte move"); + if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &source) { + self.emit("# cellscript abi: fail closed because fixed-byte move pointer is unavailable"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return true; } + self.emit_sp_addi("a1", dest_offset); + self.emit(format!("li a2, {}", width)); + self.emit("call __cellscript_memcpy_fixed"); + self.emit_sp_addi("t0", dest_offset); + self.emit_stack_store("t0", dest.id * 8); + true } - /// Emit `addi rd, sp, offset` handling offsets that don't fit in 12 bits. - fn emit_sp_addi(&mut self, rd: &str, offset: usize) { - if offset <= 2047 { - self.emit(format!("addi {}, sp, {}", rd, offset)); - } else if rd == "sp" { - self.emit_large_addi("sp", "sp", offset as i64); - } else { - self.emit(format!("li {}, {}", rd, offset)); - self.emit(format!("add {}, sp, {}", rd, rd)); + fn emit_tuple(&mut self, dest: &IrVar, fields: &[IrOperand]) -> Result<()> { + self.emit(format!("# cellscript abi: construct tuple aggregate var{} fields={}", dest.id, fields.len())); + if self.emit_fixed_named_tuple(dest, fields) { + return Ok(()); } + self.emit_stack_store("zero", dest.id * 8); + Ok(()) } - fn prepare_function_layout(&mut self, body: &IrBody, params: &[IrParam]) { - let mut max_var_id = None; - let mut fixed_byte_locals = HashMap::::new(); - let mut named_vars = BTreeSet::::new(); - for param in params { - self.record_var(¶m.binding, &mut max_var_id); - } - for block in &body.blocks { - for instruction in &block.instructions { - self.record_instruction_var(instruction, &mut max_var_id); - self.record_instruction_fixed_byte_local(instruction, &mut fixed_byte_locals); - if let IrInstruction::StoreVar { name, .. } = instruction { - named_vars.insert(name.clone()); - } - } - self.record_terminator_var(&block.terminator, &mut max_var_id); + fn emit_enum_construct(&mut self, dest: &IrVar, enum_name: &str, variant_name: &str, fields: &[IrOperand]) -> Result<()> { + let Some(layout) = self.enum_layouts.get(enum_name).cloned() else { + return Err(CompileError::new( + format!("payload enum '{}' reached codegen without an IR layout", enum_name), + crate::error::Span::default(), + )); + }; + let Some(variant) = layout.variants.iter().find(|variant| variant.name == variant_name).cloned() else { + return Err(CompileError::new( + format!("payload enum '{}::{}' reached codegen without a variant layout", enum_name, variant_name), + crate::error::Span::default(), + )); + }; + if fields.len() != variant.fields.len() { + return Err(CompileError::new( + format!( + "payload enum '{}::{}' codegen arity mismatch: expected {}, found {}", + enum_name, + variant_name, + variant.fields.len(), + fields.len() + ), + crate::error::Span::default(), + )); } + let Some(dest_offset) = self.fixed_byte_local_offsets.get(&dest.id).copied() else { + return Err(CompileError::new( + format!("payload enum '{}' destination has no fixed-byte local storage", enum_name), + crate::error::Span::default(), + )); + }; - let locals_size = max_var_id.map(|id| (id + 1) * 8).unwrap_or(0); - self.fixed_byte_local_offsets.clear(); - self.named_var_offsets.clear(); - self.cell_buffer_offsets.clear(); - self.cell_buffer_size_offsets.clear(); - self.dynamic_value_size_offsets.clear(); - self.empty_molecule_vector_vars.clear(); - self.constructed_byte_vectors.clear(); - self.constructed_byte_vector_roots.clear(); - self.verified_collection_construction_vectors.clear(); - self.output_type_hash_sources.clear(); - self.consume_order.clear(); - self.consume_indices.clear(); - self.consume_type_names.clear(); - self.consume_binding_ids.clear(); - self.read_ref_order.clear(); - self.read_ref_indices.clear(); - self.read_ref_param_ids.clear(); - self.read_ref_param_input_indices.clear(); - self.read_ref_param_dep_indices.clear(); - self.output_param_ids.clear(); - self.mutate_param_ids.clear(); - self.schema_pointer_size_offsets.clear(); - self.fixed_byte_param_size_offsets.clear(); - self.param_type_hash_pointer_offsets.clear(); - self.param_type_hash_size_offsets.clear(); - self.param_type_hash_sources.clear(); - self.u128_value_offsets.clear(); - self.collection_region_start = 0; - self.next_collection_slot = 0; - - let schema_param_ids = - params.iter().filter(|param| named_type_name(¶m.ty).is_some()).map(|param| param.binding.id).collect::>(); - let mut param_type_hash_ids = BTreeSet::new(); - for block in &body.blocks { - for instruction in &block.instructions { - if let IrInstruction::TypeHash { dest, operand: IrOperand::Var(var) } = instruction { - if schema_param_ids.contains(&var.id) { - param_type_hash_ids.insert(var.id); - self.param_type_hash_sources.insert(dest.id, var.id); - } - } - } - } + self.emit(format!( + "# cellscript abi: construct payload enum {}::{} var{} tagged-union-v1 size={}", + enum_name, variant_name, dest.id, layout.encoded_size + )); + self.emit_sp_addi("a0", dest_offset); + self.emit(format!("li a1, {}", layout.encoded_size)); + self.emit("call __cellscript_memzero_fixed"); + self.emit_sp_addi("t4", dest_offset); + self.emit(format!("li t0, {}", variant.tag)); + self.emit_memory_store_with_avoid("sb", "t0", "t4", 0, &["t0", "t4"]); - let mut next_cell_slot = locals_size; - let mut fixed_byte_locals = fixed_byte_locals.into_iter().collect::>(); - fixed_byte_locals.sort_unstable_by_key(|(var_id, _)| *var_id); - for (var_id, width) in fixed_byte_locals { - next_cell_slot = align_up(next_cell_slot, 8); - self.fixed_byte_local_offsets.insert(var_id, next_cell_slot); - next_cell_slot += align_up(width, 8); - } - for name in named_vars { - next_cell_slot = align_up(next_cell_slot, 8); - self.named_var_offsets.insert(name, next_cell_slot); - next_cell_slot += 8; - } - for param in params { - if param.source == ParamSource::Output { - self.output_param_ids.insert(param.name.clone(), param.binding.id); - self.schema_pointer_size_offsets.insert(param.binding.id, next_cell_slot); - self.cell_buffer_size_offsets.insert(param.binding.id, next_cell_slot); - self.cell_buffer_offsets.insert(param.binding.id, next_cell_slot + 8); - next_cell_slot += RUNTIME_CELL_SLOT_SIZE; + for (operand, field) in fields.iter().zip(&variant.fields) { + if field.width == 0 { continue; } - if named_type_name(¶m.ty).is_some() { - self.schema_pointer_size_offsets.insert(param.binding.id, next_cell_slot); - next_cell_slot += 8; - } else if fixed_byte_pointer_param_width(¶m.ty).is_some() || fixed_aggregate_pointer_param_width(¶m.ty).is_some() { - self.fixed_byte_param_size_offsets.insert(param.binding.id, next_cell_slot); - next_cell_slot += 8; - } - } - for param in params { - if param_type_hash_ids.contains(¶m.binding.id) { - self.param_type_hash_pointer_offsets.insert(param.binding.id, next_cell_slot); - next_cell_slot += 8; - self.param_type_hash_size_offsets.insert(param.binding.id, next_cell_slot); - next_cell_slot += 8; - } - } - - if self.bind_readonly_schema_params { - let consumed_param_names = body.consume_set.iter().map(|pattern| pattern.binding.as_str()).collect::>(); - let mutate_param_names = body.mutate_set.iter().map(|pattern| pattern.binding.as_str()).collect::>(); - let read_ref_indices_by_binding = - body.read_refs.iter().enumerate().map(|(index, pattern)| (pattern.binding.as_str(), index)).collect::>(); - let mut read_ref_param_index = 0usize; - for param in params { - if matches!(param.source, ParamSource::Output | ParamSource::LockArgs) { - continue; - } - if !self.param_is_runtime_bound(param) { - continue; - } - if mutate_param_names.contains(param.name.as_str()) || consumed_param_names.contains(param.name.as_str()) { - continue; - } - self.read_ref_param_ids.insert(param.name.clone(), param.binding.id); - if let Some(dep_index) = read_ref_indices_by_binding.get(param.name.as_str()).copied() { - self.read_ref_param_dep_indices.insert(param.binding.id, dep_index); - } else { - let input_index = body.consume_set.len() + body.mutate_set.len() + read_ref_param_index; - self.read_ref_param_input_indices.insert(param.binding.id, input_index); - read_ref_param_index += 1; + if field.linear || (field.width <= 8 && is_fixed_scalar_ir_type(&field.ty)) { + self.emit(format!( + "# cellscript abi: payload enum field {}.{}[{}] offset={} size={}{}", + enum_name, + variant_name, + field.index, + field.offset, + field.width, + if field.linear { " local-linear-handle" } else { "" } + )); + self.emit_operand_to_register("t0", operand); + for byte_index in 0..field.width { + self.emit_memory_store_with_avoid("sb", "t0", "t4", field.offset + byte_index, &["t0", "t4"]); + if byte_index + 1 < field.width { + self.emit("srli t0, t0, 8"); + } } - self.schema_pointer_size_offsets.insert(param.binding.id, next_cell_slot); - self.cell_buffer_size_offsets.insert(param.binding.id, next_cell_slot); - self.cell_buffer_offsets.insert(param.binding.id, next_cell_slot + 8); - next_cell_slot += RUNTIME_CELL_SLOT_SIZE; - } - } - - for pattern in &body.mutate_set { - let Some(param) = params.iter().find(|param| param.name == pattern.binding) else { continue; - }; - self.mutate_param_ids.insert(pattern.binding.clone(), param.binding.id); - self.consume_type_names.insert(param.binding.id, pattern.ty.clone()); - self.consume_binding_ids.insert(pattern.binding.clone(), param.binding.id); - self.consume_indices.insert(param.binding.id, pattern.input_index); - self.schema_pointer_size_offsets.insert(param.binding.id, next_cell_slot); - self.cell_buffer_size_offsets.insert(param.binding.id, next_cell_slot); - self.cell_buffer_offsets.insert(param.binding.id, next_cell_slot + 8); - next_cell_slot += RUNTIME_CELL_SLOT_SIZE; - } + } - let consume_pattern_indices = - body.consume_set.iter().enumerate().map(|(index, pattern)| (pattern.binding.as_str(), index)).collect::>(); - for pattern in &body.consume_set { - let Some(param) = params.iter().find(|param| param.name == pattern.binding) else { - continue; + let Some(source) = self.expected_fixed_byte_source(operand, field.width) else { + self.emit("# cellscript abi: payload enum field source is unavailable; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(()); }; - if self.consume_binding_ids.contains_key(&pattern.binding) { - continue; - } - if let Some(type_name) = named_type_name(¶m.ty) { - self.consume_type_names.insert(param.binding.id, type_name.to_string()); - } - self.consume_binding_ids.insert(pattern.binding.clone(), param.binding.id); - self.schema_pointer_size_offsets.insert(param.binding.id, next_cell_slot); - self.cell_buffer_size_offsets.insert(param.binding.id, next_cell_slot); - self.cell_buffer_offsets.insert(param.binding.id, next_cell_slot + 8); - self.consume_order.push(param.binding.id); - self.consume_indices.insert(param.binding.id, consume_pattern_indices.get(pattern.binding.as_str()).copied().unwrap_or(0)); - next_cell_slot += RUNTIME_CELL_SLOT_SIZE; - } - for block in &body.blocks { - for instruction in &block.instructions { - if let Some(var) = consumed_operand_var(instruction) { - if self.consume_binding_ids.contains_key(&var.name) { - continue; - } - if let Some(type_name) = named_type_name(&var.ty) { - self.consume_type_names.insert(var.id, type_name.to_string()); - } - self.consume_binding_ids.insert(var.name.clone(), var.id); - self.schema_pointer_size_offsets.insert(var.id, next_cell_slot); - self.cell_buffer_size_offsets.insert(var.id, next_cell_slot); - self.cell_buffer_offsets.insert(var.id, next_cell_slot + 8); - self.consume_order.push(var.id); - self.consume_indices.insert( - var.id, - consume_pattern_indices.get(var.name.as_str()).copied().unwrap_or(self.consume_order.len() - 1), - ); - next_cell_slot += RUNTIME_CELL_SLOT_SIZE; - } + self.emit_prepare_fixed_byte_source(&source, field.width, "payload enum field"); + if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &source) { + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(()); } + self.emit_sp_addi("a1", dest_offset + field.offset); + self.emit(format!("li a2, {}", field.width)); + self.emit("call __cellscript_memcpy_fixed"); + self.emit_sp_addi("t4", dest_offset); } + self.emit_sp_addi("t0", dest_offset); + self.emit_stack_store("t0", dest.id * 8); + Ok(()) + } - let mut read_ref_index = 0usize; - for block in &body.blocks { - for instruction in &block.instructions { - if let IrInstruction::ReadRef { dest, .. } = instruction { - self.cell_buffer_size_offsets.insert(dest.id, next_cell_slot); - self.cell_buffer_offsets.insert(dest.id, next_cell_slot + 8); - self.read_ref_order.push(dest.id); - self.read_ref_indices.insert(dest.id, read_ref_index); - next_cell_slot += RUNTIME_CELL_SLOT_SIZE; - read_ref_index += 1; - } - } + fn emit_enum_tag(&mut self, dest: &IrVar, operand: &IrOperand, enum_name: &str) -> Result<()> { + let Some(layout) = self.enum_layouts.get(enum_name).cloned() else { + return Err(CompileError::new( + format!("payload enum '{}' tag read has no IR layout", enum_name), + crate::error::Span::default(), + )); + }; + let Some(source) = self.expected_fixed_byte_source(operand, layout.encoded_size) else { + self.emit("# cellscript abi: payload enum tag source is unavailable; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(()); + }; + self.emit_prepare_fixed_byte_source(&source, layout.encoded_size, "payload enum tag"); + if !self.emit_fixed_byte_source_pointer_or_const_to("t4", &source) { + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(()); } + self.emit_memory_load_with_avoid("lbu", "t0", "t4", 0, &["t0", "t4"]); + self.emit_stack_store("t0", dest.id * 8); + Ok(()) + } - let mut create_dest_outputs = HashMap::new(); - let mut next_create_output_index = - body.create_set.iter().position(|pattern| pattern.operation == "create").unwrap_or(body.create_set.len()); - for block in &body.blocks { - for instruction in &block.instructions { - match instruction { - IrInstruction::FieldAccess { dest, obj: IrOperand::Var(obj), field } => { - if named_type_name(&dest.ty).is_some() - && named_type_name(&obj.ty) - .and_then(|type_name| self.type_layouts.get(type_name)) - .and_then(|fields| fields.get(field)) - .is_some_and(|layout| { - layout_fixed_byte_width(layout).is_none() - && molecule_vector_element_fixed_width( - &layout.ty, - &self.type_fixed_sizes, - &self.enum_fixed_sizes, - ) - .is_some() - }) - { - self.dynamic_value_size_offsets.insert(dest.id, next_cell_slot); - next_cell_slot += 8; - } - } - IrInstruction::Create { dest, pattern } => { - let output_index = if pattern.operation == "create" { - let output_index = next_create_output_index; - next_create_output_index += 1; - Some(output_index) - } else { - Self::create_output_index(body, &pattern.operation, &pattern.binding, &pattern.ty) - }; - if let Some(output_index) = output_index { - create_dest_outputs.insert(dest.id, output_index); - } - } - IrInstruction::CreateUnique { dest, pattern, .. } | IrInstruction::ReplaceUnique { dest, pattern, .. } => { - if let Some(output_index) = Self::create_output_index(body, &pattern.operation, &pattern.binding, &pattern.ty) - { - create_dest_outputs.insert(dest.id, output_index); - } - } - IrInstruction::Transfer { dest, .. } => { - if let Some(output_index) = Self::create_output_index_for_dest(body, "transfer", dest) { - create_dest_outputs.insert(dest.id, output_index); - } - } - IrInstruction::Claim { dest, .. } => { - if let Some(output_index) = Self::create_output_index_for_dest(body, "claim", dest) { - create_dest_outputs.insert(dest.id, output_index); - } - } - IrInstruction::Settle { dest, .. } => { - if let Some(output_index) = Self::create_output_index_for_dest(body, "settle", dest) { - create_dest_outputs.insert(dest.id, output_index); - } - } - IrInstruction::TypeHash { dest, operand: IrOperand::Var(var) } => { - if let Some(output_index) = create_dest_outputs.get(&var.id).copied() { - self.output_type_hash_sources.insert(dest.id, output_index); - self.cell_buffer_size_offsets.insert(dest.id, next_cell_slot); - self.cell_buffer_offsets.insert(dest.id, next_cell_slot + 8); - next_cell_slot += RUNTIME_CELL_SLOT_SIZE; - } else if self.consume_indices.contains_key(&var.id) - || self.read_ref_indices.contains_key(&var.id) - || self.read_ref_param_input_indices.contains_key(&var.id) - { - self.cell_buffer_size_offsets.insert(dest.id, next_cell_slot); - self.cell_buffer_offsets.insert(dest.id, next_cell_slot + 8); - next_cell_slot += RUNTIME_CELL_SLOT_SIZE; - } - } - IrInstruction::Call { dest: Some(dest), func, args } - if func == "__ckb_current_script_hash" && args.is_empty() && dest.ty == IrType::Hash => - { - self.cell_buffer_size_offsets.insert(dest.id, next_cell_slot); - self.cell_buffer_offsets.insert(dest.id, next_cell_slot + 8); - next_cell_slot += RUNTIME_CELL_SLOT_SIZE; - } - IrInstruction::Call { dest: Some(dest), func, args } - if matches!( - func.as_str(), - "__ckb_input_out_point_tx_hash" - | "__ckb_cell_lock_hash" - | "__ckb_cell_type_hash" - | "__ckb_cell_data_hash" - | "__ckb_cell_data_hash_at" - | "__ckb_cell_lock_code_hash" - | "__ckb_cell_type_code_hash" - | "__ckb_cell_lock_args_hash" - | "__ckb_cell_type_args_hash" - ) && (args.len() == 1 || (func == "__ckb_cell_data_hash_at" && args.len() == 2)) - && dest.ty == IrType::Hash => - { - self.cell_buffer_size_offsets.insert(dest.id, next_cell_slot); - self.cell_buffer_offsets.insert(dest.id, next_cell_slot + 8); - next_cell_slot += RUNTIME_CELL_SLOT_SIZE; - } - IrInstruction::Call { dest: Some(dest), func, args } - if matches!( - func.as_str(), - "__ckb_witness_raw" | "__ckb_witness_lock" | "__ckb_witness_input_type" | "__ckb_witness_output_type" - ) && args.len() == 1 - && dest.ty == IrType::Hash => - { - self.cell_buffer_size_offsets.insert(dest.id, next_cell_slot); - self.cell_buffer_offsets.insert(dest.id, next_cell_slot + 8); - next_cell_slot += RUNTIME_CELL_SLOT_SIZE; - } - _ => {} - } - } + fn emit_enum_payload( + &mut self, + dest: &IrVar, + operand: &IrOperand, + enum_name: &str, + variant_name: &str, + field_index: usize, + ) -> Result<()> { + let Some(layout) = self.enum_layouts.get(enum_name).cloned() else { + return Err(CompileError::new( + format!("payload enum '{}' projection has no IR layout", enum_name), + crate::error::Span::default(), + )); + }; + let Some(field) = layout + .variants + .iter() + .find(|variant| variant.name == variant_name) + .and_then(|variant| variant.fields.get(field_index)) + .cloned() + else { + return Err(CompileError::new( + format!("payload enum '{}::{}' field {} has no IR layout", enum_name, variant_name, field_index), + crate::error::Span::default(), + )); + }; + let Some(source) = self.expected_fixed_byte_source(operand, layout.encoded_size) else { + self.emit("# cellscript abi: payload enum projection source is unavailable; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(()); + }; + self.emit_prepare_fixed_byte_source(&source, layout.encoded_size, "payload enum projection"); + if !self.emit_fixed_byte_source_pointer_or_const_to("t4", &source) { + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(()); } - - let mut u128_value_ids = BTreeSet::new(); - for param in params { - if param.ty == IrType::U128 { - u128_value_ids.insert(param.binding.id); - } + if field.width == 0 { + self.emit_stack_store("zero", dest.id * 8); + return Ok(()); } - for block in &body.blocks { - for instruction in &block.instructions { - self.collect_u128_instruction_vars(instruction, &mut u128_value_ids); + if field.linear || (field.width <= 8 && is_fixed_scalar_ir_type(&field.ty)) { + self.emit_unaligned_scalar_load("t4", "t0", "t2", field.offset, field.width); + if field.ty == IrType::I32 { + self.emit_sign_extend_i32("t0"); } - self.collect_u128_terminator_vars(&block.terminator, &mut u128_value_ids); - } - for var_id in u128_value_ids { - self.u128_value_offsets.insert(var_id, next_cell_slot); - next_cell_slot += 16; + self.emit_stack_store("t0", dest.id * 8); + return Ok(()); } - let collection_slot_size = 8 + RUNTIME_COLLECTION_BUFFER_SIZE; - let collection_count = body - .blocks - .iter() - .flat_map(|block| block.instructions.iter()) - .filter(|instruction| matches!(instruction, IrInstruction::CollectionNew { .. })) - .count(); - self.collection_region_start = next_cell_slot; - next_cell_slot += collection_count * collection_slot_size; - - self.frame_size = align_frame(next_cell_slot + RUNTIME_EXPR_TEMP_SIZE + RUNTIME_SCRATCH_SIZE + 16); - } - - fn runtime_expr_temp_offset(&self, depth: usize) -> usize { - debug_assert!(depth < RUNTIME_EXPR_TEMP_SLOTS); - self.runtime_scratch_size_offset() - RUNTIME_EXPR_TEMP_SIZE + depth * 8 - } - - fn checked_runtime_expr_temp_offset(&self, depth: usize) -> Option { - (depth < RUNTIME_EXPR_TEMP_SLOTS).then(|| self.runtime_expr_temp_offset(depth)) - } - - fn runtime_scratch_size_offset(&self) -> usize { - self.frame_size - 16 - RUNTIME_SCRATCH_SIZE - } - - fn runtime_scratch_buffer_offset(&self) -> usize { - self.runtime_scratch_size_offset() + 8 - } - - fn runtime_scratch2_size_offset(&self) -> usize { - self.runtime_scratch_size_offset() + RUNTIME_SCRATCH_SLOT_SIZE - } - - fn runtime_scratch2_buffer_offset(&self) -> usize { - self.runtime_scratch2_size_offset() + 8 - } - - fn emit_store_data_args_at(&mut self, max_bytes: usize, size_offset: usize, buffer_offset: usize) { - self.emit(format!("li t0, {}", max_bytes)); - self.emit_stack_store("t0", size_offset); - self.emit_sp_addi("a0", buffer_offset); - self.emit_sp_addi("a1", size_offset); - self.emit("li a2, 0"); - } - - fn emit_load_cell_data_syscall(&mut self, reason: &str, source: u64, index: usize) { - let size_offset = self.runtime_scratch_size_offset(); - let buffer_offset = self.runtime_scratch_buffer_offset(); - self.emit_load_cell_data_syscall_to_offsets(reason, source, index, size_offset, buffer_offset, RUNTIME_SCRATCH_BUFFER_SIZE); - } - - fn emit_load_cell_data_syscall_to_offsets( - &mut self, - reason: &str, - source: u64, - index: usize, - size_offset: usize, - buffer_offset: usize, - max_bytes: usize, - ) { - self.emit(format!("# cellscript abi: LOAD_CELL_DATA reason={} source={} index={}", reason, ckb_source_name(source), index)); - self.emit_store_data_args_at(max_bytes, size_offset, buffer_offset); - self.emit(format!("li a3, {}", index)); - self.emit(format!("li a4, {}", source)); - self.emit(format!("li a7, {}", self.runtime_abi().load_cell_data)); - self.emit("ecall"); - self.emit("# a0 = CKB syscall return code"); - } - - fn emit_load_witness_syscall_to_offsets( - &mut self, - reason: &str, - source: u64, - index: usize, - size_offset: usize, - buffer_offset: usize, - max_bytes: usize, - ) { - self.emit(format!("# cellscript abi: LOAD_WITNESS reason={} source={} index={}", reason, ckb_source_name(source), index)); - self.emit_store_data_args_at(max_bytes, size_offset, buffer_offset); - self.emit(format!("li a3, {}", index)); - self.emit(format!("li a4, {}", source)); - self.emit(format!("li a7, {}", self.runtime_abi().load_witness)); - self.emit("ecall"); - self.emit("# a0 = CKB syscall return code"); - } - - fn emit_load_script_syscall_to_offsets(&mut self, reason: &str, size_offset: usize, buffer_offset: usize, max_bytes: usize) { - self.emit(format!("# cellscript abi: LOAD_SCRIPT reason={}", reason)); - self.emit_store_data_args_at(max_bytes, size_offset, buffer_offset); - self.emit(format!("li a7, {}", self.runtime_abi().load_script)); - self.emit("ecall"); - self.emit("# a0 = CKB syscall return code"); - } - - fn emit_load_cell_by_field_syscall_to_offsets( - &mut self, - reason: &str, - source: u64, - index: usize, - field: u64, - size_offset: usize, - buffer_offset: usize, - max_bytes: usize, - ) { - self.emit(format!( - "# cellscript abi: LOAD_CELL_BY_FIELD reason={} source={} index={} field={}", - reason, - ckb_source_name(source), - index, - field - )); - self.emit_store_data_args_at(max_bytes, size_offset, buffer_offset); - self.emit(format!("li a3, {}", index)); - self.emit(format!("li a4, {}", source)); - self.emit(format!("li a5, {}", field)); - self.emit(format!("li a7, {}", self.runtime_abi().load_cell_by_field)); - self.emit("ecall"); - self.emit("# a0 = CKB syscall return code"); - } - - fn emit_load_cell_by_field_syscall_to_offsets_dynamic_index( - &mut self, - reason: &str, - source: u64, - index_reg: &str, - field: u64, - size_offset: usize, - buffer_offset: usize, - max_bytes: usize, - ) { - self.emit(format!( - "# cellscript abi: LOAD_CELL_BY_FIELD reason={} source={} index={} field={}", - reason, - ckb_source_name(source), - index_reg, - field - )); - self.emit_store_data_args_at(max_bytes, size_offset, buffer_offset); - self.emit(format!("addi a3, {}, 0", index_reg)); - self.emit(format!("li a4, {}", source)); - self.emit(format!("li a5, {}", field)); - self.emit(format!("li a7, {}", self.runtime_abi().load_cell_by_field)); - self.emit("ecall"); - self.emit("# a0 = CKB syscall return code"); - } - - fn emit_return_on_syscall_error(&mut self, error: CellScriptRuntimeError) { - let ok_label = self.fresh_label("ckb_syscall_ok"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_fail(error); - self.emit_label(&ok_label); - } - - fn emit_loaded_schema_bounds_check(&mut self, size_offset: usize, required_size: usize, context: &str) { - self.emit(format!("# cellscript abi: bounds check {} required={}", context, required_size)); - let ok_label = self.fresh_label("schema_bounds_ok"); - self.emit_stack_load("a0", size_offset); - self.emit(format!("li a1, {}", required_size)); - self.emit("call __cellscript_require_min_size"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); - self.emit_label(&ok_label); - } - - fn emit_loaded_schema_exact_size_check(&mut self, size_offset: usize, expected_size: usize, context: &str) { - self.emit(format!("# cellscript abi: exact size check {} expected={}", context, expected_size)); - let ok_label = self.fresh_label("schema_size_ok"); - self.emit_stack_load("a0", size_offset); - self.emit(format!("li a1, {}", expected_size)); - self.emit("call __cellscript_require_exact_size"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_fail(CellScriptRuntimeError::ExactSizeMismatch); - self.emit_label(&ok_label); + let Some(dest_offset) = self.fixed_byte_local_offsets.get(&dest.id).copied() else { + self.emit("# cellscript abi: payload enum projection destination has no fixed-byte storage; fail closed"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return Ok(()); + }; + self.emit_large_addi("a0", "t4", field.offset as i64); + self.emit_sp_addi("a1", dest_offset); + self.emit(format!("li a2, {}", field.width)); + self.emit("call __cellscript_memcpy_fixed"); + self.emit_sp_addi("t0", dest_offset); + self.emit_stack_store("t0", dest.id * 8); + Ok(()) } - fn emit_molecule_table_field_bounds_to_t5( - &mut self, - base_reg: &str, - size_offset: usize, - field_index: usize, - field_width: usize, - context: &str, - ) { - self.emit(format!("# cellscript abi: molecule table field {} index={} min_width={}", context, field_index, field_width)); - let field_count = field_index + 1; - let header_size = 4 + 4 * field_count; - self.emit_loaded_schema_bounds_check(size_offset, header_size, context); - - self.emit_stack_load("a0", size_offset); - let total_ok = self.fresh_label("molecule_table_total_ok"); - self.emit_unaligned_scalar_load(base_reg, "t0", "t2", 0, 4); - self.emit("sub t2, t0, a0"); - self.emit(format!("beqz t2, {}", total_ok)); - self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); - self.emit_label(&total_ok); - - self.emit_unaligned_scalar_load(base_reg, "t5", "t2", 4 + 4 * field_index, 4); - self.emit(format!("li t1, {}", header_size)); - self.emit("sltu t2, t5, t1"); - let start_ok = self.fresh_label("molecule_table_start_ok"); - self.emit(format!("beqz t2, {}", start_ok)); - self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); - self.emit_label(&start_ok); + fn emit_fixed_named_tuple(&mut self, dest: &IrVar, fields: &[IrOperand]) -> bool { + let IrType::Named(type_name) = &dest.ty else { + return false; + }; + let Some(width) = self.type_fixed_sizes.get(type_name).copied() else { + return false; + }; + let Some(dest_offset) = self.fixed_byte_local_offsets.get(&dest.id).copied() else { + return false; + }; + let Some(layouts) = self.type_layouts.get(type_name) else { + return false; + }; + let mut ordered = layouts.values().cloned().collect::>(); + ordered.sort_by_key(|layout| layout.offset); + if ordered.len() != fields.len() { + return false; + } - if field_width > 0 { - self.emit(format!("li t1, {}", field_width)); - self.emit("add t3, t5, t1"); - self.emit("sltu t2, t3, t5"); - let overflow_ok = self.fresh_label("molecule_table_field_overflow_ok"); - self.emit(format!("beqz t2, {}", overflow_ok)); - self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); - self.emit_label(&overflow_ok); - self.emit("sltu t2, a0, t3"); - let end_ok = self.fresh_label("molecule_table_end_ok"); - self.emit(format!("beqz t2, {}", end_ok)); - self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); - self.emit_label(&end_ok); + self.emit(format!("# cellscript abi: materialize fixed aggregate {} var{} size={}", type_name, dest.id, width)); + for (field, layout) in fields.iter().zip(ordered.iter()) { + let Some(field_width) = layout_fixed_byte_width(layout).or_else(|| self.fixed_named_type_width(&layout.ty)) else { + return false; + }; + let Some(source) = self.expected_fixed_byte_source(field, field_width) else { + self.emit("# cellscript abi: fail closed because fixed aggregate field source is unavailable"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return true; + }; + self.emit_prepare_fixed_byte_source(&source, field_width, &format!("{} aggregate field", type_name)); + if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &source) { + self.emit("# cellscript abi: fail closed because fixed aggregate field pointer is unavailable"); + self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); + return true; + } + self.emit_sp_addi("a1", dest_offset + layout.offset); + self.emit(format!("li a2, {}", field_width)); + self.emit("call __cellscript_memcpy_fixed"); } + self.emit_sp_addi("t0", dest_offset); + self.emit_stack_store("t0", dest.id * 8); + true } - fn emit_molecule_table_field_span_to_t5_t6( - &mut self, - base_reg: &str, - size_offset: usize, - field_index: usize, - field_count: usize, - context: &str, - ) { - self.emit(format!( - "# cellscript abi: molecule table dynamic field {} index={} field_count={}", - context, field_index, field_count - )); - let header_size = 4 + 4 * field_count; - self.emit_loaded_schema_bounds_check(size_offset, header_size, context); - - self.emit_stack_load("a0", size_offset); - let total_ok = self.fresh_label("molecule_table_total_ok"); - self.emit_unaligned_scalar_load(base_reg, "t0", "t2", 0, 4); - self.emit("sub t2, t0, a0"); - self.emit(format!("beqz t2, {}", total_ok)); - self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); - self.emit_label(&total_ok); - - self.emit_unaligned_scalar_load(base_reg, "t5", "t2", 4 + 4 * field_index, 4); - if field_index + 1 < field_count { - self.emit_unaligned_scalar_load(base_reg, "t6", "t2", 4 + 4 * (field_index + 1), 4); - } else { - self.emit("add t6, a0, zero"); + fn emit_operand_to_register(&mut self, register: &str, operand: &IrOperand) { + match operand { + IrOperand::Const(IrConst::U8(n)) => self.emit(format!("li {}, {}", register, n)), + IrOperand::Const(IrConst::U16(n)) => self.emit(format!("li {}, {}", register, n)), + IrOperand::Const(IrConst::U32(n)) => self.emit(format!("li {}, {}", register, n)), + IrOperand::Const(IrConst::U64(n)) => self.emit(format!("li {}, {}", register, n)), + IrOperand::Const(IrConst::Bool(b)) => self.emit(format!("li {}, {}", register, if *b { 1 } else { 0 })), + IrOperand::Const(value) => { + if let Some(bytes) = fixed_byte_const_bytes(value) { + let label = self.const_data_label_for_bytes(bytes); + self.emit(format!("la {}, {}", register, label)); + } else { + self.emit(format!("li {}, 0", register)); + } + } + IrOperand::Var(v) => self.emit_stack_load(register, v.id * 8), } - - self.emit(format!("li t1, {}", header_size)); - self.emit("sltu t2, t5, t1"); - let start_ok = self.fresh_label("molecule_table_start_ok"); - self.emit(format!("beqz t2, {}", start_ok)); - self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); - self.emit_label(&start_ok); - - self.emit("sltu t2, t6, t5"); - let order_ok = self.fresh_label("molecule_table_order_ok"); - self.emit(format!("beqz t2, {}", order_ok)); - self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); - self.emit_label(&order_ok); - - self.emit("sltu t2, a0, t6"); - let end_ok = self.fresh_label("molecule_table_end_ok"); - self.emit(format!("beqz t2, {}", end_ok)); - self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); - self.emit_label(&end_ok); } - fn emit_mutate_replacement_field_hash_check( - &mut self, - pattern: &MutatePattern, - cell_field: u64, - field_name: &str, - error: CellScriptRuntimeError, - ) { - let input_size_offset = self.runtime_scratch_size_offset(); - let input_buffer_offset = self.runtime_scratch_buffer_offset(); - let output_size_offset = self.runtime_scratch2_size_offset(); - let output_buffer_offset = self.runtime_scratch2_buffer_offset(); - - self.emit_load_cell_by_field_syscall_to_offsets( - &format!("mutate_input_{}", field_name), - CKB_SOURCE_INPUT, - pattern.input_index, - cell_field, - input_size_offset, - input_buffer_offset, - 32, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_load_cell_by_field_syscall_to_offsets( - &format!("mutate_output_{}", field_name), - CKB_SOURCE_OUTPUT, - pattern.output_index, - cell_field, - output_size_offset, - output_buffer_offset, - 32, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_loaded_schema_exact_size_check(input_size_offset, 32, &format!("mutate input {}", field_name)); - self.emit_loaded_schema_exact_size_check(output_size_offset, 32, &format!("mutate output {}", field_name)); - self.emit(format!( - "# cellscript abi: verify mutate output {} {} Input#{} == Output#{} size=32", - pattern.ty, field_name, pattern.input_index, pattern.output_index - )); - self.emit_sp_addi("t4", input_buffer_offset); - self.emit_sp_addi("t5", output_buffer_offset); - for byte_index in 0..32 { - self.emit(format!("lbu t0, {}(t4)", byte_index)); - self.emit(format!("lbu t1, {}(t5)", byte_index)); - self.emit("sub t2, t0, t1"); - let ok_label = self.fresh_label("mutate_identity_byte_ok"); - self.emit(format!("beqz t2, {}", ok_label)); - self.emit_runtime_error_comment(error); - self.emit(format!("li a0, {}", error.code())); - self.emit_epilogue(); - self.emit_label(&ok_label); + /// consume + fn emit_consume(&mut self, operand: &IrOperand) -> Result<()> { + self.emit("# consume"); + if let IrOperand::Var(var) = operand { + if self.consume_indices.contains_key(&var.id) { + self.emit("# cellscript abi: consumed input pointer retained for verifier field checks"); + return Ok(()); + } + // Consume a local variable: the actual LOAD_CELL input data loading + // already happened in the action prelude (generate_consume). + // Here we only zero out the local binding to enforce linear ownership. + self.emit_stack_store("zero", var.id * 8); + return Ok(()); } + // Non-Var consume: this should not happen in valid IR, but fail with + // a specific error code instead of blocking ELF emission. + self.emit("# cellscript abi: fail closed because consume operand is not a variable"); + self.emit_fail(CellScriptRuntimeError::ConsumeInvalidOperand); + Ok(()) } - fn emit_cell_metadata_equality(&mut self, left: &IrOperand, right: &IrOperand, field: CellMetadataField) -> Result<()> { - let Some((left_source, left_index)) = self.operand_cell_location(left) else { - self.emit("# cellscript abi: fail closed because left cell metadata source cannot be determined"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(()); - }; - let Some((right_source, right_index)) = self.operand_cell_location(right) else { - self.emit("# cellscript abi: fail closed because right cell metadata source cannot be determined"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(()); - }; - let (cell_field, field_name, width, mismatch_error) = match field { - CellMetadataField::LockHash => { - (CKB_CELL_FIELD_LOCK_HASH, "lock_hash", 32usize, CellScriptRuntimeError::LockHashPreservationMismatch) - } - CellMetadataField::Capacity => { - (CKB_CELL_FIELD_CAPACITY, "capacity", 8usize, CellScriptRuntimeError::CapacityPreservationMismatch) - } - }; - - let left_size_offset = self.runtime_scratch_size_offset(); - let left_buffer_offset = self.runtime_scratch_buffer_offset(); - let right_size_offset = self.runtime_scratch2_size_offset(); - let right_buffer_offset = self.runtime_scratch2_buffer_offset(); - - self.emit_load_cell_by_field_syscall_to_offsets( - &format!("cell_metadata_left_{}", field_name), - left_source, - left_index, - cell_field, - left_size_offset, - left_buffer_offset, - RUNTIME_SCRATCH_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_load_cell_by_field_syscall_to_offsets( - &format!("cell_metadata_right_{}", field_name), - right_source, - right_index, - cell_field, - right_size_offset, - right_buffer_offset, - RUNTIME_SCRATCH_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_loaded_schema_exact_size_check(left_size_offset, width, &format!("cell metadata left {}", field_name)); - self.emit_loaded_schema_exact_size_check(right_size_offset, width, &format!("cell metadata right {}", field_name)); - self.emit(format!( - "# cellscript abi: verify cell metadata {} equality {}#{} == {}#{} size={}", - field_name, - ckb_source_name(left_source), - left_index, - ckb_source_name(right_source), - right_index, - width - )); - self.emit_sp_addi("t4", left_buffer_offset); - self.emit_sp_addi("t5", right_buffer_offset); - for byte_index in 0..width { - self.emit(format!("lbu t0, {}(t4)", byte_index)); - self.emit(format!("lbu t1, {}(t5)", byte_index)); - self.emit("sub t2, t0, t1"); - let ok_label = self.fresh_label("cell_metadata_byte_ok"); - self.emit(format!("beqz t2, {}", ok_label)); - self.emit_runtime_error_comment(mismatch_error); - self.emit(format!("li a0, {}", mismatch_error.code())); - self.emit_epilogue(); - self.emit_label(&ok_label); - } - Ok(()) - } - - fn emit_cell_field_hash_equality( - &mut self, - left_reason: &str, - left_source: u64, - left_index: usize, - right_reason: &str, - right_source: u64, - right_index: usize, - cell_field: u64, - field_name: &str, - detail: &str, - error: CellScriptRuntimeError, - ) { - let left_size_offset = self.runtime_scratch_size_offset(); - let left_buffer_offset = self.runtime_scratch_buffer_offset(); - let right_size_offset = self.runtime_scratch2_size_offset(); - let right_buffer_offset = self.runtime_scratch2_buffer_offset(); - - self.emit_load_cell_by_field_syscall_to_offsets( - left_reason, - left_source, - left_index, - cell_field, - left_size_offset, - left_buffer_offset, - 32, - ); - self.emit_return_on_syscall_error(error); - self.emit_load_cell_by_field_syscall_to_offsets( - right_reason, - right_source, - right_index, - cell_field, - right_size_offset, - right_buffer_offset, - 32, - ); - self.emit_return_on_syscall_error(error); - self.emit_loaded_schema_exact_size_check(left_size_offset, 32, &format!("{} {}", left_reason, field_name)); - self.emit_loaded_schema_exact_size_check(right_size_offset, 32, &format!("{} {}", right_reason, field_name)); - self.emit(format!( - "# cellscript abi: verify {} {} {}#{} == {}#{} size=32", - detail, - field_name, - ckb_source_name(left_source), - left_index, - ckb_source_name(right_source), - right_index - )); - self.emit_sp_addi("a0", left_buffer_offset); - self.emit_sp_addi("a1", right_buffer_offset); - self.emit("li a2, 32"); - self.emit("call __cellscript_memcmp_fixed"); - let ok_label = self.fresh_label("identity_hash_ok"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_runtime_error_comment(error); - self.emit(format!("li a0, {}", error.code())); - self.emit_epilogue(); - self.emit_label(&ok_label); - } - - fn emit_output_type_hash_present_check(&mut self, output_index: usize, context: &str) { - let size_offset = self.runtime_scratch2_size_offset(); - let buffer_offset = self.runtime_scratch2_buffer_offset(); - self.emit_load_cell_by_field_syscall_to_offsets( - context, - CKB_SOURCE_OUTPUT, - output_index, - CKB_CELL_FIELD_TYPE_HASH, - size_offset, - buffer_offset, - 32, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::TypeHashMismatch); - self.emit_loaded_schema_exact_size_check(size_offset, 32, context); - self.emit(format!("# cellscript abi: verify {} Output#{} TypeHash is present size=32", context, output_index)); - } - - fn emit_loaded_fixed_field_pointer_to_stack( - &mut self, - size_offset: usize, - buffer_offset: usize, - layout: &SchemaFieldLayout, - width: usize, - context: &str, - pointer_stack_offset: usize, - ) { - self.emit_loaded_schema_bounds_check(size_offset, layout.offset + width, context); - self.emit_sp_addi("t5", buffer_offset + layout.offset); - self.emit_stack_store("t5", pointer_stack_offset); - } - - fn emit_dynamic_fixed_field_pointer_to_stack( - &mut self, - size_offset: usize, - buffer_offset: usize, - layout: &SchemaFieldLayout, - field_count: usize, - width: usize, - context: &str, - pointer_stack_offset: usize, - len_stack_offset: usize, - ) { - self.emit_dynamic_table_field_span_to_stack( - size_offset, - buffer_offset, - layout.index, - field_count, - context, - pointer_stack_offset, - len_stack_offset, - ); - self.emit_stack_load("t0", len_stack_offset); - self.emit(format!("li t1, {}", width)); - self.emit("sub t2, t0, t1"); - let ok_label = self.fresh_label("identity_field_len_ok"); - self.emit(format!("beqz t2, {}", ok_label)); - self.emit_runtime_error_comment(CellScriptRuntimeError::DynamicFieldValueMismatch); - self.emit(format!("li a0, {}", CellScriptRuntimeError::DynamicFieldValueMismatch.code())); - self.emit_epilogue(); - self.emit_label(&ok_label); - } - - fn emit_fixed_pointer_equality( - &mut self, - left_pointer_stack_offset: usize, - right_pointer_stack_offset: usize, - width: usize, - context: &str, - error: CellScriptRuntimeError, - ) { - self.emit(format!("# cellscript abi: verify {} size={}", context, width)); - self.emit_stack_load("a0", left_pointer_stack_offset); - self.emit_stack_load("a1", right_pointer_stack_offset); - self.emit(format!("li a2, {}", width)); - self.emit("call __cellscript_memcmp_fixed"); - let ok_label = self.fresh_label("identity_field_ok"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_runtime_error_comment(error); - self.emit(format!("li a0, {}", error.code())); - self.emit_epilogue(); - self.emit_label(&ok_label); - } - - fn operand_cell_location(&self, operand: &IrOperand) -> Option<(u64, usize)> { - let IrOperand::Var(var) = operand else { - return None; - }; - if let Some(input_index) = self.consume_indices.get(&var.id).copied() { - Some((CKB_SOURCE_INPUT, input_index)) - } else if let Some(output_index) = self.operation_output_indices.get(&var.id).copied() { - Some((CKB_SOURCE_OUTPUT, output_index)) - } else if let Some(dep_index) = self.read_ref_indices.get(&var.id).copied() { - Some((CKB_SOURCE_CELL_DEP, dep_index)) - } else if let Some(input_index) = self.read_ref_param_input_indices.get(&var.id).copied() { - Some((CKB_SOURCE_INPUT, input_index)) - } else { - self.read_ref_param_dep_indices.get(&var.id).copied().map(|dep_index| (CKB_SOURCE_CELL_DEP, dep_index)) - } - } - - fn emit_destroy_group_output_absence_scan(&mut self, pattern: &CellPattern, input_index: usize) { - let input_size_offset = self.runtime_scratch_size_offset(); - let input_buffer_offset = self.runtime_scratch_buffer_offset(); - let output_size_offset = self.runtime_scratch2_size_offset(); - let output_buffer_offset = self.runtime_scratch2_buffer_offset(); - let loop_label = self.fresh_label("destroy_output_scan"); - let type_hash_label = self.fresh_label("destroy_output_type_hash"); - let next_label = self.fresh_label("destroy_output_next"); - let done_label = self.fresh_label("destroy_output_done"); - - self.emit(format!("# cellscript abi: destroy output type-hash absence scan binding={} size=32", pattern.binding)); - self.emit_load_cell_by_field_syscall_to_offsets( - "destroy_input_type_hash", - CKB_SOURCE_INPUT, - input_index, - CKB_CELL_FIELD_TYPE_HASH, - input_size_offset, - input_buffer_offset, - 32, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_loaded_schema_exact_size_check(input_size_offset, 32, "destroy input type hash"); - self.emit("li t6, 0"); - self.emit_label(&loop_label); - self.emit_load_cell_by_field_syscall_to_offsets_dynamic_index( - "destroy_output_type_hash", - CKB_SOURCE_OUTPUT, - "t6", - CKB_CELL_FIELD_TYPE_HASH, - output_size_offset, - output_buffer_offset, - 32, - ); - self.emit(format!("beqz a0, {}", type_hash_label)); - self.emit(format!("li t0, {}", CKB_INDEX_OUT_OF_BOUND)); - self.emit("sub t1, a0, t0"); - self.emit(format!("beqz t1, {}", done_label)); - self.emit(format!("li t0, {}", CKB_ITEM_MISSING)); - self.emit("sub t1, a0, t0"); - self.emit(format!("beqz t1, {}", next_label)); - self.emit_fail(CellScriptRuntimeError::DynamicFieldBoundsInvalid); - - self.emit_label(&type_hash_label); - self.emit_loaded_schema_exact_size_check(output_size_offset, 32, "destroy output type hash"); - self.emit(format!("# cellscript abi: reject destroy successor when Output#t6 TypeHash matches consumed {}", pattern.binding)); - self.emit_sp_addi("t4", output_buffer_offset); - self.emit_sp_addi("t5", input_buffer_offset); - for byte_index in 0..32 { - self.emit(format!("lbu t0, {}(t4)", byte_index)); - self.emit(format!("lbu t1, {}(t5)", byte_index)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", next_label)); - } - self.emit_fail(CellScriptRuntimeError::DynamicFieldBoundsInvalid); - - self.emit_label(&next_label); - self.emit("addi t6, t6, 1"); - self.emit(format!("j {}", loop_label)); - self.emit_label(&done_label); - self.emit("li a0, 0"); - } - - fn mutate_preserved_field_layouts(&self, pattern: &MutatePattern) -> Vec<(String, SchemaFieldLayout, usize)> { - let Some(type_size) = self.type_fixed_sizes.get(&pattern.ty).copied() else { - return Vec::new(); - }; - if type_size > RUNTIME_SCRATCH_BUFFER_SIZE { - return Vec::new(); - } - pattern - .preserved_fields - .iter() - .filter_map(|field| { - let layout = self.type_layouts.get(&pattern.ty).and_then(|fields| fields.get(field)).cloned()?; - let width = layout_fixed_byte_width(&layout)?; - (layout.offset + width <= RUNTIME_SCRATCH_BUFFER_SIZE).then(|| (field.clone(), layout, width)) - }) - .collect() - } - - fn mutate_transition_exclusion_ranges(&self, pattern: &MutatePattern) -> Option> { - if pattern.transitions.len() != pattern.fields.len() { - return None; - } - let type_size = self.type_fixed_sizes.get(&pattern.ty).copied()?; - if type_size > RUNTIME_SCRATCH_BUFFER_SIZE { - return None; - } - let mut ranges = Vec::new(); - for transition in &pattern.transitions { - let layout = self.type_layouts.get(&pattern.ty).and_then(|fields| fields.get(&transition.field))?; - let width = layout_fixed_byte_width(layout)?; - if layout.offset + width > RUNTIME_SCRATCH_BUFFER_SIZE { - return None; + /// create + fn emit_create(&mut self, dest: &IrVar, pattern: &CreatePattern) -> Result<()> { + let output_index = self.operation_output_indices.get(&dest.id).copied().unwrap_or(self.next_virtual_output); + if pattern.operation == "output" { + self.emit(format!("# constrain named output {}", pattern.ty)); + for (field, value) in &pattern.fields { + match value { + IrOperand::Const(IrConst::U64(n)) => self.emit(format!("# field {} = {}", field, n)), + IrOperand::Const(IrConst::Bool(b)) => self.emit(format!("# field {} = {}", field, b)), + IrOperand::Var(var) => self.emit(format!("# field {} <- {}", field, var.name)), + _ => self.emit(format!("# field {} <- ", field)), + } } - ranges.push((layout.offset, layout.offset + width)); - } - ranges.sort_unstable(); - let mut merged: Vec<(usize, usize)> = Vec::new(); - for (start, end) in ranges { - if start >= end { - continue; + if pattern.lock.is_some() { + self.emit("# with_lock "); } - if let Some(last) = merged.last_mut() { - if start <= last.1 { - last.1 = last.1.max(end); - continue; + if let Some(var_id) = self.output_param_ids.get(&pattern.binding).copied() { + let Some(size_offset) = self.cell_buffer_size_offsets.get(&var_id).copied() else { + self.emit_fail(CellScriptRuntimeError::AssertionFailed); + return Ok(()); + }; + let Some(buffer_offset) = self.cell_buffer_offsets.get(&var_id).copied() else { + self.emit_fail(CellScriptRuntimeError::AssertionFailed); + return Ok(()); + }; + if pattern.fields.is_empty() { + self.emit_state_transition_check(pattern, size_offset, buffer_offset); + } else if self.can_verify_create_output_fields(pattern) { + self.emit_create_output_checks_at(pattern, size_offset, buffer_offset); + } else { + self.emit("# cellscript abi: ordered named output field verification incomplete"); + self.emit("# cellscript abi: fail closed because the output state is not fully verified"); + self.emit_fail(CellScriptRuntimeError::AssertionFailed); + return Ok(()); + } + if let Some(lock) = &pattern.lock + && !(self.can_verify_output_lock(pattern) && self.emit_output_lock_hash_check(output_index, lock)) + { + self.emit("# cellscript abi: output lock verification incomplete for this named output"); + self.emit("# cellscript abi: fail closed because the output lock is not fully verified"); + self.emit_fail(CellScriptRuntimeError::EntryWitnessMagicMismatch); + return Ok(()); } + } else { + self.emit_fail(CellScriptRuntimeError::AssertionFailed); + return Ok(()); } - merged.push((start, end)); + self.emit(format!("li t0, {}", output_index)); + self.emit_stack_store("t0", dest.id * 8); + self.next_virtual_output = self.next_virtual_output.max(output_index + 1); + return Ok(()); } - Some(merged) - } - fn emit_mutate_replacement_preserved_field_checks(&mut self, pattern: &MutatePattern) { - let preserved_fields = self.mutate_preserved_field_layouts(pattern); - if !pattern.preserved_fields.is_empty() && preserved_fields.len() != pattern.preserved_fields.len() { - if self.emit_mutate_replacement_dynamic_table_preserved_field_checks(pattern) { - return; - } - if self.emit_mutate_replacement_data_except_transition_checks(pattern) { - return; - } - self.emit("# cellscript abi: fail closed because not all preserved fields are verifier-addressable"); - self.emit_fail(CellScriptRuntimeError::FieldPreservationMismatch); - return; - } - if preserved_fields.is_empty() { - return; - } - let input_size_offset = self.runtime_scratch_size_offset(); - let input_buffer_offset = self.runtime_scratch_buffer_offset(); - let output_size_offset = self.runtime_scratch2_size_offset(); - let output_buffer_offset = self.runtime_scratch2_buffer_offset(); - self.emit_load_cell_data_syscall_to_offsets( - "mutate_input_data", - CKB_SOURCE_INPUT, - pattern.input_index, - input_size_offset, - input_buffer_offset, - RUNTIME_SCRATCH_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_load_cell_data_syscall_to_offsets( - "mutate_output_data", - CKB_SOURCE_OUTPUT, - pattern.output_index, - output_size_offset, - output_buffer_offset, - RUNTIME_SCRATCH_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - if let Some(expected_size) = self.type_fixed_sizes.get(&pattern.ty).copied() { - self.emit_loaded_schema_exact_size_check(input_size_offset, expected_size, &format!("{} mutate input", pattern.ty)); - self.emit_loaded_schema_exact_size_check(output_size_offset, expected_size, &format!("{} mutate output", pattern.ty)); - } - self.emit(format!( - "# cellscript abi: verify mutate preserved fields {} Input#{} == Output#{}", - pattern.ty, pattern.input_index, pattern.output_index - )); - self.emit_sp_addi("t4", input_buffer_offset); - self.emit_sp_addi("t5", output_buffer_offset); - for (field, layout, width) in preserved_fields { - self.emit_loaded_schema_bounds_check(input_size_offset, layout.offset + width, &format!("{} input.{}", pattern.ty, field)); - self.emit_loaded_schema_bounds_check( - output_size_offset, - layout.offset + width, - &format!("{} output.{}", pattern.ty, field), - ); - self.emit(format!( - "# cellscript abi: verify mutate preserved field {}.{} Input#{} == Output#{} offset={} size={}", - pattern.ty, field, pattern.input_index, pattern.output_index, layout.offset, width - )); - let mismatch_label = self.fresh_label("mutate_preserved_byte_mismatch"); - for byte_index in 0..width { - self.emit(format!("lbu t0, {}(t4)", layout.offset + byte_index)); - self.emit(format!("lbu t1, {}(t5)", layout.offset + byte_index)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", mismatch_label)); + self.generate_create(pattern, output_index, false, false)?; + self.emit(format!("# create {}", pattern.ty)); + for (field, value) in &pattern.fields { + match value { + IrOperand::Const(IrConst::U64(n)) => self.emit(format!("# field {} = {}", field, n)), + IrOperand::Const(IrConst::Bool(b)) => self.emit(format!("# field {} = {}", field, b)), + IrOperand::Var(var) => self.emit(format!("# field {} <- {}", field, var.name)), + _ => self.emit(format!("# field {} <- ", field)), } - self.emit_fixed_byte_mismatch_fail(&mismatch_label, CellScriptRuntimeError::FieldPreservationMismatch); - } - } - - fn emit_mutate_replacement_dynamic_table_preserved_field_checks(&mut self, pattern: &MutatePattern) -> bool { - if self.type_fixed_sizes.contains_key(&pattern.ty) || pattern.preserved_fields.is_empty() { - return false; - } - let Some(layouts) = self.type_layouts.get(&pattern.ty).cloned() else { - return false; - }; - let field_count = layouts.len(); - if field_count == 0 || !pattern.preserved_fields.iter().all(|field| layouts.contains_key(field)) { - return false; - } - - let input_size_offset = self.runtime_scratch_size_offset(); - let input_buffer_offset = self.runtime_scratch_buffer_offset(); - let output_size_offset = self.runtime_scratch2_size_offset(); - let output_buffer_offset = self.runtime_scratch2_buffer_offset(); - self.emit_load_cell_data_syscall_to_offsets( - "mutate_input_table_preserved", - CKB_SOURCE_INPUT, - pattern.input_index, - input_size_offset, - input_buffer_offset, - RUNTIME_SCRATCH_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_load_cell_data_syscall_to_offsets( - "mutate_output_table_preserved", - CKB_SOURCE_OUTPUT, - pattern.output_index, - output_size_offset, - output_buffer_offset, - RUNTIME_SCRATCH_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit(format!( - "# cellscript abi: verify mutate preserved Molecule table fields {} Input#{} == Output#{}", - pattern.ty, pattern.input_index, pattern.output_index - )); - for field in &pattern.preserved_fields { - let Some(layout) = layouts.get(field).cloned() else { - return false; - }; - self.emit_dynamic_table_field_equality_check( - &pattern.ty, - field, - &layout, - field_count, - input_size_offset, - input_buffer_offset, - output_size_offset, - output_buffer_offset, - CellScriptRuntimeError::FieldPreservationMismatch, - ); } - true - } - - #[allow(clippy::too_many_arguments)] - fn emit_dynamic_table_field_equality_check( - &mut self, - type_name: &str, - field: &str, - layout: &SchemaFieldLayout, - field_count: usize, - input_size_offset: usize, - input_buffer_offset: usize, - output_size_offset: usize, - output_buffer_offset: usize, - fail_code: CellScriptRuntimeError, - ) { - let start_offset = self.runtime_expr_temp_offset(0); - let len_offset = self.runtime_expr_temp_offset(1); - let output_start_offset = self.runtime_expr_temp_offset(2); - if let Some(width) = layout_fixed_byte_width(layout) { - self.emit_dynamic_table_fixed_field_pointer_to_stack( - input_size_offset, - input_buffer_offset, - layout, - width, - &format!("{} input.{}", type_name, field), - start_offset, - ); - self.emit_dynamic_table_fixed_field_pointer_to_stack( - output_size_offset, - output_buffer_offset, - layout, - width, - &format!("{} output.{}", type_name, field), - output_start_offset, - ); - self.emit(format!("li t0, {}", width)); - self.emit_stack_store("t0", len_offset); - } else { - self.emit_dynamic_table_field_span_to_stack( - input_size_offset, - input_buffer_offset, - layout.index, - field_count, - &format!("{} input.{}", type_name, field), - start_offset, - len_offset, - ); - self.emit_dynamic_table_field_span_to_stack( - output_size_offset, - output_buffer_offset, - layout.index, - field_count, - &format!("{} output.{}", type_name, field), - output_start_offset, - self.runtime_expr_temp_offset(3), - ); - self.emit_stack_load("t0", len_offset); - self.emit_stack_load("t1", self.runtime_expr_temp_offset(3)); - self.emit("sub t2, t0, t1"); - let len_ok = self.fresh_label("mutate_table_field_len_ok"); - self.emit(format!("beqz t2, {}", len_ok)); - self.emit_fail(fail_code); - self.emit_label(&len_ok); + if pattern.lock.is_some() { + self.emit("# with_lock "); } - - self.emit(format!( - "# cellscript abi: verify mutate preserved Molecule table field {}.{} Input#{} == Output#{}", - type_name, field, 0, 1 - )); - let mismatch_label = self.fresh_label("mutate_table_field_mismatch"); - self.emit_stack_load("a0", start_offset); - self.emit_stack_load("a1", output_start_offset); - self.emit_stack_load("a2", len_offset); - self.emit("call __cellscript_memcmp_fixed"); - self.emit(format!("bnez a0, {}", mismatch_label)); - self.emit_fixed_byte_mismatch_fail(&mismatch_label, fail_code); - } - - fn emit_dynamic_table_field_span_to_stack( - &mut self, - size_offset: usize, - buffer_offset: usize, - field_index: usize, - field_count: usize, - context: &str, - start_stack_offset: usize, - len_stack_offset: usize, - ) { - self.emit_sp_addi("t4", buffer_offset); - self.emit_molecule_table_field_span_to_t5_t6("t4", size_offset, field_index, field_count, context); - self.emit_sp_addi("t4", buffer_offset); - self.emit("add t5, t4, t5"); - self.emit("add t6, t4, t6"); - self.emit("sub t0, t6, t5"); - self.emit_stack_store("t5", start_stack_offset); - self.emit_stack_store("t0", len_stack_offset); - } - - fn emit_dynamic_table_fixed_field_pointer_to_stack( - &mut self, - size_offset: usize, - buffer_offset: usize, - layout: &SchemaFieldLayout, - width: usize, - context: &str, - start_stack_offset: usize, - ) { - self.emit_sp_addi("t4", buffer_offset); - self.emit_molecule_table_field_bounds_to_t5("t4", size_offset, layout.index, width, context); - self.emit_sp_addi("t4", buffer_offset); - self.emit("add t5, t4, t5"); - self.emit_stack_store("t5", start_stack_offset); + self.emit(format!("li t0, {}", output_index)); + self.emit_stack_store("t0", dest.id * 8); + self.next_virtual_output = self.next_virtual_output.max(output_index + 1); + Ok(()) } - fn emit_mutate_replacement_dynamic_table_append_checks(&mut self, pattern: &MutatePattern) -> bool { - if self.type_fixed_sizes.contains_key(&pattern.ty) || pattern.transitions.is_empty() { - return false; - } - let Some(layouts) = self.type_layouts.get(&pattern.ty).cloned() else { - return false; - }; - let field_count = layouts.len(); - let appends = pattern - .transitions - .iter() - .filter_map(|transition| { - if transition.op != MutateTransitionOp::Append { - return None; - } - let layout = layouts.get(&transition.field).cloned()?; - let element_width = molecule_vector_element_fixed_width(&layout.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes)?; - self.fixed_append_fields(&transition.operand, element_width) - .map(|fields| (transition.clone(), layout, element_width, fields)) - }) - .collect::>(); - if appends.len() != pattern.transitions.len() { - return false; - } - - let input_size_offset = self.runtime_scratch_size_offset(); - let input_buffer_offset = self.runtime_scratch_buffer_offset(); - let output_size_offset = self.runtime_scratch2_size_offset(); - let output_buffer_offset = self.runtime_scratch2_buffer_offset(); - self.emit_load_cell_data_syscall_to_offsets( - "mutate_input_table_append", - CKB_SOURCE_INPUT, - pattern.input_index, - input_size_offset, - input_buffer_offset, - RUNTIME_SCRATCH_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_load_cell_data_syscall_to_offsets( - "mutate_output_table_append", - CKB_SOURCE_OUTPUT, - pattern.output_index, - output_size_offset, - output_buffer_offset, - RUNTIME_SCRATCH_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); + fn emit_create_unique_identity_check(&mut self, output_index: usize, pattern: &CreatePattern, identity: &IrIdentityPolicy) { self.emit(format!( - "# cellscript abi: verify mutate Molecule table append fields {} Input#{} -> Output#{}", - pattern.ty, pattern.input_index, pattern.output_index + "# cellscript abi: create_unique identity policy {} for Output#{}", + identity_policy_label(identity), + output_index )); - for (transition, layout, element_width, fields) in appends { - self.emit_dynamic_table_vector_append_check( - &pattern.ty, - &transition.field, - &layout, - field_count, - element_width, - &fields, - input_size_offset, - input_buffer_offset, - output_size_offset, - output_buffer_offset, - ); - } - true - } - - fn fixed_append_fields(&self, operand: &IrOperand, expected_width: usize) -> Option> { - if self.expected_fixed_byte_source(operand, expected_width).is_some() { - let ty = match operand { - IrOperand::Var(var) => var.ty.clone(), - IrOperand::Const(IrConst::Address(_)) => IrType::Address, - IrOperand::Const(IrConst::Hash(_)) => IrType::Hash, - IrOperand::Const(IrConst::Array(items)) => IrType::Array(Box::new(IrType::U8), items.len()), - IrOperand::Const(_) => return None, - }; - return Some(vec![( - operand.clone(), - SchemaFieldLayout { index: 0, offset: 0, ty, fixed_size: Some(expected_width), fixed_enum_size: None }, - expected_width, - )]); - } - let IrOperand::Var(var) = operand else { - return None; - }; - let fields = self.tuple_aggregate_fields.get(&var.id)?; - let type_name = named_type_name(&var.ty)?; - let mut layouts = self.type_layouts.get(type_name)?.values().cloned().collect::>(); - layouts.sort_by_key(|layout| layout.offset); - if layouts.len() != fields.len() { - return None; - } - let total_width = self.type_fixed_sizes.get(type_name).copied()?; - if total_width != expected_width { - return None; - } - fields - .iter() - .cloned() - .zip(layouts) - .map(|(field_operand, layout)| { - let width = layout_fixed_byte_width(&layout)?; - self.expected_fixed_byte_source(&field_operand, width)?; - Some((field_operand, layout, width)) - }) - .collect() - } - - #[allow(clippy::too_many_arguments)] - fn emit_dynamic_table_vector_append_check( - &mut self, - type_name: &str, - field: &str, - layout: &SchemaFieldLayout, - field_count: usize, - element_width: usize, - fields: &[(IrOperand, SchemaFieldLayout, usize)], - input_size_offset: usize, - input_buffer_offset: usize, - output_size_offset: usize, - output_buffer_offset: usize, - ) { - let input_start_offset = self.runtime_expr_temp_offset(0); - let input_len_offset = self.runtime_expr_temp_offset(1); - let output_start_offset = self.runtime_expr_temp_offset(2); - let output_len_offset = self.runtime_expr_temp_offset(3); - self.emit_dynamic_table_field_span_to_stack( - input_size_offset, - input_buffer_offset, - layout.index, - field_count, - &format!("{} input.{}", type_name, field), - input_start_offset, - input_len_offset, - ); - self.emit_dynamic_table_field_span_to_stack( - output_size_offset, - output_buffer_offset, - layout.index, - field_count, - &format!("{} output.{}", type_name, field), - output_start_offset, - output_len_offset, - ); - self.emit(format!( - "# cellscript abi: verify mutate Molecule vector append {}.{} element_size={}", - type_name, field, element_width - )); - self.emit_loaded_schema_bounds_check(input_len_offset, 4, &format!("{} input.{} vector", type_name, field)); - self.emit_loaded_schema_bounds_check(output_len_offset, 4 + element_width, &format!("{} output.{} vector", type_name, field)); - - self.emit_stack_load("t4", input_start_offset); - self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, 4); - self.emit_stack_load("t1", input_len_offset); - self.emit(format!("li t2, {}", element_width)); - self.emit("mul t3, t0, t2"); - self.emit("addi t3, t3, 4"); - self.emit("sub t2, t1, t3"); - let input_size_ok = self.fresh_label("molecule_append_input_size_ok"); - self.emit(format!("beqz t2, {}", input_size_ok)); - self.emit_fail(CellScriptRuntimeError::MutateTransitionMismatch); - self.emit_label(&input_size_ok); - - self.emit_stack_load("t4", output_start_offset); - self.emit_unaligned_scalar_load("t4", "t1", "t2", 0, 4); - self.emit("addi t0, t0, 1"); - self.emit("sub t2, t1, t0"); - let count_ok = self.fresh_label("molecule_append_count_ok"); - self.emit(format!("beqz t2, {}", count_ok)); - self.emit_fail(CellScriptRuntimeError::MutateTransitionMismatch); - self.emit_label(&count_ok); - - self.emit_stack_load("t0", input_len_offset); - self.emit(format!("li t1, {}", element_width)); - self.emit("add t0, t0, t1"); - self.emit_stack_load("t1", output_len_offset); - self.emit("sub t2, t1, t0"); - let len_ok = self.fresh_label("molecule_append_len_ok"); - self.emit(format!("beqz t2, {}", len_ok)); - self.emit_fail(CellScriptRuntimeError::MutateTransitionMismatch); - self.emit_label(&len_ok); - - let prefix_ok = self.fresh_label("molecule_append_prefix_ok"); - self.emit_stack_load("a0", input_start_offset); - self.emit("addi a0, a0, 4"); - self.emit_stack_load("a1", output_start_offset); - self.emit("addi a1, a1, 4"); - self.emit_stack_load("a2", input_len_offset); - self.emit("addi a2, a2, -4"); - self.emit("call __cellscript_memcmp_fixed"); - self.emit(format!("beqz a0, {}", prefix_ok)); - self.emit_fail(CellScriptRuntimeError::MutateTransitionMismatch); - self.emit_label(&prefix_ok); - - self.emit_stack_load("t0", output_start_offset); - self.emit_stack_load("t1", input_len_offset); - self.emit("add t0, t0, t1"); - self.emit_stack_store("t0", output_start_offset); - for (operand, field_layout, width) in fields { - let Some(source) = self.expected_fixed_byte_source(operand, *width) else { - self.emit_fail(CellScriptRuntimeError::MutateTransitionMismatch); - continue; - }; - self.emit_prepare_fixed_byte_source(&source, *width, &format!("append {}.{}", type_name, field)); - self.emit_pointer_fixed_bytes_against_source( - output_start_offset, - field_layout.offset, - &source, - *width, - CellScriptRuntimeError::MutateTransitionMismatch, - ); - } - } - - fn emit_pointer_fixed_bytes_against_source( - &mut self, - output_pointer_stack_offset: usize, - output_field_offset: usize, - source: &ExpectedFixedByteSource, - width: usize, - fail_code: CellScriptRuntimeError, - ) { - let mismatch_label = self.fresh_label("fixed_byte_mismatch"); - match source { - ExpectedFixedByteSource::Const(bytes) => { - self.emit_stack_load("t4", output_pointer_stack_offset); - for (byte_index, byte) in bytes.iter().take(width).enumerate() { - self.emit(format!("lbu t0, {}(t4)", output_field_offset + byte_index)); - self.emit(format!("li t1, {}", byte)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", mismatch_label)); - } - } - ExpectedFixedByteSource::SchemaField(source) => { - if self.emit_schema_field_source_pointer_to("a1", source, width) { - self.emit_stack_load("a0", output_pointer_stack_offset); - if output_field_offset != 0 { - self.emit_large_addi("a0", "a0", output_field_offset as i64); - } - self.emit(format!("li a2, {}", width)); - self.emit("call __cellscript_memcmp_fixed"); - self.emit(format!("bnez a0, {}", mismatch_label)); - } else { - self.emit_fail(CellScriptRuntimeError::DynamicFieldBoundsInvalid); - } - } - ExpectedFixedByteSource::StackSlot { var_id, .. } => { - self.emit_stack_load("a0", output_pointer_stack_offset); - if output_field_offset != 0 { - self.emit_large_addi("a0", "a0", output_field_offset as i64); - } - self.emit_sp_addi("a1", var_id * 8); - self.emit(format!("li a2, {}", width)); - self.emit("call __cellscript_memcmp_fixed"); - self.emit(format!("bnez a0, {}", mismatch_label)); - } - ExpectedFixedByteSource::PointerBytes { var_id, .. } - | ExpectedFixedByteSource::ParamBytes { var_id, .. } - | ExpectedFixedByteSource::LoadedBytes { var_id, .. } => { - self.emit_stack_load("a0", output_pointer_stack_offset); - if output_field_offset != 0 { - self.emit_large_addi("a0", "a0", output_field_offset as i64); - } - self.emit_stack_load("a1", var_id * 8); - self.emit(format!("li a2, {}", width)); - self.emit("call __cellscript_memcmp_fixed"); - self.emit(format!("bnez a0, {}", mismatch_label)); - } - } - self.emit_fixed_byte_mismatch_fail(&mismatch_label, fail_code); - } - - fn emit_mutate_replacement_data_except_transition_checks(&mut self, pattern: &MutatePattern) -> bool { - let Some(exclusion_ranges) = self.mutate_transition_exclusion_ranges(pattern) else { - return false; - }; - if exclusion_ranges.is_empty() { - return false; - } - let input_size_offset = self.runtime_scratch_size_offset(); - let input_buffer_offset = self.runtime_scratch_buffer_offset(); - let output_size_offset = self.runtime_scratch2_size_offset(); - let output_buffer_offset = self.runtime_scratch2_buffer_offset(); - self.emit_load_cell_data_syscall_to_offsets( - "mutate_input_preserved_data", - CKB_SOURCE_INPUT, - pattern.input_index, - input_size_offset, - input_buffer_offset, - RUNTIME_SCRATCH_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_load_cell_data_syscall_to_offsets( - "mutate_output_preserved_data", - CKB_SOURCE_OUTPUT, - pattern.output_index, - output_size_offset, - output_buffer_offset, - RUNTIME_SCRATCH_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - if let Some(expected_size) = self.type_fixed_sizes.get(&pattern.ty).copied() { - self.emit_loaded_schema_exact_size_check( - input_size_offset, - expected_size, - &format!("{} preserved-data input", pattern.ty), - ); - self.emit_loaded_schema_exact_size_check( - output_size_offset, - expected_size, - &format!("{} preserved-data output", pattern.ty), - ); - } - let size_ok_label = self.fresh_label("mutate_preserved_data_size_ok"); - self.emit_stack_load("t0", input_size_offset); - self.emit_stack_load("t1", output_size_offset); - self.emit("sub t2, t0, t1"); - self.emit(format!("beqz t2, {}", size_ok_label)); - self.emit_fail(CellScriptRuntimeError::FieldPreservationMismatch); - self.emit_label(&size_ok_label); - - self.emit(format!( - "# cellscript abi: verify mutate preserved data {} Input#{} == Output#{} except transition ranges {:?}", - pattern.ty, pattern.input_index, pattern.output_index, exclusion_ranges - )); - let loop_label = self.fresh_label("mutate_preserved_data_loop"); - let compare_label = self.fresh_label("mutate_preserved_data_compare"); - let skip_label = self.fresh_label("mutate_preserved_data_skip"); - let done_label = self.fresh_label("mutate_preserved_data_done"); - let mismatch_label = self.fresh_label("mutate_preserved_data_mismatch"); - self.emit_sp_addi("a3", input_buffer_offset); - self.emit_sp_addi("a4", output_buffer_offset); - self.emit("li t6, 0"); - self.emit_label(&loop_label); - self.emit("sltu t2, t6, t0"); - self.emit(format!("beqz t2, {}", done_label)); - for (range_index, (start, end)) in exclusion_ranges.iter().enumerate() { - let next_range_label = self.fresh_label(&format!("mutate_preserved_data_next_range_{}", range_index)); - self.emit(format!("li t3, {}", start)); - self.emit("sltu t2, t6, t3"); - self.emit(format!("bnez t2, {}", compare_label)); - self.emit(format!("li t3, {}", end)); - self.emit("sltu t2, t6, t3"); - self.emit(format!("beqz t2, {}", next_range_label)); - self.emit(format!("j {}", skip_label)); - self.emit_label(&next_range_label); - } - self.emit_label(&compare_label); - self.emit("add t3, a3, t6"); - self.emit("lbu t4, 0(t3)"); - self.emit("add t3, a4, t6"); - self.emit("lbu t5, 0(t3)"); - self.emit("sub t2, t4, t5"); - self.emit(format!("bnez t2, {}", mismatch_label)); - self.emit_label(&skip_label); - self.emit("addi t6, t6, 1"); - self.emit(format!("j {}", loop_label)); - self.emit_label(&mismatch_label); - self.emit_fail(CellScriptRuntimeError::FieldPreservationMismatch); - self.emit_label(&done_label); - true - } - - fn mutate_u128_transition_layouts(&self, pattern: &MutatePattern) -> Vec<(MutateFieldTransition, SchemaFieldLayout)> { - let Some(type_size) = self.type_fixed_sizes.get(&pattern.ty).copied() else { - return Vec::new(); - }; - if type_size > RUNTIME_SCRATCH_BUFFER_SIZE { - return Vec::new(); - } - pattern - .transitions - .iter() - .filter_map(|transition| { - if transition.op == MutateTransitionOp::Set { - return None; - } - let layout = self.type_layouts.get(&pattern.ty).and_then(|fields| fields.get(&transition.field)).cloned()?; - // Only u128 fields (16 bytes) that don't fit in a single register. - if layout.ty != IrType::U128 || layout.fixed_size != Some(16) { - return None; - } - if layout.offset + 16 > RUNTIME_SCRATCH_BUFFER_SIZE { - return None; - } - // u128 transition: the operand must be a u64 value (delta always fits in 64 bits). - self.prelude_u64_operand_source(&transition.operand)?; - Some((transition.clone(), layout)) - }) - .collect() - } - - fn mutate_transition_layouts(&self, pattern: &MutatePattern) -> Vec<(MutateFieldTransition, SchemaFieldLayout, usize)> { - let Some(type_size) = self.type_fixed_sizes.get(&pattern.ty).copied() else { - return Vec::new(); - }; - if type_size > RUNTIME_SCRATCH_BUFFER_SIZE { - return Vec::new(); - } - pattern - .transitions - .iter() - .filter_map(|transition| { - if transition.op == MutateTransitionOp::Set { - return None; - } - let layout = self.type_layouts.get(&pattern.ty).and_then(|fields| fields.get(&transition.field)).cloned()?; - let width = fixed_register_width(&layout.ty, layout.fixed_size)?; - if layout.offset + width > RUNTIME_SCRATCH_BUFFER_SIZE { - return None; - } - self.prelude_u64_operand_source(&transition.operand)?; - Some((transition.clone(), layout, width)) - }) - .collect() - } - - fn mutate_set_transition_layouts(&self, pattern: &MutatePattern) -> Vec<(MutateFieldTransition, SchemaFieldLayout, usize)> { - let Some(type_size) = self.type_fixed_sizes.get(&pattern.ty).copied() else { - return Vec::new(); - }; - if type_size > RUNTIME_SCRATCH_BUFFER_SIZE { - return Vec::new(); - } - pattern - .transitions - .iter() - .filter_map(|transition| { - if transition.op != MutateTransitionOp::Set { - return None; - } - let layout = self.type_layouts.get(&pattern.ty).and_then(|fields| fields.get(&transition.field)).cloned()?; - let width = layout_fixed_byte_width(&layout)?; - if layout.offset + width > RUNTIME_SCRATCH_BUFFER_SIZE { - return None; - } - if layout_fixed_scalar_width(&layout).is_none() - && self.expected_fixed_byte_source(&transition.operand, width).is_none() - { - return None; - } - Some((transition.clone(), layout, width)) - }) - .collect() - } - - fn emit_mutate_replacement_transition_checks(&mut self, pattern: &MutatePattern) { - if self.emit_mutate_replacement_dynamic_table_append_checks(pattern) { - return; - } - if self.emit_mutate_replacement_dynamic_table_transition_checks(pattern) { - return; - } - let transitions = self.mutate_transition_layouts(pattern); - if transitions.is_empty() { - return; - } - let input_size_offset = self.runtime_scratch_size_offset(); - let input_buffer_offset = self.runtime_scratch_buffer_offset(); - let output_size_offset = self.runtime_scratch2_size_offset(); - let output_buffer_offset = self.runtime_scratch2_buffer_offset(); - self.emit_load_cell_data_syscall_to_offsets( - "mutate_input_transition", - CKB_SOURCE_INPUT, - pattern.input_index, - input_size_offset, - input_buffer_offset, - RUNTIME_SCRATCH_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_load_cell_data_syscall_to_offsets( - "mutate_output_transition", - CKB_SOURCE_OUTPUT, - pattern.output_index, - output_size_offset, - output_buffer_offset, - RUNTIME_SCRATCH_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - if let Some(expected_size) = self.type_fixed_sizes.get(&pattern.ty).copied() { - self.emit_loaded_schema_exact_size_check( - input_size_offset, - expected_size, - &format!("{} mutate transition input", pattern.ty), - ); - self.emit_loaded_schema_exact_size_check( - output_size_offset, - expected_size, - &format!("{} mutate transition output", pattern.ty), - ); - } - self.emit(format!( - "# cellscript abi: verify mutate transition fields {} Input#{} -> Output#{}", - pattern.ty, pattern.input_index, pattern.output_index - )); - for (transition, layout, width) in transitions { - let Some(delta) = self.prelude_u64_operand_source(&transition.operand) else { - continue; - }; - self.emit_loaded_schema_bounds_check( - input_size_offset, - layout.offset + width, - &format!("{} input.{}", pattern.ty, transition.field), - ); - self.emit_loaded_schema_bounds_check( - output_size_offset, - layout.offset + width, - &format!("{} output.{}", pattern.ty, transition.field), - ); - self.emit(format!( - "# cellscript abi: verify mutate transition field {}.{} {:?} Input#{} -> Output#{} offset={} size={}", - pattern.ty, transition.field, transition.op, pattern.input_index, pattern.output_index, layout.offset, width - )); - self.emit_sp_addi("t4", input_buffer_offset); - self.emit_unaligned_scalar_load("t4", "t0", "t2", layout.offset, width); - let input_value_offset = self.runtime_expr_temp_offset(RUNTIME_EXPR_TEMP_SLOTS - 2); - self.emit("# cellscript abi: preserve mutate input scalar before transition expression"); - self.emit_stack_store("t0", input_value_offset); - self.emit_prelude_u64_operand_source_to_t1(&delta); - self.emit_stack_load("t0", input_value_offset); - match transition.op { - MutateTransitionOp::Add => self.emit("add t1, t0, t1"), - MutateTransitionOp::Sub => self.emit("sub t1, t0, t1"), - MutateTransitionOp::Set => { - unreachable!("set transitions are verified by emit_mutate_replacement_set_transition_checks") - } - MutateTransitionOp::Append => { - unreachable!("append transitions are verified by emit_mutate_replacement_dynamic_table_append_checks") - } - } - let expected_value_offset = self.runtime_expr_temp_offset(RUNTIME_EXPR_TEMP_SLOTS - 1); - self.emit("# cellscript abi: preserve mutate expected scalar across output field load"); - self.emit_stack_store("t1", expected_value_offset); - self.emit_sp_addi("t4", output_buffer_offset); - self.emit_unaligned_scalar_load("t4", "t0", "t2", layout.offset, width); - self.emit_stack_load("t1", expected_value_offset); - self.emit("sub t2, t0, t1"); - let ok_label = self.fresh_label("mutate_transition_ok"); - self.emit(format!("beqz t2, {}", ok_label)); - self.emit_fail(CellScriptRuntimeError::MutateTransitionMismatch); - self.emit_label(&ok_label); - } - } - - fn emit_mutate_replacement_dynamic_table_transition_checks(&mut self, pattern: &MutatePattern) -> bool { - if self.type_fixed_sizes.contains_key(&pattern.ty) || pattern.transitions.is_empty() { - return false; - } - let Some(layouts) = self.type_layouts.get(&pattern.ty).cloned() else { - return false; - }; - let field_count = layouts.len(); - let transitions = pattern - .transitions - .iter() - .filter_map(|transition| { - let layout = layouts.get(&transition.field).cloned()?; - let width = layout_fixed_scalar_width(&layout)?; - (width <= 8 && self.prelude_u64_operand_source(&transition.operand).is_some()) - .then(|| (transition.clone(), layout, width)) - }) - .collect::>(); - if transitions.len() != pattern.transitions.len() { - return false; - } - - let input_size_offset = self.runtime_scratch_size_offset(); - let input_buffer_offset = self.runtime_scratch_buffer_offset(); - let output_size_offset = self.runtime_scratch2_size_offset(); - let output_buffer_offset = self.runtime_scratch2_buffer_offset(); - self.emit_load_cell_data_syscall_to_offsets( - "mutate_input_table_transition", - CKB_SOURCE_INPUT, - pattern.input_index, - input_size_offset, - input_buffer_offset, - RUNTIME_SCRATCH_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_load_cell_data_syscall_to_offsets( - "mutate_output_table_transition", - CKB_SOURCE_OUTPUT, - pattern.output_index, - output_size_offset, - output_buffer_offset, - RUNTIME_SCRATCH_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit(format!( - "# cellscript abi: verify mutate Molecule table transition fields {} Input#{} -> Output#{}", - pattern.ty, pattern.input_index, pattern.output_index - )); - for (transition, layout, width) in transitions { - let Some(delta) = self.prelude_u64_operand_source(&transition.operand) else { - continue; - }; - self.emit_sp_addi("t4", input_buffer_offset); - self.emit_molecule_table_field_bounds_to_t5( - "t4", - input_size_offset, - layout.index, - width, - &format!("{} input.{}", pattern.ty, transition.field), - ); - self.emit("add t4, t4, t5"); - self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, width); - let input_value_offset = self.runtime_expr_temp_offset(RUNTIME_EXPR_TEMP_SLOTS - 2); - self.emit("# cellscript abi: preserve mutate table input scalar before transition expression"); - self.emit_stack_store("t0", input_value_offset); - self.emit_prelude_u64_operand_source_to_t1(&delta); - self.emit_stack_load("t0", input_value_offset); - match transition.op { - MutateTransitionOp::Add => self.emit("add t1, t0, t1"), - MutateTransitionOp::Sub => self.emit("sub t1, t0, t1"), - MutateTransitionOp::Set => {} - MutateTransitionOp::Append => {} - } - let expected_value_offset = self.runtime_expr_temp_offset(RUNTIME_EXPR_TEMP_SLOTS - 1); - self.emit("# cellscript abi: preserve mutate table expected scalar across output field load"); - self.emit_stack_store("t1", expected_value_offset); - self.emit_sp_addi("t4", output_buffer_offset); - self.emit_molecule_table_field_bounds_to_t5( - "t4", - output_size_offset, - layout.index, - width, - &format!("{} output.{}", pattern.ty, transition.field), - ); - self.emit("add t4, t4, t5"); - self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, width); - self.emit_stack_load("t1", expected_value_offset); - self.emit("sub t2, t0, t1"); - let ok_label = self.fresh_label("mutate_table_transition_ok"); - self.emit(format!("beqz t2, {}", ok_label)); - self.emit_fail(CellScriptRuntimeError::MutateTransitionMismatch); - self.emit_label(&ok_label); - } - let _ = field_count; - true - } - - fn emit_mutate_replacement_set_transition_checks(&mut self, pattern: &MutatePattern) { - let transitions = self.mutate_set_transition_layouts(pattern); - if transitions.is_empty() { - return; - } - let output_size_offset = self.runtime_scratch2_size_offset(); - let output_buffer_offset = self.runtime_scratch2_buffer_offset(); - self.emit_load_cell_data_syscall_to_offsets( - "mutate_output_set_transition", - CKB_SOURCE_OUTPUT, - pattern.output_index, - output_size_offset, - output_buffer_offset, - RUNTIME_SCRATCH_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - if let Some(expected_size) = self.type_fixed_sizes.get(&pattern.ty).copied() { - self.emit_loaded_schema_exact_size_check( - output_size_offset, - expected_size, - &format!("{} mutate set transition output", pattern.ty), - ); - } - self.emit(format!("# cellscript abi: verify mutate set transition fields {} Output#{}", pattern.ty, pattern.output_index)); - for (transition, layout, width) in transitions { - self.emit(format!( - "# cellscript abi: verify mutate set transition field {}.{} Output#{} offset={} size={}", - pattern.ty, transition.field, pattern.output_index, layout.offset, width - )); - if !self.emit_loaded_field_bytes_equals_expected( - output_size_offset, - output_buffer_offset, - &layout, - &transition.operand, - &format!("{} set.{}", pattern.ty, transition.field), - ) { - self.emit_fail(CellScriptRuntimeError::MutateTransitionMismatch); - } - } - } - - /// u128 transition verification using 128-bit add/sub with carry. - /// Layout: field is 16 bytes (low 8 + high 8, little-endian). - /// Delta is always u64 (fits in a single register). - /// Verification: output == input +/- delta, with carry propagation. - fn emit_mutate_replacement_u128_transition_checks(&mut self, pattern: &MutatePattern) { - let transitions = self.mutate_u128_transition_layouts(pattern); - if transitions.is_empty() { - return; - } - let input_size_offset = self.runtime_scratch_size_offset(); - let input_buffer_offset = self.runtime_scratch_buffer_offset(); - let output_size_offset = self.runtime_scratch2_size_offset(); - let output_buffer_offset = self.runtime_scratch2_buffer_offset(); - // Load Input and Output cell data (already done by the caller for - // preserved field checks, but we need it for transition checks too). - // If the scratch buffers were already loaded by the preserved-field - // path, the syscall results are cached in the buffer; we only need - // to reload if this function is called independently. - self.emit_load_cell_data_syscall_to_offsets( - "mutate_input_u128_transition", - CKB_SOURCE_INPUT, - pattern.input_index, - input_size_offset, - input_buffer_offset, - RUNTIME_SCRATCH_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_load_cell_data_syscall_to_offsets( - "mutate_output_u128_transition", - CKB_SOURCE_OUTPUT, - pattern.output_index, - output_size_offset, - output_buffer_offset, - RUNTIME_SCRATCH_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - if let Some(expected_size) = self.type_fixed_sizes.get(&pattern.ty).copied() { - self.emit_loaded_schema_exact_size_check( - input_size_offset, - expected_size, - &format!("{} mutate u128 transition input", pattern.ty), - ); - self.emit_loaded_schema_exact_size_check( - output_size_offset, - expected_size, - &format!("{} mutate u128 transition output", pattern.ty), - ); - } - for (transition, layout) in transitions { - let Some(delta) = self.prelude_u64_operand_source(&transition.operand) else { - continue; - }; - self.emit_loaded_schema_bounds_check( - input_size_offset, - layout.offset + 16, - &format!("{} input.{}", pattern.ty, transition.field), - ); - self.emit_loaded_schema_bounds_check( - output_size_offset, - layout.offset + 16, - &format!("{} output.{}", pattern.ty, transition.field), - ); - self.emit(format!( - "# cellscript abi: verify mutate u128 transition field {}.{} {:?} Input#{} -> Output#{} offset={} size=16", - pattern.ty, transition.field, transition.op, pattern.input_index, pattern.output_index, layout.offset - )); - - // Load input low 64 bits (little-endian bytes 0..8) into t0 - // Load input high 64 bits (little-endian bytes 8..16) into t3 - self.emit_sp_addi("t4", input_buffer_offset); - self.emit_unaligned_scalar_load("t4", "t0", "t2", layout.offset, 8); - self.emit_unaligned_scalar_load("t4", "t3", "t2", layout.offset + 8, 8); - - // Load delta into t1 - self.emit_prelude_u64_operand_source_to_t1(&delta); - - // Compute expected output = input +/- delta with carry - match transition.op { - MutateTransitionOp::Add => { - // expected_lo = input_lo + delta - // expected_hi = input_hi + carry - // where carry = (input_lo + delta < input_lo) ? 1 : 0 - self.emit("add t5, t0, t1"); // expected_lo = input_lo + delta - self.emit("sltu t2, t5, t0"); // carry = 1 if addition overflowed - self.emit("add t6, t3, t2"); // expected_hi = input_hi + carry - } - MutateTransitionOp::Sub => { - // expected_lo = input_lo - delta - // expected_hi = input_hi - borrow - // where borrow = (input_lo < delta) ? 1 : 0 - self.emit("sub t5, t0, t1"); // expected_lo = input_lo - delta - self.emit("sltu t2, t0, t1"); // borrow = 1 if subtraction underflowed - self.emit("sub t6, t3, t2"); // expected_hi = input_hi - borrow - } - MutateTransitionOp::Set => { - unreachable!("set transitions are verified by emit_mutate_replacement_set_transition_checks") - } - MutateTransitionOp::Append => { - unreachable!("append transitions are verified by emit_mutate_replacement_dynamic_table_append_checks") - } - } - - // Load actual output low 64 bits into t0, high 64 bits into t3 - self.emit_sp_addi("t4", output_buffer_offset); - self.emit_unaligned_scalar_load("t4", "t0", "t2", layout.offset, 8); - self.emit_unaligned_scalar_load("t4", "t3", "t2", layout.offset + 8, 8); - - // Compare: expected (t5, t6) == actual (t0, t3) - let ok_label = self.fresh_label("mutate_u128_transition_ok"); - self.emit("sub t2, t0, t5"); // diff_lo = actual_lo - expected_lo - self.emit("sub t1, t3, t6"); // diff_hi = actual_hi - expected_hi - self.emit("or t2, t2, t1"); // combined diff = diff_lo | diff_hi - self.emit(format!("beqz t2, {}", ok_label)); - self.emit_fail(CellScriptRuntimeError::MutateTransitionMismatch); - self.emit_label(&ok_label); - } - } - - fn emit_loaded_field_equals_expected( - &mut self, - size_offset: usize, - buffer_offset: usize, - layout: &SchemaFieldLayout, - expected: &IrOperand, - context: &str, - ) { - let Some(width) = layout_fixed_scalar_width(layout) else { - return; - }; - self.emit_loaded_schema_bounds_check(size_offset, layout.offset + width, context); - self.emit(format!("# cellscript abi: verify output field {} offset={} size={}", context, layout.offset, width)); - self.emit_sp_addi("t4", buffer_offset); - self.emit_unaligned_scalar_load("t4", "t0", "t2", layout.offset, width); - let actual_value_offset = self.runtime_expr_temp_offset(RUNTIME_EXPR_TEMP_SLOTS - 1); - self.emit("# cellscript abi: preserve output scalar before expected expression"); - self.emit_stack_store("t0", actual_value_offset); - self.emit_expected_operand_to_t1(expected); - self.emit_stack_load("t0", actual_value_offset); - self.emit("sub t2, t0, t1"); - let ok_label = self.fresh_label("output_field_ok"); - self.emit(format!("beqz t2, {}", ok_label)); - self.emit_fail(CellScriptRuntimeError::CellLoadFailed); - self.emit_label(&ok_label); - } - - fn emit_loaded_fixed_bytes_against_source( - &mut self, - output_buffer_offset: usize, - output_field_offset: usize, - source: &ExpectedFixedByteSource, - width: usize, - fail_code: CellScriptRuntimeError, - ) { - let mismatch_label = self.fresh_label("fixed_byte_mismatch"); - self.emit_sp_addi("t4", output_buffer_offset); - match source { - ExpectedFixedByteSource::SchemaField(source) => { - if self.emit_schema_field_source_pointer_to("a1", source, width) { - self.emit_sp_addi("a0", output_buffer_offset + output_field_offset); - self.emit(format!("li a2, {}", width)); - self.emit("call __cellscript_memcmp_fixed"); - self.emit(format!("bnez a0, {}", mismatch_label)); - } else { - self.emit("# cellscript abi: fail closed because schema field byte source is not addressable"); - self.emit_fail(CellScriptRuntimeError::DynamicFieldBoundsInvalid); - } - } - ExpectedFixedByteSource::Const(bytes) => { - if width >= 8 && bytes.iter().take(width).all(|byte| *byte == 0) { - self.emit_sp_addi("a0", output_buffer_offset + output_field_offset); - self.emit(format!("li a1, {}", width)); - self.emit("call __cellscript_memzero_fixed"); - self.emit(format!("bnez a0, {}", mismatch_label)); - } else { - for (byte_index, byte) in bytes.iter().take(width).enumerate() { - self.emit(format!("lbu t0, {}(t4)", output_field_offset + byte_index)); - self.emit(format!("li t1, {}", byte)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", mismatch_label)); - } - } - } - ExpectedFixedByteSource::StackSlot { var_id, .. } => { - self.emit_loaded_fixed_bytes_helper_call( - output_buffer_offset, - output_field_offset, - SourcePointer::StackAddress { offset: var_id * 8 }, - width, - &mismatch_label, - ); - } - ExpectedFixedByteSource::PointerBytes { var_id, .. } - | ExpectedFixedByteSource::ParamBytes { var_id, .. } - | ExpectedFixedByteSource::LoadedBytes { var_id, .. } => { - self.emit_loaded_fixed_bytes_helper_call( - output_buffer_offset, - output_field_offset, - SourcePointer::LoadedStackPointer { var_id: *var_id, offset: 0 }, - width, - &mismatch_label, - ); - } - } - self.emit_fixed_byte_mismatch_fail(&mismatch_label, fail_code); - } - - fn emit_loaded_fixed_bytes_helper_call( - &mut self, - output_buffer_offset: usize, - output_field_offset: usize, - source: SourcePointer, - width: usize, - mismatch_label: &str, - ) { - self.emit_sp_addi("a0", output_buffer_offset + output_field_offset); - match source { - SourcePointer::LoadedStackPointer { var_id, offset } => { - self.emit_stack_load("a1", var_id * 8); - if offset != 0 { - self.emit_large_addi("a1", "a1", offset as i64); - } - } - SourcePointer::StackAddress { offset } => { - self.emit_sp_addi("a1", offset); - } - } - self.emit(format!("li a2, {}", width)); - self.emit("call __cellscript_memcmp_fixed"); - self.emit(format!("bnez a0, {}", mismatch_label)); - } - - fn emit_loaded_field_bytes_equals_expected( - &mut self, - size_offset: usize, - buffer_offset: usize, - layout: &SchemaFieldLayout, - expected: &IrOperand, - context: &str, - ) -> bool { - if layout_fixed_scalar_width(layout).is_some() { - self.emit_loaded_field_equals_expected(size_offset, buffer_offset, layout, expected, context); - return true; - } - let Some(width) = layout_fixed_byte_width(layout).or_else(|| self.fixed_named_type_width(&layout.ty)) else { - return false; - }; - let Some(source) = self.expected_fixed_byte_source(expected, width) else { - return false; - }; - self.emit_loaded_schema_bounds_check(size_offset, layout.offset + width, context); - match source { - ExpectedFixedByteSource::SchemaField(source) => { - if let Some(source_size_offset) = self.schema_pointer_size_offsets.get(&source.obj_var_id).copied() { - if let Some(expected_size) = self.type_fixed_sizes.get(&source.type_name).copied() { - self.emit_loaded_schema_exact_size_check(source_size_offset, expected_size, &source.type_name); - } - self.emit_loaded_schema_bounds_check( - source_size_offset, - source.layout.offset + width, - &format!("{}.{}", source.type_name, source.field), - ); - } - self.emit(format!("# cellscript abi: verify output bytes field {} offset={} size={}", context, layout.offset, width)); - self.emit(format!( - "# cellscript abi: expected bytes field {}.{} offset={} size={}", - source.type_name, source.field, source.layout.offset, width - )); - self.emit_loaded_fixed_bytes_against_source( - buffer_offset, - layout.offset, - &ExpectedFixedByteSource::SchemaField(source), - width, - CellScriptRuntimeError::CellLoadFailed, - ); - } - ExpectedFixedByteSource::Const(bytes) => { - self.emit(format!( - "# cellscript abi: verify output bytes field {} offset={} size={} against const", - context, layout.offset, width - )); - self.emit_loaded_fixed_bytes_against_source( - buffer_offset, - layout.offset, - &ExpectedFixedByteSource::Const(bytes), - width, - CellScriptRuntimeError::CellLoadFailed, - ); - } - ExpectedFixedByteSource::StackSlot { var_id, width } => { - self.emit(format!( - "# cellscript abi: verify output bytes field {} offset={} size={} against stack slot var{}", - context, layout.offset, width, var_id - )); - self.emit_loaded_fixed_bytes_against_source( - buffer_offset, - layout.offset, - &ExpectedFixedByteSource::StackSlot { var_id, width }, - width, - CellScriptRuntimeError::CellLoadFailed, - ); - } - ExpectedFixedByteSource::PointerBytes { var_id, width } => { - self.emit(format!( - "# cellscript abi: verify output bytes field {} offset={} size={} against pointer var{}", - context, layout.offset, width, var_id - )); - self.emit_loaded_fixed_bytes_against_source( - buffer_offset, - layout.offset, - &ExpectedFixedByteSource::PointerBytes { var_id, width }, - width, - CellScriptRuntimeError::CellLoadFailed, - ); - } - ExpectedFixedByteSource::ParamBytes { var_id, size_offset, width } => { - self.emit_loaded_schema_exact_size_check(size_offset, width, &format!("param var{}", var_id)); - self.emit(format!( - "# cellscript abi: verify output bytes field {} offset={} size={} against fixed-byte param var{}", - context, layout.offset, width, var_id - )); - self.emit_loaded_fixed_bytes_against_source( - buffer_offset, - layout.offset, - &ExpectedFixedByteSource::ParamBytes { var_id, size_offset, width }, - width, - CellScriptRuntimeError::CellLoadFailed, - ); - } - ExpectedFixedByteSource::LoadedBytes { var_id, size_offset, width } => { - self.emit_loaded_schema_exact_size_check(size_offset, width, &format!("loaded bytes var{}", var_id)); - self.emit(format!( - "# cellscript abi: verify output bytes field {} offset={} size={} against loaded bytes var{}", - context, layout.offset, width, var_id - )); - self.emit_loaded_fixed_bytes_against_source( - buffer_offset, - layout.offset, - &ExpectedFixedByteSource::LoadedBytes { var_id, size_offset, width }, - width, - CellScriptRuntimeError::CellLoadFailed, - ); - } - } - true - } - - fn emit_prepare_fixed_byte_source(&mut self, source: &ExpectedFixedByteSource, width: usize, context: &str) { - match source { - ExpectedFixedByteSource::SchemaField(source) => { - self.emit_prepare_schema_field_source(source, width); - } - ExpectedFixedByteSource::ParamBytes { var_id, size_offset, width } => { - self.emit_loaded_schema_exact_size_check(*size_offset, *width, &format!("{} param var{}", context, var_id)); - } - ExpectedFixedByteSource::LoadedBytes { var_id, size_offset, width } => { - self.emit_loaded_schema_exact_size_check(*size_offset, *width, &format!("{} loaded bytes var{}", context, var_id)); - } - ExpectedFixedByteSource::Const(_) - | ExpectedFixedByteSource::StackSlot { .. } - | ExpectedFixedByteSource::PointerBytes { .. } => {} - } - } - - fn emit_fixed_byte_source_byte_to(&mut self, dest_reg: &str, base_reg: &str, source: &ExpectedFixedByteSource, byte_index: usize) { - match source { - ExpectedFixedByteSource::SchemaField(source) => { - if self.emit_schema_field_source_pointer_to(base_reg, source, byte_index + 1) { - self.emit(format!("lbu {}, {}({})", dest_reg, byte_index, base_reg)); - } else { - self.emit("# cellscript abi: fail closed because schema field byte source is not addressable"); - self.emit_fail(CellScriptRuntimeError::DynamicFieldBoundsInvalid); - } - } - ExpectedFixedByteSource::Const(bytes) => { - self.emit(format!("li {}, {}", dest_reg, bytes[byte_index])); - } - ExpectedFixedByteSource::StackSlot { var_id, .. } => { - self.emit_sp_addi(base_reg, var_id * 8); - self.emit(format!("lbu {}, {}({})", dest_reg, byte_index, base_reg)); - } - ExpectedFixedByteSource::PointerBytes { var_id, .. } - | ExpectedFixedByteSource::ParamBytes { var_id, .. } - | ExpectedFixedByteSource::LoadedBytes { var_id, .. } => { - self.emit_stack_load(base_reg, var_id * 8); - self.emit(format!("lbu {}, {}({})", dest_reg, byte_index, base_reg)); - } - } - } - - fn emit_fixed_byte_source_pointer_to(&mut self, dest_reg: &str, source: &ExpectedFixedByteSource) -> bool { - match source { - ExpectedFixedByteSource::SchemaField(source) => { - let Some(width) = layout_fixed_byte_width(&source.layout).or_else(|| self.fixed_named_type_width(&source.layout.ty)) - else { - return false; - }; - self.emit_schema_field_source_pointer_to(dest_reg, source, width) - } - ExpectedFixedByteSource::StackSlot { var_id, .. } => { - self.emit_sp_addi(dest_reg, var_id * 8); - true - } - ExpectedFixedByteSource::PointerBytes { var_id, .. } - | ExpectedFixedByteSource::ParamBytes { var_id, .. } - | ExpectedFixedByteSource::LoadedBytes { var_id, .. } => { - self.emit_stack_load(dest_reg, var_id * 8); - true - } - ExpectedFixedByteSource::Const(_) => false, - } - } - - fn emit_fixed_byte_source_pointer_or_const_to(&mut self, dest_reg: &str, source: &ExpectedFixedByteSource) -> bool { - if let ExpectedFixedByteSource::Const(bytes) = source { - let label = self.const_data_label_for_bytes(bytes.clone()); - self.emit(format!("la {}, {}", dest_reg, label)); - true - } else { - self.emit_fixed_byte_source_pointer_to(dest_reg, source) - } - } - - fn emit_fixed_byte_mismatch_fail(&mut self, mismatch_label: &str, fail_code: CellScriptRuntimeError) { - let done_label = self.fresh_label("fixed_byte_verify_done"); - self.emit(format!("j {}", done_label)); - self.emit_label(mismatch_label); - self.emit_fail(fail_code); - self.emit_label(&done_label); - } - - fn emit_fixed_byte_comparison(&mut self, dest: &IrVar, op: BinaryOp, left: &IrOperand, right: &IrOperand) -> bool { - let Some(width) = operand_fixed_byte_width(left) else { - return false; - }; - if operand_fixed_byte_width(right) != Some(width) { - return false; - } - let Some(left_source) = self.expected_fixed_byte_source(left, width) else { - return false; - }; - let Some(right_source) = self.expected_fixed_byte_source(right, width) else { - return false; - }; - self.emit(format!("# cellscript abi: fixed-byte {:?} comparison size={}", op, width)); - self.emit_prepare_fixed_byte_source(&left_source, width, "left fixed-byte comparison"); - self.emit_prepare_fixed_byte_source(&right_source, width, "right fixed-byte comparison"); - if width >= 8 && self.emit_fixed_byte_comparison_helper(dest, op, &left_source, &right_source, width) { - return true; - } - let mismatch_label = self.fresh_label("fixed_byte_mismatch"); - let done_label = self.fresh_label("fixed_byte_done"); - for byte_index in 0..width { - self.emit_fixed_byte_source_byte_to("t0", "t4", &left_source, byte_index); - self.emit_fixed_byte_source_byte_to("t1", "t5", &right_source, byte_index); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", mismatch_label)); - } - let equal_value = if matches!(op, BinaryOp::Eq) { 1 } else { 0 }; - let mismatch_value = if matches!(op, BinaryOp::Eq) { 0 } else { 1 }; - self.emit(format!("li t3, {}", equal_value)); - self.emit(format!("j {}", done_label)); - self.emit_label(&mismatch_label); - self.emit(format!("li t3, {}", mismatch_value)); - self.emit_label(&done_label); - self.emit_stack_store("t3", dest.id * 8); - true - } - - fn emit_fixed_byte_comparison_helper( - &mut self, - dest: &IrVar, - op: BinaryOp, - left_source: &ExpectedFixedByteSource, - right_source: &ExpectedFixedByteSource, - width: usize, - ) -> bool { - match (left_source, right_source) { - (ExpectedFixedByteSource::Const(bytes), source) if bytes.iter().take(width).all(|byte| *byte == 0) => { - if !self.emit_fixed_byte_source_pointer_to("a0", source) { - return false; - } - self.emit(format!("li a1, {}", width)); - self.emit("call __cellscript_memzero_fixed"); - } - (source, ExpectedFixedByteSource::Const(bytes)) if bytes.iter().take(width).all(|byte| *byte == 0) => { - if !self.emit_fixed_byte_source_pointer_to("a0", source) { - return false; - } - self.emit(format!("li a1, {}", width)); - self.emit("call __cellscript_memzero_fixed"); - } - (ExpectedFixedByteSource::Const(_), _) | (_, ExpectedFixedByteSource::Const(_)) => return false, - _ => { - if !self.emit_fixed_byte_source_pointer_to("a0", left_source) { - return false; - } - let Some(left_pointer_offset) = self.checked_runtime_expr_temp_offset(0) else { - return false; - }; - self.emit_stack_store("a0", left_pointer_offset); - if !self.emit_fixed_byte_source_pointer_to("a1", right_source) { - return false; - } - self.emit_stack_load("a0", left_pointer_offset); - self.emit(format!("li a2, {}", width)); - self.emit("call __cellscript_memcmp_fixed"); - } - } - if matches!(op, BinaryOp::Eq) { - self.emit("seqz t3, a0"); - } else { - self.emit("snez t3, a0"); - } - self.emit_stack_store("t3", dest.id * 8); - true - } - - fn expected_fixed_byte_source(&self, operand: &IrOperand, expected_width: usize) -> Option { - match operand { - IrOperand::Const(value) => { - let bytes = fixed_byte_const_bytes(value).or_else(|| { - fixed_scalar_const_value(value) - .and_then(|value| (expected_width <= 8).then(|| value.to_le_bytes()[..expected_width].to_vec())) - })?; - (bytes.len() == expected_width).then_some(ExpectedFixedByteSource::Const(bytes)) - } - IrOperand::Var(var) - if self.fixed_byte_like_width(&var.ty).or_else(|| fixed_aggregate_pointer_param_width(&var.ty)).is_some() => - { - let var_width = self.fixed_byte_like_width(&var.ty).or_else(|| fixed_aggregate_pointer_param_width(&var.ty))?; - if let Some(source) = self.schema_field_value_sources.get(&var.id).cloned() { - let source_width = - layout_fixed_byte_width(&source.layout).or_else(|| self.fixed_named_type_width(&source.layout.ty))?; - if source_width == expected_width { - return Some(ExpectedFixedByteSource::SchemaField(source)); - } - } - if let Some(bytes) = self.prelude_fixed_byte_constants.get(&var.id).cloned() { - if bytes.len() == expected_width { - return Some(ExpectedFixedByteSource::Const(bytes)); - } - } - if self.schema_pointer_vars.contains(&var.id) && var_width == expected_width { - if let Some(size_offset) = self.schema_pointer_size_offsets.get(&var.id).copied() { - return Some(ExpectedFixedByteSource::LoadedBytes { var_id: var.id, size_offset, width: expected_width }); - } - return Some(ExpectedFixedByteSource::PointerBytes { var_id: var.id, width: expected_width }); - } - if let Some(size_offset) = self.cell_buffer_size_offsets.get(&var.id).copied() { - if var_width == expected_width { - return Some(ExpectedFixedByteSource::LoadedBytes { var_id: var.id, size_offset, width: expected_width }); - } - } - if self.fixed_byte_local_offsets.contains_key(&var.id) && var_width == expected_width { - return Some(ExpectedFixedByteSource::PointerBytes { var_id: var.id, width: expected_width }); - } - if expected_width <= 8 - && (fixed_scalar_width(&var.ty, type_static_length(&var.ty)).is_some() - || (var_width == expected_width && fixed_byte_width(&var.ty, type_static_length(&var.ty)).is_some())) - && expected_width <= var_width - { - return Some(ExpectedFixedByteSource::StackSlot { var_id: var.id, width: expected_width }); - } - if self.u128_value_offsets.contains_key(&var.id) - && !self.fixed_byte_param_size_offsets.contains_key(&var.id) - && var_width == expected_width - { - return Some(ExpectedFixedByteSource::PointerBytes { var_id: var.id, width: expected_width }); - } - if self.aggregate_pointer_sources.contains_key(&var.id) && var_width == expected_width { - return Some(ExpectedFixedByteSource::PointerBytes { var_id: var.id, width: expected_width }); - } - if self.param_vars.contains(&var.id) && var_width == expected_width { - if let Some(size_offset) = self.fixed_byte_param_size_offsets.get(&var.id).copied() { - return Some(ExpectedFixedByteSource::ParamBytes { var_id: var.id, size_offset, width: expected_width }); - } - } - if let Some(param_id) = self.param_type_hash_sources.get(&var.id).copied() { - if var_width == expected_width { - if let Some(size_offset) = self.param_type_hash_size_offsets.get(¶m_id).copied() { - return Some(ExpectedFixedByteSource::LoadedBytes { var_id: var.id, size_offset, width: expected_width }); - } - } - } - None - } - _ => None, - } - } - - /// Generic fixed-byte comparison: when `emit_fixed_byte_comparison` can't determine - /// the source of bytes, this method loads pointers from stack slots and performs - /// a byte-by-byte comparison. Works for Var operands whose stack slots contain - /// pointers to the fixed-byte data. - fn emit_generic_fixed_byte_comparison(&mut self, dest: &IrVar, op: BinaryOp, left: &IrOperand, right: &IrOperand) -> bool { - let left_width = operand_fixed_byte_width(left); - let right_width = operand_fixed_byte_width(right); - - // Need at least one Var operand with known width for this to work - let width = match (left_width, right_width) { - (Some(w), Some(r)) if w == r => w, - (Some(w), None) | (None, Some(w)) => w, - _ => return false, - }; - - if width == 0 { - return false; - } - - // We need at least one Var operand - let left_var = match left { - IrOperand::Var(v) => Some(v), - _ => None, - }; - let right_var = match right { - IrOperand::Var(v) => Some(v), - _ => None, - }; - if left_var.is_none() && right_var.is_none() { - return false; - } - - self.emit(format!("# cellscript abi: generic fixed-byte {:?} comparison size={}", op, width)); - - // Load left pointer to t4 - if let Some(v) = left_var { - self.emit_stack_load("t4", v.id * 8); - } else { - // Left is a constant – store it to scratch buffer and point t4 there - let size_offset = self.runtime_scratch_size_offset(); - let buffer_offset = self.runtime_scratch_buffer_offset(); - self.emit_store_fixed_byte_const_to_scratch(left, size_offset, buffer_offset, width); - self.emit_sp_addi("t4", buffer_offset); - } - - // Load right pointer to t5 - if let Some(v) = right_var { - self.emit_stack_load("t5", v.id * 8); - } else { - let size_offset = self.runtime_scratch2_size_offset(); - let buffer_offset = self.runtime_scratch2_buffer_offset(); - self.emit_store_fixed_byte_const_to_scratch(right, size_offset, buffer_offset, width); - self.emit_sp_addi("t5", buffer_offset); - } - - let mismatch_label = self.fresh_label("gen_fb_mismatch"); - let done_label = self.fresh_label("gen_fb_done"); - for byte_index in 0..width { - self.emit(format!("lbu t0, {}(t4)", byte_index)); - self.emit(format!("lbu t1, {}(t5)", byte_index)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", mismatch_label)); - } - let equal_value = if matches!(op, BinaryOp::Eq) { 1 } else { 0 }; - let mismatch_value = if matches!(op, BinaryOp::Eq) { 0 } else { 1 }; - self.emit(format!("li t3, {}", equal_value)); - self.emit(format!("j {}", done_label)); - self.emit_label(&mismatch_label); - self.emit(format!("li t3, {}", mismatch_value)); - self.emit_label(&done_label); - self.emit_stack_store("t3", dest.id * 8); - true - } - - /// Store fixed-byte constant value to scratch buffer area. - fn emit_store_fixed_byte_const_to_scratch(&mut self, operand: &IrOperand, size_offset: usize, buffer_offset: usize, width: usize) { - match operand { - IrOperand::Const(IrConst::Address(bytes)) | IrOperand::Const(IrConst::Hash(bytes)) => { - self.emit(format!("# cellscript abi: store fixed-byte const size={}", width)); - self.emit(format!("li t0, {}", width)); - self.emit_stack_store("t0", size_offset); - for (i, byte) in bytes.iter().enumerate() { - self.emit(format!("li t0, {}", byte)); - if buffer_offset + i <= 2047 { - self.emit_stack_store_byte("t0", buffer_offset + i); - } else { - self.emit(format!("li t6, {}", buffer_offset + i)); - self.emit("add t6, sp, t6"); - self.emit("sb t0, 0(t6)"); - } - } - } - IrOperand::Const(IrConst::U128(value)) => { - self.emit(format!("# cellscript abi: store u128 const size={}", width)); - self.emit(format!("li t0, {}", width)); - self.emit_stack_store("t0", size_offset); - for (i, byte) in value.to_le_bytes().iter().enumerate() { - self.emit(format!("li t0, {}", byte)); - self.emit_stack_store_byte("t0", buffer_offset + i); - } - } - IrOperand::Const(IrConst::Array(values)) => { - self.emit(format!("# cellscript abi: store fixed-byte array const size={}", width)); - self.emit(format!("li t0, {}", width)); - self.emit_stack_store("t0", size_offset); - for (i, value) in values.iter().enumerate() { - if let IrConst::U8(byte) = value { - self.emit(format!("li t0, {}", byte)); - if buffer_offset + i <= 2047 { - self.emit_stack_store_byte("t0", buffer_offset + i); - } else { - self.emit(format!("li t6, {}", buffer_offset + i)); - self.emit("add t6, sp, t6"); - self.emit("sb t0, 0(t6)"); - } - } - } - } - _ => { - self.emit("# cellscript abi: cannot store unknown const type to scratch".to_string()); - } - } - } - - fn emit_fixed_byte_source_scalar_to( - &mut self, - dest_reg: &str, - scratch_reg: &str, - base_reg: &str, - source: &ExpectedFixedByteSource, - start: usize, - width: usize, - ) { - self.emit(format!("li {}, 0", dest_reg)); - for byte_index in 0..width { - self.emit_fixed_byte_source_byte_to(scratch_reg, base_reg, source, start + byte_index); - if byte_index != 0 { - self.emit(format!("slli {}, {}, {}", scratch_reg, scratch_reg, byte_index * 8)); - } - self.emit(format!("or {}, {}, {}", dest_reg, dest_reg, scratch_reg)); - } - } - - fn operand_is_u128(&self, operand: &IrOperand) -> bool { - match operand { - IrOperand::Const(IrConst::U128(_)) => true, - IrOperand::Var(var) => var.ty == IrType::U128, - _ => false, - } - } - - fn emit_u128_add_sub_with_u64(&mut self, dest: &IrVar, op: BinaryOp, left: &IrOperand, right: &IrOperand) -> bool { - if dest.ty != IrType::U128 || !matches!(op, BinaryOp::Add | BinaryOp::Sub) { - return false; - } - let Some(dest_offset) = self.fixed_byte_local_offsets.get(&dest.id).copied() else { - return false; - }; - - let (wide_operand, delta_operand) = match op { - BinaryOp::Add if self.operand_is_u128(left) => (left, right), - BinaryOp::Add if self.operand_is_u128(right) => (right, left), - BinaryOp::Sub if self.operand_is_u128(left) => (left, right), - _ => return false, - }; - let Some(source) = self.expected_fixed_byte_source(wide_operand, 16) else { - return false; - }; - let Some(delta) = self.prelude_u64_operand_source(delta_operand) else { - return false; - }; - - match op { - BinaryOp::Add => self.emit("# cellscript abi: u128 add with carry"), - BinaryOp::Sub => self.emit("# cellscript abi: u128 sub with borrow"), - _ => unreachable!("guarded u128 binary op"), - } - self.emit_fixed_byte_source_scalar_to("t0", "t2", "t4", &source, 0, 8); - self.emit_fixed_byte_source_scalar_to("t3", "t2", "t4", &source, 8, 8); - self.emit_prelude_u64_operand_source_to_t1(&delta); - match op { - BinaryOp::Add => { - self.emit("add t5, t0, t1"); - self.emit("sltu t2, t5, t0"); - self.emit("add t6, t3, t2"); - } - BinaryOp::Sub => { - self.emit("sub t5, t0, t1"); - self.emit("sltu t2, t0, t1"); - self.emit("sub t6, t3, t2"); - } - _ => unreachable!("guarded u128 binary op"), - } - self.emit_stack_store("t5", dest_offset); - self.emit_stack_store("t6", dest_offset + 8); - self.emit_sp_addi("t0", dest_offset); - self.emit_stack_store("t0", dest.id * 8); - true - } - - fn emit_expected_operand_to_t1(&mut self, operand: &IrOperand) { - match operand { - IrOperand::Const(IrConst::Bool(b)) => self.emit(format!("li t1, {}", if *b { 1 } else { 0 })), - IrOperand::Const(IrConst::U8(n)) => self.emit(format!("li t1, {}", n)), - IrOperand::Const(IrConst::U16(n)) => self.emit(format!("li t1, {}", n)), - IrOperand::Const(IrConst::U32(n)) => self.emit(format!("li t1, {}", n)), - IrOperand::Const(IrConst::U64(n)) => self.emit(format!("li t1, {}", n)), - IrOperand::Var(var) => { - if let Some(source) = self.schema_field_value_sources.get(&var.id).cloned() { - self.emit_schema_field_source_to_t1(&source); - } else if let Some(source) = self.prelude_u64_value_sources.get(&var.id).cloned() { - self.emit_prelude_u64_value_source_to_t1(&source); - } else if matches!(var.ty, IrType::Bool | IrType::U8 | IrType::U16 | IrType::U32 | IrType::I32 | IrType::U64) { - self.emit_stack_load("t1", var.id * 8); - } else if let Some(value) = self.prelude_scalar_immediates.get(&var.id).copied() { - self.emit(format!("li t1, {}", value)); - } else { - self.emit_stack_load("t1", var.id * 8); - } - } - _ => self.emit("li t1, 0"), - } - } - - fn emit_prelude_u64_value_source_to_t1(&mut self, source: &PreludeU64ValueSource) { - self.emit_prelude_u64_value_source_to_t1_at_depth(source, 0); - } - - fn emit_prelude_u64_value_source_to_t1_at_depth(&mut self, source: &PreludeU64ValueSource, _depth: usize) { - match source { - PreludeU64ValueSource::Const(n) => self.emit(format!("li t1, {}", n)), - PreludeU64ValueSource::ParamVar(var_id) => self.emit_stack_load("t1", var_id * 8), - PreludeU64ValueSource::StackVar(var_id) => self.emit_stack_load("t1", var_id * 8), - PreludeU64ValueSource::Field(source) => self.emit_schema_field_source_to_t1(source), - PreludeU64ValueSource::Binary { op, left, right } => { - self.emit(format!("# cellscript abi: expected expression u64 {:?}", op)); - let Some(temp_offset) = self.checked_runtime_expr_temp_offset(_depth) else { - self.emit("# cellscript abi: fail closed because expression verifier temp stack is exhausted"); - self.emit_fail(CellScriptRuntimeError::DataPreservationMismatch); - return; - }; - self.emit_prelude_u64_value_source_to_t1_at_depth(left, _depth + 1); - self.emit_stack_store("t1", temp_offset); - self.emit_prelude_u64_operand_source_to_t1_at_depth(right, _depth + 1); - self.emit_stack_load("t3", temp_offset); - match op { - BinaryOp::Add => self.emit("add t1, t3, t1"), - BinaryOp::Sub => self.emit("sub t1, t3, t1"), - BinaryOp::Mul => self.emit("mul t1, t3, t1"), - BinaryOp::Div => self.emit("divu t1, t3, t1"), - _ => unreachable!("prelude u64 binary source only supports add/sub/mul/div"), - } - } - PreludeU64ValueSource::Min { left, right } => { - self.emit("# cellscript abi: expected expression u64 min"); - let Some(temp_offset) = self.checked_runtime_expr_temp_offset(_depth) else { - self.emit("# cellscript abi: fail closed because expression verifier temp stack is exhausted"); - self.emit_fail(CellScriptRuntimeError::DataPreservationMismatch); - return; - }; - self.emit_prelude_u64_value_source_to_t1_at_depth(left, _depth + 1); - self.emit_stack_store("t1", temp_offset); - self.emit_prelude_u64_operand_source_to_t1_at_depth(right, _depth + 1); - self.emit_stack_load("t3", temp_offset); - self.emit("slt t2, t3, t1"); - let right_ok_label = self.fresh_label("prelude_min_right_ok"); - self.emit(format!("beqz t2, {}", right_ok_label)); - self.emit("add t1, t3, zero"); - self.emit_label(&right_ok_label); - } - } - } - - fn emit_prelude_u64_operand_source_to_t1(&mut self, source: &PreludeU64OperandSource) { - self.emit_prelude_u64_operand_source_to_t1_at_depth(source, 0); - } - - fn emit_prelude_u64_operand_source_to_t1_at_depth(&mut self, source: &PreludeU64OperandSource, _depth: usize) { - match source { - PreludeU64OperandSource::Const(n) => self.emit(format!("li t1, {}", n)), - PreludeU64OperandSource::ParamVar(var_id) => self.emit_stack_load("t1", var_id * 8), - PreludeU64OperandSource::StackVar(var_id) => self.emit_stack_load("t1", var_id * 8), - PreludeU64OperandSource::Field(source) => self.emit_schema_field_source_to_t1(source), - PreludeU64OperandSource::Expr(source) => self.emit_prelude_u64_value_source_to_t1_at_depth(source, _depth), - } - } - - fn emit_schema_field_source_to_t1(&mut self, source: &SchemaFieldValueSource) { - let context = format!("{}.{}", source.type_name, source.field); - let Some(width) = layout_fixed_scalar_width(&source.layout) else { - self.emit("li t1, 0"); - return; - }; - if !self.type_fixed_sizes.contains_key(&source.type_name) { - if self.emit_schema_field_source_pointer_to("t4", source, width) { - self.emit(format!("# cellscript abi: expected table field {} index={} size={}", context, source.layout.index, width)); - self.emit_unaligned_scalar_load("t4", "t1", "t2", 0, width); - } else { - self.emit("li t1, 0"); - } - return; - } - if let Some(size_offset) = self.schema_pointer_size_offsets.get(&source.obj_var_id).copied() { - if let Some(expected_size) = self.type_fixed_sizes.get(&source.type_name).copied() { - self.emit_loaded_schema_exact_size_check(size_offset, expected_size, &source.type_name); - } - self.emit_loaded_schema_bounds_check(size_offset, source.layout.offset + width, &context); - } - self.emit(format!("# cellscript abi: expected field {} offset={} size={}", context, source.layout.offset, width)); - self.emit_stack_load("t4", source.obj_var_id * 8); - self.emit_unaligned_scalar_load("t4", "t1", "t2", source.layout.offset, width); - } - - fn emit_prepare_schema_field_source(&mut self, source: &SchemaFieldValueSource, width: usize) { - let context = format!("{}.{}", source.type_name, source.field); - let Some(size_offset) = self.schema_pointer_size_offsets.get(&source.obj_var_id).copied() else { - return; - }; - if let Some(expected_size) = self.type_fixed_sizes.get(&source.type_name).copied() { - self.emit_loaded_schema_exact_size_check(size_offset, expected_size, &source.type_name); - self.emit_loaded_schema_bounds_check(size_offset, source.layout.offset + width, &context); - } else { - self.emit_stack_load("t4", source.obj_var_id * 8); - self.emit_molecule_table_field_bounds_to_t5("t4", size_offset, source.layout.index, width, &context); - } - } - - fn emit_schema_field_source_pointer_to(&mut self, dest_reg: &str, source: &SchemaFieldValueSource, width: usize) -> bool { - let context = format!("{}.{}", source.type_name, source.field); - if let Some(size_offset) = self.schema_pointer_size_offsets.get(&source.obj_var_id).copied() { - if let Some(expected_size) = self.type_fixed_sizes.get(&source.type_name).copied() { - self.emit_loaded_schema_exact_size_check(size_offset, expected_size, &source.type_name); - self.emit_loaded_schema_bounds_check(size_offset, source.layout.offset + width, &context); - self.emit_stack_load(dest_reg, source.obj_var_id * 8); - if source.layout.offset != 0 { - self.emit_large_addi(dest_reg, dest_reg, source.layout.offset as i64); - } - } else { - self.emit_stack_load("t4", source.obj_var_id * 8); - self.emit_molecule_table_field_bounds_to_t5("t4", size_offset, source.layout.index, width, &context); - self.emit(format!("add {}, t4, t5", dest_reg)); - } - true - } else if self.aggregate_pointer_sources.contains_key(&source.obj_var_id) - || self.type_fixed_sizes.contains_key(&source.type_name) - { - self.emit_stack_load(dest_reg, source.obj_var_id * 8); - if source.layout.offset != 0 { - self.emit_large_addi(dest_reg, dest_reg, source.layout.offset as i64); - } - true - } else { - false - } - } - - fn can_verify_create_output_fields(&self, pattern: &CreatePattern) -> bool { - if pattern.fields.is_empty() { - return false; - } - if !self.create_output_fields_cover_type(pattern) { - return false; - } - pattern.fields.iter().all(|(field, value)| { - self.type_layouts.get(&pattern.ty).and_then(|layouts| layouts.get(field)).is_some_and(|layout| { - if let Some(width) = layout_fixed_byte_width(layout).or_else(|| self.fixed_named_type_width(&layout.ty)) { - self.is_prelude_available_fixed_value(value, width) - } else { - self.can_verify_dynamic_create_output_field_value(value, layout) - } - }) - }) - } - - fn create_output_fields_cover_type(&self, pattern: &CreatePattern) -> bool { - let Some(layouts) = self.type_layouts.get(&pattern.ty) else { - return false; - }; - let covered_fields = pattern.fields.iter().map(|(field, _)| field.as_str()).collect::>(); - layouts.keys().all(|field| covered_fields.contains(field.as_str())) - } - - fn can_verify_dynamic_create_output_field_value(&self, value: &IrOperand, layout: &SchemaFieldLayout) -> bool { - let IrOperand::Var(var) = value else { - return false; - }; - (self.schema_pointer_vars.contains(&var.id) && self.schema_pointer_size_offsets.contains_key(&var.id)) - || self.constructed_byte_vectors.contains_key(&var.id) - || (self.empty_molecule_vector_vars.contains(&var.id) - && molecule_vector_element_fixed_width(&layout.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes).is_some()) - } - - fn can_verify_output_lock(&self, pattern: &CreatePattern) -> bool { - match &pattern.lock { - Some(lock) => self.expected_fixed_byte_source(lock, 32).is_some(), - None => true, - } - } - - fn emit_create_output_checks(&mut self, pattern: &CreatePattern) { - let size_offset = self.runtime_scratch_size_offset(); - let buffer_offset = self.runtime_scratch_buffer_offset(); - self.emit_create_output_checks_at(pattern, size_offset, buffer_offset); - } - - fn emit_create_output_checks_at(&mut self, pattern: &CreatePattern, size_offset: usize, buffer_offset: usize) { - let is_fixed_type = self.type_fixed_sizes.contains_key(&pattern.ty); - if let Some(expected_size) = self.type_fixed_sizes.get(&pattern.ty).copied() { - self.emit_loaded_schema_exact_size_check(size_offset, expected_size, &pattern.ty); - } - for (field, value) in &pattern.fields { - let Some(layout) = self.type_layouts.get(&pattern.ty).and_then(|fields| fields.get(field)).cloned() else { - continue; - }; - if layout_fixed_byte_width(&layout).or_else(|| self.fixed_named_type_width(&layout.ty)).is_some() { - if is_fixed_type { - self.emit_loaded_field_bytes_equals_expected( - size_offset, - buffer_offset, - &layout, - value, - &format!("{}.{}", pattern.ty, field), - ); - } else { - let Some(field_count) = self.type_layouts.get(&pattern.ty).map(|fields| fields.len()) else { - self.emit_fail(CellScriptRuntimeError::AssertionFailed); - continue; - }; - if !self.emit_dynamic_create_output_fixed_field_equals_expected( - size_offset, - buffer_offset, - &pattern.ty, - field, - &layout, - field_count, - value, - ) { - self.emit_fail(CellScriptRuntimeError::AssertionFailed); - } - } - } else { - let Some(field_count) = self.type_layouts.get(&pattern.ty).map(|fields| fields.len()) else { - self.emit_fail(CellScriptRuntimeError::AssertionFailed); - continue; - }; - if !self.emit_dynamic_create_output_field_equals_expected( - size_offset, - buffer_offset, - &pattern.ty, - field, - &layout, - field_count, - value, - ) { - self.emit_fail(CellScriptRuntimeError::AssertionFailed); - } - } - } - if pattern.operation == "settle" { - self.emit_settle_final_state_check(pattern, size_offset, buffer_offset); - } else { - self.emit_state_transition_check(pattern, size_offset, buffer_offset); - } - } - - fn emit_dynamic_create_output_fixed_field_equals_expected( - &mut self, - output_size_offset: usize, - output_buffer_offset: usize, - type_name: &str, - field: &str, - layout: &SchemaFieldLayout, - field_count: usize, - expected: &IrOperand, - ) -> bool { - let Some(width) = layout_fixed_byte_width(layout).or_else(|| self.fixed_named_type_width(&layout.ty)) else { - return false; - }; - let output_start_offset = self.runtime_expr_temp_offset(0); - let output_len_offset = self.runtime_expr_temp_offset(1); - self.emit_dynamic_table_field_span_to_stack( - output_size_offset, - output_buffer_offset, - layout.index, - field_count, - &format!("{}.{}", type_name, field), - output_start_offset, - output_len_offset, - ); - self.emit_stack_load("t0", output_len_offset); - self.emit(format!("li t1, {}", width)); - self.emit("sub t2, t0, t1"); - let len_ok = self.fresh_label("create_fixed_table_field_len_ok"); - self.emit(format!("beqz t2, {}", len_ok)); - self.emit_fail(CellScriptRuntimeError::CellLoadFailed); - self.emit_label(&len_ok); - - if layout_fixed_scalar_width(layout).is_some() { - self.emit(format!( - "# cellscript abi: verify output Molecule table scalar field {}.{} index={} size={}", - type_name, field, layout.index, width - )); - self.emit_stack_load("t4", output_start_offset); - self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, width); - let actual_value_offset = self.runtime_expr_temp_offset(RUNTIME_EXPR_TEMP_SLOTS - 1); - self.emit("# cellscript abi: preserve output table scalar before expected expression"); - self.emit_stack_store("t0", actual_value_offset); - self.emit_expected_operand_to_t1(expected); - self.emit_stack_load("t0", actual_value_offset); - self.emit("sub t2, t0, t1"); - let ok_label = self.fresh_label("output_table_field_ok"); - self.emit(format!("beqz t2, {}", ok_label)); - self.emit_fail(CellScriptRuntimeError::CellLoadFailed); - self.emit_label(&ok_label); - return true; - } - - let Some(source) = self.expected_fixed_byte_source(expected, width) else { - return false; - }; - self.emit(format!( - "# cellscript abi: verify output Molecule table bytes field {}.{} index={} size={}", - type_name, field, layout.index, width - )); - self.emit_prepare_fixed_byte_source(&source, width, &format!("{}.{}", type_name, field)); - self.emit_pointer_fixed_bytes_against_source( - output_start_offset, - 0, - &source, - width, - CellScriptRuntimeError::DynamicFieldValueMismatch, - ); - true - } - - fn emit_dynamic_create_output_field_equals_expected( - &mut self, - output_size_offset: usize, - output_buffer_offset: usize, - type_name: &str, - field: &str, - layout: &SchemaFieldLayout, - field_count: usize, - expected: &IrOperand, - ) -> bool { - let IrOperand::Var(var) = expected else { - return false; - }; - let output_start_offset = self.runtime_expr_temp_offset(0); - let output_len_offset = self.runtime_expr_temp_offset(1); - self.emit_dynamic_table_field_span_to_stack( - output_size_offset, - output_buffer_offset, - layout.index, - field_count, - &format!("{}.{}", type_name, field), - output_start_offset, - output_len_offset, - ); - if let Some(parts) = self.constructed_byte_vectors.get(&var.id).cloned() { - if let Some(element_width) = - molecule_vector_element_fixed_width(&layout.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) - { - if parts.is_empty() && element_width != 1 { - self.emit_empty_molecule_vector_field_check(type_name, field, output_start_offset, output_len_offset); - return true; - } - self.emit_constructed_molecule_vector_field_check( - type_name, - field, - output_start_offset, - output_len_offset, - &parts, - element_width, - ); - return true; - } - } - if self.empty_molecule_vector_vars.contains(&var.id) - && molecule_vector_element_fixed_width(&layout.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes).is_some() - { - self.emit_empty_molecule_vector_field_check(type_name, field, output_start_offset, output_len_offset); - return true; - } - if !self.schema_pointer_vars.contains(&var.id) { - return false; - } - let Some(expected_size_offset) = self.schema_pointer_size_offsets.get(&var.id).copied() else { - return false; - }; - self.emit_stack_load("t0", output_len_offset); - self.emit_stack_load("t1", expected_size_offset); - self.emit("sub t2, t0, t1"); - let len_ok = self.fresh_label("create_dynamic_field_len_ok"); - self.emit(format!("beqz t2, {}", len_ok)); - self.emit_fail(CellScriptRuntimeError::CellLoadFailed); - self.emit_label(&len_ok); - - self.emit(format!("# cellscript abi: verify output dynamic field {}.{} as Molecule bytes", type_name, field)); - let mismatch_label = self.fresh_label("create_dynamic_field_mismatch"); - self.emit_stack_load("a0", output_start_offset); - self.emit_stack_load("a1", var.id * 8); - self.emit_stack_load("a2", output_len_offset); - self.emit("call __cellscript_memcmp_fixed"); - self.emit(format!("bnez a0, {}", mismatch_label)); - self.emit_fixed_byte_mismatch_fail(&mismatch_label, CellScriptRuntimeError::CellLoadFailed); - true - } - - fn emit_empty_molecule_vector_field_check( - &mut self, - type_name: &str, - field: &str, - output_start_offset: usize, - output_len_offset: usize, - ) { - self.emit(format!("# cellscript abi: verify output dynamic field {}.{} as empty Molecule vector", type_name, field)); - self.emit_stack_load("t0", output_len_offset); - self.emit("li t1, 4"); - self.emit("sub t2, t0, t1"); - let len_ok = self.fresh_label("create_empty_vector_len_ok"); - self.emit(format!("beqz t2, {}", len_ok)); - self.emit_fail(CellScriptRuntimeError::CellLoadFailed); - self.emit_label(&len_ok); - self.emit_stack_load("t0", output_start_offset); - for offset in 0..4 { - self.emit(format!("lbu t1, {}(t0)", offset)); - let byte_ok = self.fresh_label("create_empty_vector_byte_ok"); - self.emit(format!("beqz t1, {}", byte_ok)); - self.emit_fail(CellScriptRuntimeError::CellLoadFailed); - self.emit_label(&byte_ok); - } - } - - fn emit_constructed_molecule_vector_field_check( - &mut self, - type_name: &str, - field: &str, - output_start_offset: usize, - output_len_offset: usize, - parts: &[IrOperand], - element_width: usize, - ) { - let Some(expected_bytes) = - parts.iter().try_fold(0usize, |acc, part| self.constructed_byte_vector_part_width(part).map(|width| acc + width)) - else { - self.emit_fail(CellScriptRuntimeError::CellLoadFailed); - return; - }; - if element_width == 0 || expected_bytes % element_width != 0 { - self.emit_fail(CellScriptRuntimeError::CellLoadFailed); - return; - } - let expected_elements = expected_bytes / element_width; - let expected_len = 4 + expected_bytes; - if element_width == 1 { - self.emit(format!( - "# cellscript abi: verify output dynamic field {}.{} as constructed Molecule byte vector len={}", - type_name, field, expected_bytes - )); - } else { - self.emit(format!( - "# cellscript abi: verify output dynamic field {}.{} as constructed Molecule vector elements={} bytes={} element_size={}", - type_name, field, expected_elements, expected_bytes, element_width - )); - } - self.emit_stack_load("t0", output_len_offset); - self.emit(format!("li t1, {}", expected_len)); - self.emit("sub t2, t0, t1"); - let len_ok = self.fresh_label("create_constructed_vector_len_ok"); - self.emit(format!("beqz t2, {}", len_ok)); - self.emit_fail(CellScriptRuntimeError::CellLoadFailed); - self.emit_label(&len_ok); - - self.emit_stack_load("t4", output_start_offset); - for (offset, byte) in (expected_elements as u32).to_le_bytes().iter().enumerate() { - self.emit(format!("lbu t0, {}(t4)", offset)); - self.emit(format!("li t1, {}", byte)); - self.emit("sub t2, t0, t1"); - let byte_ok = self.fresh_label("create_constructed_vector_count_ok"); - self.emit(format!("beqz t2, {}", byte_ok)); - self.emit_fail(CellScriptRuntimeError::CellLoadFailed); - self.emit_label(&byte_ok); - } - - let mut cursor = 4usize; - for part in parts { - let Some(width) = self.constructed_byte_vector_part_width(part) else { - self.emit_fail(CellScriptRuntimeError::CellLoadFailed); - continue; - }; - let Some(source) = self.expected_fixed_byte_source(part, width) else { - self.emit_fail(CellScriptRuntimeError::CellLoadFailed); - continue; - }; - self.emit_prepare_fixed_byte_source(&source, width, &format!("constructed {}.{}", type_name, field)); - self.emit_pointer_fixed_bytes_against_source( - output_start_offset, - cursor, - &source, - width, - CellScriptRuntimeError::CellLoadFailed, - ); - cursor += width; - } - } - - fn emit_output_lock_hash_check(&mut self, output_index: usize, expected: &IrOperand) -> bool { - if self.expected_fixed_byte_source(expected, 32).is_none() { - return false; - } - let size_offset = self.runtime_scratch_size_offset(); - let buffer_offset = self.runtime_scratch_buffer_offset(); - self.emit_load_cell_by_field_syscall_to_offsets( - "output_lock_hash", - CKB_SOURCE_OUTPUT, - output_index, - CKB_CELL_FIELD_LOCK_HASH, - size_offset, - buffer_offset, - RUNTIME_SCRATCH_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_loaded_schema_exact_size_check(size_offset, 32, "output lock hash"); - self.emit("# cellscript abi: verify output lock hash offset=0 size=32"); - let layout = SchemaFieldLayout { index: 0, offset: 0, ty: IrType::Hash, fixed_size: Some(32), fixed_enum_size: None }; - self.emit_loaded_field_bytes_equals_expected(size_offset, buffer_offset, &layout, expected, "output lock hash") - } - - fn emit_state_transition_check(&mut self, pattern: &CreatePattern, output_size_offset: usize, output_buffer_offset: usize) { - let Some(states) = self.flow_states.get(&pattern.ty) else { - return; - }; - let state_count = states.len(); - let action_edges = self.state_transition_edges_for_pattern(pattern); - let Some(consumed_var_id) = self.consumed_var_for_state_transition(&pattern.ty, &action_edges) else { - if !action_edges.is_empty() { - self.emit_fail(CellScriptRuntimeError::FlowTransitionMismatch); - } - return; - }; - let Some(input_size_offset) = self.cell_buffer_size_offsets.get(&consumed_var_id).copied() else { - return; - }; - let Some(input_buffer_offset) = self.cell_buffer_offsets.get(&consumed_var_id).copied() else { - return; - }; - let state_field = self.flow_state_fields.get(&pattern.ty).cloned().unwrap_or_else(|| FLOW_STATE_FIELD_NAME.to_string()); - let Some(state_layout) = self.type_layouts.get(&pattern.ty).and_then(|fields| fields.get(&state_field)).cloned() else { - return; - }; - let Some(width) = layout_flow_state_width(&state_layout) else { - return; - }; - let Some(expected_size) = self.type_fixed_sizes.get(&pattern.ty).copied() else { - return; - }; - - self.emit(format!("# cellscript abi: state transition {}.{} state_count={}", pattern.ty, state_field, state_count)); - self.emit_loaded_schema_exact_size_check(input_size_offset, expected_size, &format!("{} input", pattern.ty)); - self.emit_loaded_schema_bounds_check( - input_size_offset, - state_layout.offset + width, - &format!("{} input.{}", pattern.ty, state_field), - ); - self.emit_loaded_schema_bounds_check( - output_size_offset, - state_layout.offset + width, - &format!("{} output.{}", pattern.ty, state_field), - ); - self.emit_sp_addi("t4", input_buffer_offset); - self.emit_unaligned_scalar_load("t4", "t0", "t2", state_layout.offset, width); - let old_range_ok_label = self.fresh_label("flow_old_state_range_ok"); - self.emit(format!("li t3, {}", state_count)); - self.emit("sltu t2, t0, t3"); - self.emit(format!("bnez t2, {}", old_range_ok_label)); - self.emit_fail(CellScriptRuntimeError::FlowOldStateInvalid); - self.emit_label(&old_range_ok_label); - - self.emit_sp_addi("t4", output_buffer_offset); - self.emit_unaligned_scalar_load("t4", "t1", "t2", state_layout.offset, width); - let ok_label = self.fresh_label("flow_transition_ok"); - let rules = self.state_transition_rules_for_pattern(pattern, &action_edges); - if rules.is_empty() { - self.emit("addi t0, t0, 1"); - self.emit("sub t2, t1, t0"); - self.emit(format!("beqz t2, {}", ok_label)); - } else { - for rule in rules { - let next_rule_label = self.fresh_label("flow_transition_next_rule"); - self.emit(format!("li t3, {}", rule.from_index)); - self.emit("sub t2, t0, t3"); - self.emit(format!("bnez t2, {}", next_rule_label)); - self.emit(format!("li t3, {}", rule.to_index)); - self.emit("sub t2, t1, t3"); - self.emit(format!("beqz t2, {}", ok_label)); - self.emit_label(&next_rule_label); - } - } - self.emit_fail(CellScriptRuntimeError::FlowTransitionMismatch); - self.emit_label(&ok_label); - - let range_ok_label = self.fresh_label("flow_state_range_ok"); - self.emit(format!("li t3, {}", state_count)); - self.emit("sltu t2, t1, t3"); - self.emit(format!("bnez t2, {}", range_ok_label)); - self.emit_fail(CellScriptRuntimeError::FlowNewStateInvalid); - self.emit_label(&range_ok_label); - } - - fn state_transition_edges_for_pattern(&self, pattern: &CreatePattern) -> Vec { - self.current_state_transition_edges - .iter() - .filter(|state_edge| { - state_edge.type_name == pattern.ty - && state_edge.output_binding.as_ref().is_none_or(|binding| binding == &pattern.binding) - }) - .cloned() - .collect() - } - - fn state_transition_rules_for_pattern(&self, pattern: &CreatePattern, action_edges: &[IrStateTransitionEdge]) -> Vec { - if !action_edges.is_empty() { - return action_edges - .iter() - .map(|state_edge| IrFlowRule { - from: state_edge.from.clone(), - to: state_edge.to.clone(), - from_index: state_edge.from_index, - to_index: state_edge.to_index, - }) - .collect(); - } - self.flow_rules.get(&pattern.ty).cloned().unwrap_or_default() - } - - fn consumed_var_for_state_transition(&self, type_name: &str, action_edges: &[IrStateTransitionEdge]) -> Option { - if let Some(binding) = action_edges.iter().filter_map(|state_edge| state_edge.input_binding.as_ref()).next() { - let var_id = self.consume_binding_ids.get(binding).copied()?; - if self.consume_type_names.get(&var_id).is_some_and(|consumed_type| consumed_type == type_name) { - return Some(var_id); - } - return None; - } - self.consumed_var_for_type(type_name) - } - - fn emit_settle_final_state_check(&mut self, pattern: &CreatePattern, output_size_offset: usize, output_buffer_offset: usize) { - let Some(states) = self.flow_states.get(&pattern.ty) else { - return; - }; - if states.len() < 2 { - return; - } - let final_state = states.len() - 1; - let Some(consumed_var_id) = self.consumed_var_for_type(&pattern.ty) else { - return; - }; - let Some(input_size_offset) = self.cell_buffer_size_offsets.get(&consumed_var_id).copied() else { - return; - }; - let Some(input_buffer_offset) = self.cell_buffer_offsets.get(&consumed_var_id).copied() else { - return; - }; - let state_field = self.flow_state_fields.get(&pattern.ty).cloned().unwrap_or_else(|| FLOW_STATE_FIELD_NAME.to_string()); - let Some(state_layout) = self.type_layouts.get(&pattern.ty).and_then(|fields| fields.get(&state_field)).cloned() else { - return; - }; - let Some(width) = layout_flow_state_width(&state_layout) else { - return; - }; - let Some(expected_size) = self.type_fixed_sizes.get(&pattern.ty).copied() else { - return; - }; - - self.emit(format!( - "# cellscript abi: settle final-state {}.{} final_state={} state_count={}", - pattern.ty, - state_field, - final_state, - states.len() - )); - self.emit_loaded_schema_exact_size_check(input_size_offset, expected_size, &format!("{} input", pattern.ty)); - self.emit_loaded_schema_bounds_check( - input_size_offset, - state_layout.offset + width, - &format!("{} input.{}", pattern.ty, state_field), - ); - self.emit_loaded_schema_bounds_check( - output_size_offset, - state_layout.offset + width, - &format!("{} output.{}", pattern.ty, state_field), - ); - - self.emit_sp_addi("t4", input_buffer_offset); - self.emit_unaligned_scalar_load("t4", "t0", "t2", state_layout.offset, width); - self.emit(format!("li t3, {}", final_state)); - self.emit("sub t2, t0, t3"); - let input_ok_label = self.fresh_label("settle_input_final_state_ok"); - self.emit(format!("beqz t2, {}", input_ok_label)); - self.emit_fail(CellScriptRuntimeError::NumericOrDiscriminantInvalid); - self.emit_label(&input_ok_label); - - self.emit_sp_addi("t4", output_buffer_offset); - self.emit_unaligned_scalar_load("t4", "t1", "t2", state_layout.offset, width); - self.emit("sub t2, t1, t3"); - let output_ok_label = self.fresh_label("settle_output_final_state_ok"); - self.emit(format!("beqz t2, {}", output_ok_label)); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - self.emit_label(&output_ok_label); - } - - fn consumed_var_for_type(&self, type_name: &str) -> Option { - self.consume_order - .iter() - .copied() - .find(|var_id| self.consume_type_names.get(var_id).is_some_and(|consumed_type| consumed_type == type_name)) - } - - fn is_prelude_available_scalar(&self, operand: &IrOperand) -> bool { - match operand { - IrOperand::Const(IrConst::Bool(_) | IrConst::U8(_) | IrConst::U16(_) | IrConst::U32(_) | IrConst::U64(_)) => true, - IrOperand::Var(var) => matches!(var.ty, IrType::Bool | IrType::U8 | IrType::U16 | IrType::U32 | IrType::I32 | IrType::U64), - _ => false, - } - } - - fn is_prelude_available_fixed_value(&self, operand: &IrOperand, expected_width: usize) -> bool { - if self.is_prelude_available_scalar(operand) { - return true; - } - self.expected_fixed_byte_source(operand, expected_width).is_some() - } - - fn emit_unaligned_scalar_load(&mut self, base_reg: &str, dest_reg: &str, scratch_reg: &str, offset: usize, width: usize) { - self.emit(format!("li {}, 0", dest_reg)); - for byte_index in 0..width { - self.emit_memory_load_with_avoid("lbu", scratch_reg, base_reg, offset + byte_index, &[dest_reg, scratch_reg, base_reg]); - if byte_index != 0 { - self.emit(format!("slli {}, {}, {}", scratch_reg, scratch_reg, byte_index * 8)); - } - self.emit(format!("or {}, {}, {}", dest_reg, dest_reg, scratch_reg)); - } - } - - fn emit_sign_extend_i32(&mut self, register: &str) { - self.emit(format!("# cellscript abi: sign-extend i32 in {}", register)); - self.emit(format!("slli {}, {}, 32", register, register)); - self.emit(format!("srai {}, {}, 32", register, register)); - } - - fn fresh_label(&mut self, prefix: &str) -> String { - let label = format!(".L{}_{}", prefix, self.next_runtime_label); - self.next_runtime_label += 1; - label - } - - fn emit_param_spills(&mut self, params: &[IrParam]) -> Result<()> { - let mut abi_index = 0usize; - for param in params { - if named_type_name(¶m.ty).is_some() { - self.emit(format!( - "# cellscript abi: schema param {} pointer={} length={}", - param.name, - abi_arg_label(abi_index), - abi_arg_label(abi_index + 1) - )); - self.emit_spill_abi_arg(abi_index, param.binding.id * 8); - if let Some(size_offset) = self.schema_pointer_size_offsets.get(¶m.binding.id).copied() { - self.emit_spill_abi_arg(abi_index + 1, size_offset); - } - abi_index += 2; - if let (Some(pointer_offset), Some(size_offset)) = ( - self.param_type_hash_pointer_offsets.get(¶m.binding.id).copied(), - self.param_type_hash_size_offsets.get(¶m.binding.id).copied(), - ) { - self.emit(format!( - "# cellscript abi: schema param {} type_hash pointer={} length={} size=32", - param.name, - abi_arg_label(abi_index), - abi_arg_label(abi_index + 1) - )); - self.emit_spill_abi_arg(abi_index, pointer_offset); - self.emit_spill_abi_arg(abi_index + 1, size_offset); - abi_index += 2; - } - } else if let Some(width) = fixed_byte_pointer_param_width(¶m.ty) { - self.emit(format!( - "# cellscript abi: fixed-byte param {} pointer={} length={} size={}", - param.name, - abi_arg_label(abi_index), - abi_arg_label(abi_index + 1), - width - )); - self.emit_spill_abi_arg(abi_index, param.binding.id * 8); - if let Some(size_offset) = self.fixed_byte_param_size_offsets.get(¶m.binding.id).copied() { - self.emit_spill_abi_arg(abi_index + 1, size_offset); - } - abi_index += 2; - } else if let Some(width) = fixed_aggregate_pointer_param_width(¶m.ty) { - self.emit(format!( - "# cellscript abi: fixed-aggregate param {} pointer={} length={} size={}", - param.name, - abi_arg_label(abi_index), - abi_arg_label(abi_index + 1), - width - )); - self.emit_spill_abi_arg(abi_index, param.binding.id * 8); - if let Some(size_offset) = self.fixed_byte_param_size_offsets.get(¶m.binding.id).copied() { - self.emit_spill_abi_arg(abi_index + 1, size_offset); - } - abi_index += 2; - } else { - self.emit_spill_abi_arg(abi_index, param.binding.id * 8); - abi_index += 1; - } - } - - Ok(()) - } - - fn emit_spill_abi_arg(&mut self, abi_index: usize, stack_offset: usize) { - if abi_index < 8 { - self.emit_stack_store(&format!("a{}", abi_index), stack_offset); - } else { - let caller_stack_offset = (abi_index - 8) * 8; - self.emit(format!("# cellscript abi: arg{} loaded from caller stack +{}", abi_index, caller_stack_offset)); - self.emit(format!("ld t0, {}(fp)", caller_stack_offset)); - self.emit_stack_store("t0", stack_offset); - } - } - - fn record_instruction_var(&self, instruction: &IrInstruction, max_var_id: &mut Option) { - match instruction { - IrInstruction::LoadConst { dest, .. } - | IrInstruction::LoadVar { dest, .. } - | IrInstruction::Unary { dest, .. } - | IrInstruction::FieldAccess { dest, .. } - | IrInstruction::Index { dest, .. } - | IrInstruction::Length { dest, .. } - | IrInstruction::TypeHash { dest, .. } - | IrInstruction::Create { dest, .. } - | IrInstruction::CreateUnique { dest, .. } - | IrInstruction::ReadRef { dest, .. } => self.record_var(dest, max_var_id), - IrInstruction::CollectionNew { dest, capacity, .. } => { - self.record_var(dest, max_var_id); - if let Some(capacity) = capacity { - self.record_operand(capacity, max_var_id); - } - } - IrInstruction::Move { dest, src } => { - self.record_var(dest, max_var_id); - self.record_operand(src, max_var_id); - } - IrInstruction::Tuple { dest, fields } => { - self.record_var(dest, max_var_id); - for field in fields { - self.record_operand(field, max_var_id); - } - } - IrInstruction::EnumConstruct { dest, fields, .. } => { - self.record_var(dest, max_var_id); - for field in fields { - self.record_operand(field, max_var_id); - } - } - IrInstruction::EnumTag { dest, operand, .. } | IrInstruction::EnumPayload { dest, operand, .. } => { - self.record_var(dest, max_var_id); - self.record_operand(operand, max_var_id); - } - IrInstruction::Binary { dest, left, right, .. } => { - self.record_var(dest, max_var_id); - self.record_operand(left, max_var_id); - self.record_operand(right, max_var_id); - } - IrInstruction::StoreVar { src, .. } => self.record_operand(src, max_var_id), - IrInstruction::Call { dest, args, .. } => { - if let Some(dest) = dest { - self.record_var(dest, max_var_id); - } - for arg in args { - self.record_operand(arg, max_var_id); - } - } - IrInstruction::Consume { operand } | IrInstruction::Destroy { operand, policy: _ } => { - self.record_operand(operand, max_var_id) - } - IrInstruction::Transfer { dest, operand, to } => { - self.record_var(dest, max_var_id); - self.record_operand(operand, max_var_id); - self.record_operand(to, max_var_id); - } - IrInstruction::Claim { dest, receipt } => { - self.record_var(dest, max_var_id); - self.record_operand(receipt, max_var_id); - } - IrInstruction::Settle { dest, operand } => { - self.record_var(dest, max_var_id); - self.record_operand(operand, max_var_id) - } - IrInstruction::ReplaceUnique { dest, operand, .. } => { - self.record_var(dest, max_var_id); - self.record_operand(operand, max_var_id) - } - IrInstruction::CellMetadataEquality { left, right, .. } => { - self.record_operand(left, max_var_id); - self.record_operand(right, max_var_id); - } - IrInstruction::CollectionPush { collection, value } => { - self.record_operand(collection, max_var_id); - self.record_operand(value, max_var_id); - } - IrInstruction::CollectionCapacity { dest, collection } => { - self.record_var(dest, max_var_id); - self.record_operand(collection, max_var_id); - } - IrInstruction::CollectionExtend { collection, slice } => { - self.record_operand(collection, max_var_id); - self.record_operand(slice, max_var_id); - } - IrInstruction::CollectionClear { collection } => { - self.record_operand(collection, max_var_id); - } - IrInstruction::CollectionReverse { collection } => { - self.record_operand(collection, max_var_id); - } - IrInstruction::CollectionTruncate { collection, len } => { - self.record_operand(collection, max_var_id); - self.record_operand(len, max_var_id); - } - IrInstruction::CollectionSwap { collection, left, right } => { - self.record_operand(collection, max_var_id); - self.record_operand(left, max_var_id); - self.record_operand(right, max_var_id); - } - IrInstruction::CollectionContains { dest, collection, value } => { - self.record_var(dest, max_var_id); - self.record_operand(collection, max_var_id); - self.record_operand(value, max_var_id); - } - IrInstruction::CollectionRemove { dest, collection, index } => { - self.record_var(dest, max_var_id); - self.record_operand(collection, max_var_id); - self.record_operand(index, max_var_id); - } - IrInstruction::CollectionInsert { collection, index, value } => { - self.record_operand(collection, max_var_id); - self.record_operand(index, max_var_id); - self.record_operand(value, max_var_id); - } - IrInstruction::CollectionSet { collection, index, value } => { - self.record_operand(collection, max_var_id); - self.record_operand(index, max_var_id); - self.record_operand(value, max_var_id); - } - IrInstruction::CollectionPop { dest, collection } => { - self.record_var(dest, max_var_id); - self.record_operand(collection, max_var_id); - } - } - } - - fn record_instruction_fixed_byte_local(&self, instruction: &IrInstruction, offsets: &mut HashMap) { - let record = |offsets: &mut HashMap, var: &IrVar| { - if var.ty == IrType::U128 { - offsets.insert(var.id, 16); - } - if let Some(width) = fixed_byte_width(&var.ty, type_static_length(&var.ty)).filter(|width| *width > 8) { - offsets.insert(var.id, width); - } - if let Some(width) = self.fixed_named_type_width(&var.ty) { - offsets.insert(var.id, width); - } - }; - - match instruction { - IrInstruction::LoadConst { dest, .. } - | IrInstruction::LoadVar { dest, .. } - | IrInstruction::Unary { dest, .. } - | IrInstruction::FieldAccess { dest, .. } - | IrInstruction::Index { dest, .. } - | IrInstruction::Length { dest, .. } - | IrInstruction::TypeHash { dest, .. } - | IrInstruction::Create { dest, .. } - | IrInstruction::CreateUnique { dest, .. } - | IrInstruction::ReplaceUnique { dest, .. } - | IrInstruction::Transfer { dest, .. } - | IrInstruction::Claim { dest, .. } - | IrInstruction::Settle { dest, .. } - | IrInstruction::ReadRef { dest, .. } - | IrInstruction::CollectionCapacity { dest, .. } - | IrInstruction::CollectionContains { dest, .. } - | IrInstruction::CollectionRemove { dest, .. } - | IrInstruction::CollectionPop { dest, .. } - | IrInstruction::CollectionNew { dest, .. } - | IrInstruction::Move { dest, .. } - | IrInstruction::Tuple { dest, .. } - | IrInstruction::EnumConstruct { dest, .. } - | IrInstruction::EnumTag { dest, .. } - | IrInstruction::Binary { dest, .. } => record(offsets, dest), - IrInstruction::EnumPayload { dest, enum_name, variant, field_index, .. } => { - record(offsets, dest); - if let Some(field) = self - .enum_layouts - .get(enum_name) - .and_then(|layout| layout.variants.iter().find(|candidate| candidate.name == *variant)) - .and_then(|variant| variant.fields.get(*field_index)) - { - if !field.linear && !is_fixed_scalar_ir_type(&field.ty) && field.width > 0 { - offsets.insert(dest.id, field.width); - } - } - } - IrInstruction::Call { dest, func, .. } => { - if let Some(dest) = dest { - if is_ckb_fixed_hash_helper(func) && dest.ty == IrType::Hash { - offsets.insert(dest.id, 32); - } - record(offsets, dest); - } - } - IrInstruction::StoreVar { .. } - | IrInstruction::Consume { .. } - | IrInstruction::Destroy { .. } - | IrInstruction::CellMetadataEquality { .. } - | IrInstruction::CollectionPush { .. } - | IrInstruction::CollectionExtend { .. } - | IrInstruction::CollectionClear { .. } - | IrInstruction::CollectionReverse { .. } - | IrInstruction::CollectionTruncate { .. } - | IrInstruction::CollectionSwap { .. } - | IrInstruction::CollectionInsert { .. } - | IrInstruction::CollectionSet { .. } => {} - } - } - - fn record_terminator_var(&self, terminator: &IrTerminator, max_var_id: &mut Option) { - match terminator { - IrTerminator::Return(Some(operand)) | IrTerminator::Branch { cond: operand, .. } => { - self.record_operand(operand, max_var_id) - } - IrTerminator::Return(None) | IrTerminator::Jump(_) => {} - } - } - - fn collect_u128_instruction_vars(&self, instruction: &IrInstruction, out: &mut BTreeSet) { - match instruction { - IrInstruction::LoadConst { dest, .. } - | IrInstruction::LoadVar { dest, .. } - | IrInstruction::Unary { dest, .. } - | IrInstruction::FieldAccess { dest, .. } - | IrInstruction::Index { dest, .. } - | IrInstruction::Length { dest, .. } - | IrInstruction::TypeHash { dest, .. } - | IrInstruction::Create { dest, .. } - | IrInstruction::CreateUnique { dest, .. } - | IrInstruction::ReplaceUnique { dest, .. } - | IrInstruction::Claim { dest, .. } - | IrInstruction::ReadRef { dest, .. } - | IrInstruction::CollectionCapacity { dest, .. } - | IrInstruction::CollectionContains { dest, .. } - | IrInstruction::CollectionRemove { dest, .. } - | IrInstruction::CollectionPop { dest, .. } - | IrInstruction::Settle { dest, .. } - | IrInstruction::Transfer { dest, .. } - | IrInstruction::Move { dest, .. } - | IrInstruction::Tuple { dest, .. } - | IrInstruction::EnumConstruct { dest, .. } - | IrInstruction::EnumTag { dest, .. } - | IrInstruction::EnumPayload { dest, .. } - | IrInstruction::Binary { dest, .. } - | IrInstruction::Call { dest: Some(dest), .. } => { - if dest.ty == IrType::U128 { - out.insert(dest.id); - } - } - IrInstruction::CollectionNew { dest, .. } => { - if dest.ty == IrType::U128 { - out.insert(dest.id); - } - } - IrInstruction::StoreVar { .. } - | IrInstruction::Call { dest: None, .. } - | IrInstruction::Consume { .. } - | IrInstruction::Destroy { .. } - | IrInstruction::CellMetadataEquality { .. } - | IrInstruction::CollectionPush { .. } - | IrInstruction::CollectionExtend { .. } - | IrInstruction::CollectionClear { .. } - | IrInstruction::CollectionReverse { .. } - | IrInstruction::CollectionTruncate { .. } - | IrInstruction::CollectionSwap { .. } - | IrInstruction::CollectionInsert { .. } - | IrInstruction::CollectionSet { .. } => {} - } - } - - fn collect_u128_terminator_vars(&self, terminator: &IrTerminator, out: &mut BTreeSet) { - if let IrTerminator::Return(Some(IrOperand::Var(var))) = terminator { - if var.ty == IrType::U128 { - out.insert(var.id); - } - } - } - - fn record_operand(&self, operand: &IrOperand, max_var_id: &mut Option) { - if let IrOperand::Var(var) = operand { - self.record_var(var, max_var_id); - } - } - - fn record_var(&self, var: &IrVar, max_var_id: &mut Option) { - *max_var_id = Some(max_var_id.map(|current| current.max(var.id)).unwrap_or(var.id)); - } - - fn const_as_u128(value: &IrConst) -> Option { - match value { - IrConst::U8(value) => Some((*value).into()), - IrConst::U16(value) => Some((*value).into()), - IrConst::U32(value) => Some((*value).into()), - IrConst::U64(value) => Some((*value).into()), - IrConst::U128(value) => Some(*value), - _ => None, - } - } - - fn expected_u128_source(&self, operand: &IrOperand) -> Option { - match operand { - IrOperand::Const(value) => { - Self::const_as_u128(value).map(|value| ExpectedFixedByteSource::Const(value.to_le_bytes().to_vec())) - } - _ => self.expected_fixed_byte_source(operand, 16), - } - } - - fn emit_store_byte_to_stack_offset(&mut self, src_reg: &str, offset: usize) { - self.emit_stack_store_byte(src_reg, offset); - } - - fn emit_store_u128_const_to_stack_offset(&mut self, value: u128, offset: usize) { - self.emit(format!("# cellscript abi: materialize u128 const at stack+{}", offset)); - for (index, byte) in value.to_le_bytes().iter().enumerate() { - self.emit(format!("li t0, {}", byte)); - self.emit_store_byte_to_stack_offset("t0", offset + index); - } - } - - fn emit_store_u128_pointer_for_var(&mut self, var_id: usize, offset: usize) { - self.emit_sp_addi("t0", offset); - self.emit_stack_store("t0", var_id * 8); - } - - fn emit_materialize_u128_operand_to_var(&mut self, dest: &IrVar, src: &IrOperand) -> bool { - let Some(dest_offset) = self.u128_value_offsets.get(&dest.id).copied() else { - self.emit("# cellscript abi: u128 destination has no 16-byte storage; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return true; - }; - if let IrOperand::Const(value) = src { - if let Some(value) = Self::const_as_u128(value) { - self.emit_store_u128_const_to_stack_offset(value, dest_offset); - self.emit_store_u128_pointer_for_var(dest.id, dest_offset); - return true; - } - } - let Some(source) = self.expected_u128_source(src) else { - self.emit("# cellscript abi: u128 source is not addressable; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return true; - }; - self.emit_prepare_fixed_byte_source(&source, 16, "u128 materialize"); - self.emit(format!("# cellscript abi: materialize u128 operand into var{}", dest.id)); - for byte_index in 0..16 { - self.emit_fixed_byte_source_byte_to("t0", "t4", &source, byte_index); - self.emit_store_byte_to_stack_offset("t0", dest_offset + byte_index); - } - self.emit_store_u128_pointer_for_var(dest.id, dest_offset); - true - } - - fn emit_u64_le_from_fixed_byte_source( - &mut self, - dest_reg: &str, - scratch_reg: &str, - base_reg: &str, - source: &ExpectedFixedByteSource, - start: usize, - ) { - self.emit(format!("li {}, 0", dest_reg)); - for byte_offset in 0..8 { - self.emit_fixed_byte_source_byte_to(scratch_reg, base_reg, source, start + byte_offset); - if byte_offset != 0 { - self.emit(format!("slli {}, {}, {}", scratch_reg, scratch_reg, byte_offset * 8)); - } - self.emit(format!("or {}, {}, {}", dest_reg, dest_reg, scratch_reg)); - } - } - - fn emit_u128_operand_limbs( - &mut self, - low_reg: &str, - high_reg: &str, - scratch_reg: &str, - base_reg: &str, - operand: &IrOperand, - context: &str, - ) -> bool { - let Some(source) = self.expected_u128_source(operand) else { - self.emit(format!("# cellscript abi: {} u128 operand is not addressable; fail closed", context)); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return false; - }; - self.emit_prepare_fixed_byte_source(&source, 16, context); - self.emit_u64_le_from_fixed_byte_source(low_reg, scratch_reg, base_reg, &source, 0); - self.emit_u64_le_from_fixed_byte_source(high_reg, scratch_reg, base_reg, &source, 8); - true - } - - fn operand_is_u128_like(&self, operand: &IrOperand) -> bool { - match operand { - IrOperand::Var(var) => var.ty == IrType::U128, - IrOperand::Const(IrConst::U128(_)) => true, - _ => false, - } - } - - fn emit_store_const_bytes_to_stack(&mut self, bytes: &[u8], offset: usize) { - for (index, byte) in bytes.iter().enumerate() { - self.emit(format!("li t0, {}", byte)); - self.emit_stack_store_byte("t0", offset + index); - } - } - - fn emit_load_const(&mut self, dest: &IrVar, value: &IrConst) -> Result<()> { - if dest.ty == IrType::U128 { - self.emit_materialize_u128_operand_to_var(dest, &IrOperand::Const(value.clone())); - return Ok(()); - } - match value { - IrConst::Unit => self.emit("li t0, 0"), - IrConst::U8(n) => self.emit(format!("li t0, {}", n)), - IrConst::U16(n) => self.emit(format!("li t0, {}", n)), - IrConst::U32(n) => self.emit(format!("li t0, {}", n)), - IrConst::U64(n) => self.emit(format!("li t0, {}", n)), - IrConst::U128(value) => { - if let Some(offset) = self.fixed_byte_local_offsets.get(&dest.id).copied() { - self.emit_store_const_bytes_to_stack(&value.to_le_bytes(), offset); - self.emit_sp_addi("t0", offset); - self.emit_stack_store("t0", dest.id * 8); - return Ok(()); - } - let label = self.const_data_label_for_bytes(value.to_le_bytes().to_vec()); - self.emit(format!("la t0, {}", label)); - } - IrConst::Bool(b) => self.emit(format!("li t0, {}", if *b { 1 } else { 0 })), - IrConst::Address(_) | IrConst::Hash(_) | IrConst::Array(_) => { - let Some(bytes) = fixed_byte_const_bytes(value) else { - self.emit("# cellscript abi: fail closed because fixed-byte constant bytes are not materializable"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - self.emit("li t0, 0"); - self.emit_stack_store("t0", dest.id * 8); - return Ok(()); - }; - let label = self.const_data_label_for_bytes(bytes); - self.emit(format!("la t0, {}", label)); - } - } - self.emit_stack_store("t0", dest.id * 8); - Ok(()) - } - - fn emit_load_var(&mut self, dest: &IrVar, name: &str) -> Result<()> { - self.emit(format!("# load var {}", name)); - let Some(offset) = self.named_var_offsets.get(name).copied() else { - self.emit("# cellscript abi: fail closed because named variable slot was not allocated"); - self.emit_fail(CellScriptRuntimeError::ConsumeInvalidOperand); - return Ok(()); - }; - self.emit_stack_load("t0", offset); - self.emit_stack_store("t0", dest.id * 8); - Ok(()) - } - - fn emit_store_var(&mut self, name: &str, src: &IrOperand) -> Result<()> { - self.emit(format!("# store var {}", name)); - let Some(offset) = self.named_var_offsets.get(name).copied() else { - self.emit("# cellscript abi: fail closed because named variable slot was not allocated"); - self.emit_fail(CellScriptRuntimeError::ConsumeInvalidOperand); - return Ok(()); - }; - self.emit_operand_to_register("t0", src); - self.emit_stack_store("t0", offset); - Ok(()) - } - - fn emit_binary(&mut self, dest: &IrVar, op: BinaryOp, left: &IrOperand, right: &IrOperand) -> Result<()> { - if self.emit_u128_add_sub_with_u64(dest, op, left, right) { - return Ok(()); - } - if self.emit_u128_binary(dest, op, left, right) { - return Ok(()); - } - if matches!(op, BinaryOp::Eq | BinaryOp::Ne) && self.emit_dynamic_byte_comparison(dest, op, left, right) { - return Ok(()); - } - if matches!(op, BinaryOp::Eq | BinaryOp::Ne) - && (operand_fixed_byte_width(left).is_some() || operand_fixed_byte_width(right).is_some()) - { - if self.emit_fixed_byte_comparison(dest, op, left, right) { - return Ok(()); - } - if self.emit_generic_fixed_byte_comparison(dest, op, left, right) { - return Ok(()); - } - // Final fallback: emit a fail-closed trap with specific error code - self.emit(format!("# binary {:?} over fixed-byte operands (unresolved)", op)); - self.emit("# cellscript abi: fail closed because fixed-byte operand sources are not available"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonMaterializationUnresolved); - return Ok(()); - } - - if dest.ty == IrType::U128 || self.operand_is_u128(left) || self.operand_is_u128(right) { - self.emit(format!("# binary {:?} over unsupported u128 operand shape", op)); - self.emit("# cellscript abi: fail closed because generic u128 arithmetic/comparison shape is not lowered"); - self.emit_fail(CellScriptRuntimeError::NumericOrDiscriminantInvalid); - return Ok(()); - } - - self.emit_expected_operand_to_t1(left); - self.emit_stack_store("t1", dest.id * 8); - self.emit_expected_operand_to_t1(right); - self.emit_stack_load("t0", dest.id * 8); - - match op { - BinaryOp::Add => self.emit("add t0, t0, t1"), - BinaryOp::Sub => self.emit("sub t0, t0, t1"), - BinaryOp::Mul => self.emit("mul t0, t0, t1"), - BinaryOp::Div if binary_operands_signed_i32(left, right) => self.emit("div t0, t0, t1"), - BinaryOp::Div => self.emit("divu t0, t0, t1"), - BinaryOp::Mod if binary_operands_signed_i32(left, right) => self.emit("rem t0, t0, t1"), - BinaryOp::Mod => self.emit("remu t0, t0, t1"), - BinaryOp::Eq => { - self.emit("sub t0, t0, t1"); - self.emit("seqz t0, t0"); - } - BinaryOp::Ne => { - self.emit("sub t0, t0, t1"); - self.emit("snez t0, t0"); - } - BinaryOp::Lt if binary_operands_signed_i32(left, right) => self.emit("slt t0, t0, t1"), - BinaryOp::Lt => self.emit("sltu t0, t0, t1"), - BinaryOp::Le if binary_operands_signed_i32(left, right) => { - self.emit("slt t0, t1, t0"); - self.emit("xori t0, t0, 1"); - } - BinaryOp::Le => { - self.emit("sltu t0, t1, t0"); - self.emit("xori t0, t0, 1"); - } - BinaryOp::Gt if binary_operands_signed_i32(left, right) => self.emit("slt t0, t1, t0"), - BinaryOp::Gt => self.emit("sltu t0, t1, t0"), - BinaryOp::Ge if binary_operands_signed_i32(left, right) => { - self.emit("slt t0, t0, t1"); - self.emit("xori t0, t0, 1"); - } - BinaryOp::Ge => { - self.emit("sltu t0, t0, t1"); - self.emit("xori t0, t0, 1"); - } - BinaryOp::And => self.emit("and t0, t0, t1"), - BinaryOp::Or => self.emit("or t0, t0, t1"), - } - - self.emit_stack_store("t0", dest.id * 8); - Ok(()) - } - - fn emit_u128_binary(&mut self, dest: &IrVar, op: BinaryOp, left: &IrOperand, right: &IrOperand) -> bool { - let arithmetic_u128 = dest.ty == IrType::U128 || self.operand_is_u128_like(left) || self.operand_is_u128_like(right); - let comparison_u128 = matches!(op, BinaryOp::Eq | BinaryOp::Ne | BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge) - && (self.operand_is_u128_like(left) || self.operand_is_u128_like(right)); - if !arithmetic_u128 && !comparison_u128 { - return false; - } - - match op { - BinaryOp::Add | BinaryOp::Sub if dest.ty == IrType::U128 => { - self.emit_u128_add_sub(dest, op, left, right); - true - } - BinaryOp::Eq | BinaryOp::Ne | BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge => { - self.emit_u128_compare(dest, op, left, right); - true - } - BinaryOp::Mul if dest.ty == IrType::U128 => { - self.emit_u128_mul(dest, left, right); - true - } - BinaryOp::Div if dest.ty == IrType::U128 => { - self.emit_u128_div(dest, left, right); - true - } - BinaryOp::Mod if arithmetic_u128 => { - self.emit("# cellscript abi: u128 Mod requires full-width lowering; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - true - } - BinaryOp::Add | BinaryOp::Sub if arithmetic_u128 => { - self.emit(format!("# cellscript abi: u128 {:?} result is not materialized as u128; fail closed", op)); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - true - } - _ => false, - } - } - - fn emit_u128_add_sub(&mut self, dest: &IrVar, op: BinaryOp, left: &IrOperand, right: &IrOperand) { - let Some(dest_offset) = self.u128_value_offsets.get(&dest.id).copied() else { - self.emit("# cellscript abi: u128 arithmetic destination has no storage; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return; - }; - if !self.emit_u128_operand_limbs("t0", "t1", "t6", "t4", left, "u128 arithmetic left") { - return; - } - if !self.emit_u128_operand_limbs("t2", "t3", "t6", "t5", right, "u128 arithmetic right") { - return; - } - let ok_label = self.fresh_label("u128_arithmetic_ok"); - let overflow_label = self.fresh_label("u128_arithmetic_overflow"); - match op { - BinaryOp::Add => { - self.emit("# cellscript abi: u128 add with carry"); - self.emit("add t4, t0, t2"); - self.emit("sltu t6, t4, t0"); - self.emit("add t5, t1, t3"); - self.emit("sltu a6, t5, t1"); - self.emit(format!("bnez a6, {}", overflow_label)); - self.emit("add t5, t5, t6"); - self.emit("sltu a6, t5, t6"); - self.emit(format!("bnez a6, {}", overflow_label)); - } - BinaryOp::Sub => { - self.emit("# cellscript abi: u128 sub with borrow"); - self.emit("sltu t6, t0, t2"); - self.emit("sltu a6, t1, t3"); - self.emit(format!("bnez a6, {}", overflow_label)); - self.emit("sub t4, t0, t2"); - self.emit("sub t5, t1, t3"); - self.emit(format!("beqz t6, {}", ok_label)); - self.emit(format!("beqz t5, {}", overflow_label)); - self.emit("addi t5, t5, -1"); - } - _ => unreachable!("u128 add/sub only"), - } - self.emit_label(&ok_label); - self.emit_stack_store("t4", dest_offset); - self.emit_stack_store("t5", dest_offset + 8); - self.emit_store_u128_pointer_for_var(dest.id, dest_offset); - let done_label = self.fresh_label("u128_arithmetic_done"); - self.emit(format!("j {}", done_label)); - self.emit_label(&overflow_label); - self.emit_runtime_error_comment(CellScriptRuntimeError::AggregateAmountMismatch); - self.emit(format!("li a0, {}", CellScriptRuntimeError::AggregateAmountMismatch.code())); - self.emit_epilogue(); - self.emit_label(&done_label); - } - - fn emit_u128_compare(&mut self, dest: &IrVar, op: BinaryOp, left: &IrOperand, right: &IrOperand) { - if !self.emit_u128_operand_limbs("t0", "t1", "t6", "t4", left, "u128 compare left") { - return; - } - if !self.emit_u128_operand_limbs("t2", "t3", "t6", "t5", right, "u128 compare right") { - return; - } - self.emit("# cellscript abi: u128 compare high limb first"); - let high_lt = self.fresh_label("u128_compare_high_lt"); - let high_gt = self.fresh_label("u128_compare_high_gt"); - let same_high = self.fresh_label("u128_compare_same_high"); - let done = self.fresh_label("u128_compare_done"); - self.emit("sltu t4, t1, t3"); - self.emit(format!("bnez t4, {}", high_lt)); - self.emit("sltu t4, t3, t1"); - self.emit(format!("bnez t4, {}", high_gt)); - self.emit_label(&same_high); - match op { - BinaryOp::Eq => { - self.emit("sub t4, t0, t2"); - self.emit("seqz t0, t4"); - } - BinaryOp::Ne => { - self.emit("sub t4, t0, t2"); - self.emit("snez t0, t4"); - } - BinaryOp::Lt => self.emit("sltu t0, t0, t2"), - BinaryOp::Le => { - self.emit("sltu t0, t2, t0"); - self.emit("xori t0, t0, 1"); - } - BinaryOp::Gt => self.emit("sltu t0, t2, t0"), - BinaryOp::Ge => { - self.emit("sltu t0, t0, t2"); - self.emit("xori t0, t0, 1"); - } - _ => unreachable!("u128 compare only"), - } - self.emit(format!("j {}", done)); - self.emit_label(&high_lt); - let high_lt_value = matches!(op, BinaryOp::Ne | BinaryOp::Lt | BinaryOp::Le); - self.emit(format!("li t0, {}", u8::from(high_lt_value))); - self.emit(format!("j {}", done)); - self.emit_label(&high_gt); - let high_gt_value = matches!(op, BinaryOp::Ne | BinaryOp::Gt | BinaryOp::Ge); - self.emit(format!("li t0, {}", u8::from(high_gt_value))); - self.emit_label(&done); - self.emit_stack_store("t0", dest.id * 8); - } - - fn emit_u128_mul(&mut self, dest: &IrVar, left: &IrOperand, right: &IrOperand) { - let Some(dest_offset) = self.u128_value_offsets.get(&dest.id).copied() else { - self.emit("# cellscript abi: u128 multiplication destination has no storage; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return; - }; - if !self.emit_u128_operand_limbs("t0", "t1", "t6", "t4", left, "u128 multiplication left") { - return; - } - if !self.emit_u128_operand_limbs("t2", "t3", "t6", "t5", right, "u128 multiplication right") { - return; - } - self.emit("# cellscript abi: checked u128 multiplication"); - let overflow_label = self.fresh_label("u128_mul_overflow"); - let high_left_zero = self.fresh_label("u128_mul_high_left_zero"); - let high_pair_ok = self.fresh_label("u128_mul_high_pair_ok"); - let done_label = self.fresh_label("u128_mul_done"); - - self.emit(format!("beqz t1, {}", high_left_zero)); - self.emit(format!("bnez t3, {}", overflow_label)); - self.emit_label(&high_left_zero); - self.emit(format!("beqz t3, {}", high_pair_ok)); - self.emit(format!("bnez t1, {}", overflow_label)); - self.emit_label(&high_pair_ok); - - self.emit("mul t4, t0, t2"); - self.emit("mulhu a2, t0, t2"); - - self.emit("mul a3, t0, t3"); - self.emit("mulhu a4, t0, t3"); - self.emit(format!("bnez a4, {}", overflow_label)); - - self.emit("mul a5, t1, t2"); - self.emit("mulhu a6, t1, t2"); - self.emit(format!("bnez a6, {}", overflow_label)); - - self.emit("add t5, a2, a3"); - self.emit("sltu a7, t5, a2"); - self.emit(format!("bnez a7, {}", overflow_label)); - self.emit("add t5, t5, a5"); - self.emit("sltu a7, t5, a5"); - self.emit(format!("bnez a7, {}", overflow_label)); - - self.emit_stack_store("t4", dest_offset); - self.emit_stack_store("t5", dest_offset + 8); - self.emit_store_u128_pointer_for_var(dest.id, dest_offset); - self.emit(format!("j {}", done_label)); - - self.emit_label(&overflow_label); - self.emit_runtime_error_comment(CellScriptRuntimeError::AggregateAmountMismatch); - self.emit(format!("li a0, {}", CellScriptRuntimeError::AggregateAmountMismatch.code())); - self.emit_epilogue(); - self.emit_label(&done_label); - } - - fn emit_u128_div(&mut self, dest: &IrVar, left: &IrOperand, right: &IrOperand) { - let Some(dest_offset) = self.u128_value_offsets.get(&dest.id).copied() else { - self.emit("# cellscript abi: u128 division destination has no storage; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return; - }; - if !self.emit_u128_operand_limbs("t0", "t1", "t6", "t4", left, "u128 division numerator") { - return; - } - if !self.emit_u128_operand_limbs("t2", "t3", "t6", "t5", right, "u128 division denominator") { - return; - } - self.emit("# cellscript abi: checked u128 division by restoring long division"); - let ok_label = self.fresh_label("u128_div_denominator_ok"); - let loop_label = self.fresh_label("u128_div_loop"); - let skip_sub_label = self.fresh_label("u128_div_skip_subtract"); - let subtract_label = self.fresh_label("u128_div_subtract"); - let done_label = self.fresh_label("u128_div_done"); - let fail_label = self.fresh_label("u128_div_zero_denominator"); - - self.emit("or t4, t2, t3"); - self.emit(format!("bnez t4, {}", ok_label)); - self.emit(format!("j {}", fail_label)); - self.emit_label(&ok_label); - self.emit("li t4, 0"); // remainder low - self.emit("li t5, 0"); // remainder high - self.emit("li a2, 0"); // quotient low - self.emit("li a3, 0"); // quotient high - self.emit("li a4, 128"); - self.emit_label(&loop_label); - - self.emit("slt a5, t1, zero"); // next numerator bit - self.emit("slt a6, t4, zero"); // carry from remainder low - self.emit("slli t4, t4, 1"); - self.emit("or t4, t4, a5"); - self.emit("slli t5, t5, 1"); - self.emit("or t5, t5, a6"); - - self.emit("slt a5, t0, zero"); // carry from numerator low - self.emit("slli t0, t0, 1"); - self.emit("slli t1, t1, 1"); - self.emit("or t1, t1, a5"); - - self.emit("slt a5, a2, zero"); // carry from quotient low - self.emit("slli a2, a2, 1"); - self.emit("slli a3, a3, 1"); - self.emit("or a3, a3, a5"); - - self.emit("sltu a5, t5, t3"); - self.emit(format!("bnez a5, {}", skip_sub_label)); - self.emit("sltu a5, t3, t5"); - self.emit(format!("bnez a5, {}", subtract_label)); - self.emit("sltu a5, t4, t2"); - self.emit(format!("bnez a5, {}", skip_sub_label)); - - self.emit_label(&subtract_label); - self.emit("sltu a5, t4, t2"); - self.emit("sub t4, t4, t2"); - self.emit("sub t5, t5, t3"); - self.emit("sub t5, t5, a5"); - self.emit("addi a2, a2, 1"); - - self.emit_label(&skip_sub_label); - self.emit("addi a4, a4, -1"); - self.emit(format!("bnez a4, {}", loop_label)); - self.emit_stack_store("a2", dest_offset); - self.emit_stack_store("a3", dest_offset + 8); - self.emit_store_u128_pointer_for_var(dest.id, dest_offset); - self.emit(format!("j {}", done_label)); - - self.emit_label(&fail_label); - self.emit_runtime_error_comment(CellScriptRuntimeError::NumericOrDiscriminantInvalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::NumericOrDiscriminantInvalid.code())); - self.emit_epilogue(); - self.emit_label(&done_label); - } - - fn emit_dynamic_byte_comparison(&mut self, dest: &IrVar, op: BinaryOp, left: &IrOperand, right: &IrOperand) -> bool { - let (IrOperand::Var(left_var), IrOperand::Var(right_var)) = (left, right) else { - return false; - }; - let Some(left_len_offset) = self.dynamic_value_size_offsets.get(&left_var.id).copied() else { - return false; - }; - let Some(right_len_offset) = self.dynamic_value_size_offsets.get(&right_var.id).copied() else { - return false; - }; - - let equal_value = if matches!(op, BinaryOp::Eq) { 1 } else { 0 }; - let mismatch_value = if matches!(op, BinaryOp::Eq) { 0 } else { 1 }; - let len_equal_label = self.fresh_label("dynamic_bytes_len_equal"); - let bytes_equal_label = self.fresh_label("dynamic_bytes_equal"); - let done_label = self.fresh_label("dynamic_bytes_cmp_done"); - - self.emit(format!("# binary {:?} over dynamic byte operands", op)); - self.emit_stack_load("t0", left_len_offset); - self.emit_stack_load("t1", right_len_offset); - self.emit("sub t2, t0, t1"); - self.emit(format!("beqz t2, {}", len_equal_label)); - self.emit(format!("li t0, {}", mismatch_value)); - self.emit_stack_store("t0", dest.id * 8); - self.emit(format!("j {}", done_label)); - - self.emit_label(&len_equal_label); - self.emit_stack_load("a0", left_var.id * 8); - self.emit_stack_load("a1", right_var.id * 8); - self.emit_stack_load("a2", left_len_offset); - self.emit("call __cellscript_memcmp_fixed"); - self.emit(format!("beqz a0, {}", bytes_equal_label)); - self.emit(format!("li t0, {}", mismatch_value)); - self.emit_stack_store("t0", dest.id * 8); - self.emit(format!("j {}", done_label)); - - self.emit_label(&bytes_equal_label); - self.emit(format!("li t0, {}", equal_value)); - self.emit_stack_store("t0", dest.id * 8); - self.emit_label(&done_label); - true - } - - fn emit_unary(&mut self, dest: &IrVar, op: UnaryOp, operand: &IrOperand) -> Result<()> { - match operand { - IrOperand::Const(IrConst::U64(n)) => self.emit(format!("li t0, {}", n)), - IrOperand::Var(v) => self.emit_stack_load("t0", v.id * 8), - _ => self.emit("li t0, 0"), - } - - match op { - UnaryOp::Neg => self.emit("neg t0, t0"), - UnaryOp::Not => self.emit("xori t0, t0, 1"), - UnaryOp::Ref | UnaryOp::Deref => self.emit("# reference conversion (no-op in asm backend)"), - } - - self.emit_stack_store("t0", dest.id * 8); - Ok(()) - } - - fn emit_field_access(&mut self, dest: &IrVar, obj: &IrOperand, field: &str) -> Result<()> { - if self.emit_fixed_byte_field_access(dest, obj, field) { - return Ok(()); - } - if self.emit_schema_field_access(dest, obj, field) { - return Ok(()); - } - if self.emit_aggregate_field_access(dest, obj, field) { - return Ok(()); - } - if self.emit_tuple_call_return_field_access(dest, obj, field) { - return Ok(()); - } - if self.emit_generic_field_access(dest, obj, field) { - return Ok(()); - } - - self.emit(format!("# field access .{} (unresolved)", field)); - self.emit("# cellscript abi: fail closed because field offset is not computable from available type layout"); - self.emit_fail(CellScriptRuntimeError::DynamicFieldBoundsInvalid); - Ok(()) - } - - fn emit_fixed_byte_field_access(&mut self, dest: &IrVar, obj: &IrOperand, field: &str) -> bool { - let IrOperand::Var(var) = obj else { - return false; - }; - let layout = aggregate_field_layout(&var.ty, field).or_else(|| { - named_type_name(&var.ty) - .and_then(|type_name| self.type_layouts.get(type_name).and_then(|fields| fields.get(field)).cloned()) - }); - let Some(layout) = layout else { - return false; - }; - let Some(parent_width) = self.fixed_byte_like_width(&var.ty) else { - return false; - }; - let Some(source) = self.expected_fixed_byte_source(obj, parent_width) else { - return false; - }; - if is_fixed_scalar_ir_type(&dest.ty) { - let Some(width) = layout_fixed_scalar_width(&layout) else { - return false; - }; - self.emit(format!( - "# cellscript abi: fixed-byte scalar field {}.{} offset={} size={}", - aggregate_type_label(&var.ty), - field, - layout.offset, - width - )); - self.emit_prepare_fixed_byte_source(&source, parent_width, "fixed-byte scalar field access"); - if !self.emit_fixed_byte_source_pointer_or_const_to("t4", &source) { - return false; - } - self.emit_unaligned_scalar_load("t4", "t0", "t2", layout.offset, width); - if layout.ty == IrType::I32 { - self.emit_sign_extend_i32("t0"); - } - self.emit_stack_store("t0", dest.id * 8); - if let ExpectedFixedByteSource::SchemaField(parent) = &source { - let mut nested_layout = layout.clone(); - nested_layout.offset += parent.layout.offset; - let nested_source = SchemaFieldValueSource { - obj_var_id: parent.obj_var_id, - type_name: parent.type_name.clone(), - field: format!("{}.{}", parent.field, field), - layout: nested_layout, - }; - self.schema_field_value_sources.insert(dest.id, nested_source.clone()); - if dest.ty == IrType::U64 { - self.prelude_u64_value_sources.insert(dest.id, PreludeU64ValueSource::Field(nested_source)); - } - } - return true; - } - let Some(width) = layout_fixed_byte_width(&layout).or_else(|| self.fixed_named_type_width(&layout.ty)) else { - return false; - }; - let Some(dest_offset) = self.fixed_byte_local_offsets.get(&dest.id).copied() else { - return false; - }; - - self.emit(format!( - "# cellscript abi: fixed-byte field {}.{} offset={} size={}", - aggregate_type_label(&var.ty), - field, - layout.offset, - width - )); - self.emit_prepare_fixed_byte_source(&source, parent_width, "fixed-byte field access"); - if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &source) { - return false; - } - self.emit(format!("addi a0, a0, {}", layout.offset)); - self.emit_sp_addi("a1", dest_offset); - self.emit(format!("li a2, {}", width)); - self.emit("call __cellscript_memcpy_fixed"); - self.emit_sp_addi("t0", dest_offset); - self.emit_stack_store("t0", dest.id * 8); - if let ExpectedFixedByteSource::SchemaField(parent) = &source { - let mut nested_layout = layout.clone(); - nested_layout.offset += parent.layout.offset; - let nested_source = SchemaFieldValueSource { - obj_var_id: parent.obj_var_id, - type_name: parent.type_name.clone(), - field: format!("{}.{}", parent.field, field), - layout: nested_layout, - }; - self.schema_field_value_sources.insert(dest.id, nested_source); - } - true - } - - fn emit_schema_field_access(&mut self, dest: &IrVar, obj: &IrOperand, field: &str) -> bool { - let IrOperand::Var(var) = obj else { - return false; - }; - if !self.schema_pointer_vars.contains(&var.id) { - return false; - } - let Some(type_name) = named_type_name(&var.ty) else { - return false; - }; - let Some(layout) = self.type_layouts.get(type_name).and_then(|fields| fields.get(field)).cloned() else { - return false; - }; - let Some(width) = layout_fixed_byte_width(&layout) else { - return self.emit_dynamic_schema_field_access(dest, var, type_name, field, &layout); - }; - - self.emit(format!("# field access .{}", field)); - self.emit(format!("# cellscript abi: schema field {}.{} offset={} size={}", type_name, field, layout.offset, width)); - self.emit_stack_load("t4", var.id * 8); - if let Some(size_offset) = self.schema_pointer_size_offsets.get(&var.id).copied() { - if let Some(expected_size) = self.type_fixed_sizes.get(type_name).copied() { - self.emit_loaded_schema_exact_size_check(size_offset, expected_size, type_name); - self.emit_loaded_schema_bounds_check(size_offset, layout.offset + width, &format!("{}.{}", type_name, field)); - if layout_fixed_scalar_width(&layout).is_some() { - self.emit_unaligned_scalar_load("t4", "t0", "t2", layout.offset, width); - } else { - self.emit(format!("addi t0, t4, {}", layout.offset)); - } - } else { - self.emit_molecule_table_field_bounds_to_t5( - "t4", - size_offset, - layout.index, - width, - &format!("{}.{}", type_name, field), - ); - self.emit("add t4, t4, t5"); - if layout_fixed_scalar_width(&layout).is_some() { - self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, width); - } else { - self.emit("addi t0, t4, 0"); - } - } - } else { - if !self.type_fixed_sizes.contains_key(type_name) { - return false; - } - if layout_fixed_scalar_width(&layout).is_some() { - self.emit_unaligned_scalar_load("t4", "t0", "t2", layout.offset, width); - } else { - self.emit(format!("addi t0, t4, {}", layout.offset)); - } - } - self.emit_stack_store("t0", dest.id * 8); - true - } - - fn emit_dynamic_schema_field_access( - &mut self, - dest: &IrVar, - obj: &IrVar, - type_name: &str, - field: &str, - layout: &SchemaFieldLayout, - ) -> bool { - if molecule_vector_element_fixed_width(&layout.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes).is_none() { - return false; - } - let Some(size_offset) = self.schema_pointer_size_offsets.get(&obj.id).copied() else { - return false; - }; - let Some(dest_size_offset) = self.dynamic_value_size_offsets.get(&dest.id).copied() else { - return false; - }; - let Some(field_count) = self.type_layouts.get(type_name).map(|fields| fields.len()) else { - return false; - }; - - let context = format!("{}.{}", type_name, field); - self.emit(format!("# field access .{}", field)); - self.emit(format!("# cellscript abi: dynamic schema field {} index={} as Molecule vector bytes", context, layout.index)); - self.emit_stack_load("t4", obj.id * 8); - self.emit_molecule_table_field_span_to_t5_t6("t4", size_offset, layout.index, field_count, &context); - self.emit("add t0, t4, t5"); - self.emit("sub t1, t6, t5"); - self.emit_stack_store("t0", dest.id * 8); - self.emit_stack_store("t1", dest_size_offset); - true - } - - fn emit_aggregate_field_access(&mut self, dest: &IrVar, obj: &IrOperand, field: &str) -> bool { - let IrOperand::Var(var) = obj else { - return false; - }; - let Some(source) = self.aggregate_pointer_sources.get(&var.id) else { - return false; - }; - let source_ty = source.ty.clone(); - let Some(layout) = aggregate_field_layout(&source_ty, field) else { - return false; - }; - let Some(width) = layout_fixed_byte_width(&layout) else { - return false; - }; - - self.emit(format!("# field access .{}", field)); - self.emit(format!( - "# cellscript abi: fixed aggregate field {}.{} offset={} size={}", - aggregate_type_label(&source_ty), - field, - layout.offset, - width - )); - self.emit_stack_load("t4", var.id * 8); - if layout_fixed_scalar_width(&layout).is_some() { - self.emit_unaligned_scalar_load("t4", "t0", "t2", layout.offset, width); - if layout.ty == IrType::I32 { - self.emit_sign_extend_i32("t0"); - } - } else { - self.emit(format!("addi t0, t4, {}", layout.offset)); - } - self.emit_stack_store("t0", dest.id * 8); - true - } - - fn emit_tuple_call_return_field_access(&mut self, dest: &IrVar, obj: &IrOperand, field: &str) -> bool { - let IrOperand::Var(var) = obj else { - return false; - }; - let Some(slot_var_id) = self.tuple_call_return_field_slots.get(&(var.id, field.to_string())).copied() else { - return false; - }; - if slot_var_id != dest.id { - return false; - } - self.emit(format!("# field access .{}", field)); - self.emit(format!("# cellscript abi: tuple call return field .{} projected from return register", field)); - true - } - - /// Generic field access: when specialized paths don't match, try to compute the - /// field offset from type_layouts and emit an unaligned load from the pointer - /// stored in the object's stack slot. This works for any named-type variable - /// whose type has a registered layout, even if it wasn't classified as a - /// schema_pointer_var or aggregate_pointer_source. - fn emit_generic_field_access(&mut self, dest: &IrVar, obj: &IrOperand, field: &str) -> bool { - let IrOperand::Var(var) = obj else { - return false; - }; - let Some(type_name) = named_type_name(&var.ty) else { - return false; - }; - if !self.type_fixed_sizes.contains_key(type_name) { - return false; - } - let Some(layout) = self.type_layouts.get(type_name).and_then(|fields| fields.get(field)).cloned() else { - return false; - }; - let Some(width) = layout_fixed_byte_width(&layout) else { - return false; - }; - - self.emit(format!("# field access .{}", field)); - self.emit(format!("# cellscript abi: generic field {}.{} offset={} size={}", type_name, field, layout.offset, width)); - - // Bounds check: if the object has a known size offset, verify the data - // is large enough to contain this field. - if let Some(size_offset) = self.schema_pointer_size_offsets.get(&var.id).copied() { - self.emit_loaded_schema_bounds_check(size_offset, layout.offset + width, &format!("{}.{}", type_name, field)); - } else if let Some(size_offset) = self.fixed_byte_param_size_offsets.get(&var.id).copied() { - self.emit_loaded_schema_bounds_check(size_offset, layout.offset + width, &format!("{}.{}", type_name, field)); - } - - // Load the object pointer from the stack slot - self.emit_stack_load("t4", var.id * 8); - if layout_fixed_scalar_width(&layout).is_some() { - self.emit_unaligned_scalar_load("t4", "t0", "t2", layout.offset, width); - } else { - self.emit(format!("addi t0, t4, {}", layout.offset)); - } - self.emit_stack_store("t0", dest.id * 8); - true - } - - fn emit_index(&mut self, dest: &IrVar, arr: &IrOperand, idx: &IrOperand) -> Result<()> { - if self.emit_fixed_aggregate_index(dest, arr, idx) { - return Ok(()); - } - if self.emit_dynamic_molecule_vector_index(dest, arr, idx) { - return Ok(()); - } - if self.emit_stack_collection_index(dest, arr, idx) { - return Ok(()); - } - if self.emit_dynamic_index_access(dest, arr, idx) { - return Ok(()); - } - - self.emit("# index access (unresolved)"); - self.emit("# cellscript abi: fail closed because element layout is not statically computable"); - self.emit_fail(CellScriptRuntimeError::TypeHashMismatch); - Ok(()) - } - - fn emit_fixed_aggregate_index(&mut self, dest: &IrVar, arr: &IrOperand, idx: &IrOperand) -> bool { - let (IrOperand::Var(arr_var), Some(index)) = (arr, const_usize_operand(idx)) else { - return false; - }; - if !self.aggregate_pointer_sources.contains_key(&arr_var.id) { - return false; - } - let IrType::Array(inner, len) = &arr_var.ty else { - return false; - }; - if index >= *len { - return false; - } - let Some(element_width) = type_static_length(inner) else { - return false; - }; - let Some(total_width) = type_static_length(&arr_var.ty) else { - return false; - }; - let offset = index * element_width; - self.emit(format!("# index access [{}]", index)); - self.emit(format!("# cellscript abi: fixed aggregate index element_offset={} element_size={}", offset, element_width)); - if let Some(size_offset) = self.fixed_byte_param_size_offsets.get(&arr_var.id).copied() { - self.emit_loaded_schema_exact_size_check(size_offset, total_width, "fixed aggregate param"); - self.emit_loaded_schema_bounds_check(size_offset, offset + element_width, "fixed aggregate index"); - } - self.emit_stack_load("t4", arr_var.id * 8); - if let Some(width) = fixed_scalar_width(inner, Some(element_width)) { - self.emit_unaligned_scalar_load("t4", "t0", "t2", offset, width); - } else { - self.emit(format!("addi t0, t4, {}", offset)); - } - self.emit_stack_store("t0", dest.id * 8); - true - } - - fn emit_dynamic_molecule_vector_index(&mut self, dest: &IrVar, arr: &IrOperand, idx: &IrOperand) -> bool { - let IrOperand::Var(arr_var) = arr else { - return false; - }; - let Some(size_offset) = self - .dynamic_value_size_offsets - .get(&arr_var.id) - .copied() - .or_else(|| self.schema_pointer_size_offsets.get(&arr_var.id).copied()) - else { - return false; - }; - let Some(element_width) = molecule_vector_element_fixed_width(&arr_var.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) - else { - return false; - }; - - self.emit("# index access"); - self.emit(format!( - "# cellscript abi: dynamic Molecule vector index element_size={} size_offset={}", - element_width, size_offset - )); - self.emit_loaded_schema_bounds_check(size_offset, 4, "dynamic Molecule vector index"); - self.emit_stack_load("t4", arr_var.id * 8); - self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, 4); - - self.emit_stack_load("t3", size_offset); - self.emit(format!("li t2, {}", element_width)); - self.emit("mul t5, t0, t2"); - self.emit("addi t5, t5, 4"); - self.emit("sub t2, t3, t5"); - let size_ok = self.fresh_label("molecule_vector_index_size_ok"); - self.emit(format!("beqz t2, {}", size_ok)); - self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); - self.emit_label(&size_ok); - - match idx { - IrOperand::Var(v) => self.emit_stack_load("t1", v.id * 8), - IrOperand::Const(IrConst::U8(n)) => self.emit(format!("li t1, {}", n)), - IrOperand::Const(IrConst::U16(n)) => self.emit(format!("li t1, {}", n)), - IrOperand::Const(IrConst::U32(n)) => self.emit(format!("li t1, {}", n)), - IrOperand::Const(IrConst::U64(n)) => self.emit(format!("li t1, {}", n)), - _ => self.emit("li t1, 0"), - } - - let bounds_ok = self.fresh_label("molecule_vector_index_bounds_ok"); - self.emit("sltu t2, t1, t0"); - self.emit(format!("bnez t2, {}", bounds_ok)); - self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); - self.emit_label(&bounds_ok); - - self.emit(format!("li t2, {}", element_width)); - self.emit("mul t1, t1, t2"); - self.emit("addi t1, t1, 4"); - self.emit("add t4, t4, t1"); - if fixed_scalar_width(&dest.ty, Some(element_width)).is_some() { - self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, element_width.min(8)); - } else { - self.emit("addi t0, t4, 0"); - } - self.emit_stack_store("t0", dest.id * 8); - true - } - - fn emit_stack_collection_index(&mut self, dest: &IrVar, arr: &IrOperand, idx: &IrOperand) -> bool { - let IrOperand::Var(arr_var) = arr else { - return false; - }; - if !self.stack_collection_vars.contains(&arr_var.id) { - return false; - } - let Some(element_width) = molecule_vector_element_fixed_width(&arr_var.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) - else { - return false; - }; - let dest_scalar = fixed_scalar_width(&dest.ty, Some(element_width)).is_some(); - let dest_fixed_bytes = self.fixed_byte_like_width(&dest.ty).is_some_and(|width| width == element_width); - if !dest_scalar && !dest_fixed_bytes { - return false; - } - - self.emit("# index access"); - self.emit(format!("# cellscript abi: stack collection index element_size={}", element_width)); - self.emit_stack_load("t4", arr_var.id * 8); - self.emit("ld t0, -8(t4)"); - self.emit_operand_to_register("t1", idx); - - let bounds_ok = self.fresh_label("stack_collection_index_bounds_ok"); - self.emit("sltu t2, t1, t0"); - self.emit(format!("bnez t2, {}", bounds_ok)); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - self.emit_label(&bounds_ok); - - self.emit(format!("li t2, {}", element_width)); - self.emit("mul t1, t1, t2"); - self.emit("add t4, t4, t1"); - if dest_scalar { - self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, element_width); - } else { - self.emit("addi t0, t4, 0"); - } - self.emit_stack_store("t0", dest.id * 8); - true - } - - /// Dynamic index access: compute element offset from array type layout. - /// Handles cases where the index is not a constant or the array is not in - /// aggregate_pointer_sources, but the element size is still statically known. - fn emit_dynamic_index_access(&mut self, dest: &IrVar, arr: &IrOperand, idx: &IrOperand) -> bool { - let IrOperand::Var(arr_var) = arr else { - return false; - }; - let IrType::Array(inner, len) = &arr_var.ty else { - return false; - }; - let Some(element_width) = type_static_length(inner) else { - return false; - }; - let Some(total_width) = type_static_length(&arr_var.ty) else { - return false; - }; - - self.emit("# index access"); - self.emit(format!("# cellscript abi: dynamic index element_size={}", element_width)); - - // Bounds check: if we have a size offset, verify total data is large enough - if let Some(size_offset) = self.fixed_byte_param_size_offsets.get(&arr_var.id).copied() { - self.emit_loaded_schema_exact_size_check(size_offset, total_width, "dynamic index aggregate"); - } - - // Load array base pointer - self.emit_stack_load("t4", arr_var.id * 8); - - // Load index value into t1 - match idx { - IrOperand::Var(v) => self.emit_stack_load("t1", v.id * 8), - IrOperand::Const(IrConst::U8(n)) => self.emit(format!("li t1, {}", n)), - IrOperand::Const(IrConst::U16(n)) => self.emit(format!("li t1, {}", n)), - IrOperand::Const(IrConst::U32(n)) => self.emit(format!("li t1, {}", n)), - IrOperand::Const(IrConst::U64(n)) => self.emit(format!("li t1, {}", n)), - _ => self.emit("li t1, 0"), - } - - // Bounds check: index < len - let bounds_ok = self.fresh_label("idx_bounds_ok"); - self.emit(format!("li t2, {}", len)); - self.emit("slt t3, t1, t2"); - self.emit(format!("bnez t3, {}", bounds_ok)); - self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); - self.emit_label(&bounds_ok); - - // Compute offset = index * element_width - self.emit(format!("li t2, {}", element_width)); - self.emit("mul t1, t1, t2"); - - if fixed_scalar_width(inner, Some(element_width)).is_some() { - // Scalar element: load from base + offset - self.emit("add t4, t4, t1"); - self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, element_width.min(8)); - } else { - // Pointer-sized element: compute base + offset - self.emit("add t0, t4, t1"); - } - self.emit_stack_store("t0", dest.id * 8); - true - } - - fn emit_length(&mut self, dest: &IrVar, operand: &IrOperand) -> Result<()> { - self.emit("# length"); - if let Some(static_len) = self.static_length(operand) { - self.emit(format!("li t0, {}", static_len)); - } else if self.emit_stack_collection_length(operand) || self.emit_dynamic_molecule_vector_length(operand) { - } else if let Some(size_offset) = self.dynamic_length_from_size_offset(operand) { - // For schema-backed or fixed-byte params, the actual size word is already - // stored at the size offset; load it directly. - self.emit(format!("# cellscript abi: dynamic length from size word at offset={}", size_offset)); - self.emit_stack_load("t0", size_offset); - } else { - self.emit("# cellscript abi: fail closed because dynamic length is not available"); - self.emit_fail(CellScriptRuntimeError::CollectionRuntimeUnsupported); - return Ok(()); - } - self.emit_stack_store("t0", dest.id * 8); - Ok(()) - } - - fn emit_stack_collection_length(&mut self, operand: &IrOperand) -> bool { - let IrOperand::Var(var) = operand else { - return false; - }; - if !self.stack_collection_vars.contains(&var.id) { - return false; - } - self.emit("# cellscript abi: stack collection length"); - self.emit_stack_load("t4", var.id * 8); - self.emit("ld t0, -8(t4)"); - true - } - - fn emit_dynamic_molecule_vector_length(&mut self, operand: &IrOperand) -> bool { - let IrOperand::Var(var) = operand else { - return false; - }; - let Some(size_offset) = - self.dynamic_value_size_offsets.get(&var.id).copied().or_else(|| self.schema_pointer_size_offsets.get(&var.id).copied()) - else { - return false; - }; - let Some(element_width) = molecule_vector_element_fixed_width(&var.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) else { - return false; - }; - - self.emit(format!( - "# cellscript abi: dynamic Molecule vector length element_size={} size_offset={}", - element_width, size_offset - )); - self.emit_loaded_schema_bounds_check(size_offset, 4, "dynamic Molecule vector length"); - self.emit_stack_load("t4", var.id * 8); - self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, 4); - - self.emit_stack_load("t1", size_offset); - self.emit(format!("li t2, {}", element_width)); - self.emit("mul t3, t0, t2"); - self.emit("addi t3, t3, 4"); - self.emit("sub t2, t1, t3"); - let size_ok = self.fresh_label("molecule_vector_size_ok"); - self.emit(format!("beqz t2, {}", size_ok)); - self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); - self.emit_label(&size_ok); - true - } - - /// Try to obtain the size offset for a dynamically-sized operand. - fn dynamic_length_from_size_offset(&self, operand: &IrOperand) -> Option { - let IrOperand::Var(var) = operand else { - return None; - }; - // Check schema pointer size offsets (named-type params, consumed inputs, read_refs) - if let Some(size_offset) = self.schema_pointer_size_offsets.get(&var.id).copied() { - return Some(size_offset); - } - // Check fixed-byte param size offsets - if let Some(size_offset) = self.fixed_byte_param_size_offsets.get(&var.id).copied() { - return Some(size_offset); - } - // Check cell buffer size offsets (consumed inputs, read_refs, type_hash) - if let Some(size_offset) = self.cell_buffer_size_offsets.get(&var.id).copied() { - return Some(size_offset); - } - None - } - - fn emit_type_hash(&mut self, dest: &IrVar, operand: &IrOperand) -> Result<()> { - if let Some(output_index) = self.output_type_hash_sources.get(&dest.id).copied() { - let Some(size_offset) = self.cell_buffer_size_offsets.get(&dest.id).copied() else { - return Ok(()); - }; - let Some(buffer_offset) = self.cell_buffer_offsets.get(&dest.id).copied() else { - return Ok(()); - }; - self.emit("# type_hash"); - self.emit_operand_comment("type_hash source", operand); - self.emit_load_cell_by_field_syscall_to_offsets( - "output_type_hash", - CKB_SOURCE_OUTPUT, - output_index, - CKB_CELL_FIELD_TYPE_HASH, - size_offset, - buffer_offset, - 32, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_loaded_schema_exact_size_check(size_offset, 32, "output type hash"); - self.emit_sp_addi("t0", buffer_offset); - self.emit_stack_store("t0", dest.id * 8); - return Ok(()); - } - if self.emit_runtime_type_hash(dest, operand) { - return Ok(()); - } - if let Some(param_id) = self.param_type_hash_sources.get(&dest.id).copied() { - let Some(pointer_offset) = self.param_type_hash_pointer_offsets.get(¶m_id).copied() else { - return Ok(()); - }; - let Some(size_offset) = self.param_type_hash_size_offsets.get(¶m_id).copied() else { - return Ok(()); - }; - self.emit("# type_hash"); - self.emit_operand_comment("type_hash source", operand); - self.emit_loaded_schema_exact_size_check(size_offset, 32, "param type hash"); - self.emit_stack_load("t0", pointer_offset); - self.emit_stack_store("t0", dest.id * 8); - return Ok(()); - } - - self.emit("# type_hash (unresolved)"); - self.emit("# cellscript abi: fail closed because type_hash source cell cannot be determined"); - self.emit_fail(CellScriptRuntimeError::NumericOrDiscriminantInvalid); - Ok(()) - } - - /// Runtime type_hash: try to load the type hash from a cell identified by the operand's - /// association with a consumed input, created output, or read_ref cell dep. - fn emit_runtime_type_hash(&mut self, dest: &IrVar, operand: &IrOperand) -> bool { - let IrOperand::Var(var) = operand else { - return false; - }; - - // Try to find which cell this var is associated with - let (source, index) = if let Some(input_index) = self.consume_indices.get(&var.id).copied() { - (CKB_SOURCE_INPUT, input_index) - } else if let Some(output_index) = self.operation_output_indices.get(&var.id).copied() { - (CKB_SOURCE_OUTPUT, output_index) - } else if let Some(dep_index) = self.read_ref_indices.get(&var.id).copied() { - (CKB_SOURCE_CELL_DEP, dep_index) - } else { - return false; - }; - - let size_offset = self.cell_buffer_size_offsets.get(&dest.id).copied().unwrap_or_else(|| self.runtime_scratch_size_offset()); - let buffer_offset = self.cell_buffer_offsets.get(&dest.id).copied().unwrap_or_else(|| self.runtime_scratch_buffer_offset()); - - self.emit("# type_hash"); - self.emit_operand_comment("type_hash source", operand); - self.emit_load_cell_by_field_syscall_to_offsets( - "runtime_type_hash", - source, - index, - CKB_CELL_FIELD_TYPE_HASH, - size_offset, - buffer_offset, - 32, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_loaded_schema_exact_size_check(size_offset, 32, "runtime type hash"); - self.emit_sp_addi("t0", buffer_offset); - self.emit_stack_store("t0", dest.id * 8); - true - } - - fn emit_collection_new(&mut self, dest: &IrVar, ty: &str, capacity: Option<&IrOperand>) -> Result<()> { - // Stack-allocated collection: the stack slot stores a pointer to the - // collection buffer area, with the length word immediately before the buffer. - // Layout: [length: u64][buffer: RUNTIME_COLLECTION_BUFFER_SIZE bytes] - // We allocate space in the stack frame and initialize length to 0. - let collection_slot_size = 8 + RUNTIME_COLLECTION_BUFFER_SIZE; - let length_offset = self.collection_region_start + collection_slot_size * self.next_collection_slot; - let buffer_offset = length_offset + 8; - - self.emit(format!("# collection new {}", ty)); - self.emit(format!( - "# cellscript abi: stack collection buffer_offset={} max_size={}", - buffer_offset, RUNTIME_COLLECTION_BUFFER_SIZE - )); - if let Some(capacity) = capacity { - self.emit("# cellscript abi: stack collection with_capacity uses fixed backing buffer"); - self.emit_operand_comment("capacity", capacity); - } - - // Initialize length to 0 - self.emit_stack_store("zero", length_offset); - self.emit_sp_addi("t0", buffer_offset); - self.emit_stack_store("t0", dest.id * 8); - self.empty_molecule_vector_vars.insert(dest.id); - self.stack_collection_vars.insert(dest.id); - self.next_collection_slot += 1; - Ok(()) - } - - fn emit_collection_capacity(&mut self, dest: &IrVar, collection: &IrOperand) -> Result<()> { - self.emit("# collection capacity"); - self.emit_operand_comment("collection", collection); - if self.emit_stack_collection_capacity(dest, collection) { - return Ok(()); - } - self.emit("# cellscript abi: collection capacity is not available for this collection"); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - Ok(()) - } - - fn emit_stack_collection_capacity(&mut self, dest: &IrVar, collection: &IrOperand) -> bool { - if dest.ty != IrType::U64 { - return false; - } - let IrOperand::Var(collection) = collection else { - return false; - }; - if !self.stack_collection_vars.contains(&collection.id) { - return false; - } - let Some(element_width) = molecule_vector_element_fixed_width(&collection.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) - else { - return false; - }; - if element_width == 0 { - return false; - } - - self.emit(format!("# cellscript abi: stack collection capacity element_size={}", element_width)); - self.emit(format!("li t0, {}", RUNTIME_COLLECTION_BUFFER_SIZE / element_width)); - self.emit_stack_store("t0", dest.id * 8); - true - } - - fn emit_collection_push(&mut self, collection: &IrOperand, value: &IrOperand) -> Result<()> { - self.emit("# collection push"); - self.emit_operand_comment("collection", collection); - self.emit_operand_comment("value", value); - if matches!(value, IrOperand::Var(var) if self.verified_collection_push_values.contains(&var.id)) { - self.emit("# cellscript abi: collection push is covered by mutate append verifier"); - return Ok(()); - } - if matches!(collection, IrOperand::Var(var) if self.verified_collection_construction_vectors.contains(&var.id)) { - self.emit("# cellscript abi: collection push is covered by create-output vector verifier"); - return Ok(()); - } - if self.emit_stack_collection_push(collection, value) { - return Ok(()); - } - // In the verifier context, collection push is used for building output data. - // The verifier doesn't need to actually build the data; it needs to verify - // that the output cell data matches expectations. The collection operations - // in the verifier body are vestigial from the source-level specification. - // For now, emit a fail-closed trap because runtime collection mutation is not - // needed in the verifier path – the prelude already verified the output. - self.emit("# cellscript abi: collection push is not needed for verifier execution"); - self.emit("# cellscript abi: if this path is reached, the source program uses dynamic collections"); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - Ok(()) - } - - fn emit_stack_collection_push(&mut self, collection: &IrOperand, value: &IrOperand) -> bool { - let IrOperand::Var(collection) = collection else { - return false; - }; - if !self.stack_collection_vars.contains(&collection.id) { - return false; - } - let Some(width) = self.constructed_byte_vector_part_width(value) else { - return false; - }; - if width > RUNTIME_COLLECTION_BUFFER_SIZE { - return false; - } - - self.emit(format!("# cellscript abi: stack collection push element_size={}", width)); - self.emit_stack_load("t4", collection.id * 8); - self.emit("ld t0, -8(t4)"); - self.emit(format!("li t1, {}", width)); - self.emit("mul t2, t0, t1"); - self.emit(format!("li t3, {}", RUNTIME_COLLECTION_BUFFER_SIZE)); - self.emit("sub t5, t3, t2"); - self.emit("sltu t5, t5, t1"); - let capacity_ok = self.fresh_label("stack_collection_push_capacity_ok"); - self.emit(format!("beqz t5, {}", capacity_ok)); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - self.emit_label(&capacity_ok); - - self.emit("add t5, t4, t2"); - if width <= 8 && fixed_scalar_operand_width(value).is_some() { - self.emit_operand_to_register("t1", value); - match width { - 1 => self.emit("sb t1, 0(t5)"), - 2 => self.emit("sh t1, 0(t5)"), - 4 => self.emit("sw t1, 0(t5)"), - 8 => self.emit("sd t1, 0(t5)"), - _ => return false, - } - } else { - let Some(source) = self.expected_fixed_byte_source(value, width) else { - return false; - }; - self.emit_prepare_fixed_byte_source(&source, width, "stack collection push"); - self.emit(format!("# cellscript abi: stack collection copy fixed bytes size={}", width)); - for byte_index in 0..width { - self.emit_fixed_byte_source_byte_to("t1", "t6", &source, byte_index); - self.emit_stack_load("t4", collection.id * 8); - self.emit("ld t0, -8(t4)"); - self.emit(format!("li t2, {}", width)); - self.emit("mul t2, t0, t2"); - self.emit("add t4, t4, t2"); - if byte_index <= 2047 { - self.emit(format!("sb t1, {}(t4)", byte_index)); - } else { - self.emit_large_addi("t0", "t4", byte_index as i64); - self.emit("sb t1, 0(t0)"); - } - } - } - self.emit_stack_load("t4", collection.id * 8); - self.emit("ld t0, -8(t4)"); - self.emit("addi t0, t0, 1"); - self.emit("sd t0, -8(t4)"); - true - } - - fn emit_collection_extend(&mut self, collection: &IrOperand, slice: &IrOperand) -> Result<()> { - self.emit("# collection extend_from_slice"); - self.emit_operand_comment("collection", collection); - self.emit_operand_comment("slice", slice); - if matches!(collection, IrOperand::Var(var) if self.verified_collection_construction_vectors.contains(&var.id)) { - self.emit("# cellscript abi: collection extend is covered by create-output vector verifier"); - return Ok(()); - } - if self.emit_stack_collection_extend(collection, slice) { - return Ok(()); - } - self.emit("# cellscript abi: collection extend is not needed for verifier execution"); - self.emit("# cellscript abi: if this path is reached, the source program uses dynamic collections"); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - Ok(()) - } - - fn emit_stack_collection_extend(&mut self, collection: &IrOperand, slice: &IrOperand) -> bool { - let IrOperand::Var(collection) = collection else { - return false; - }; - if !self.stack_collection_vars.contains(&collection.id) { - return false; - } - let Some(width) = operand_fixed_byte_width(slice) else { - return false; - }; - let element_width = - molecule_vector_element_fixed_width(&collection.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes).unwrap_or(1); - if element_width == 0 || width % element_width != 0 { - return false; - } - let element_count = width / element_width; - if width > RUNTIME_COLLECTION_BUFFER_SIZE { - return false; - } - let Some(source) = self.expected_fixed_byte_source(slice, width) else { - return false; - }; - - self.emit(format!( - "# cellscript abi: stack collection extend bytes={} elements={} element_size={}", - width, element_count, element_width - )); - self.emit_stack_load("t4", collection.id * 8); - self.emit("ld t0, -8(t4)"); - self.emit(format!("li t1, {}", element_width)); - self.emit("mul t2, t0, t1"); - self.emit(format!("li t3, {}", RUNTIME_COLLECTION_BUFFER_SIZE)); - self.emit("sub t5, t3, t2"); - self.emit(format!("li t1, {}", width)); - self.emit("sltu t5, t5, t1"); - let capacity_ok = self.fresh_label("stack_collection_extend_capacity_ok"); - self.emit(format!("beqz t5, {}", capacity_ok)); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - self.emit_label(&capacity_ok); - - self.emit_prepare_fixed_byte_source(&source, width, "stack collection extend"); - self.emit(format!("# cellscript abi: stack collection extend copy fixed bytes size={}", width)); - for byte_index in 0..width { - self.emit_fixed_byte_source_byte_to("t1", "t6", &source, byte_index); - self.emit_stack_load("t4", collection.id * 8); - self.emit("ld t0, -8(t4)"); - self.emit(format!("li t2, {}", element_width)); - self.emit("mul t2, t0, t2"); - self.emit("add t4, t4, t2"); - if byte_index <= 2047 { - self.emit(format!("sb t1, {}(t4)", byte_index)); - } else { - self.emit_large_addi("t0", "t4", byte_index as i64); - self.emit("sb t1, 0(t0)"); - } - } - self.emit_stack_load("t4", collection.id * 8); - self.emit("ld t0, -8(t4)"); - self.emit(format!("addi t0, t0, {}", element_count)); - self.emit("sd t0, -8(t4)"); - true - } - - fn emit_collection_clear(&mut self, collection: &IrOperand) -> Result<()> { - self.emit("# collection clear"); - self.emit_operand_comment("collection", collection); - if matches!(collection, IrOperand::Var(var) if self.verified_collection_construction_vectors.contains(&var.id)) { - self.emit("# cellscript abi: collection clear is covered by create-output vector verifier"); - return Ok(()); - } - if self.emit_stack_collection_clear(collection) { - return Ok(()); - } - self.emit("# cellscript abi: collection clear is not needed for verifier execution"); - self.emit("# cellscript abi: if this path is reached, the source program uses dynamic collections"); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - Ok(()) - } - - fn emit_stack_collection_clear(&mut self, collection: &IrOperand) -> bool { - let IrOperand::Var(collection) = collection else { - return false; - }; - if !self.stack_collection_vars.contains(&collection.id) { - return false; - } - self.emit("# cellscript abi: stack collection clear"); - self.emit_stack_load("t4", collection.id * 8); - self.emit("sd zero, -8(t4)"); - true - } - - fn emit_collection_reverse(&mut self, collection: &IrOperand) -> Result<()> { - self.emit("# collection reverse"); - self.emit_operand_comment("collection", collection); - if self.emit_stack_collection_reverse(collection) { - return Ok(()); - } - self.emit("# cellscript abi: collection reverse is not available for this collection"); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - Ok(()) - } - - fn emit_stack_collection_reverse(&mut self, collection: &IrOperand) -> bool { - let IrOperand::Var(collection) = collection else { - return false; - }; - if !self.stack_collection_vars.contains(&collection.id) { - return false; - } - let Some(element_width) = molecule_vector_element_fixed_width(&collection.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) - else { - return false; - }; - if element_width == 0 || element_width > RUNTIME_COLLECTION_BUFFER_SIZE { - return false; - } - - self.emit(format!("# cellscript abi: stack collection reverse element_size={}", element_width)); - self.emit_stack_load("t4", collection.id * 8); - self.emit("ld t0, -8(t4)"); - let done_label = self.fresh_label("stack_collection_reverse_done"); - self.emit("li t1, 2"); - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", done_label)); - - let left_offset = self.runtime_expr_temp_offset(0); - let right_offset = self.runtime_expr_temp_offset(1); - self.emit_stack_store("zero", left_offset); - self.emit("addi t0, t0, -1"); - self.emit_stack_store("t0", right_offset); - - let loop_label = self.fresh_label("stack_collection_reverse_loop"); - self.emit_label(&loop_label); - self.emit_stack_load("t0", left_offset); - self.emit_stack_load("t1", right_offset); - self.emit("sltu t2, t0, t1"); - self.emit(format!("beqz t2, {}", done_label)); - - self.emit_stack_load("t4", collection.id * 8); - self.emit(format!("li t3, {}", element_width)); - self.emit("mul t5, t0, t3"); - self.emit("add t5, t4, t5"); - self.emit("mul t6, t1, t3"); - self.emit("add t6, t4, t6"); - self.emit(format!("# cellscript abi: stack collection reverse swap element_size={}", element_width)); - for byte_index in 0..element_width { - if byte_index <= 2047 { - self.emit(format!("lbu t0, {}(t5)", byte_index)); - self.emit(format!("lbu t1, {}(t6)", byte_index)); - self.emit(format!("sb t1, {}(t5)", byte_index)); - self.emit(format!("sb t0, {}(t6)", byte_index)); - } else { - self.emit_large_addi("t2", "t5", byte_index as i64); - self.emit_large_addi("t3", "t6", byte_index as i64); - self.emit("lbu t0, 0(t2)"); - self.emit("lbu t1, 0(t3)"); - self.emit("sb t1, 0(t2)"); - self.emit("sb t0, 0(t3)"); - } - } - self.emit_stack_load("t0", left_offset); - self.emit("addi t0, t0, 1"); - self.emit_stack_store("t0", left_offset); - self.emit_stack_load("t1", right_offset); - self.emit("addi t1, t1, -1"); - self.emit_stack_store("t1", right_offset); - self.emit(format!("j {}", loop_label)); - self.emit_label(&done_label); - true - } - - fn emit_collection_truncate(&mut self, collection: &IrOperand, len: &IrOperand) -> Result<()> { - self.emit("# collection truncate"); - self.emit_operand_comment("collection", collection); - self.emit_operand_comment("len", len); - if self.emit_stack_collection_truncate(collection, len) { - return Ok(()); - } - self.emit("# cellscript abi: collection truncate is not available for this collection"); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - Ok(()) - } - - fn emit_stack_collection_truncate(&mut self, collection: &IrOperand, len: &IrOperand) -> bool { - let IrOperand::Var(collection) = collection else { - return false; - }; - if !self.stack_collection_vars.contains(&collection.id) { - return false; - } - - self.emit("# cellscript abi: stack collection truncate"); - self.emit_stack_load("t4", collection.id * 8); - self.emit("ld t0, -8(t4)"); - self.emit_operand_to_register("t1", len); - let done_label = self.fresh_label("stack_collection_truncate_done"); - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", done_label)); - self.emit("sd t1, -8(t4)"); - self.emit_label(&done_label); - true - } - - fn emit_collection_swap(&mut self, collection: &IrOperand, left: &IrOperand, right: &IrOperand) -> Result<()> { - self.emit("# collection swap"); - self.emit_operand_comment("collection", collection); - self.emit_operand_comment("left", left); - self.emit_operand_comment("right", right); - if self.emit_stack_collection_swap(collection, left, right) { - return Ok(()); - } - self.emit("# cellscript abi: collection swap is not available for this collection"); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - Ok(()) - } - - fn emit_stack_collection_swap(&mut self, collection: &IrOperand, left: &IrOperand, right: &IrOperand) -> bool { - let IrOperand::Var(collection) = collection else { - return false; - }; - if !self.stack_collection_vars.contains(&collection.id) { - return false; - } - let Some(element_width) = molecule_vector_element_fixed_width(&collection.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) - else { - return false; - }; - if element_width == 0 || element_width > RUNTIME_COLLECTION_BUFFER_SIZE { - return false; - } - - self.emit(format!("# cellscript abi: stack collection swap element_size={}", element_width)); - self.emit_stack_load("t4", collection.id * 8); - self.emit("ld t0, -8(t4)"); - self.emit_operand_to_register("t1", left); - self.emit_operand_to_register("t2", right); - - let left_ok = self.fresh_label("stack_collection_swap_left_ok"); - self.emit("sltu t3, t1, t0"); - self.emit(format!("bnez t3, {}", left_ok)); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - self.emit_label(&left_ok); - - let right_ok = self.fresh_label("stack_collection_swap_right_ok"); - self.emit("sltu t3, t2, t0"); - self.emit(format!("bnez t3, {}", right_ok)); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - self.emit_label(&right_ok); - - self.emit(format!("li t3, {}", element_width)); - self.emit("mul t5, t1, t3"); - self.emit("add t5, t4, t5"); - self.emit("mul t6, t2, t3"); - self.emit("add t6, t4, t6"); - self.emit(format!("# cellscript abi: stack collection swap bytes element_size={}", element_width)); - for byte_index in 0..element_width { - if byte_index <= 2047 { - self.emit(format!("lbu t0, {}(t5)", byte_index)); - self.emit(format!("lbu t1, {}(t6)", byte_index)); - self.emit(format!("sb t1, {}(t5)", byte_index)); - self.emit(format!("sb t0, {}(t6)", byte_index)); - } else { - self.emit_large_addi("t2", "t5", byte_index as i64); - self.emit_large_addi("t3", "t6", byte_index as i64); - self.emit("lbu t0, 0(t2)"); - self.emit("lbu t1, 0(t3)"); - self.emit("sb t1, 0(t2)"); - self.emit("sb t0, 0(t3)"); - } - } - true - } - - fn emit_collection_contains(&mut self, dest: &IrVar, collection: &IrOperand, value: &IrOperand) -> Result<()> { - self.emit("# collection contains"); - self.emit_operand_comment("collection", collection); - self.emit_operand_comment("value", value); - if self.emit_stack_collection_contains(dest, collection, value) { - return Ok(()); - } - self.emit("# cellscript abi: collection contains is not available for this collection"); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - Ok(()) - } - - fn emit_stack_collection_contains(&mut self, dest: &IrVar, collection: &IrOperand, value: &IrOperand) -> bool { - let IrOperand::Var(collection) = collection else { - return false; - }; - if !self.stack_collection_vars.contains(&collection.id) { - return false; - } - let Some(value_width) = self.constructed_byte_vector_part_width(value) else { - return false; - }; - let element_width = - molecule_vector_element_fixed_width(&collection.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes).unwrap_or(value_width); - if element_width == 0 || element_width != value_width { - return false; - } - - self.emit(format!("# cellscript abi: stack collection contains element_size={}", element_width)); - let index_offset = self.runtime_expr_temp_offset(0); - self.emit_stack_store("zero", index_offset); - self.emit_stack_store("zero", dest.id * 8); - let loop_label = self.fresh_label("stack_collection_contains_loop"); - let next_label = self.fresh_label("stack_collection_contains_next"); - let found_label = self.fresh_label("stack_collection_contains_found"); - let done_label = self.fresh_label("stack_collection_contains_done"); - self.emit_label(&loop_label); - self.emit_stack_load("t1", index_offset); - self.emit_stack_load("t4", collection.id * 8); - self.emit("ld t2, -8(t4)"); - self.emit(format!("beq t1, t2, {}", done_label)); - - if element_width <= 8 && fixed_scalar_operand_width(value).is_some() { - self.emit(format!("li t2, {}", element_width)); - self.emit("mul t3, t1, t2"); - self.emit("add t4, t4, t3"); - self.emit_unaligned_scalar_load("t4", "t0", "t2", 0, element_width); - self.emit_operand_to_register("t5", value); - self.emit("sub t6, t0, t5"); - self.emit(format!("beqz t6, {}", found_label)); - } else { - let Some(source) = self.expected_fixed_byte_source(value, element_width) else { - return false; - }; - self.emit_prepare_fixed_byte_source(&source, element_width, "stack collection contains"); - for byte_index in 0..element_width { - self.emit_stack_load("t1", index_offset); - self.emit_stack_load("t4", collection.id * 8); - self.emit(format!("li t2, {}", element_width)); - self.emit("mul t3, t1, t2"); - self.emit("add t4, t4, t3"); - if byte_index <= 2047 { - self.emit(format!("lbu t0, {}(t4)", byte_index)); - } else { - self.emit_large_addi("t2", "t4", byte_index as i64); - self.emit("lbu t0, 0(t2)"); - } - self.emit_fixed_byte_source_byte_to("t5", "t6", &source, byte_index); - self.emit("sub t0, t0, t5"); - self.emit(format!("bnez t0, {}", next_label)); - } - self.emit(format!("j {}", found_label)); - } - - self.emit_label(&next_label); - self.emit_stack_load("t1", index_offset); - self.emit("addi t1, t1, 1"); - self.emit_stack_store("t1", index_offset); - self.emit(format!("j {}", loop_label)); - self.emit_label(&found_label); - self.emit("li t0, 1"); - self.emit_stack_store("t0", dest.id * 8); - self.emit_label(&done_label); - true - } - - fn emit_collection_remove(&mut self, dest: &IrVar, collection: &IrOperand, index: &IrOperand) -> Result<()> { - self.emit("# collection remove"); - self.emit_operand_comment("collection", collection); - self.emit_operand_comment("index", index); - if self.emit_stack_collection_remove(dest, collection, index) { - return Ok(()); - } - self.emit("# cellscript abi: collection remove is not available for this collection"); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - Ok(()) - } - - fn emit_stack_collection_remove(&mut self, dest: &IrVar, collection: &IrOperand, index: &IrOperand) -> bool { - let IrOperand::Var(collection) = collection else { - return false; - }; - if !self.stack_collection_vars.contains(&collection.id) { - return false; - } - let Some(element_width) = molecule_vector_element_fixed_width(&collection.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) - else { - return false; - }; - let dest_scalar = fixed_scalar_width(&dest.ty, Some(element_width)).is_some(); - let dest_fixed_bytes = self.fixed_byte_like_width(&dest.ty).is_some_and(|width| width == element_width); - if !dest_scalar && !dest_fixed_bytes { - return false; - } - let removed_value_slots = if dest_fixed_bytes { element_width.div_ceil(8) } else { 0 }; - if dest_fixed_bytes && removed_value_slots + 1 > RUNTIME_EXPR_TEMP_SLOTS { - return false; - } - let Some(index_offset) = self.checked_runtime_expr_temp_offset(removed_value_slots) else { - return false; - }; - - self.emit(format!("# cellscript abi: stack collection remove element_size={}", element_width)); - self.emit_stack_load("t4", collection.id * 8); - self.emit("ld t0, -8(t4)"); - self.emit_operand_to_register("t1", index); - - let bounds_ok = self.fresh_label("stack_collection_remove_bounds_ok"); - self.emit("sltu t2, t1, t0"); - self.emit(format!("bnez t2, {}", bounds_ok)); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - self.emit_label(&bounds_ok); - - self.emit(format!("li t2, {}", element_width)); - self.emit("mul t3, t1, t2"); - self.emit("add t5, t4, t3"); - if dest_scalar { - self.emit_unaligned_scalar_load("t5", "t6", "t2", 0, element_width); - self.emit_stack_store("t6", dest.id * 8); - } else { - let removed_offset = self.runtime_expr_temp_offset(0); - self.emit(format!("# cellscript abi: stack collection remove snapshot fixed bytes size={}", element_width)); - for byte_index in 0..element_width { - if byte_index <= 2047 { - self.emit(format!("lbu t6, {}(t5)", byte_index)); - } else { - self.emit_large_addi("t2", "t5", byte_index as i64); - self.emit("lbu t6, 0(t2)"); - } - self.emit_sp_addi("t2", removed_offset + byte_index); - self.emit("sb t6, 0(t2)"); - } - self.emit_sp_addi("t6", removed_offset); - self.emit_stack_store("t6", dest.id * 8); - } - - self.emit_stack_store("t1", index_offset); - let shift_loop = self.fresh_label("stack_collection_remove_shift_loop"); - let shift_done = self.fresh_label("stack_collection_remove_shift_done"); - self.emit(format!("# cellscript abi: stack collection remove shift element_size={}", element_width)); - self.emit_label(&shift_loop); - self.emit_stack_load("t1", index_offset); - self.emit("addi t2, t1, 1"); - self.emit_stack_load("t4", collection.id * 8); - self.emit("ld t0, -8(t4)"); - self.emit("sltu t3, t2, t0"); - self.emit(format!("beqz t3, {}", shift_done)); - self.emit(format!("li t3, {}", element_width)); - self.emit("mul t5, t1, t3"); - self.emit("add t5, t4, t5"); - self.emit("mul t6, t2, t3"); - self.emit("add t6, t4, t6"); - for byte_index in 0..element_width { - if byte_index <= 2047 { - self.emit(format!("lbu t0, {}(t6)", byte_index)); - self.emit(format!("sb t0, {}(t5)", byte_index)); - } else { - self.emit_large_addi("t0", "t6", byte_index as i64); - self.emit("lbu t0, 0(t0)"); - self.emit_large_addi("t2", "t5", byte_index as i64); - self.emit("sb t0, 0(t2)"); - } - } - self.emit_stack_load("t1", index_offset); - self.emit("addi t1, t1, 1"); - self.emit_stack_store("t1", index_offset); - self.emit(format!("j {}", shift_loop)); - self.emit_label(&shift_done); - self.emit_stack_load("t4", collection.id * 8); - self.emit("ld t0, -8(t4)"); - self.emit("addi t0, t0, -1"); - self.emit("sd t0, -8(t4)"); - true - } - - fn emit_collection_pop(&mut self, dest: &IrVar, collection: &IrOperand) -> Result<()> { - self.emit("# collection pop"); - self.emit_operand_comment("collection", collection); - if self.emit_stack_collection_pop(dest, collection) { - return Ok(()); - } - self.emit("# cellscript abi: collection pop is not available for this collection"); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - Ok(()) - } - - fn emit_stack_collection_pop(&mut self, dest: &IrVar, collection: &IrOperand) -> bool { - let IrOperand::Var(collection) = collection else { - return false; - }; - if !self.stack_collection_vars.contains(&collection.id) { - return false; - } - let Some(element_width) = molecule_vector_element_fixed_width(&collection.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) - else { - return false; - }; - let dest_scalar = fixed_scalar_width(&dest.ty, Some(element_width)).is_some(); - let dest_fixed_bytes = self.fixed_byte_like_width(&dest.ty).is_some_and(|width| width == element_width); - if !dest_scalar && !dest_fixed_bytes { - return false; - } - - self.emit(format!("# cellscript abi: stack collection pop element_size={}", element_width)); - self.emit_stack_load("t4", collection.id * 8); - self.emit("ld t0, -8(t4)"); - let bounds_ok = self.fresh_label("stack_collection_pop_bounds_ok"); - self.emit(format!("bnez t0, {}", bounds_ok)); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - self.emit_label(&bounds_ok); - - self.emit("addi t1, t0, -1"); - self.emit(format!("li t2, {}", element_width)); - self.emit("mul t3, t1, t2"); - self.emit("add t5, t4, t3"); - if dest_scalar { - self.emit_unaligned_scalar_load("t5", "t6", "t2", 0, element_width); - self.emit_stack_store("t6", dest.id * 8); - } else { - self.emit("# cellscript abi: stack collection pop fixed bytes"); - self.emit_stack_store("t5", dest.id * 8); - } - self.emit("sd t1, -8(t4)"); - true - } - - fn emit_collection_insert(&mut self, collection: &IrOperand, index: &IrOperand, value: &IrOperand) -> Result<()> { - self.emit("# collection insert"); - self.emit_operand_comment("collection", collection); - self.emit_operand_comment("index", index); - self.emit_operand_comment("value", value); - if self.emit_stack_collection_insert(collection, index, value) { - return Ok(()); - } - self.emit("# cellscript abi: collection insert is not available for this collection"); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - Ok(()) - } - - fn emit_stack_collection_insert(&mut self, collection: &IrOperand, index: &IrOperand, value: &IrOperand) -> bool { - let IrOperand::Var(collection) = collection else { - return false; - }; - if !self.stack_collection_vars.contains(&collection.id) { - return false; - } - let Some(value_width) = self.constructed_byte_vector_part_width(value) else { - return false; - }; - let Some(element_width) = molecule_vector_element_fixed_width(&collection.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) - else { - return false; - }; - if element_width != value_width { - return false; - } - let value_scalar = element_width <= 8 && fixed_scalar_operand_width(value).is_some(); - let fixed_byte_source = if value_scalar { - None - } else { - if element_width > (RUNTIME_EXPR_TEMP_SLOTS - 2) * 8 { - return false; - } - let Some(source) = self.expected_fixed_byte_source(value, element_width) else { - return false; - }; - Some(source) - }; - - self.emit(format!("# cellscript abi: stack collection insert element_size={}", element_width)); - self.emit_stack_load("t4", collection.id * 8); - self.emit("ld t0, -8(t4)"); - self.emit_operand_to_register("t1", index); - - let bounds_ok = self.fresh_label("stack_collection_insert_bounds_ok"); - self.emit("sltu t2, t0, t1"); - self.emit(format!("beqz t2, {}", bounds_ok)); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - self.emit_label(&bounds_ok); - - self.emit(format!("li t2, {}", element_width)); - self.emit("mul t3, t0, t2"); - self.emit(format!("li t5, {}", RUNTIME_COLLECTION_BUFFER_SIZE)); - self.emit("sub t6, t5, t3"); - self.emit("sltu t6, t6, t2"); - let capacity_ok = self.fresh_label("stack_collection_insert_capacity_ok"); - self.emit(format!("beqz t6, {}", capacity_ok)); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - self.emit_label(&capacity_ok); - - let index_offset = self.runtime_expr_temp_offset(0); - let current_offset = self.runtime_expr_temp_offset(1); - self.emit_stack_store("t1", index_offset); - self.emit_stack_store("t0", current_offset); - if let Some(source) = fixed_byte_source.as_ref() { - self.emit_prepare_fixed_byte_source(source, element_width, "stack collection insert"); - let value_offset = self.runtime_expr_temp_offset(2); - self.emit(format!("# cellscript abi: stack collection insert snapshot fixed bytes size={}", element_width)); - for byte_index in 0..element_width { - self.emit_fixed_byte_source_byte_to("t1", "t6", source, byte_index); - self.emit_sp_addi("t6", value_offset + byte_index); - self.emit("sb t1, 0(t6)"); - } - } - let shift_loop = self.fresh_label("stack_collection_insert_shift_loop"); - let shift_done = self.fresh_label("stack_collection_insert_shift_done"); - self.emit(format!("# cellscript abi: stack collection insert shift element_size={}", element_width)); - self.emit_label(&shift_loop); - self.emit_stack_load("t0", current_offset); - self.emit_stack_load("t1", index_offset); - self.emit(format!("beq t0, t1, {}", shift_done)); - self.emit("addi t2, t0, -1"); - self.emit_stack_load("t4", collection.id * 8); - self.emit(format!("li t3, {}", element_width)); - self.emit("mul t5, t0, t3"); - self.emit("add t5, t4, t5"); - self.emit("mul t6, t2, t3"); - self.emit("add t6, t4, t6"); - if element_width <= 8 { - self.emit_unaligned_scalar_load("t6", "t0", "t2", 0, element_width); - match element_width { - 1 => self.emit("sb t0, 0(t5)"), - 2 => self.emit("sh t0, 0(t5)"), - 4 => self.emit("sw t0, 0(t5)"), - 8 => self.emit("sd t0, 0(t5)"), - _ => return false, - } - } else { - for byte_index in 0..element_width { - if byte_index <= 2047 { - self.emit(format!("lbu t0, {}(t6)", byte_index)); - self.emit(format!("sb t0, {}(t5)", byte_index)); - } else { - self.emit_large_addi("t0", "t6", byte_index as i64); - self.emit("lbu t0, 0(t0)"); - self.emit_large_addi("t2", "t5", byte_index as i64); - self.emit("sb t0, 0(t2)"); - } - } - } - self.emit_stack_load("t0", current_offset); - self.emit("addi t0, t0, -1"); - self.emit_stack_store("t0", current_offset); - self.emit(format!("j {}", shift_loop)); - self.emit_label(&shift_done); - - self.emit_stack_load("t4", collection.id * 8); - self.emit_stack_load("t0", index_offset); - self.emit(format!("li t2, {}", element_width)); - self.emit("mul t3, t0, t2"); - self.emit("add t5, t4, t3"); - if value_scalar { - self.emit_operand_to_register("t1", value); - match element_width { - 1 => self.emit("sb t1, 0(t5)"), - 2 => self.emit("sh t1, 0(t5)"), - 4 => self.emit("sw t1, 0(t5)"), - 8 => self.emit("sd t1, 0(t5)"), - _ => return false, - } - } else { - let value_offset = self.runtime_expr_temp_offset(2); - self.emit(format!("# cellscript abi: stack collection insert copy fixed bytes size={}", element_width)); - for byte_index in 0..element_width { - self.emit_sp_addi("t6", value_offset + byte_index); - self.emit("lbu t1, 0(t6)"); - if byte_index <= 2047 { - self.emit(format!("sb t1, {}(t5)", byte_index)); - } else { - self.emit_large_addi("t0", "t5", byte_index as i64); - self.emit("sb t1, 0(t0)"); - } - } - } - self.emit_stack_load("t4", collection.id * 8); - self.emit("ld t0, -8(t4)"); - self.emit("addi t0, t0, 1"); - self.emit("sd t0, -8(t4)"); - true - } - - fn emit_collection_set(&mut self, collection: &IrOperand, index: &IrOperand, value: &IrOperand) -> Result<()> { - self.emit("# collection set"); - self.emit_operand_comment("collection", collection); - self.emit_operand_comment("index", index); - self.emit_operand_comment("value", value); - if self.emit_stack_collection_set(collection, index, value) { - return Ok(()); - } - self.emit("# cellscript abi: collection set is not available for this collection"); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - Ok(()) - } - - fn emit_stack_collection_set(&mut self, collection: &IrOperand, index: &IrOperand, value: &IrOperand) -> bool { - let IrOperand::Var(collection) = collection else { - return false; - }; - if !self.stack_collection_vars.contains(&collection.id) { - return false; - } - let Some(value_width) = self.constructed_byte_vector_part_width(value) else { - return false; - }; - let Some(element_width) = molecule_vector_element_fixed_width(&collection.ty, &self.type_fixed_sizes, &self.enum_fixed_sizes) - else { - return false; - }; - if element_width == 0 || element_width > RUNTIME_COLLECTION_BUFFER_SIZE || element_width != value_width { - return false; - } - let value_scalar = element_width <= 8 && fixed_scalar_operand_width(value).is_some(); - let fixed_byte_source = if value_scalar { - None - } else { - let Some(source) = self.expected_fixed_byte_source(value, element_width) else { - return false; - }; - Some(source) - }; - - self.emit(format!("# cellscript abi: stack collection set element_size={}", element_width)); - if let Some(source) = fixed_byte_source.as_ref() { - self.emit_prepare_fixed_byte_source(source, element_width, "stack collection set"); - } - self.emit_stack_load("t4", collection.id * 8); - self.emit("ld t0, -8(t4)"); - self.emit_operand_to_register("t1", index); - - let bounds_ok = self.fresh_label("stack_collection_set_bounds_ok"); - self.emit("sltu t2, t1, t0"); - self.emit(format!("bnez t2, {}", bounds_ok)); - self.emit_fail(CellScriptRuntimeError::CollectionBoundsInvalid); - self.emit_label(&bounds_ok); - - self.emit(format!("li t2, {}", element_width)); - self.emit("mul t3, t1, t2"); - self.emit("add t5, t4, t3"); - if value_scalar { - self.emit_operand_to_register("t1", value); - match element_width { - 1 => self.emit("sb t1, 0(t5)"), - 2 => self.emit("sh t1, 0(t5)"), - 4 => self.emit("sw t1, 0(t5)"), - 8 => self.emit("sd t1, 0(t5)"), - _ => return false, - } - } else { - let source = fixed_byte_source.as_ref().expect("fixed byte source"); - self.emit(format!("# cellscript abi: stack collection set copy fixed bytes size={}", element_width)); - for byte_index in 0..element_width { - self.emit_fixed_byte_source_byte_to("t1", "t6", source, byte_index); - if byte_index <= 2047 { - self.emit(format!("sb t1, {}(t5)", byte_index)); - } else { - self.emit_large_addi("t0", "t5", byte_index as i64); - self.emit("sb t1, 0(t0)"); - } - } - } - true - } - - fn emit_ckb_fixed_hash_call(&mut self, dest: Option<&IrVar>, func: &str, args: &[IrOperand]) -> Result { - if !is_ckb_fixed_hash_helper(func) { - return Ok(false); - } - self.emit(format!("# call {}", func)); - let Some(dest) = dest else { - self.emit("# cellscript abi: fail closed because hash helper result has no destination"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - let Some(dest_offset) = self.fixed_byte_local_offsets.get(&dest.id).copied() else { - self.emit("# cellscript abi: fail closed because hash helper output buffer was not allocated"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - if matches!(func, "__ckb_hash_pair" | "__ckb_hash_sha256_pair" | "__ckb_hash_sha256d_pair") { - if args.len() != 2 { - self.emit("# cellscript abi: fail closed because hash_pair needs two inputs"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - } - let Some(left) = self.expected_fixed_byte_source(&args[0], 32) else { - self.emit("# cellscript abi: fail closed because hash_pair left input is not a 32-byte value"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - let Some(right) = self.expected_fixed_byte_source(&args[1], 32) else { - self.emit("# cellscript abi: fail closed because hash_pair right input is not a 32-byte value"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - self.emit_prepare_fixed_byte_source(&left, 32, "pair hash left input"); - self.emit_prepare_fixed_byte_source(&right, 32, "pair hash right input"); - if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &left) { - self.emit("# cellscript abi: fail closed because hash_pair left pointer is not materializable"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - } - if !self.emit_fixed_byte_source_pointer_or_const_to("a1", &right) { - self.emit("# cellscript abi: fail closed because hash_pair right pointer is not materializable"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - } - self.emit_sp_addi("a2", dest_offset); - self.emit(format!("call {}", func)); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_sp_addi("t0", dest_offset); - self.emit_stack_store("t0", dest.id * 8); - return Ok(true); - } - if func == "__ckb_hash_blake2b_packed" { - let Some(arg) = args.first() else { - self.emit("# cellscript abi: fail closed because hash_blake2b_packed is missing input"); - self.emit_fail(CellScriptRuntimeError::PackedHashPreimageMaterializationUnresolved); - return Ok(true); - }; - let Some(width) = operand_fixed_byte_width(arg).or_else(|| match arg { - IrOperand::Var(var) => self.fixed_byte_like_width(&var.ty), - _ => None, - }) else { - self.emit("# cellscript abi: fail closed because hash_blake2b_packed input has no static packed width"); - self.emit_fail(CellScriptRuntimeError::PackedHashPreimageMaterializationUnresolved); - return Ok(true); - }; - let Some(source) = self.expected_fixed_byte_source(arg, width) else { - self.emit("# cellscript abi: fail closed because hash_blake2b_packed input is not materializable"); - self.emit_fail(CellScriptRuntimeError::PackedHashPreimageMaterializationUnresolved); - return Ok(true); - }; - let type_name = match arg { - IrOperand::Var(var) => named_type_name(&var.ty).map(str::to_string).unwrap_or_else(|| aggregate_type_label(&var.ty)), - IrOperand::Const(_) => "const".to_string(), - }; - let mut header = b"CellScriptPackedHashV0\0".to_vec(); - header.extend_from_slice(type_name.as_bytes()); - header.push(0); - header.extend_from_slice(&(width as u32).to_le_bytes()); - let total_width = header.len() + width; - if total_width > RUNTIME_SCRATCH_BUFFER_SIZE { - self.emit("# cellscript abi: fail closed because hash_blake2b_packed preimage exceeds scratch buffer"); - self.emit_fail(CellScriptRuntimeError::PackedHashPreimageMaterializationUnresolved); - return Ok(true); - } - let buffer_offset = self.runtime_scratch_buffer_offset(); - for (index, byte) in header.iter().enumerate() { - self.emit(format!("li t0, {}", byte)); - self.emit_stack_store_byte("t0", buffer_offset + index); - } - self.emit_prepare_fixed_byte_source(&source, width, "hash_blake2b_packed input"); - if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &source) { - self.emit("# cellscript abi: fail closed because hash_blake2b_packed input pointer is not materializable"); - self.emit_fail(CellScriptRuntimeError::PackedHashPreimageMaterializationUnresolved); - return Ok(true); - } - self.emit_sp_addi("a1", buffer_offset + header.len()); - self.emit(format!("li a2, {}", width)); - self.emit("call __cellscript_memcpy_fixed"); - self.emit_sp_addi("a0", buffer_offset); - self.emit(format!("li a1, {}", total_width)); - self.emit_sp_addi("a2", dest_offset); - self.emit("call __ckb_hash_blake2b_var"); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_sp_addi("t0", dest_offset); - self.emit_stack_store("t0", dest.id * 8); - return Ok(true); - } - if func == "__ckb_hash_data_packed" { - let Some(arg) = args.first() else { - self.emit("# cellscript abi: fail closed because hash_data_packed is missing input"); - self.emit_fail(CellScriptRuntimeError::PackedHashPreimageMaterializationUnresolved); - return Ok(true); - }; - let Some(width) = operand_fixed_byte_width(arg).or_else(|| match arg { - IrOperand::Var(var) => self.fixed_byte_like_width(&var.ty), - _ => None, - }) else { - self.emit("# cellscript abi: fail closed because hash_data_packed input has no static packed width"); - self.emit_fail(CellScriptRuntimeError::PackedHashPreimageMaterializationUnresolved); - return Ok(true); - }; - let Some(source) = self.expected_fixed_byte_source(arg, width) else { - self.emit("# cellscript abi: fail closed because hash_data_packed input is not materializable"); - self.emit_fail(CellScriptRuntimeError::PackedHashPreimageMaterializationUnresolved); - return Ok(true); - }; - self.emit_prepare_fixed_byte_source(&source, width, "hash_data_packed input"); - if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &source) { - self.emit("# cellscript abi: fail closed because hash_data_packed input pointer is not materializable"); - self.emit_fail(CellScriptRuntimeError::PackedHashPreimageMaterializationUnresolved); - return Ok(true); - } - self.emit(format!("li a1, {}", width)); - self.emit_sp_addi("a2", dest_offset); - self.emit("call __ckb_hash_blake2b_var"); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_sp_addi("t0", dest_offset); - self.emit_stack_store("t0", dest.id * 8); - return Ok(true); - } - let Some(arg) = args.first() else { - self.emit("# cellscript abi: fail closed because hash helper is missing input"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - let Some(source) = self.expected_fixed_byte_source(arg, 32) else { - self.emit("# cellscript abi: fail closed because hash helper input is not a 32-byte value"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - self.emit_prepare_fixed_byte_source(&source, 32, "fixed hash input"); - if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &source) { - self.emit("# cellscript abi: fail closed because hash helper input pointer is not materializable"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - } - self.emit_sp_addi("a1", dest_offset); - self.emit(format!("call {}", func)); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_sp_addi("t0", dest_offset); - self.emit_stack_store("t0", dest.id * 8); - Ok(true) - } - - fn emit_call(&mut self, dest: Option<&IrVar>, func: &str, args: &[IrOperand]) -> Result<()> { - if self.emit_ckb_fixed_hash_call(dest, func, args)? { - return Ok(()); - } - if matches!(func, "__novaseal_bip340_require_signature" | "__novaseal_bip340_require_signature_from_cell_dep") { - self.emit(format!("# call {} args={}", func, args.len())); - let explicit_dep = func == "__novaseal_bip340_require_signature_from_cell_dep"; - let value_offset = usize::from(explicit_dep); - if args.len() != 3 + value_offset { - self.emit("# cellscript abi: fail closed because BIP340 verifier requires message, pubkey, signature"); - self.emit_fail(CellScriptRuntimeError::Bip340MessageMaterializationUnresolved); - return Ok(()); - } - let Some(message) = self.expected_fixed_byte_source(&args[value_offset], 32) else { - self.emit("# cellscript abi: fail closed because BIP340 message is not a 32-byte value"); - self.emit_fail(CellScriptRuntimeError::Bip340MessageMaterializationUnresolved); - return Ok(()); - }; - let Some(pubkey) = self.expected_fixed_byte_source(&args[value_offset + 1], 32) else { - self.emit("# cellscript abi: fail closed because BIP340 pubkey is not a 32-byte value"); - self.emit_fail(CellScriptRuntimeError::Bip340PubkeyMaterializationUnresolved); - return Ok(()); - }; - let Some(signature) = self.expected_fixed_byte_source(&args[value_offset + 2], 64) else { - self.emit("# cellscript abi: fail closed because BIP340 signature is not a 64-byte value"); - self.emit_fail(CellScriptRuntimeError::Bip340SignatureMaterializationUnresolved); - return Ok(()); - }; - self.emit_prepare_fixed_byte_source(&message, 32, "novaseal bip340 message"); - self.emit_prepare_fixed_byte_source(&pubkey, 32, "novaseal bip340 pubkey"); - self.emit_prepare_fixed_byte_source(&signature, 64, "novaseal bip340 signature"); - let Some(read_fd_offset) = self.checked_runtime_expr_temp_offset(0) else { - self.emit_fail(CellScriptRuntimeError::Bip340MessageMaterializationUnresolved); - return Ok(()); - }; - let Some(write_fd_offset) = self.checked_runtime_expr_temp_offset(1) else { - self.emit_fail(CellScriptRuntimeError::Bip340MessageMaterializationUnresolved); - return Ok(()); - }; - let Some(child_pid_offset) = self.checked_runtime_expr_temp_offset(2) else { - self.emit_fail(CellScriptRuntimeError::Bip340MessageMaterializationUnresolved); - return Ok(()); - }; - let ipc_buffer_offset = self.runtime_scratch_buffer_offset(); - self.emit("# cellscript abi: NovaSeal BIP340 verifier IPC envelope via VM2 pipe/spawn/wait"); - let pipe_ok = self.fresh_label("novaseal_bip340_pipe_ok"); - self.emit("call __ckb_pipe"); - self.emit(format!("beqz a0, {}", pipe_ok)); - self.emit_fail(CellScriptRuntimeError::Bip340PipeCreateFailed); - self.emit_label(&pipe_ok); - self.emit_stack_store("a1", read_fd_offset); - self.emit_stack_store("a2", write_fd_offset); - self.emit("# cellscript abi: materialize cellscript-btc-bip340-ipc-v0 envelope in scratch"); - for (index, byte) in b"NSBV0IPC".iter().enumerate() { - self.emit(format!("li t0, {}", byte)); - self.emit_stack_store_byte("t0", ipc_buffer_offset + index); - } - for (index, byte) in [0u8, 0, 1, 0, 0, 0, 0, 0].iter().enumerate() { - self.emit(format!("li t0, {}", byte)); - self.emit_stack_store_byte("t0", ipc_buffer_offset + 8 + index); - } - if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &message) { - self.emit_fail(CellScriptRuntimeError::Bip340MessageMaterializationUnresolved); - return Ok(()); - } - self.emit_sp_addi("a1", ipc_buffer_offset + 16); - self.emit("li a2, 32"); - self.emit("call __cellscript_memcpy_fixed"); - if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &pubkey) { - self.emit_fail(CellScriptRuntimeError::Bip340PubkeyMaterializationUnresolved); - return Ok(()); - } - self.emit_sp_addi("a1", ipc_buffer_offset + 48); - self.emit("li a2, 32"); - self.emit("call __cellscript_memcpy_fixed"); - if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &signature) { - self.emit_fail(CellScriptRuntimeError::Bip340SignatureMaterializationUnresolved); - return Ok(()); - } - self.emit_sp_addi("a1", ipc_buffer_offset + 80); - self.emit("li a2, 64"); - self.emit("call __cellscript_memcpy_fixed"); - self.emit("# cellscript abi: spawn manifest-bound verifier CellDep with prepared read fd inherited"); - if explicit_dep { - self.emit_operand_to_register("a0", &args[0]); - } else { - self.emit("li a0, 0"); - } - self.emit_stack_load("a1", read_fd_offset); - self.emit("call __ckb_spawn_with_fd1"); - let spawn_ok = self.fresh_label("novaseal_bip340_spawn_ok"); - self.emit(format!("beqz a0, {}", spawn_ok)); - self.emit_fail(CellScriptRuntimeError::Bip340SpawnFailed); - self.emit_label(&spawn_ok); - self.emit_stack_store("a1", child_pid_offset); - self.emit("# cellscript abi: BIP340 IPC write canonical 18-word little-endian envelope"); - for word_index in 0..18 { - self.emit(format!("# cellscript abi: novaseal bip340 ipc word {}", word_index)); - self.emit_stack_load("a0", write_fd_offset); - self.emit_stack_load("a1", ipc_buffer_offset + word_index * 8); - self.emit("call __ckb_pipe_write"); - let write_ok = self.fresh_label("novaseal_bip340_write_ok"); - self.emit(format!("beqz a0, {}", write_ok)); - self.emit_fail(CellScriptRuntimeError::Bip340MessageWriteFailed); - self.emit_label(&write_ok); - } - self.emit_stack_load("a0", write_fd_offset); - self.emit("call __ckb_close"); - let close_ok = self.fresh_label("novaseal_bip340_close_ok"); - self.emit(format!("beqz a0, {}", close_ok)); - self.emit_fail(CellScriptRuntimeError::Bip340VerifierReadFailed); - self.emit_label(&close_ok); - self.emit_stack_load("a0", child_pid_offset); - self.emit("call __ckb_wait"); - let wait_ok = self.fresh_label("novaseal_bip340_wait_ok"); - self.emit(format!("beqz a0, {}", wait_ok)); - self.emit_fail(CellScriptRuntimeError::Bip340ChildRejected); - self.emit_label(&wait_ok); - return Ok(()); - } - if func.contains("::") { - return Err(CompileError::new( - format!("qualified function call '{}' reached codegen without IR label normalization; this is a compiler bug", func), - crate::error::Span::default(), - )); - } - self.emit(format!("# call {}", func)); - - if self.emit_runtime_fixed_hash_requirement_call(func, args)? { - return Ok(()); - } - if self.emit_runtime_bounded_cell_dep_requirement_call(func, args)? { - return Ok(()); - } - if self.emit_runtime_sha256d_merkle_requirement_call(func, args)? { - return Ok(()); - } - if self.emit_runtime_cell_script_args_exact_requirement_call(func, args)? { - return Ok(()); - } - if self.emit_runtime_cell_script_hash_type_requirement_call(func, args)? { - return Ok(()); - } - if self.emit_runtime_input_out_point_requirement_call(func, args)? { - return Ok(()); - } - if self.emit_runtime_xudt_type_args_requirement_call(func, args)? { - return Ok(()); - } - if self.emit_runtime_xudt_group_amount_delta_call(func, args)? { - return Ok(()); - } - if self.emit_runtime_metapoint_filtered_pair_call(func, args)? { - return Ok(()); - } - if self.emit_runtime_c256_product_requirement_call(func, args)? { - return Ok(()); - } - if self.emit_runtime_c256_sum2_product_requirement_call(func, args)? { - return Ok(()); - } - if self.emit_runtime_current_script_hash_call(dest, func, args)? { - return Ok(()); - } - if self.emit_runtime_input_out_point_tx_hash_call(dest, func, args)? { - return Ok(()); - } - if self.emit_runtime_cell_data_hash_at_call(dest, func, args)? { - return Ok(()); - } - if self.emit_runtime_cell_script_hash_field_call(dest, func, args)? { - return Ok(()); - } - if self.emit_runtime_witness_hash_call(dest, func, args)? { - return Ok(()); - } - - let abi = self.callable_abis.get(func).cloned(); - let outgoing_stack_arg_bytes = align_stack_arg_bytes(call_abi_arg_count(abi.as_ref(), args).saturating_sub(8) * 8); - let mut abi_index = 0usize; - for (arg_index, arg) in args.iter().enumerate() { - if let Some(abi) = &abi { - if let Some(param) = abi.params.get(arg_index) { - let needs_type_hash = abi.type_hash_param_indices.contains(&arg_index); - if !self.emit_call_param_arg(func, param, needs_type_hash, &mut abi_index, arg, outgoing_stack_arg_bytes) { - return Ok(()); - } - continue; - } - } - if !self.emit_call_scalar_arg(func, &format!("arg{}", arg_index), &mut abi_index, arg, outgoing_stack_arg_bytes) { - return Ok(()); - } - } - - if outgoing_stack_arg_bytes > 0 { - self.emit(format!("# cellscript abi: reserve {} bytes for outgoing stack call arguments", outgoing_stack_arg_bytes)); - self.emit_large_addi("sp", "sp", -(outgoing_stack_arg_bytes as i64)); - } - self.emit(format!("call {}", func)); - if outgoing_stack_arg_bytes > 0 { - self.emit_large_addi("sp", "sp", outgoing_stack_arg_bytes as i64); - } - - if is_runtime_scalar_failclosed_call(func) { - let ok_label = self.fresh_label("runtime_scalar_ok"); - self.emit("# cellscript abi: scalar runtime helper status check (a1 == 0)"); - self.emit(format!("beqz a1, {}", ok_label)); - self.emit("addi a0, a1, 0"); - self.emit_epilogue(); - self.emit_label(&ok_label); - } - - if dest.is_none() && is_void_runtime_requirement_call(func) { - let ok_label = self.fresh_label("runtime_requirement_ok"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_epilogue(); - self.emit_label(&ok_label); - } - - if let Some(d) = dest { - let payload_enum = match &d.ty { - IrType::Named(name) => { - self.enum_layouts.get(name).filter(|layout| layout.has_payload()).map(|layout| (name.clone(), layout.clone())) - } - _ => None, - }; - if let Some((name, layout)) = payload_enum { - if let Some(offset) = self.fixed_byte_local_offsets.get(&d.id).copied() { - self.emit(format!( - "# cellscript abi: receive payload enum {} size={} from a0/a1 register pair", - name, layout.encoded_size - )); - let low_width = layout.encoded_size.min(8); - for byte_index in 0..low_width { - self.emit_stack_store_byte("a0", offset + byte_index); - if byte_index + 1 < low_width { - self.emit("srli a0, a0, 8"); - } - } - if layout.encoded_size > 8 { - let high_width = layout.encoded_size - 8; - for byte_index in 0..high_width { - self.emit_stack_store_byte("a1", offset + 8 + byte_index); - if byte_index + 1 < high_width { - self.emit("srli a1, a1, 8"); - } - } - } - self.emit_sp_addi("t0", offset); - self.emit_stack_store("t0", d.id * 8); - } else { - self.emit("# cellscript abi: payload enum call destination has no storage; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - } - } else if d.ty == IrType::U128 { - if let Some(offset) = self.u128_value_offsets.get(&d.id).copied() { - self.emit("# cellscript abi: receive u128 return from a0(low)/a1(high)"); - self.emit_stack_store("a0", offset); - self.emit_stack_store("a1", offset + 8); - self.emit_store_u128_pointer_for_var(d.id, offset); - } else { - self.emit("# cellscript abi: u128 call destination has no storage; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - } - } else if let IrType::Tuple(items) = &d.ty { - self.emit_stack_store("a0", d.id * 8); - for index in 0..items.len().min(8) { - let field = index.to_string(); - if let Some(field_var_id) = self.tuple_call_return_field_slots.get(&(d.id, field)).copied() { - self.emit_stack_store(&format!("a{}", index), field_var_id * 8); - } - } - } else { - self.emit_stack_store("a0", d.id * 8); - } - } - - Ok(()) - } - - fn emit_runtime_current_script_hash_call(&mut self, dest: Option<&IrVar>, func: &str, args: &[IrOperand]) -> Result { - if func != "__ckb_current_script_hash" { - return Ok(false); - } - let Some(dest) = dest else { - return Ok(false); - }; - if !args.is_empty() || dest.ty != IrType::Hash { - return Ok(false); - } - let Some(size_offset) = self.cell_buffer_size_offsets.get(&dest.id).copied() else { - self.emit("# cellscript abi: current script hash destination has no 32-byte storage; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - let Some(buffer_offset) = self.cell_buffer_offsets.get(&dest.id).copied() else { - self.emit("# cellscript abi: current script hash destination has no buffer storage; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - - self.emit("# cellscript abi: load current script hash into addressable Hash"); - self.emit("li t0, 32"); - self.emit_stack_store("t0", size_offset); - self.emit_sp_addi("a0", buffer_offset); - self.emit_sp_addi("a1", size_offset); - self.emit("call __ckb_current_script_hash"); - let ok_label = self.fresh_label("current_script_hash_ok"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_epilogue(); - self.emit_label(&ok_label); - self.emit_sp_addi("t0", buffer_offset); - self.emit_stack_store("t0", dest.id * 8); - Ok(true) - } - - fn emit_runtime_input_out_point_tx_hash_call(&mut self, dest: Option<&IrVar>, func: &str, args: &[IrOperand]) -> Result { - if func != "__ckb_input_out_point_tx_hash" { - return Ok(false); - } - let Some(dest) = dest else { - return Ok(false); - }; - if args.len() != 1 || dest.ty != IrType::Hash { - return Ok(false); - } - let Some(size_offset) = self.cell_buffer_size_offsets.get(&dest.id).copied() else { - self.emit("# cellscript abi: input OutPoint tx hash destination has no 32-byte storage; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - let Some(buffer_offset) = self.cell_buffer_offsets.get(&dest.id).copied() else { - self.emit("# cellscript abi: input OutPoint tx hash destination has no buffer storage; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - - self.emit("# cellscript abi: load SourceView input OutPoint tx hash into addressable Hash"); - self.emit("li t0, 32"); - self.emit_stack_store("t0", size_offset); - self.emit_operand_to_register("a0", &args[0]); - self.emit_sp_addi("a1", buffer_offset); - self.emit_sp_addi("a2", size_offset); - self.emit("call __ckb_input_out_point_tx_hash"); - let ok_label = self.fresh_label("input_out_point_tx_hash_ok"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_epilogue(); - self.emit_label(&ok_label); - self.emit_sp_addi("t0", buffer_offset); - self.emit_stack_store("t0", dest.id * 8); - Ok(true) - } - - fn emit_runtime_cell_script_hash_field_call(&mut self, dest: Option<&IrVar>, func: &str, args: &[IrOperand]) -> Result { - if !matches!( - func, - "__ckb_cell_lock_hash" - | "__ckb_cell_type_hash" - | "__ckb_cell_data_hash" - | "__ckb_cell_lock_code_hash" - | "__ckb_cell_type_code_hash" - | "__ckb_cell_lock_args_hash" - | "__ckb_cell_type_args_hash" - ) { - return Ok(false); - } - let Some(dest) = dest else { - return Ok(false); - }; - if args.len() != 1 || dest.ty != IrType::Hash { - return Ok(false); - } - let Some(size_offset) = self.cell_buffer_size_offsets.get(&dest.id).copied() else { - self.emit("# cellscript abi: ScriptRef hash destination has no 32-byte storage; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - let Some(buffer_offset) = self.cell_buffer_offsets.get(&dest.id).copied() else { - self.emit("# cellscript abi: ScriptRef hash destination has no buffer storage; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - - self.emit("# cellscript abi: load SourceView ScriptRef hash field into addressable Hash"); - self.emit("li t0, 32"); - self.emit_stack_store("t0", size_offset); - self.emit_operand_to_register("a0", &args[0]); - self.emit_sp_addi("a1", buffer_offset); - self.emit_sp_addi("a2", size_offset); - self.emit(format!("call {}", func)); - let ok_label = self.fresh_label("script_ref_hash_ok"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_epilogue(); - self.emit_label(&ok_label); - self.emit_sp_addi("t0", buffer_offset); - self.emit_stack_store("t0", dest.id * 8); - Ok(true) - } - - fn emit_runtime_cell_data_hash_at_call(&mut self, dest: Option<&IrVar>, func: &str, args: &[IrOperand]) -> Result { - if func != "__ckb_cell_data_hash_at" { - return Ok(false); - } - let Some(dest) = dest else { - return Ok(false); - }; - if args.len() != 2 || dest.ty != IrType::Hash { - return Ok(false); - } - let Some(size_offset) = self.cell_buffer_size_offsets.get(&dest.id).copied() else { - self.emit("# cellscript abi: cell data hash-at destination has no 32-byte storage; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - let Some(buffer_offset) = self.cell_buffer_offsets.get(&dest.id).copied() else { - self.emit("# cellscript abi: cell data hash-at destination has no buffer storage; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - - self.emit("# cellscript abi: load 32 bytes from SourceView cell data into addressable Hash"); - self.emit("li t0, 32"); - self.emit_stack_store("t0", size_offset); - self.emit_operand_to_register("a0", &args[0]); - self.emit_operand_to_register("a1", &args[1]); - self.emit_sp_addi("a2", buffer_offset); - self.emit_sp_addi("a3", size_offset); - self.emit("call __ckb_cell_data_hash_at"); - let ok_label = self.fresh_label("cell_data_hash_at_ok"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_epilogue(); - self.emit_label(&ok_label); - self.emit_sp_addi("t0", buffer_offset); - self.emit_stack_store("t0", dest.id * 8); - Ok(true) - } - - fn emit_runtime_witness_hash_call(&mut self, dest: Option<&IrVar>, func: &str, args: &[IrOperand]) -> Result { - if !matches!(func, "__ckb_witness_raw" | "__ckb_witness_lock" | "__ckb_witness_input_type" | "__ckb_witness_output_type") { - return Ok(false); - } - let Some(dest) = dest else { - return Ok(false); - }; - if args.len() != 1 || dest.ty != IrType::Hash { - return Ok(false); - } - let Some(size_offset) = self.cell_buffer_size_offsets.get(&dest.id).copied() else { - self.emit("# cellscript abi: witness hash destination has no 32-byte storage; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - let Some(buffer_offset) = self.cell_buffer_offsets.get(&dest.id).copied() else { - self.emit("# cellscript abi: witness hash destination has no buffer storage; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - - self.emit("# cellscript abi: load witness hash into addressable Hash"); - self.emit("li t0, 32"); - self.emit_stack_store("t0", size_offset); - self.emit_operand_to_register("a0", &args[0]); - self.emit_sp_addi("a1", buffer_offset); - self.emit(format!("call {}", func)); - let ok_label = self.fresh_label("witness_hash_ok"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_epilogue(); - self.emit_label(&ok_label); - self.emit_sp_addi("t0", buffer_offset); - self.emit_stack_store("t0", dest.id * 8); - Ok(true) - } - - fn emit_runtime_fixed_hash_requirement_call(&mut self, func: &str, args: &[IrOperand]) -> Result { - if !matches!( - func, - "__ckb_require_cell_lock_hash" - | "__ckb_require_cell_type_hash" - | "__ckb_require_cell_data_hash" - | "__ckb_require_cell_lock_args_hash" - | "__ckb_require_cell_type_args_hash" - | "__ckb_require_cell_lock_args_prefix_hash" - | "__ckb_require_cell_type_args_prefix_hash" - | "__ckb_require_cell_lock_args_suffix_hash" - | "__ckb_require_cell_type_args_suffix_hash" - | "__ckb_require_input_out_point_tx_hash" - | "__xudt_require_owner_mode_input_type" - ) { - return Ok(false); - } - if args.len() != 2 { - return Ok(false); - } - - let expected = self.expected_fixed_byte_source(&args[1], 32); - match expected { - Some(ExpectedFixedByteSource::Const(bytes)) => { - let size_offset = self.runtime_scratch_size_offset(); - let buffer_offset = self.runtime_scratch_buffer_offset(); - let hash: [u8; 32] = bytes.as_slice().try_into().expect("expected fixed hash width"); - self.emit_store_fixed_byte_const_to_scratch(&IrOperand::Const(IrConst::Hash(hash)), size_offset, buffer_offset, 32); - self.emit_sp_addi("a1", buffer_offset); - self.emit("li a2, 32"); - } - Some(source) => { - self.emit_prepare_fixed_byte_source(&source, 32, "runtime expected hash"); - if self.emit_fixed_byte_source_pointer_to("a1", &source) { - self.emit("li a2, 32"); - } else { - self.emit("# cellscript abi: runtime expected hash source is not addressable; pass null to fail closed"); - self.emit("li a1, 0"); - self.emit("li a2, 0"); - } - } - None => { - self.emit("# cellscript abi: runtime expected hash source is unavailable; pass null to fail closed"); - self.emit("li a1, 0"); - self.emit("li a2, 0"); - } - } - - self.emit_operand_to_register("a0", &args[0]); - self.emit("call ".to_string() + func); - let ok_label = self.fresh_label("runtime_hash_requirement_ok"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_epilogue(); - self.emit_label(&ok_label); - Ok(true) - } - - fn emit_runtime_bounded_cell_dep_requirement_call(&mut self, func: &str, args: &[IrOperand]) -> Result { - if func != "__ckb_require_bounded_cell_dep_data_hash" { - return Ok(false); - } - if args.len() != 2 { - return Ok(false); - } - - let expected = self.expected_fixed_byte_source(&args[1], 32); - match expected { - Some(ExpectedFixedByteSource::Const(bytes)) => { - let size_offset = self.runtime_scratch_size_offset(); - let buffer_offset = self.runtime_scratch_buffer_offset(); - let hash: [u8; 32] = bytes.as_slice().try_into().expect("expected fixed hash width"); - self.emit_store_fixed_byte_const_to_scratch(&IrOperand::Const(IrConst::Hash(hash)), size_offset, buffer_offset, 32); - self.emit_sp_addi("a1", buffer_offset); - } - Some(source) => { - self.emit_prepare_fixed_byte_source(&source, 32, "bounded CellDep expected data hash"); - if !self.emit_fixed_byte_source_pointer_to("a1", &source) { - self.emit("# cellscript abi: bounded CellDep expected hash is not addressable; pass null to fail closed"); - self.emit("li a1, 0"); - } - } - None => { - self.emit("# cellscript abi: bounded CellDep expected hash is unavailable; pass null to fail closed"); - self.emit("li a1, 0"); - } - } - self.emit_operand_to_register("a0", &args[0]); - self.emit("call __ckb_require_bounded_cell_dep_data_hash"); - let ok_label = self.fresh_label("bounded_cell_dep_requirement_ok"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_epilogue(); - self.emit_label(&ok_label); - Ok(true) - } - - fn emit_runtime_sha256d_merkle_requirement_call(&mut self, func: &str, args: &[IrOperand]) -> Result { - if func != "__ckb_require_sha256d_merkle_root" { - return Ok(false); - } - if args.len() != 5 { - return Ok(false); - } - let Some(leaf) = self.expected_fixed_byte_source(&args[0], 32) else { - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - let Some(siblings) = self.expected_fixed_byte_source(&args[1], 16 * 32) else { - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - let Some(expected_root) = self.expected_fixed_byte_source(&args[4], 32) else { - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - self.emit_prepare_fixed_byte_source(&leaf, 32, "SHA256d Merkle leaf"); - self.emit_prepare_fixed_byte_source(&siblings, 16 * 32, "SHA256d Merkle siblings"); - self.emit_prepare_fixed_byte_source(&expected_root, 32, "SHA256d Merkle expected root"); - if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &leaf) - || !self.emit_fixed_byte_source_pointer_or_const_to("a1", &siblings) - || !self.emit_fixed_byte_source_pointer_or_const_to("a4", &expected_root) - { - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - } - self.emit_operand_to_register("a2", &args[2]); - self.emit_operand_to_register("a3", &args[3]); - self.emit("call __ckb_require_sha256d_merkle_root"); - let ok = self.fresh_label("sha256d_merkle_requirement_ok"); - self.emit(format!("beqz a0, {}", ok)); - self.emit_epilogue(); - self.emit_label(&ok); - Ok(true) - } - - fn emit_runtime_cell_script_hash_type_requirement_call(&mut self, func: &str, args: &[IrOperand]) -> Result { - if !matches!(func, "__ckb_require_cell_lock_script_hash_type" | "__ckb_require_cell_type_script_hash_type") { - return Ok(false); - } - if args.len() != 3 { - return Ok(false); - } - - let expected = self.expected_fixed_byte_source(&args[1], 32); - match expected { - Some(ExpectedFixedByteSource::Const(bytes)) => { - let size_offset = self.runtime_scratch_size_offset(); - let buffer_offset = self.runtime_scratch_buffer_offset(); - let hash: [u8; 32] = bytes.as_slice().try_into().expect("expected fixed hash width"); - self.emit_store_fixed_byte_const_to_scratch(&IrOperand::Const(IrConst::Hash(hash)), size_offset, buffer_offset, 32); - self.emit_sp_addi("a1", buffer_offset); - self.emit("li a2, 32"); - } - Some(source) => { - self.emit_prepare_fixed_byte_source(&source, 32, "runtime expected Script code hash"); - if self.emit_fixed_byte_source_pointer_to("a1", &source) { - self.emit("li a2, 32"); - } else { - self.emit( - "# cellscript abi: runtime expected Script code hash source is not addressable; pass null to fail closed", - ); - self.emit("li a1, 0"); - self.emit("li a2, 0"); - } - } - None => { - self.emit("# cellscript abi: runtime expected Script code hash is unavailable; pass null to fail closed"); - self.emit("li a1, 0"); - self.emit("li a2, 0"); - } - } - - self.emit_operand_to_register("a0", &args[0]); - self.emit_operand_to_register("a3", &args[2]); - self.emit("call ".to_string() + func); - let ok_label = self.fresh_label("runtime_script_identity_ok"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_epilogue(); - self.emit_label(&ok_label); - Ok(true) - } - - fn emit_runtime_cell_script_args_exact_requirement_call(&mut self, func: &str, args: &[IrOperand]) -> Result { - if !matches!(func, "__ckb_require_cell_lock_args_exact" | "__ckb_require_cell_type_args_exact") { - return Ok(false); - } - if args.len() != 2 { - return Ok(false); - } - let Some(width) = operand_fixed_byte_width(&args[1]) else { - self.emit("# cellscript abi: runtime expected Script args source has no fixed byte width; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - let Some(expected) = self.expected_fixed_byte_source(&args[1], width) else { - self.emit("# cellscript abi: runtime expected Script args source is unavailable; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - }; - - match expected { - ExpectedFixedByteSource::Const(bytes) => { - let size_offset = self.runtime_scratch_size_offset(); - let buffer_offset = self.runtime_scratch_buffer_offset(); - self.emit_store_fixed_byte_const_to_scratch( - &IrOperand::Const(IrConst::Array(bytes.into_iter().map(IrConst::U8).collect())), - size_offset, - buffer_offset, - width, - ); - self.emit_sp_addi("a1", buffer_offset); - } - source => { - self.emit_prepare_fixed_byte_source(&source, width, "runtime expected Script args"); - if !self.emit_fixed_byte_source_pointer_to("a1", &source) { - self.emit("# cellscript abi: runtime expected Script args source is not addressable; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(true); - } - } - } - - self.emit_operand_to_register("a0", &args[0]); - self.emit(format!("li a2, {}", width)); - self.emit("call ".to_string() + func); - let ok_label = self.fresh_label("runtime_script_args_exact_ok"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_epilogue(); - self.emit_label(&ok_label); - Ok(true) - } - - fn emit_runtime_input_out_point_requirement_call(&mut self, func: &str, args: &[IrOperand]) -> Result { - if func != "__ckb_require_input_out_point" { - return Ok(false); - } - if args.len() != 3 { - return Ok(false); - } - - let expected = self.expected_fixed_byte_source(&args[1], 32); - match expected { - Some(ExpectedFixedByteSource::Const(bytes)) => { - let size_offset = self.runtime_scratch_size_offset(); - let buffer_offset = self.runtime_scratch_buffer_offset(); - let hash: [u8; 32] = bytes.as_slice().try_into().expect("expected fixed hash width"); - self.emit_store_fixed_byte_const_to_scratch(&IrOperand::Const(IrConst::Hash(hash)), size_offset, buffer_offset, 32); - self.emit_sp_addi("a1", buffer_offset); - self.emit("li a2, 32"); - } - Some(source) => { - self.emit_prepare_fixed_byte_source(&source, 32, "runtime expected input out point tx hash"); - if self.emit_fixed_byte_source_pointer_to("a1", &source) { - self.emit("li a2, 32"); - } else { - self.emit( - "# cellscript abi: runtime expected input out point hash source is not addressable; pass null to fail closed", - ); - self.emit("li a1, 0"); - self.emit("li a2, 0"); - } - } - None => { - self.emit("# cellscript abi: runtime expected input out point hash source is unavailable; pass null to fail closed"); - self.emit("li a1, 0"); - self.emit("li a2, 0"); - } - } - - self.emit_operand_to_register("a3", &args[2]); - self.emit_operand_to_register("a0", &args[0]); - self.emit("call __ckb_require_input_out_point"); - let ok_label = self.fresh_label("runtime_input_out_point_requirement_ok"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_epilogue(); - self.emit_label(&ok_label); - Ok(true) - } - - fn emit_runtime_xudt_type_args_requirement_call(&mut self, func: &str, args: &[IrOperand]) -> Result { - if func != "__xudt_require_owner_mode_type_args" { - return Ok(false); - } - if args.len() != 3 { - return Ok(false); - } - - let expected = self.expected_fixed_byte_source(&args[1], 32); - match expected { - Some(ExpectedFixedByteSource::Const(bytes)) => { - let size_offset = self.runtime_scratch_size_offset(); - let buffer_offset = self.runtime_scratch_buffer_offset(); - let hash: [u8; 32] = bytes.as_slice().try_into().expect("expected fixed hash width"); - self.emit_store_fixed_byte_const_to_scratch(&IrOperand::Const(IrConst::Hash(hash)), size_offset, buffer_offset, 32); - self.emit_sp_addi("a1", buffer_offset); - self.emit("li a2, 32"); - } - Some(source) => { - self.emit_prepare_fixed_byte_source(&source, 32, "runtime expected xUDT owner hash"); - if self.emit_fixed_byte_source_pointer_to("a1", &source) { - self.emit("li a2, 32"); - } else { - self.emit("# cellscript abi: runtime xUDT owner hash source is not addressable; pass null to fail closed"); - self.emit("li a1, 0"); - self.emit("li a2, 0"); - } - } - None => { - self.emit("# cellscript abi: runtime xUDT owner hash source is unavailable; pass null to fail closed"); - self.emit("li a1, 0"); - self.emit("li a2, 0"); - } - } - - self.emit_operand_to_register("a0", &args[0]); - self.emit_operand_to_register("a3", &args[2]); - self.emit("call __xudt_require_owner_mode_type_args"); - let ok_label = self.fresh_label("runtime_xudt_args_requirement_ok"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_epilogue(); - self.emit_label(&ok_label); - Ok(true) - } - - fn emit_runtime_xudt_group_amount_delta_call(&mut self, func: &str, args: &[IrOperand]) -> Result { - if !matches!(func, "__xudt_require_group_amount_minted" | "__xudt_require_group_amount_burned") { - return Ok(false); - } - if args.len() != 1 { - return Ok(false); - } - - let source = self.expected_fixed_byte_source(&args[0], 16); - match source { - Some(ExpectedFixedByteSource::Const(bytes)) => { - let value = u128::from_le_bytes(bytes.as_slice().try_into().expect("expected fixed u128 width")); - let buffer_offset = self.runtime_scratch_buffer_offset(); - self.emit_store_fixed_byte_const_to_scratch( - &IrOperand::Const(IrConst::U128(value)), - self.runtime_scratch_size_offset(), - buffer_offset, - 16, - ); - self.emit_sp_addi("a0", buffer_offset); - } - Some(source) => { - self.emit_prepare_fixed_byte_source(&source, 16, "runtime xUDT group amount delta"); - if !self.emit_fixed_byte_source_pointer_to("a0", &source) { - self.emit("# cellscript abi: xUDT group amount delta is not addressable; pass null to fail closed"); - self.emit("li a0, 0"); - } - } - None => { - self.emit("# cellscript abi: xUDT group amount delta is unavailable; pass null to fail closed"); - self.emit("li a0, 0"); - } - } - - self.emit("call ".to_string() + func); - let ok_label = self.fresh_label("runtime_xudt_delta_requirement_ok"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_epilogue(); - self.emit_label(&ok_label); - Ok(true) - } - - fn emit_runtime_metapoint_filtered_pair_call(&mut self, func: &str, args: &[IrOperand]) -> Result { - if !matches!( - func, - "__ckb_require_lock_type_metapoint_pairs_from_i32_data_filtered" - | "__ckb_require_type_lock_metapoint_pairs_from_i32_data_filtered" - ) { - return Ok(false); - } - if args.len() != 4 { - return Ok(false); - } - - let expected = self.expected_fixed_byte_source(&args[2], 32); - match expected { - Some(ExpectedFixedByteSource::Const(bytes)) => { - let size_offset = self.runtime_scratch_size_offset(); - let buffer_offset = self.runtime_scratch_buffer_offset(); - let hash: [u8; 32] = bytes.as_slice().try_into().expect("expected fixed hash width"); - self.emit_store_fixed_byte_const_to_scratch(&IrOperand::Const(IrConst::Hash(hash)), size_offset, buffer_offset, 32); - self.emit_sp_addi("a2", buffer_offset); - self.emit("li a3, 32"); - } - Some(source) => { - self.emit_prepare_fixed_byte_source(&source, 32, "runtime filtered MetaPoint related type hash"); - if self.emit_fixed_byte_source_pointer_to("a2", &source) { - self.emit("li a3, 32"); - } else { - self.emit("# cellscript abi: filtered MetaPoint expected type hash is not addressable; pass null to fail closed"); - self.emit("li a2, 0"); - self.emit("li a3, 0"); - } - } - None => { - self.emit("# cellscript abi: filtered MetaPoint expected type hash is unavailable; pass null to fail closed"); - self.emit("li a2, 0"); - self.emit("li a3, 0"); - } - } - - self.emit_operand_to_register("a0", &args[0]); - self.emit_operand_to_register("a1", &args[1]); - self.emit_operand_to_register("a4", &args[3]); - self.emit("call ".to_string() + func); - let ok_label = self.fresh_label("runtime_metapoint_filtered_pair_ok"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_epilogue(); - self.emit_label(&ok_label); - Ok(true) - } - - fn emit_runtime_c256_product_requirement_call(&mut self, func: &str, args: &[IrOperand]) -> Result { - if !matches!(func, "__c256_require_u128_product_lte" | "__c256_require_u128_product_eq") { - return Ok(false); - } - if args.len() != 4 { - return Ok(false); - } - - let scratch_base = self.runtime_scratch_buffer_offset(); - for (index, (register, arg)) in ["a0", "a1", "a2", "a3"].into_iter().zip(args.iter()).enumerate() { - let source = self.expected_fixed_byte_source(arg, 16); - match source { - Some(ExpectedFixedByteSource::Const(bytes)) => { - let value = u128::from_le_bytes(bytes.as_slice().try_into().expect("expected fixed u128 width")); - let buffer_offset = scratch_base + index * 16; - self.emit_store_fixed_byte_const_to_scratch( - &IrOperand::Const(IrConst::U128(value)), - self.runtime_scratch_size_offset(), - buffer_offset, - 16, - ); - self.emit_sp_addi(register, buffer_offset); - } - Some(source) => { - self.emit_prepare_fixed_byte_source(&source, 16, "runtime c256 u128 product operand"); - if !self.emit_fixed_byte_source_pointer_to(register, &source) { - self.emit(format!( - "# cellscript abi: c256 product operand {} is not addressable; pass null to fail closed", - index - )); - self.emit(format!("li {}, 0", register)); - } - } - None => { - self.emit(format!("# cellscript abi: c256 product operand {} is unavailable; pass null to fail closed", index)); - self.emit(format!("li {}, 0", register)); - } - } - } - - self.emit("call ".to_string() + func); - let ok_label = self.fresh_label("runtime_c256_requirement_ok"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_epilogue(); - self.emit_label(&ok_label); - Ok(true) - } - - fn emit_runtime_c256_sum2_product_requirement_call(&mut self, func: &str, args: &[IrOperand]) -> Result { - if !matches!(func, "__c256_require_u128_sum2_products_lte" | "__c256_require_u128_sum2_products_eq") { - return Ok(false); - } - if args.len() != 8 { - return Ok(false); - } - - let scratch_base = self.runtime_scratch_buffer_offset(); - for (index, (register, arg)) in ["a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7"].into_iter().zip(args.iter()).enumerate() { - let source = self.expected_fixed_byte_source(arg, 16); - match source { - Some(ExpectedFixedByteSource::Const(bytes)) => { - let value = u128::from_le_bytes(bytes.as_slice().try_into().expect("expected fixed u128 width")); - let buffer_offset = scratch_base + index * 16; - self.emit_store_fixed_byte_const_to_scratch( - &IrOperand::Const(IrConst::U128(value)), - self.runtime_scratch_size_offset(), - buffer_offset, - 16, - ); - self.emit_sp_addi(register, buffer_offset); - } - Some(source) => { - self.emit_prepare_fixed_byte_source(&source, 16, "runtime c256 sum-product operand"); - if !self.emit_fixed_byte_source_pointer_to(register, &source) { - self.emit(format!( - "# cellscript abi: c256 sum-product operand {} is not addressable; pass null to fail closed", - index - )); - self.emit(format!("li {}, 0", register)); - } - } - None => { - self.emit(format!( - "# cellscript abi: c256 sum-product operand {} is unavailable; pass null to fail closed", - index - )); - self.emit(format!("li {}, 0", register)); - } - } - } - - self.emit("call ".to_string() + func); - let ok_label = self.fresh_label("runtime_c256_sum_requirement_ok"); - self.emit(format!("beqz a0, {}", ok_label)); - self.emit_epilogue(); - self.emit_label(&ok_label); - Ok(true) - } - - fn emit_call_param_arg( - &mut self, - func: &str, - param: &IrParam, - needs_type_hash: bool, - abi_index: &mut usize, - arg: &IrOperand, - outgoing_stack_arg_bytes: usize, - ) -> bool { - if let IrType::Named(name) = ¶m.ty { - if let Some(layout) = self.enum_layouts.get(name).filter(|layout| layout.has_payload()) { - let width = layout.encoded_size; - self.emit(format!( - "# cellscript abi: call {} payload enum param {} pointer={} length={} size={}", - func, - param.name, - abi_arg_label(*abi_index), - abi_arg_label(*abi_index + 1), - width - )); - if !self.emit_call_pointer_arg(func, ¶m.name, abi_index, arg, Some(width), outgoing_stack_arg_bytes) { - return false; - } - if !self.emit_call_length_arg(func, ¶m.name, abi_index, arg, CallLengthKind::FixedBytes, outgoing_stack_arg_bytes) - { - return false; - } - return true; - } - } - if named_type_name(¶m.ty).is_some() { - self.emit(format!( - "# cellscript abi: call {} schema param {} pointer={} length={}", - func, - param.name, - abi_arg_label(*abi_index), - abi_arg_label(*abi_index + 1) - )); - if !self.emit_call_pointer_arg(func, ¶m.name, abi_index, arg, None, outgoing_stack_arg_bytes) { - return false; - } - if !self.emit_call_length_arg(func, ¶m.name, abi_index, arg, CallLengthKind::Schema, outgoing_stack_arg_bytes) { - return false; - } - if needs_type_hash { - self.emit(format!( - "# cellscript abi: call {} schema param {} type_hash pointer={} length={} size=32", - func, - param.name, - abi_arg_label(*abi_index), - abi_arg_label(*abi_index + 1) - )); - if !self.emit_call_type_hash_pointer_arg(func, ¶m.name, abi_index, arg, outgoing_stack_arg_bytes) { - return false; - } - if !self.emit_call_type_hash_length_arg(func, ¶m.name, abi_index, arg, outgoing_stack_arg_bytes) { - return false; - } - } - return true; - } - - let fixed_pointer_width = fixed_byte_pointer_param_width(¶m.ty).or_else(|| fixed_aggregate_pointer_param_width(¶m.ty)); - if let Some(width) = fixed_pointer_width { - self.emit(format!( - "# cellscript abi: call {} fixed-byte param {} pointer={} length={} size={}", - func, - param.name, - abi_arg_label(*abi_index), - abi_arg_label(*abi_index + 1), - width - )); - if !self.emit_call_pointer_arg(func, ¶m.name, abi_index, arg, Some(width), outgoing_stack_arg_bytes) { - return false; - } - if !self.emit_call_length_arg(func, ¶m.name, abi_index, arg, CallLengthKind::FixedBytes, outgoing_stack_arg_bytes) { - return false; - } - return true; - } - - self.emit_call_scalar_arg(func, ¶m.name, abi_index, arg, outgoing_stack_arg_bytes) - } - - fn emit_call_scalar_arg( - &mut self, - func: &str, - label: &str, - abi_index: &mut usize, - arg: &IrOperand, - outgoing_stack_arg_bytes: usize, - ) -> bool { - let register = self.call_abi_register(*abi_index); - self.emit(format!("# cellscript abi: call {} scalar {} -> {}", func, label, register)); - self.emit_operand_to_register(®ister, arg); - self.emit_outgoing_call_stack_arg_store(®ister, *abi_index, outgoing_stack_arg_bytes); - *abi_index += 1; - true - } - - fn emit_call_pointer_arg( - &mut self, - func: &str, - label: &str, - abi_index: &mut usize, - arg: &IrOperand, - const_width: Option, - outgoing_stack_arg_bytes: usize, - ) -> bool { - let register = self.call_abi_register(*abi_index); - if const_width.is_some() && matches!(arg, IrOperand::Const(_)) { - self.emit(format!( - "# cellscript abi: call {} pointer param {} uses a constant unsupported by the call ABI; pass null pointer", - func, label - )); - self.emit(format!("li {}, 0", register)); - } else { - self.emit_operand_to_register(®ister, arg); - } - self.emit_outgoing_call_stack_arg_store(®ister, *abi_index, outgoing_stack_arg_bytes); - *abi_index += 1; - true - } - - fn emit_call_length_arg( - &mut self, - func: &str, - label: &str, - abi_index: &mut usize, - arg: &IrOperand, - kind: CallLengthKind, - outgoing_stack_arg_bytes: usize, - ) -> bool { - let register = self.call_abi_register(*abi_index); - let size_offset = match (arg, kind) { - (IrOperand::Var(var), CallLengthKind::Schema) => self.schema_pointer_size_offsets.get(&var.id).copied(), - (IrOperand::Var(var), CallLengthKind::FixedBytes) => self.fixed_byte_param_size_offsets.get(&var.id).copied(), - _ => None, - }; - if let Some(size_offset) = size_offset { - self.emit_stack_load(®ister, size_offset); - } else if let (IrOperand::Var(var), CallLengthKind::FixedBytes) = (arg, kind) { - if let Some(width) = self.fixed_named_type_width(&var.ty) { - self.emit(format!("li {}, {}", register, width)); - } else { - self.emit(format!( - "# cellscript abi: call {} fixed-byte param {} has no tracked ABI length; pass zero length to fail closed", - func, label - )); - self.emit(format!("li {}, 0", register)); - } - } else if let CallLengthKind::FixedBytes = kind { - if matches!(arg, IrOperand::Const(_)) { - self.emit(format!( - "# cellscript abi: call {} fixed-byte const param {} has no materialized pointer; pass zero length to fail closed", - func, label - )); - self.emit(format!("li {}, 0", register)); - } else { - self.emit(format!( - "# cellscript abi: call {} fixed-byte param {} has no tracked ABI length; pass zero length to fail closed", - func, label - )); - self.emit(format!("li {}, 0", register)); - } - } else { - self.emit(format!( - "# cellscript abi: call {} schema param {} has no tracked ABI length; pass zero length to fail closed", - func, label - )); - self.emit(format!("li {}, 0", register)); - } - self.emit_outgoing_call_stack_arg_store(®ister, *abi_index, outgoing_stack_arg_bytes); - *abi_index += 1; - true - } - - fn emit_call_type_hash_pointer_arg( - &mut self, - func: &str, - label: &str, - abi_index: &mut usize, - arg: &IrOperand, - outgoing_stack_arg_bytes: usize, - ) -> bool { - let register = self.call_abi_register(*abi_index); - if let IrOperand::Var(var) = arg { - if let Some(pointer_offset) = self.param_type_hash_pointer_offsets.get(&var.id).copied() { - self.emit_stack_load(®ister, pointer_offset); - } else { - self.emit(format!( - "# cellscript abi: call {} schema param {} has no tracked TypeHash pointer; pass null pointer", - func, label - )); - self.emit(format!("li {}, 0", register)); - } - } else { - self.emit(format!( - "# cellscript abi: call {} schema param {} TypeHash source is not a variable; pass null pointer", - func, label - )); - self.emit(format!("li {}, 0", register)); - } - self.emit_outgoing_call_stack_arg_store(®ister, *abi_index, outgoing_stack_arg_bytes); - *abi_index += 1; - true - } - - fn emit_call_type_hash_length_arg( - &mut self, - func: &str, - label: &str, - abi_index: &mut usize, - arg: &IrOperand, - outgoing_stack_arg_bytes: usize, - ) -> bool { - let register = self.call_abi_register(*abi_index); - if let IrOperand::Var(var) = arg { - if let Some(size_offset) = self.param_type_hash_size_offsets.get(&var.id).copied() { - self.emit_stack_load(®ister, size_offset); - } else { - self.emit(format!( - "# cellscript abi: call {} schema param {} has no tracked TypeHash length; pass zero length to fail closed", - func, label - )); - self.emit(format!("li {}, 0", register)); - } - } else { - self.emit(format!( - "# cellscript abi: call {} schema param {} TypeHash length source is not a variable; pass zero length", - func, label - )); - self.emit(format!("li {}, 0", register)); - } - self.emit_outgoing_call_stack_arg_store(®ister, *abi_index, outgoing_stack_arg_bytes); - *abi_index += 1; - true - } - - fn emit_outgoing_call_stack_arg_store(&mut self, register: &str, abi_index: usize, outgoing_stack_arg_bytes: usize) { - if abi_index < 8 { - return; - } - let stack_slot_offset = (abi_index - 8) * 8; - let offset = i64::try_from(stack_slot_offset).expect("call stack slot should fit in i64") - - i64::try_from(outgoing_stack_arg_bytes).expect("call stack argument area should fit in i64"); - self.emit(format!( - "# cellscript abi: stage outgoing stack arg{} at pre-call sp{}{}", - abi_index, - if offset < 0 { "" } else { "+" }, - offset - )); - self.emit_sp_store_signed(register, offset); - } - - fn emit_sp_store_signed(&mut self, register: &str, offset: i64) { - if small_signed_immediate(offset) { - self.emit(format!("sd {}, {}(sp)", register, offset)); - } else { - let scratch = scratch_register_avoiding(&[register]); - self.emit(format!("li {}, {}", scratch, offset)); - self.emit(format!("add {}, sp, {}", scratch, scratch)); - self.emit(format!("sd {}, 0({})", register, scratch)); - } - } - - fn call_abi_register(&self, abi_index: usize) -> String { - if abi_index < 8 { - format!("a{}", abi_index) - } else { - "t0".to_string() - } - } - - fn emit_read_ref(&mut self, dest: &IrVar, ty: &str) -> Result<()> { - if self.cell_buffer_offsets.contains_key(&dest.id) { - self.emit(format!("# read_ref {} (preloaded from CellDep)", ty)); - return Ok(()); - } - - // Runtime fallback: emit LOAD_CELL_DATA syscall to load the cell dep data - // into the scratch buffer and store the pointer. - let Some(dep_index) = self.read_ref_indices.get(&dest.id).copied() else { - self.emit("# cellscript abi: fail closed because read_ref CellDep index was not allocated"); - self.emit_fail(CellScriptRuntimeError::ConsumeInvalidOperand); - return Ok(()); - }; - let size_offset = self.runtime_scratch_size_offset(); - let buffer_offset = self.runtime_scratch_buffer_offset(); - - self.emit(format!("# read_ref {}", ty)); - self.emit(format!("# cellscript abi: runtime read_ref CellDep index={}", dep_index)); - self.emit_load_cell_data_syscall_to_offsets( - "read_ref", - CKB_SOURCE_CELL_DEP, - dep_index, - size_offset, - buffer_offset, - RUNTIME_SCRATCH_BUFFER_SIZE, - ); - self.emit_return_on_syscall_error(CellScriptRuntimeError::SyscallFailed); - self.emit_sp_addi("t0", buffer_offset); - self.emit_stack_store("t0", dest.id * 8); - - // Also store the size so that subsequent schema operations can use it - self.schema_pointer_size_offsets.insert(dest.id, size_offset); - self.cell_buffer_size_offsets.insert(dest.id, size_offset); - self.cell_buffer_offsets.insert(dest.id, buffer_offset); - - Ok(()) - } - - fn emit_move(&mut self, dest: &IrVar, src: &IrOperand) -> Result<()> { - if dest.ty == IrType::U128 { - self.emit_materialize_u128_operand_to_var(dest, src); - return Ok(()); - } - if let Some(width) = self.fixed_byte_like_width(&dest.ty).filter(|width| *width > 8) { - if self.emit_materialize_fixed_byte_operand_to_var(dest, src, width) { - return Ok(()); - } - } - self.emit_operand_to_register("t0", src); - self.emit_stack_store("t0", dest.id * 8); - Ok(()) - } - - fn emit_materialize_fixed_byte_operand_to_var(&mut self, dest: &IrVar, src: &IrOperand, width: usize) -> bool { - let Some(dest_offset) = self.fixed_byte_local_offsets.get(&dest.id).copied() else { - return false; - }; - let Some(source) = self.expected_fixed_byte_source(src, width) else { - self.emit("# cellscript abi: fail closed because fixed-byte move source is unavailable"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return true; - }; - self.emit(format!("# cellscript abi: materialize fixed-byte move var{} size={}", dest.id, width)); - self.emit_prepare_fixed_byte_source(&source, width, "fixed-byte move"); - if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &source) { - self.emit("# cellscript abi: fail closed because fixed-byte move pointer is unavailable"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return true; - } - self.emit_sp_addi("a1", dest_offset); - self.emit(format!("li a2, {}", width)); - self.emit("call __cellscript_memcpy_fixed"); - self.emit_sp_addi("t0", dest_offset); - self.emit_stack_store("t0", dest.id * 8); - true - } - - fn emit_tuple(&mut self, dest: &IrVar, fields: &[IrOperand]) -> Result<()> { - self.emit(format!("# cellscript abi: construct tuple aggregate var{} fields={}", dest.id, fields.len())); - if self.emit_fixed_named_tuple(dest, fields) { - return Ok(()); - } - self.emit_stack_store("zero", dest.id * 8); - Ok(()) - } - - fn emit_enum_construct(&mut self, dest: &IrVar, enum_name: &str, variant_name: &str, fields: &[IrOperand]) -> Result<()> { - let Some(layout) = self.enum_layouts.get(enum_name).cloned() else { - return Err(CompileError::new( - format!("payload enum '{}' reached codegen without an IR layout", enum_name), - crate::error::Span::default(), - )); - }; - let Some(variant) = layout.variants.iter().find(|variant| variant.name == variant_name).cloned() else { - return Err(CompileError::new( - format!("payload enum '{}::{}' reached codegen without a variant layout", enum_name, variant_name), - crate::error::Span::default(), - )); - }; - if fields.len() != variant.fields.len() { - return Err(CompileError::new( - format!( - "payload enum '{}::{}' codegen arity mismatch: expected {}, found {}", - enum_name, - variant_name, - variant.fields.len(), - fields.len() - ), - crate::error::Span::default(), - )); - } - let Some(dest_offset) = self.fixed_byte_local_offsets.get(&dest.id).copied() else { - return Err(CompileError::new( - format!("payload enum '{}' destination has no fixed-byte local storage", enum_name), - crate::error::Span::default(), - )); - }; - - self.emit(format!( - "# cellscript abi: construct payload enum {}::{} var{} tagged-union-v1 size={}", - enum_name, variant_name, dest.id, layout.encoded_size - )); - self.emit_sp_addi("a0", dest_offset); - self.emit(format!("li a1, {}", layout.encoded_size)); - self.emit("call __cellscript_memzero_fixed"); - self.emit_sp_addi("t4", dest_offset); - self.emit(format!("li t0, {}", variant.tag)); - self.emit_memory_store_with_avoid("sb", "t0", "t4", 0, &["t0", "t4"]); - - for (operand, field) in fields.iter().zip(&variant.fields) { - if field.width == 0 { - continue; - } - if field.linear || (field.width <= 8 && is_fixed_scalar_ir_type(&field.ty)) { - self.emit(format!( - "# cellscript abi: payload enum field {}.{}[{}] offset={} size={}{}", - enum_name, - variant_name, - field.index, - field.offset, - field.width, - if field.linear { " local-linear-handle" } else { "" } - )); - self.emit_operand_to_register("t0", operand); - for byte_index in 0..field.width { - self.emit_memory_store_with_avoid("sb", "t0", "t4", field.offset + byte_index, &["t0", "t4"]); - if byte_index + 1 < field.width { - self.emit("srli t0, t0, 8"); - } - } - continue; - } - - let Some(source) = self.expected_fixed_byte_source(operand, field.width) else { - self.emit("# cellscript abi: payload enum field source is unavailable; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(()); - }; - self.emit_prepare_fixed_byte_source(&source, field.width, "payload enum field"); - if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &source) { - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(()); - } - self.emit_sp_addi("a1", dest_offset + field.offset); - self.emit(format!("li a2, {}", field.width)); - self.emit("call __cellscript_memcpy_fixed"); - self.emit_sp_addi("t4", dest_offset); - } - self.emit_sp_addi("t0", dest_offset); - self.emit_stack_store("t0", dest.id * 8); - Ok(()) - } - - fn emit_enum_tag(&mut self, dest: &IrVar, operand: &IrOperand, enum_name: &str) -> Result<()> { - let Some(layout) = self.enum_layouts.get(enum_name).cloned() else { - return Err(CompileError::new( - format!("payload enum '{}' tag read has no IR layout", enum_name), - crate::error::Span::default(), - )); - }; - let Some(source) = self.expected_fixed_byte_source(operand, layout.encoded_size) else { - self.emit("# cellscript abi: payload enum tag source is unavailable; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(()); - }; - self.emit_prepare_fixed_byte_source(&source, layout.encoded_size, "payload enum tag"); - if !self.emit_fixed_byte_source_pointer_or_const_to("t4", &source) { - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(()); - } - self.emit_memory_load_with_avoid("lbu", "t0", "t4", 0, &["t0", "t4"]); - self.emit_stack_store("t0", dest.id * 8); - Ok(()) - } - - fn emit_enum_payload( - &mut self, - dest: &IrVar, - operand: &IrOperand, - enum_name: &str, - variant_name: &str, - field_index: usize, - ) -> Result<()> { - let Some(layout) = self.enum_layouts.get(enum_name).cloned() else { - return Err(CompileError::new( - format!("payload enum '{}' projection has no IR layout", enum_name), - crate::error::Span::default(), - )); - }; - let Some(field) = layout - .variants - .iter() - .find(|variant| variant.name == variant_name) - .and_then(|variant| variant.fields.get(field_index)) - .cloned() - else { - return Err(CompileError::new( - format!("payload enum '{}::{}' field {} has no IR layout", enum_name, variant_name, field_index), - crate::error::Span::default(), - )); - }; - let Some(source) = self.expected_fixed_byte_source(operand, layout.encoded_size) else { - self.emit("# cellscript abi: payload enum projection source is unavailable; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(()); - }; - self.emit_prepare_fixed_byte_source(&source, layout.encoded_size, "payload enum projection"); - if !self.emit_fixed_byte_source_pointer_or_const_to("t4", &source) { - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(()); - } - if field.width == 0 { - self.emit_stack_store("zero", dest.id * 8); - return Ok(()); - } - if field.linear || (field.width <= 8 && is_fixed_scalar_ir_type(&field.ty)) { - self.emit_unaligned_scalar_load("t4", "t0", "t2", field.offset, field.width); - if field.ty == IrType::I32 { - self.emit_sign_extend_i32("t0"); - } - self.emit_stack_store("t0", dest.id * 8); - return Ok(()); - } - - let Some(dest_offset) = self.fixed_byte_local_offsets.get(&dest.id).copied() else { - self.emit("# cellscript abi: payload enum projection destination has no fixed-byte storage; fail closed"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return Ok(()); - }; - self.emit_large_addi("a0", "t4", field.offset as i64); - self.emit_sp_addi("a1", dest_offset); - self.emit(format!("li a2, {}", field.width)); - self.emit("call __cellscript_memcpy_fixed"); - self.emit_sp_addi("t0", dest_offset); - self.emit_stack_store("t0", dest.id * 8); - Ok(()) - } - - fn emit_fixed_named_tuple(&mut self, dest: &IrVar, fields: &[IrOperand]) -> bool { - let IrType::Named(type_name) = &dest.ty else { - return false; - }; - let Some(width) = self.type_fixed_sizes.get(type_name).copied() else { - return false; - }; - let Some(dest_offset) = self.fixed_byte_local_offsets.get(&dest.id).copied() else { - return false; - }; - let Some(layouts) = self.type_layouts.get(type_name) else { - return false; - }; - let mut ordered = layouts.values().cloned().collect::>(); - ordered.sort_by_key(|layout| layout.offset); - if ordered.len() != fields.len() { - return false; - } - - self.emit(format!("# cellscript abi: materialize fixed aggregate {} var{} size={}", type_name, dest.id, width)); - for (field, layout) in fields.iter().zip(ordered.iter()) { - let Some(field_width) = layout_fixed_byte_width(layout).or_else(|| self.fixed_named_type_width(&layout.ty)) else { - return false; - }; - let Some(source) = self.expected_fixed_byte_source(field, field_width) else { - self.emit("# cellscript abi: fail closed because fixed aggregate field source is unavailable"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return true; - }; - self.emit_prepare_fixed_byte_source(&source, field_width, &format!("{} aggregate field", type_name)); - if !self.emit_fixed_byte_source_pointer_or_const_to("a0", &source) { - self.emit("# cellscript abi: fail closed because fixed aggregate field pointer is unavailable"); - self.emit_fail(CellScriptRuntimeError::FixedByteComparisonUnresolved); - return true; - } - self.emit_sp_addi("a1", dest_offset + layout.offset); - self.emit(format!("li a2, {}", field_width)); - self.emit("call __cellscript_memcpy_fixed"); - } - self.emit_sp_addi("t0", dest_offset); - self.emit_stack_store("t0", dest.id * 8); - true - } - - fn emit_operand_to_register(&mut self, register: &str, operand: &IrOperand) { - match operand { - IrOperand::Const(IrConst::U8(n)) => self.emit(format!("li {}, {}", register, n)), - IrOperand::Const(IrConst::U16(n)) => self.emit(format!("li {}, {}", register, n)), - IrOperand::Const(IrConst::U32(n)) => self.emit(format!("li {}, {}", register, n)), - IrOperand::Const(IrConst::U64(n)) => self.emit(format!("li {}, {}", register, n)), - IrOperand::Const(IrConst::Bool(b)) => self.emit(format!("li {}, {}", register, if *b { 1 } else { 0 })), - IrOperand::Const(value) => { - if let Some(bytes) = fixed_byte_const_bytes(value) { - let label = self.const_data_label_for_bytes(bytes); - self.emit(format!("la {}, {}", register, label)); - } else { - self.emit(format!("li {}, 0", register)); - } - } - IrOperand::Var(v) => self.emit_stack_load(register, v.id * 8), - } - } - - /// consume - fn emit_consume(&mut self, operand: &IrOperand) -> Result<()> { - self.emit("# consume"); - if let IrOperand::Var(var) = operand { - if self.consume_indices.contains_key(&var.id) { - self.emit("# cellscript abi: consumed input pointer retained for verifier field checks"); - return Ok(()); - } - // Consume a local variable: the actual LOAD_CELL input data loading - // already happened in the action prelude (generate_consume). - // Here we only zero out the local binding to enforce linear ownership. - self.emit_stack_store("zero", var.id * 8); - return Ok(()); - } - // Non-Var consume: this should not happen in valid IR, but fail with - // a specific error code instead of blocking ELF emission. - self.emit("# cellscript abi: fail closed because consume operand is not a variable"); - self.emit_fail(CellScriptRuntimeError::ConsumeInvalidOperand); - Ok(()) - } - - /// create - fn emit_create(&mut self, dest: &IrVar, pattern: &CreatePattern) -> Result<()> { - let output_index = self.operation_output_indices.get(&dest.id).copied().unwrap_or(self.next_virtual_output); - if pattern.operation == "output" { - self.emit(format!("# constrain named output {}", pattern.ty)); - for (field, value) in &pattern.fields { - match value { - IrOperand::Const(IrConst::U64(n)) => self.emit(format!("# field {} = {}", field, n)), - IrOperand::Const(IrConst::Bool(b)) => self.emit(format!("# field {} = {}", field, b)), - IrOperand::Var(var) => self.emit(format!("# field {} <- {}", field, var.name)), - _ => self.emit(format!("# field {} <- ", field)), - } - } - if pattern.lock.is_some() { - self.emit("# with_lock "); - } - if let Some(var_id) = self.output_param_ids.get(&pattern.binding).copied() { - let Some(size_offset) = self.cell_buffer_size_offsets.get(&var_id).copied() else { - self.emit_fail(CellScriptRuntimeError::AssertionFailed); - return Ok(()); - }; - let Some(buffer_offset) = self.cell_buffer_offsets.get(&var_id).copied() else { - self.emit_fail(CellScriptRuntimeError::AssertionFailed); - return Ok(()); - }; - if pattern.fields.is_empty() { - self.emit_state_transition_check(pattern, size_offset, buffer_offset); - } else if self.can_verify_create_output_fields(pattern) { - self.emit_create_output_checks_at(pattern, size_offset, buffer_offset); - } else { - self.emit("# cellscript abi: ordered named output field verification incomplete"); - self.emit("# cellscript abi: fail closed because the output state is not fully verified"); - self.emit_fail(CellScriptRuntimeError::AssertionFailed); - return Ok(()); - } - if let Some(lock) = &pattern.lock { - if !(self.can_verify_output_lock(pattern) && self.emit_output_lock_hash_check(output_index, lock)) { - self.emit("# cellscript abi: output lock verification incomplete for this named output"); - self.emit("# cellscript abi: fail closed because the output lock is not fully verified"); - self.emit_fail(CellScriptRuntimeError::EntryWitnessMagicMismatch); - return Ok(()); - } - } - } else { - self.emit_fail(CellScriptRuntimeError::AssertionFailed); - return Ok(()); - } - self.emit(format!("li t0, {}", output_index)); - self.emit_stack_store("t0", dest.id * 8); - self.next_virtual_output = self.next_virtual_output.max(output_index + 1); - return Ok(()); - } - - self.generate_create(pattern, output_index, false, false)?; - self.emit(format!("# create {}", pattern.ty)); - for (field, value) in &pattern.fields { - match value { - IrOperand::Const(IrConst::U64(n)) => self.emit(format!("# field {} = {}", field, n)), - IrOperand::Const(IrConst::Bool(b)) => self.emit(format!("# field {} = {}", field, b)), - IrOperand::Var(var) => self.emit(format!("# field {} <- {}", field, var.name)), - _ => self.emit(format!("# field {} <- ", field)), - } - } - if pattern.lock.is_some() { - self.emit("# with_lock "); - } - self.emit(format!("li t0, {}", output_index)); - self.emit_stack_store("t0", dest.id * 8); - self.next_virtual_output = self.next_virtual_output.max(output_index + 1); - Ok(()) - } - - fn emit_create_unique_identity_check(&mut self, output_index: usize, pattern: &CreatePattern, identity: &IrIdentityPolicy) { - self.emit(format!( - "# cellscript abi: create_unique identity policy {} for Output#{}", - identity_policy_label(identity), - output_index - )); - match identity { - IrIdentityPolicy::None => {} - IrIdentityPolicy::CkbTypeId => { - self.emit_output_type_hash_present_check(output_index, "create_unique_ckb_type_id_output_type_hash"); - } - IrIdentityPolicy::Field(field) => { - self.emit_create_unique_field_identity_anchor(output_index, pattern, field); - } - IrIdentityPolicy::ScriptArgs => { - self.emit_cell_field_hash_equality( - "create_unique_group_input_lock_hash", - CKB_SOURCE_GROUP_INPUT, - 0, - "create_unique_output_lock_hash", - CKB_SOURCE_OUTPUT, - output_index, - CKB_CELL_FIELD_LOCK_HASH, - "LockHash", - "create_unique script_args identity anchor", - CellScriptRuntimeError::LockHashPreservationMismatch, - ); - } - IrIdentityPolicy::SingletonType => { - self.emit_cell_field_hash_equality( - "create_unique_group_input_type_hash", - CKB_SOURCE_GROUP_INPUT, - 0, - "create_unique_output_type_hash", - CKB_SOURCE_OUTPUT, - output_index, - CKB_CELL_FIELD_TYPE_HASH, - "TypeHash", - "create_unique singleton_type identity anchor", - CellScriptRuntimeError::TypeHashMismatch, - ); - } - } - } - - fn emit_create_unique_field_identity_anchor(&mut self, output_index: usize, pattern: &CreatePattern, field: &str) { - let Some(layout) = self.type_layouts.get(&pattern.ty).and_then(|fields| fields.get(field)).cloned() else { - self.emit(format!( - "# cellscript abi: fail closed because create_unique identity field {}.{} has no layout", - pattern.ty, field - )); - self.emit_fail(CellScriptRuntimeError::AssertionFailed); - return; - }; - let Some(width) = layout_fixed_byte_width(&layout) else { - self.emit(format!( - "# cellscript abi: fail closed because create_unique identity field {}.{} is not fixed-width", - pattern.ty, field - )); - self.emit_fail(CellScriptRuntimeError::DynamicFieldValueMismatch); - return; - }; - let output_size_offset = self.runtime_scratch_size_offset(); - let output_buffer_offset = self.runtime_scratch_buffer_offset(); - self.emit_load_cell_data_syscall("create_unique_identity_field", CKB_SOURCE_OUTPUT, output_index); - self.emit_return_on_syscall_error(CellScriptRuntimeError::CellLoadFailed); - let output_pointer_offset = self.runtime_expr_temp_offset(0); - let output_len_offset = self.runtime_expr_temp_offset(1); - let context = format!("create_unique identity field {}.{}", pattern.ty, field); - if self.type_fixed_sizes.contains_key(&pattern.ty) { - self.emit_loaded_fixed_field_pointer_to_stack( - output_size_offset, - output_buffer_offset, - &layout, - width, - &context, - output_pointer_offset, - ); - } else if let Some(field_count) = self.type_layouts.get(&pattern.ty).map(|fields| fields.len()) { - self.emit_dynamic_fixed_field_pointer_to_stack( - output_size_offset, - output_buffer_offset, - &layout, - field_count, - width, - &context, - output_pointer_offset, - output_len_offset, - ); - } else { - self.emit_fail(CellScriptRuntimeError::AssertionFailed); - return; - } - self.emit(format!( - "# cellscript abi: create_unique field identity anchored by verified Output#{} {}.{} size={}", - output_index, pattern.ty, field, width - )); - } - - fn emit_replace_unique_identity_check( - &mut self, - output_index: usize, - operand: &IrOperand, - pattern: &CreatePattern, - identity: &IrIdentityPolicy, - ) { - self.emit(format!( - "# cellscript abi: replace_unique identity policy {} for Output#{}", - identity_policy_label(identity), - output_index - )); - let input_index = match operand { - IrOperand::Var(var) => self.consume_indices.get(&var.id).copied().unwrap_or(0), - _ => 0, - }; - match identity { - IrIdentityPolicy::None => {} - IrIdentityPolicy::CkbTypeId | IrIdentityPolicy::SingletonType => { - self.emit_cell_field_hash_equality( - "replace_unique_input_type_hash", - CKB_SOURCE_INPUT, - input_index, - "replace_unique_output_type_hash", - CKB_SOURCE_OUTPUT, - output_index, - CKB_CELL_FIELD_TYPE_HASH, - "TypeHash", - "replace_unique type identity preservation", - CellScriptRuntimeError::TypeHashMismatch, - ); - } - IrIdentityPolicy::ScriptArgs => { - self.emit_cell_field_hash_equality( - "replace_unique_input_lock_hash", - CKB_SOURCE_INPUT, - input_index, - "replace_unique_output_lock_hash", - CKB_SOURCE_OUTPUT, - output_index, - CKB_CELL_FIELD_LOCK_HASH, - "LockHash", - "replace_unique script_args identity preservation", - CellScriptRuntimeError::LockHashPreservationMismatch, - ); - } - IrIdentityPolicy::Field(field) => { - self.emit_replace_unique_field_identity_check(output_index, operand, pattern, field); - } - } - } - - fn emit_replace_unique_field_identity_check( - &mut self, - output_index: usize, - operand: &IrOperand, - pattern: &CreatePattern, - field: &str, - ) { - let input_var = match operand { - IrOperand::Var(var) => var, - _ => { - self.emit("# cellscript abi: fail closed because replace_unique identity input is not a cell variable"); - self.emit_fail(CellScriptRuntimeError::DestroyInvalidOperand); - return; - } - }; - let (Some(input_size_offset), Some(input_buffer_offset)) = - (self.cell_buffer_size_offsets.get(&input_var.id).copied(), self.cell_buffer_offsets.get(&input_var.id).copied()) - else { - self.emit("# cellscript abi: fail closed because replace_unique identity input cell data is unavailable"); - self.emit_fail(CellScriptRuntimeError::CellLoadFailed); - return; - }; - let Some(layout) = self.type_layouts.get(&pattern.ty).and_then(|fields| fields.get(field)).cloned() else { - self.emit(format!( - "# cellscript abi: fail closed because replace_unique identity field {}.{} has no layout", - pattern.ty, field - )); - self.emit_fail(CellScriptRuntimeError::AssertionFailed); - return; - }; - let Some(width) = layout_fixed_byte_width(&layout) else { - self.emit(format!( - "# cellscript abi: fail closed because replace_unique identity field {}.{} is not fixed-width", - pattern.ty, field - )); - self.emit_fail(CellScriptRuntimeError::DynamicFieldValueMismatch); - return; - }; - - let output_size_offset = self.runtime_scratch_size_offset(); - let output_buffer_offset = self.runtime_scratch_buffer_offset(); - self.emit_load_cell_data_syscall("replace_unique_identity_field_output", CKB_SOURCE_OUTPUT, output_index); - self.emit_return_on_syscall_error(CellScriptRuntimeError::CellLoadFailed); - let input_pointer_offset = self.runtime_expr_temp_offset(0); - let input_len_offset = self.runtime_expr_temp_offset(1); - let output_pointer_offset = self.runtime_expr_temp_offset(2); - let output_len_offset = self.runtime_expr_temp_offset(3); - let input_context = format!("replace_unique input identity field {}.{}", pattern.ty, field); - let output_context = format!("replace_unique output identity field {}.{}", pattern.ty, field); - if self.type_fixed_sizes.contains_key(&pattern.ty) { - self.emit_loaded_fixed_field_pointer_to_stack( - input_size_offset, - input_buffer_offset, - &layout, - width, - &input_context, - input_pointer_offset, - ); - self.emit_loaded_fixed_field_pointer_to_stack( - output_size_offset, - output_buffer_offset, - &layout, - width, - &output_context, - output_pointer_offset, - ); - } else if let Some(field_count) = self.type_layouts.get(&pattern.ty).map(|fields| fields.len()) { - self.emit_dynamic_fixed_field_pointer_to_stack( - input_size_offset, - input_buffer_offset, - &layout, - field_count, - width, - &input_context, - input_pointer_offset, - input_len_offset, - ); - self.emit_dynamic_fixed_field_pointer_to_stack( - output_size_offset, - output_buffer_offset, - &layout, - field_count, - width, - &output_context, - output_pointer_offset, - output_len_offset, - ); - } else { - self.emit_fail(CellScriptRuntimeError::AssertionFailed); - return; - } - self.emit_fixed_pointer_equality( - input_pointer_offset, - output_pointer_offset, - width, - &format!("replace_unique identity field {}.{} Input == Output#{}", pattern.ty, field, output_index), - CellScriptRuntimeError::DynamicFieldValueMismatch, - ); - } - - /// create_unique - fn emit_create_unique(&mut self, dest: &IrVar, pattern: &CreatePattern, identity: &IrIdentityPolicy) -> Result<()> { - let output_index = self.operation_output_indices.get(&dest.id).copied().unwrap_or(self.next_virtual_output); - self.generate_create(pattern, output_index, false, false)?; - self.emit_create_unique_identity_check(output_index, pattern, identity); - self.emit(format!("# create_unique {} identity={}", pattern.ty, identity_policy_label(identity))); - for (field, value) in &pattern.fields { - match value { - IrOperand::Const(IrConst::U64(n)) => self.emit(format!("# field {} = {}", field, n)), - IrOperand::Const(IrConst::Bool(b)) => self.emit(format!("# field {} = {}", field, b)), - IrOperand::Var(var) => self.emit(format!("# field {} <- {}", field, var.name)), - _ => self.emit(format!("# field {} <- ", field)), - } - } - if pattern.lock.is_some() { - self.emit("# with_lock "); - } - self.emit(format!("li t0, {}", output_index)); - self.emit_stack_store("t0", dest.id * 8); - self.next_virtual_output = self.next_virtual_output.max(output_index + 1); - Ok(()) - } - - /// replace_unique - fn emit_replace_unique( - &mut self, - dest: &IrVar, - operand: &IrOperand, - pattern: &CreatePattern, - identity: &IrIdentityPolicy, - ) -> Result<()> { - let output_index = self.operation_output_indices.get(&dest.id).copied().unwrap_or(self.next_virtual_output); - self.emit(format!("# replace_unique {} identity={}", pattern.ty, identity_policy_label(identity))); - self.emit_operand_comment("input", operand); - for (field, value) in &pattern.fields { - match value { - IrOperand::Const(IrConst::U64(n)) => self.emit(format!("# field {} = {}", field, n)), - IrOperand::Const(IrConst::Bool(b)) => self.emit(format!("# field {} = {}", field, b)), - IrOperand::Var(var) => self.emit(format!("# field {} <- {}", field, var.name)), - _ => self.emit(format!("# field {} <- ", field)), - } - } - // replace_unique is a consume + create with identity preservation. - // The output occupies a virtual output slot, similar to transfer. - self.generate_create(pattern, output_index, false, false)?; - self.emit_replace_unique_identity_check(output_index, operand, pattern, identity); - if self.emit_verified_operation_output_handle(dest, "replace_unique") { - return Ok(()); - } - self.emit(format!("# cellscript abi: replace_unique output handle Output#{}", output_index)); - self.emit(format!("li t0, {}", output_index)); - self.emit_stack_store("t0", dest.id * 8); - self.next_virtual_output = self.next_virtual_output.max(output_index + 1); - Ok(()) - } - - /// transfer - fn emit_transfer(&mut self, dest: &IrVar, operand: &IrOperand, to: &IrOperand) -> Result<()> { - self.emit("# transfer"); - self.emit_operand_comment("asset", operand); - self.emit_operand_comment("to", to); - if self.emit_verified_operation_output_handle(dest, "transfer") { - return Ok(()); - } - if let Some(output_index) = self.operation_output_indices.get(&dest.id).copied() { - self.emit(format!("# cellscript abi: transfer output handle Output#{} (unverified)", output_index)); - self.emit(format!("li t0, {}", output_index)); - self.emit_stack_store("t0", dest.id * 8); - self.next_virtual_output = self.next_virtual_output.max(output_index + 1); - return Ok(()); - } - self.emit("# cellscript abi: fail closed because transfer output relation is unknown"); - self.emit_fail(CellScriptRuntimeError::DestroyInvalidOperand); - Ok(()) - } - - /// claim - fn emit_claim(&mut self, dest: &IrVar, receipt: &IrOperand) -> Result<()> { - self.emit("# claim"); - self.emit_operand_comment("receipt", receipt); - if self.emit_verified_operation_output_handle(dest, "claim") { - return Ok(()); - } - if let Some(output_index) = self.operation_output_indices.get(&dest.id).copied() { - self.emit(format!("# cellscript abi: claim output handle Output#{} (unverified)", output_index)); - self.emit(format!("li t0, {}", output_index)); - self.emit_stack_store("t0", dest.id * 8); - self.next_virtual_output = self.next_virtual_output.max(output_index + 1); - return Ok(()); - } - self.emit("# cellscript abi: fail closed because claim output relation is unknown"); - self.emit_fail(CellScriptRuntimeError::DestroyInvalidOperand); - Ok(()) - } - - /// settle - fn emit_settle(&mut self, dest: &IrVar, operand: &IrOperand) -> Result<()> { - self.emit("# settle"); - self.emit_operand_comment("value", operand); - if self.emit_verified_operation_output_handle(dest, "settle") { - return Ok(()); - } - if let Some(output_index) = self.operation_output_indices.get(&dest.id).copied() { - self.emit(format!("# cellscript abi: settle output handle Output#{} (unverified)", output_index)); - self.emit(format!("li t0, {}", output_index)); - self.emit_stack_store("t0", dest.id * 8); - self.next_virtual_output = self.next_virtual_output.max(output_index + 1); - return Ok(()); - } - self.emit("# cellscript abi: fail closed because settle output relation is unknown"); - self.emit_fail(CellScriptRuntimeError::DestroyInvalidOperand); - Ok(()) - } - - fn emit_verified_operation_output_handle(&mut self, dest: &IrVar, operation: &str) -> bool { - if !self.verified_operation_outputs.contains(&dest.id) { - return false; - } - let output_index = self.operation_output_indices.get(&dest.id).copied().unwrap_or(self.next_virtual_output); - self.emit(format!("# cellscript abi: {} output relation verified by prelude Output#{}", operation, output_index)); - self.emit(format!("li t0, {}", output_index)); - self.emit_stack_store("t0", dest.id * 8); - self.next_virtual_output = self.next_virtual_output.max(output_index + 1); - true - } - - /// destroy - fn emit_destroy(&mut self, operand: &IrOperand) -> Result<()> { - self.emit("# destroy"); - if let IrOperand::Var(_) = operand { - self.emit_operand_comment("destroyed input retained for verifier field checks", operand); - self.emit("# cellscript abi: destroy consumed input is checked by Output absence scan"); - self.emit("# cellscript abi: retain consumed input pointer for post-destroy output verification"); - return Ok(()); - } - // Non-Var destroy: this should not happen in valid IR, fail with specific error. - self.emit("# cellscript abi: fail closed because destroy operand is not a variable"); - self.emit_fail(CellScriptRuntimeError::ConsumeInvalidOperand); - Ok(()) - } - - fn emit_operand_comment(&mut self, label: &str, operand: &IrOperand) { - let rendered = match operand { - IrOperand::Var(var) => format!("{}: {}", label, var.name), - IrOperand::Const(IrConst::U64(n)) => format!("{}: {}", label, n), - IrOperand::Const(IrConst::Bool(b)) => format!("{}: {}", label, b), - IrOperand::Const(IrConst::Address(_)) => format!("{}:
", label), - IrOperand::Const(IrConst::Hash(_)) => format!("{}: ", label), - IrOperand::Const(IrConst::Array(items)) => format!("{}: ", label, items.len()), - IrOperand::Const(_) => format!("{}: ", label), - }; - self.emit(format!("# {}", rendered)); - } - - fn static_length(&self, operand: &IrOperand) -> Option { - match operand { - IrOperand::Var(var) => Self::static_length_from_type(&var.ty), - IrOperand::Const(IrConst::Array(items)) => Some(items.len()), - _ => None, - } - } - - fn static_length_from_type(ty: &IrType) -> Option { - match ty { - IrType::Array(_, size) => Some(*size), - IrType::Ref(inner) | IrType::MutRef(inner) => Self::static_length_from_type(inner), - _ => None, - } - } - - fn generate_runtime_support(&mut self, ir: &IrModule) { - self.emit_section(".text"); - self.emit_runtime_memcmp_fixed(); - self.emit_runtime_memzero_fixed(); - self.emit_runtime_memcpy_fixed(); - self.emit_runtime_size_guards(); - // CKB exposes epoch-number based timepoints here, not Unix timestamps. - self.emit_runtime_header_field_u64( - "__env_current_timepoint", - "ckb_epoch_number", - CKB_HEADER_FIELD_EPOCH_NUMBER, - true, - "env::current_timepoint is required for CKB profile", - ); - self.emit_runtime_header_field_u64( - "__ckb_header_epoch_number", - "ckb_epoch_number", - CKB_HEADER_FIELD_EPOCH_NUMBER, - self.options.target_profile == TargetProfile::Ckb, - "ckb::header_epoch_number is rejected outside the ckb target profile", - ); - self.emit_runtime_header_field_u64( - "__ckb_header_epoch_start_block_number", - "ckb_epoch_start_block_number", - CKB_HEADER_FIELD_EPOCH_START_BLOCK_NUMBER, - self.options.target_profile == TargetProfile::Ckb, - "ckb::header_epoch_start_block_number is rejected outside the ckb target profile", - ); - self.emit_runtime_header_field_u64( - "__ckb_header_epoch_length", - "ckb_epoch_length", - CKB_HEADER_FIELD_EPOCH_LENGTH, - self.options.target_profile == TargetProfile::Ckb, - "ckb::header_epoch_length is rejected outside the ckb target profile", - ); - self.emit_runtime_input_field_u64( - "__ckb_input_since", - "ckb_input_since", - CKB_INPUT_FIELD_SINCE, - self.options.target_profile == TargetProfile::Ckb, - "ckb::input_since is rejected outside the ckb target profile", - ); - let v014_helpers = referenced_v014_runtime_helpers(ir); - self.emit_runtime_ckb_v014_surface_helpers(&v014_helpers); - } - - fn emit_runtime_ckb_v014_surface_helpers(&mut self, referenced_helpers: &BTreeSet) { - let enabled = self.options.target_profile == TargetProfile::Ckb; - for (name, syscall, detail) in [ - ("__ckb_spawn", ckb_abi::syscall::SPAWN, "spawn bounded verifier child"), - ("__ckb_wait", ckb_abi::syscall::WAIT, "wait for bounded verifier child"), - ("__ckb_process_id", ckb_abi::syscall::PROCESS_ID, "current process id"), - ("__ckb_pipe", ckb_abi::syscall::PIPE, "create IPC pipe; returns read fd in a0 and write fd in a1"), - ("__ckb_pipe_write", ckb_abi::syscall::WRITE, "write u64 payload to IPC pipe"), - ("__ckb_pipe_read", ckb_abi::syscall::READ, "read u64 payload from IPC pipe"), - ("__ckb_inherited_fd", ckb_abi::syscall::INHERITED_FDS, "resolve inherited fd"), - ("__ckb_close", ckb_abi::syscall::CLOSE, "close fd"), - ] { - if !referenced_helpers.contains(name) { - continue; - } - self.emit_global(name); - self.emit_label(name); - self.emit(format!("# cellscript abi: CKB VM v2 syscall {} ({})", syscall, detail)); - if !enabled { - self.emit_fail(CellScriptRuntimeError::SyscallFailed); - } else { - match name { - "__ckb_pipe" => { - self.emit("addi sp, sp, -32"); - self.emit("sd ra, 24(sp)"); - self.emit("addi a0, sp, 8"); - self.emit(format!("li a7, {}", syscall)); - self.emit("ecall"); - let failed = self.fresh_label("ckb_pipe_failed"); - let done = self.fresh_label("ckb_pipe_done"); - self.emit(format!("bnez a0, {}", failed)); - self.emit("ld a1, 8(sp)"); - self.emit("ld a2, 16(sp)"); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit_label(&done); - self.emit("ld ra, 24(sp)"); - self.emit("addi sp, sp, 32"); - self.emit("ret"); - } - "__ckb_pipe_write" => { - self.emit("addi sp, sp, -32"); - self.emit("sd ra, 24(sp)"); - self.emit("sd a1, 8(sp)"); - self.emit("li t0, 8"); - self.emit("sd t0, 16(sp)"); - self.emit("addi a1, sp, 8"); - self.emit("addi a2, sp, 16"); - self.emit(format!("li a7, {}", syscall)); - self.emit("ecall"); - let done = self.fresh_label("ckb_pipe_write_done"); - self.emit(format!("beqz a0, {}", done)); - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit_label(&done); - self.emit("ld ra, 24(sp)"); - self.emit("addi sp, sp, 32"); - self.emit("ret"); - } - "__ckb_close" => { - self.emit(format!("li a7, {}", syscall)); - self.emit("ecall"); - let done = self.fresh_label("ckb_close_done"); - self.emit(format!("beqz a0, {}", done)); - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit_label(&done); - self.emit("ret"); - } - "__ckb_wait" => { - self.emit("addi sp, sp, -32"); - self.emit("sd ra, 24(sp)"); - self.emit("sd zero, 8(sp)"); - self.emit("addi a1, sp, 8"); - self.emit(format!("li a7, {}", syscall)); - self.emit("ecall"); - let failed = self.fresh_label("ckb_wait_failed"); - let exit_ok = self.fresh_label("ckb_wait_exit_ok"); - let child_failed = self.fresh_label("ckb_wait_child_failed"); - let done = self.fresh_label("ckb_wait_done"); - self.emit(format!("bnez a0, {}", failed)); - self.emit("lbu t0, 8(sp)"); - self.emit(format!("beqz t0, {}", exit_ok)); - self.emit_label(&child_failed); - self.emit("addi a0, t0, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&exit_ok); - self.emit("li a0, 0"); - self.emit_label(&done); - self.emit("ld ra, 24(sp)"); - self.emit("addi sp, sp, 32"); - self.emit("ret"); - } - "__ckb_spawn" => { - self.emit("li a0, 0"); - self.emit("ret"); - } - _ => { - self.emit(format!("li a7, {}", syscall)); - self.emit("ecall"); - self.emit("ret"); - } - } - } - } - if referenced_helpers.contains("__ckb_spawn_with_fd1") { - self.emit_global("__ckb_spawn_with_fd1"); - self.emit_label("__ckb_spawn_with_fd1"); - self.emit("# cellscript abi: CKB VM v2 spawn CellDep index a0/code with one inherited fd from a1"); - if !enabled { - self.emit_fail(CellScriptRuntimeError::SyscallFailed); - } else { - self.emit("addi sp, sp, -96"); - self.emit("sd ra, 88(sp)"); - self.emit("sd a1, 8(sp)"); - self.emit("sd a0, 64(sp)"); - self.emit("sd zero, 16(sp)"); - self.emit("sd zero, 32(sp)"); - self.emit("sd zero, 40(sp)"); - self.emit("addi t0, sp, 24"); - self.emit("sd t0, 48(sp)"); - self.emit("addi t0, sp, 8"); - self.emit("sd t0, 56(sp)"); - self.emit("ld a0, 64(sp)"); - self.emit(format!("li a1, {}", ckb_abi::source::CELL_DEP)); - self.emit("li a2, 0"); - self.emit(format!("li a3, {}", ckb_abi::place::CELL)); - self.emit("addi a4, sp, 32"); - self.emit(format!("li a7, {}", ckb_abi::syscall::SPAWN)); - self.emit("ecall"); - let failed = self.fresh_label("ckb_spawn_with_fd_failed"); - let done = self.fresh_label("ckb_spawn_with_fd_done"); - self.emit(format!("bnez a0, {}", failed)); - self.emit("ld a1, 24(sp)"); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit_label(&done); - self.emit("ld ra, 88(sp)"); - self.emit("addi sp, sp, 96"); - self.emit("ret"); - } - } - - for (name, source_view, detail) in [ - ("__ckb_source_input", CKB_SOURCE_VIEW_INPUT, "Source::Input"), - ("__ckb_source_output", CKB_SOURCE_VIEW_OUTPUT, "Source::Output"), - ("__ckb_source_cell_dep", CKB_SOURCE_VIEW_CELL_DEP, "Source::CellDep"), - ("__ckb_source_header_dep", CKB_SOURCE_VIEW_HEADER_DEP, "Source::HeaderDep"), - ("__ckb_source_group_input", CKB_SOURCE_VIEW_GROUP_INPUT, "Source::GroupInput"), - ("__ckb_source_group_output", CKB_SOURCE_VIEW_GROUP_OUTPUT, "Source::GroupOutput"), - ] { - if !referenced_helpers.contains(name) { - continue; - } - self.emit_runtime_source_view_helper(name, source_view, detail, enabled); - } - - for (name, relative, detail) in [ - ("__ckb_since_epoch_absolute", false, "CKB RFC0017 absolute epoch since encoder"), - ("__ckb_since_epoch_relative", true, "CKB RFC0017 relative epoch since encoder"), - ] { - if !referenced_helpers.contains(name) { - continue; - } - self.emit_runtime_ckb_since_epoch_helper(name, relative, detail, enabled); - } - - let needs_c256_product = referenced_helpers.contains("__c256_require_u128_product_lte") - || referenced_helpers.contains("__c256_require_u128_product_eq") - || referenced_helpers.contains("__c256_require_u128_sum2_products_lte") - || referenced_helpers.contains("__c256_require_u128_sum2_products_eq"); - let needs_c256_sum = referenced_helpers.contains("__c256_require_u128_sum2_products_lte") - || referenced_helpers.contains("__c256_require_u128_sum2_products_eq"); - if needs_c256_product { - self.emit_runtime_load_u64_le_helper(); - self.emit_runtime_mul_u128_to_u256_helper(); - if needs_c256_sum { - self.emit_runtime_add_u256_helper(); - } - } - if referenced_helpers.contains("__ckb_require_lock_type_metapoint_pairs") - || referenced_helpers.contains("__ckb_require_type_lock_metapoint_pairs") - || referenced_helpers.contains("__ckb_require_lock_type_metapoint_pairs_from_i32_data") - || referenced_helpers.contains("__ckb_require_type_lock_metapoint_pairs_from_i32_data") - || referenced_helpers.contains("__ckb_require_lock_type_metapoint_pairs_from_i32_data_filtered") - || referenced_helpers.contains("__ckb_require_type_lock_metapoint_pairs_from_i32_data_filtered") - || referenced_helpers.contains("__ckb_require_lock_match_master_out_point_pairs_from_data") - { - self.emit_runtime_current_script_role_at_helper(enabled); - } - - for (name, detail) in [ - ("__ckb_current_role", "current script role inferred from group input lock/type hashes"), - ("__ckb_current_script_hash", "current script hash loaded via LOAD_SCRIPT_HASH"), - ("__ckb_cell_capacity", "SourceView cell capacity field"), - ("__ckb_cell_occupied_capacity", "SourceView occupied capacity from CellOutput scripts and data bytes"), - ("__ckb_cell_unoccupied_capacity", "SourceView capacity minus occupied capacity"), - ("__ckb_cell_output_index", "SourceView output index"), - ("__ckb_input_out_point_index", "SourceView input OutPoint index"), - ("__ckb_input_out_point_tx_hash_low", "SourceView input OutPoint tx hash low word"), - ("__ckb_input_out_point_tx_hash", "SourceView input OutPoint full tx hash read"), - ("__ckb_require_input_out_point_tx_hash", "SourceView input OutPoint full tx-hash binding check"), - ("__ckb_require_input_out_point", "SourceView input OutPoint full tx-hash and index binding check"), - ("__ckb_require_metapoint_relative", "SourceView MetaPoint relative-distance binding check"), - ("__ckb_require_lock_type_metapoint_pairs", "current-script lock-only to type-only MetaPoint pair cardinality check"), - ("__ckb_require_type_lock_metapoint_pairs", "current-script type-only to lock-only MetaPoint pair cardinality check"), - ( - "__ckb_require_lock_type_metapoint_pairs_from_i32_data", - "current-script lock-only to type-only MetaPoint pair cardinality check using signed i32 distance loaded from base cell data", - ), - ( - "__ckb_require_type_lock_metapoint_pairs_from_i32_data", - "current-script type-only to lock-only MetaPoint pair cardinality check using signed i32 distance loaded from base cell data", - ), - ( - "__ckb_require_lock_type_metapoint_pairs_from_i32_data_filtered", - "current-script lock-only to type-only filtered MetaPoint pair cardinality check using signed i32 distance loaded from base cell data", - ), - ( - "__ckb_require_type_lock_metapoint_pairs_from_i32_data_filtered", - "current-script type-only to lock-only filtered MetaPoint pair cardinality check using signed i32 distance loaded from base cell data", - ), - ( - "__ckb_require_lock_match_master_out_point_pairs_from_data", - "current-script lock-only match order input/output pairing using master OutPoint loaded from order data", - ), - ("__ckb_cell_lock_hash_low", "SourceView lock hash low word"), - ("__ckb_cell_type_hash_low", "SourceView type hash low word"), - ("__ckb_cell_lock_hash", "SourceView lock hash full 32-byte read"), - ("__ckb_cell_type_hash", "SourceView type hash full 32-byte read"), - ("__ckb_cell_data_hash", "SourceView data hash full 32-byte read"), - ("__ckb_cell_data_hash_at", "SourceView cell data 32-byte read at byte offset"), - ("__ckb_cell_lock_code_hash", "SourceView lock Script code_hash read"), - ("__ckb_cell_type_code_hash", "SourceView type Script code_hash read"), - ("__ckb_cell_lock_hash_type", "SourceView lock Script hash_type read"), - ("__ckb_cell_type_hash_type", "SourceView type Script hash_type read"), - ("__ckb_cell_lock_args_empty", "SourceView lock Script args_empty read"), - ("__ckb_cell_type_args_empty", "SourceView type Script args_empty read"), - ("__ckb_cell_lock_args_hash", "SourceView lock Script 32-byte args read"), - ("__ckb_cell_type_args_hash", "SourceView type Script 32-byte args read"), - ("__ckb_require_cell_lock_hash", "SourceView lock hash full 32-byte binding check"), - ("__ckb_require_cell_type_hash", "SourceView type hash full 32-byte binding check"), - ("__ckb_require_cell_data_hash", "SourceView data hash full 32-byte binding check"), - ( - "__ckb_require_bounded_cell_dep_data_hash", - "bounded resolved CellDep data-hash membership check", - ), - ("__ckb_require_current_script_args_empty", "current Script empty args requirement"), - ("__ckb_require_cell_lock_args_empty", "SourceView lock Script empty args requirement"), - ("__ckb_require_cell_type_args_empty", "SourceView type Script empty args requirement"), - ("__ckb_require_cell_lock_args_hash", "SourceView lock Script 32-byte args binding check"), - ("__ckb_require_cell_type_args_hash", "SourceView type Script 32-byte args binding check"), - ("__ckb_require_cell_lock_args_exact", "SourceView lock Script arbitrary exact args binding check"), - ("__ckb_require_cell_type_args_exact", "SourceView type Script arbitrary exact args binding check"), - ("__ckb_require_cell_lock_args_prefix_hash", "SourceView lock Script 32-byte args prefix binding check"), - ("__ckb_require_cell_type_args_prefix_hash", "SourceView type Script 32-byte args prefix binding check"), - ("__ckb_require_cell_lock_args_suffix_hash", "SourceView lock Script 32-byte args suffix binding check"), - ("__ckb_require_cell_type_args_suffix_hash", "SourceView type Script 32-byte args suffix binding check"), - ("__ckb_require_cell_lock_script_hash_type", "SourceView lock Script code_hash/hash_type binding check"), - ("__ckb_require_cell_type_script_hash_type", "SourceView type Script code_hash/hash_type binding check"), - ("__c256_require_u128_product_lte", "C256 u128 product <= requirement"), - ("__c256_require_u128_product_eq", "C256 u128 product == requirement"), - ("__c256_require_u128_sum2_products_lte", "C256 u128 product-sum <= requirement"), - ("__c256_require_u128_sum2_products_eq", "C256 u128 product-sum == requirement"), - ("__ckb_cell_data_size", "SourceView cell data byte length"), - ("__ckb_cell_data_u32_le", "SourceView cell data little-endian u32 read"), - ("__ckb_cell_data_u64_le", "SourceView cell data little-endian u64 read"), - ("__dao_accumulated_rate", "DAO accumulated rate from HeaderDep SourceView"), - ( - "__dao_input_accumulated_rate", - "DAO accumulated rate from an Input/GroupInput committed header", - ), - ("__dao_has_dao_type", "DAO type hash classifier"), - ("__dao_is_deposit_data", "DAO deposit data classifier"), - ("__dao_is_withdrawal_request_data", "DAO withdrawal request data classifier"), - ("__dao_require_header_dep_for_input", "DAO input header to HeaderDep lineage requirement"), - ("__dao_require_input_since_at_least", "DAO input since lower-bound requirement"), - ("__dao_require_input_relative_epoch_since_at_least", "DAO relative epoch since maturity requirement"), - ("__xudt_amount_low", "xUDT amount low 64 bits"), - ("__xudt_amount_high", "xUDT amount high 64 bits"), - ("__xudt_owner_mode_input_type_hash", "xUDT owner-mode input-type hash low word"), - ("__xudt_require_owner_mode_input_type", "xUDT owner-mode input-type binding check"), - ("__xudt_require_owner_mode_type_args", "xUDT owner-mode type args binding check"), - ( - "__xudt_require_owner_mode_type_args_current_script", - "xUDT owner-mode type args binding check against current script hash", - ), - ( - FUNGIBLE_TYPE_GROUP_V1_CODEGEN_HELPER, - "chain-neutral fungible type-group v1 conservation", - ), - ("__xudt_require_group_amount_conserved", "xUDT group input/output amount conservation"), - ("__xudt_require_group_amount_minted", "xUDT group output-input amount delta check"), - ("__xudt_require_group_amount_burned", "xUDT group input-output amount delta check"), - ("__ckb_witness_raw", "raw witness bytes"), - ("__ckb_witness_lock", "WitnessArgs.lock"), - ("__ckb_witness_input_type", "WitnessArgs.input_type"), - ("__ckb_witness_output_type", "WitnessArgs.output_type"), - ("__ckb_witness_size", "witness byte size"), - ("__ckb_require_witness_size_at_least", "require witness size lower bound"), - ("__ckb_sighash_all", "CKB sighash-all digest"), - ("__ckb_require_maturity", "CKB block-number since maturity"), - ("__ckb_require_time", "CKB timestamp since"), - ("__ckb_require_epoch_after", "CKB absolute epoch since"), - ("__ckb_require_epoch_relative", "CKB relative epoch since"), - ("__ckb_occupied_capacity", "compile-visible occupied capacity floor"), - ] { - if !referenced_helpers.contains(name) { - continue; - } - match name { - "__ckb_current_role" => self.emit_runtime_current_role_helper(enabled), - "__ckb_current_script_hash" => self.emit_runtime_current_script_hash_helper(enabled), - "__ckb_cell_capacity" => { - self.emit_runtime_cell_field_u64_helper(name, detail, CKB_CELL_FIELD_CAPACITY, enabled); - } - "__ckb_cell_occupied_capacity" => self.emit_runtime_cell_occupied_capacity_helper(enabled), - "__ckb_cell_unoccupied_capacity" => self.emit_runtime_cell_unoccupied_capacity_helper(enabled), - "__ckb_cell_output_index" => self.emit_runtime_cell_output_index_helper(enabled), - "__ckb_input_out_point_index" => self.emit_runtime_input_out_point_word_helper(name, detail, 32, 4, enabled), - "__ckb_input_out_point_tx_hash_low" => self.emit_runtime_input_out_point_word_helper(name, detail, 0, 8, enabled), - "__ckb_input_out_point_tx_hash" => self.emit_runtime_input_out_point_tx_hash_helper(enabled), - "__ckb_require_input_out_point_tx_hash" => self.emit_runtime_input_out_point_tx_hash_requirement_helper(enabled), - "__ckb_require_input_out_point" => self.emit_runtime_input_out_point_requirement_helper(enabled), - "__ckb_require_metapoint_relative" => self.emit_runtime_metapoint_relative_requirement_helper(enabled), - "__ckb_require_lock_type_metapoint_pairs" => { - self.emit_runtime_metapoint_pair_cardinality_helper(name, detail, true, false, false, enabled) - } - "__ckb_require_type_lock_metapoint_pairs" => { - self.emit_runtime_metapoint_pair_cardinality_helper(name, detail, false, false, false, enabled) - } - "__ckb_require_lock_type_metapoint_pairs_from_i32_data" => { - self.emit_runtime_metapoint_pair_cardinality_helper(name, detail, true, true, false, enabled) - } - "__ckb_require_type_lock_metapoint_pairs_from_i32_data" => { - self.emit_runtime_metapoint_pair_cardinality_helper(name, detail, false, true, false, enabled) - } - "__ckb_require_lock_type_metapoint_pairs_from_i32_data_filtered" => { - self.emit_runtime_metapoint_pair_cardinality_helper(name, detail, true, true, true, enabled) - } - "__ckb_require_type_lock_metapoint_pairs_from_i32_data_filtered" => { - self.emit_runtime_metapoint_pair_cardinality_helper(name, detail, false, true, true, enabled) - } - "__ckb_require_lock_match_master_out_point_pairs_from_data" => { - self.emit_runtime_lock_match_master_out_point_pairs_from_data_helper(enabled) - } - "__ckb_cell_lock_hash_low" => { - self.emit_runtime_cell_field_low_word_helper(name, detail, CKB_CELL_FIELD_LOCK_HASH, enabled); - } - "__ckb_cell_type_hash_low" => { - self.emit_runtime_cell_field_low_word_helper(name, detail, CKB_CELL_FIELD_TYPE_HASH, enabled); - } - "__ckb_cell_lock_hash" => { - self.emit_runtime_cell_hash_field_helper(name, detail, CKB_CELL_FIELD_LOCK_HASH, enabled); - } - "__ckb_cell_type_hash" => { - self.emit_runtime_cell_hash_field_helper(name, detail, CKB_CELL_FIELD_TYPE_HASH, enabled); - } - "__ckb_cell_data_hash" => { - self.emit_runtime_cell_data_hash_helper(name, detail, enabled); - } - "__ckb_cell_data_hash_at" => { - self.emit_runtime_cell_data_hash_at_helper(name, detail, enabled); - } - "__ckb_cell_lock_code_hash" => { - self.emit_runtime_cell_script_hash_field_helper(name, detail, CKB_CELL_FIELD_LOCK, ScriptHashFieldRead::CodeHash, enabled); - } - "__ckb_cell_type_code_hash" => { - self.emit_runtime_cell_script_hash_field_helper(name, detail, CKB_CELL_FIELD_TYPE, ScriptHashFieldRead::CodeHash, enabled); - } - "__ckb_cell_lock_args_hash" => { - self.emit_runtime_cell_script_hash_field_helper(name, detail, CKB_CELL_FIELD_LOCK, ScriptHashFieldRead::Args32, enabled); - } - "__ckb_cell_type_args_hash" => { - self.emit_runtime_cell_script_hash_field_helper(name, detail, CKB_CELL_FIELD_TYPE, ScriptHashFieldRead::Args32, enabled); - } - "__ckb_cell_lock_hash_type" => { - self.emit_runtime_cell_script_scalar_field_helper(name, detail, CKB_CELL_FIELD_LOCK, ScriptScalarFieldRead::HashType, enabled); - } - "__ckb_cell_type_hash_type" => { - self.emit_runtime_cell_script_scalar_field_helper(name, detail, CKB_CELL_FIELD_TYPE, ScriptScalarFieldRead::HashType, enabled); - } - "__ckb_cell_lock_args_empty" => { - self.emit_runtime_cell_script_scalar_field_helper(name, detail, CKB_CELL_FIELD_LOCK, ScriptScalarFieldRead::ArgsEmpty, enabled); - } - "__ckb_cell_type_args_empty" => { - self.emit_runtime_cell_script_scalar_field_helper(name, detail, CKB_CELL_FIELD_TYPE, ScriptScalarFieldRead::ArgsEmpty, enabled); - } - "__ckb_require_cell_lock_hash" => self.emit_runtime_cell_hash_requirement_helper( - name, - detail, - CKB_CELL_FIELD_LOCK_HASH, - CellScriptRuntimeError::ScriptRoleMismatch, - enabled, - ), - "__ckb_require_cell_type_hash" => self.emit_runtime_cell_hash_requirement_helper( - name, - detail, - CKB_CELL_FIELD_TYPE_HASH, - CellScriptRuntimeError::TypeHashMismatch, - enabled, - ), - "__ckb_require_cell_data_hash" => self.emit_runtime_cell_hash_requirement_helper( - name, - detail, - CKB_CELL_FIELD_DATA_HASH, - CellScriptRuntimeError::ScriptIdentityMismatch, - enabled, - ), - "__ckb_require_bounded_cell_dep_data_hash" => { - self.emit_runtime_bounded_cell_dep_data_hash_requirement_helper(enabled) - } - "__ckb_require_current_script_args_empty" => self.emit_runtime_current_script_args_empty_requirement_helper(enabled), - "__ckb_require_cell_lock_args_empty" => { - self.emit_runtime_cell_script_args_empty_requirement_helper(name, detail, CKB_CELL_FIELD_LOCK, enabled) - } - "__ckb_require_cell_type_args_empty" => { - self.emit_runtime_cell_script_args_empty_requirement_helper(name, detail, CKB_CELL_FIELD_TYPE, enabled) - } - "__ckb_require_cell_lock_args_hash" => { - self.emit_runtime_cell_script_args_hash_requirement_helper( - name, - detail, - CKB_CELL_FIELD_LOCK, - ScriptArgsHashRequirementMode::Exact32, - enabled, - ) - } - "__ckb_require_cell_type_args_hash" => { - self.emit_runtime_cell_script_args_hash_requirement_helper( - name, - detail, - CKB_CELL_FIELD_TYPE, - ScriptArgsHashRequirementMode::Exact32, - enabled, - ) - } - "__ckb_require_cell_lock_args_exact" => { - self.emit_runtime_cell_script_args_exact_requirement_helper(name, detail, CKB_CELL_FIELD_LOCK, enabled) - } - "__ckb_require_cell_type_args_exact" => { - self.emit_runtime_cell_script_args_exact_requirement_helper(name, detail, CKB_CELL_FIELD_TYPE, enabled) - } - "__ckb_require_cell_lock_args_prefix_hash" => { - self.emit_runtime_cell_script_args_hash_requirement_helper( - name, - detail, - CKB_CELL_FIELD_LOCK, - ScriptArgsHashRequirementMode::Prefix32, - enabled, - ) - } - "__ckb_require_cell_type_args_prefix_hash" => { - self.emit_runtime_cell_script_args_hash_requirement_helper( - name, - detail, - CKB_CELL_FIELD_TYPE, - ScriptArgsHashRequirementMode::Prefix32, - enabled, - ) - } - "__ckb_require_cell_lock_args_suffix_hash" => { - self.emit_runtime_cell_script_args_hash_requirement_helper( - name, - detail, - CKB_CELL_FIELD_LOCK, - ScriptArgsHashRequirementMode::Suffix32, - enabled, - ) - } - "__ckb_require_cell_type_args_suffix_hash" => { - self.emit_runtime_cell_script_args_hash_requirement_helper( - name, - detail, - CKB_CELL_FIELD_TYPE, - ScriptArgsHashRequirementMode::Suffix32, - enabled, - ) - } - "__ckb_require_cell_lock_script_hash_type" => { - self.emit_runtime_cell_script_hash_type_requirement_helper(name, detail, CKB_CELL_FIELD_LOCK, enabled) - } - "__ckb_require_cell_type_script_hash_type" => { - self.emit_runtime_cell_script_hash_type_requirement_helper(name, detail, CKB_CELL_FIELD_TYPE, enabled) - } - "__c256_require_u128_product_lte" => self.emit_runtime_c256_product_requirement_helper(name, detail, false), - "__c256_require_u128_product_eq" => self.emit_runtime_c256_product_requirement_helper(name, detail, true), - "__c256_require_u128_sum2_products_lte" => self.emit_runtime_c256_sum2_product_requirement_helper(name, detail, false), - "__c256_require_u128_sum2_products_eq" => self.emit_runtime_c256_sum2_product_requirement_helper(name, detail, true), - "__ckb_cell_data_size" => self.emit_runtime_cell_data_size_helper(enabled), - "__ckb_cell_data_u32_le" => self.emit_runtime_cell_data_word_le_helper(name, detail, 4, enabled), - "__ckb_cell_data_u64_le" => self.emit_runtime_cell_data_word_le_helper(name, detail, 8, enabled), - "__dao_accumulated_rate" => self.emit_runtime_dao_accumulated_rate_helper(enabled), - "__dao_input_accumulated_rate" => self.emit_runtime_dao_input_accumulated_rate_helper(enabled), - "__dao_has_dao_type" => self.emit_runtime_dao_type_classifier_helper(enabled), - "__dao_is_deposit_data" => self.emit_runtime_dao_cell_data_classifier_helper(name, detail, true, enabled), - "__dao_is_withdrawal_request_data" => { - self.emit_runtime_dao_cell_data_classifier_helper(name, detail, false, enabled); - } - "__dao_require_header_dep_for_input" => self.emit_runtime_dao_require_header_dep_for_input_helper(enabled), - "__dao_require_input_since_at_least" => self.emit_runtime_dao_require_input_since_at_least_helper(enabled), - "__dao_require_input_relative_epoch_since_at_least" => { - self.emit_runtime_dao_require_input_relative_epoch_since_at_least_helper(enabled); - } - "__xudt_amount_low" => self.emit_runtime_xudt_amount_word_helper(name, detail, 0, enabled), - "__xudt_amount_high" => self.emit_runtime_xudt_amount_word_helper(name, detail, 8, enabled), - "__xudt_owner_mode_input_type_hash" => { - self.emit_runtime_cell_field_low_word_helper(name, detail, CKB_CELL_FIELD_TYPE_HASH, enabled); - } - "__xudt_require_owner_mode_input_type" => self.emit_runtime_xudt_require_owner_mode_input_type_helper(enabled), - "__xudt_require_owner_mode_type_args" => self.emit_runtime_xudt_require_owner_mode_type_args_helper(enabled), - "__xudt_require_owner_mode_type_args_current_script" => { - self.emit_runtime_xudt_require_owner_mode_type_args_current_script_helper(enabled) - } - FUNGIBLE_TYPE_GROUP_V1_CODEGEN_HELPER | "__xudt_require_group_amount_conserved" => { - self.emit_runtime_fungible_type_group_conservation_helper(name, detail, enabled) - } - "__xudt_require_group_amount_minted" => { - self.emit_runtime_xudt_require_group_amount_delta_helper(name, true, enabled); - } - "__xudt_require_group_amount_burned" => { - self.emit_runtime_xudt_require_group_amount_delta_helper(name, false, enabled); - } - "__ckb_witness_size" => self.emit_runtime_witness_size_helper(enabled), - "__ckb_require_witness_size_at_least" => { - self.emit_runtime_require_witness_size_at_least_helper(enabled) - } - "__ckb_witness_raw" => self.emit_runtime_witness_raw_helper(enabled), - "__ckb_witness_lock" => self.emit_runtime_witness_args_field_helper(name, detail, 0, enabled), - "__ckb_witness_input_type" => self.emit_runtime_witness_args_field_helper(name, detail, 1, enabled), - "__ckb_witness_output_type" => self.emit_runtime_witness_args_field_helper(name, detail, 2, enabled), - _ => { - self.emit_global(name); - self.emit_label(name); - self.emit(format!("# cellscript abi: v0.14 CKB semantic helper ({})", detail)); - if !enabled { - self.emit_fail(CellScriptRuntimeError::SyscallFailed); - } else { - self.emit("li a0, 0"); - self.emit("ret"); - } - } - } - } - - if referenced_helpers.contains("__ckb_hash_chain") { - self.emit_global("__ckb_hash_chain"); - self.emit_label("__ckb_hash_chain"); - self.emit("# cellscript abi: hash_chain aliases CKB Blake2b-256 over one 32-byte Hash input"); - if !enabled { - self.emit_fail(CellScriptRuntimeError::SyscallFailed); - } else { - self.emit("j __ckb_hash_blake2b"); - } - } - if referenced_helpers.contains("__ckb_hash_pair") { - self.emit_runtime_blake2b_hash_pair(enabled); - } - if referenced_helpers.contains("__ckb_hash_chain") - || referenced_helpers.contains("__ckb_hash_blake2b") - || referenced_helpers.contains("__ckb_hash_data_packed") - { - self.emit_runtime_blake2b_hash32(enabled); - } - if referenced_helpers.contains("__ckb_hash_blake2b_var") - || referenced_helpers.contains("__ckb_hash_data_packed") - || referenced_helpers.contains("__ckb_hash_blake2b_packed") - || referenced_helpers.contains("__ckb_cell_data_hash") - { - self.emit_runtime_blake2b_hash_var(enabled); - } - if referenced_helpers.iter().any(|helper| { - matches!( - helper.as_str(), - "__ckb_hash_sha256" - | "__ckb_hash_sha256d" - | "__ckb_hash_sha256_pair" - | "__ckb_hash_sha256d_pair" - | "__ckb_require_sha256d_merkle_root" - ) - }) { - self.emit_runtime_sha256_surface(enabled); - } - } - - fn emit_u32_normalize(&mut self, register: &str) { - self.emit(format!("slli {0}, {0}, 32", register)); - self.emit(format!("srli {0}, {0}, 32", register)); - } - - fn emit_sha256_rotr(&mut self, dest: &str, source: &str, shift: u8, scratch: &str) { - self.emit(format!("srli {}, {}, {}", dest, source, shift)); - self.emit(format!("slli {}, {}, {}", scratch, source, 32 - shift)); - self.emit_u32_normalize(scratch); - self.emit(format!("or {}, {}, {}", dest, dest, scratch)); - } - - fn emit_runtime_sha256_surface(&mut self, enabled: bool) { - for symbol in [ - "__cellscript_sha256_compress", - "__cellscript_sha256_fixed", - "__ckb_hash_sha256", - "__ckb_hash_sha256d", - "__ckb_hash_sha256_pair", - "__ckb_hash_sha256d_pair", - "__ckb_require_sha256d_merkle_root", - ] { - self.emit_global(symbol); - } - if !enabled { - for symbol in [ - "__cellscript_sha256_compress", - "__cellscript_sha256_fixed", - "__ckb_hash_sha256", - "__ckb_hash_sha256d", - "__ckb_hash_sha256_pair", - "__ckb_hash_sha256d_pair", - "__ckb_require_sha256d_merkle_root", - ] { - self.emit_label(symbol); - self.emit_fail(CellScriptRuntimeError::SyscallFailed); - } - return; - } - self.emit_runtime_sha256_compress(); - self.emit_runtime_sha256_fixed(); - self.emit_runtime_sha256_wrappers(); - self.emit_runtime_sha256d_merkle_requirement(); - } - - fn emit_runtime_sha256_compress(&mut self) { - const K: [u32; 64] = [ - 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, - 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, - 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, - 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, - 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, - 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, - 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, - ]; - self.emit_label("__cellscript_sha256_compress"); - self.emit("# cellscript abi: SHA-256 compression; a0=state[8] u32-in-u64, a1=schedule[64] u32-in-u64"); - self.emit("addi sp, sp, -112"); - self.emit("sd ra, 104(sp)"); - self.emit("sd a0, 64(sp)"); - self.emit("sd a1, 72(sp)"); - for index in 0..8 { - self.emit(format!("ld t0, {}(a0)", index * 8)); - self.emit(format!("sd t0, {}(sp)", index * 8)); - } - for index in 16..64 { - self.emit(format!("ld t0, {}(a1)", (index - 15) * 8)); - self.emit_sha256_rotr("t1", "t0", 7, "t2"); - self.emit_sha256_rotr("t3", "t0", 18, "t2"); - self.emit("xor t1, t1, t3"); - self.emit("srli t3, t0, 3"); - self.emit("xor t1, t1, t3"); - self.emit(format!("ld t3, {}(a1)", (index - 2) * 8)); - self.emit_sha256_rotr("t4", "t3", 17, "t5"); - self.emit_sha256_rotr("t6", "t3", 19, "t5"); - self.emit("xor t4, t4, t6"); - self.emit("srli t6, t3, 10"); - self.emit("xor t4, t4, t6"); - self.emit(format!("ld t0, {}(a1)", (index - 16) * 8)); - self.emit("add t1, t1, t0"); - self.emit(format!("ld t0, {}(a1)", (index - 7) * 8)); - self.emit("add t1, t1, t0"); - self.emit("add t1, t1, t4"); - self.emit_u32_normalize("t1"); - self.emit(format!("sd t1, {}(a1)", index * 8)); - } - for (round, constant) in K.iter().enumerate() { - self.emit("ld t0, 32(sp)"); - self.emit_sha256_rotr("t1", "t0", 6, "t2"); - self.emit_sha256_rotr("t3", "t0", 11, "t2"); - self.emit("xor t1, t1, t3"); - self.emit_sha256_rotr("t3", "t0", 25, "t2"); - self.emit("xor t1, t1, t3"); - self.emit("ld t2, 40(sp)"); - self.emit("and t2, t0, t2"); - self.emit("xori t3, t0, -1"); - self.emit_u32_normalize("t3"); - self.emit("ld t4, 48(sp)"); - self.emit("and t3, t3, t4"); - self.emit("xor t2, t2, t3"); - self.emit("ld t3, 56(sp)"); - self.emit("add t3, t3, t1"); - self.emit("add t3, t3, t2"); - self.emit(format!("li t1, {}", constant)); - self.emit("add t3, t3, t1"); - self.emit(format!("ld t1, {}(a1)", round * 8)); - self.emit("add t3, t3, t1"); - self.emit_u32_normalize("t3"); - self.emit("sd t3, 80(sp)"); - - self.emit("ld t0, 0(sp)"); - self.emit_sha256_rotr("t1", "t0", 2, "t2"); - self.emit_sha256_rotr("t3", "t0", 13, "t2"); - self.emit("xor t1, t1, t3"); - self.emit_sha256_rotr("t3", "t0", 22, "t2"); - self.emit("xor t1, t1, t3"); - self.emit("ld t3, 8(sp)"); - self.emit("and t2, t0, t3"); - self.emit("ld t4, 16(sp)"); - self.emit("and t5, t0, t4"); - self.emit("xor t2, t2, t5"); - self.emit("and t5, t3, t4"); - self.emit("xor t2, t2, t5"); - self.emit("add t1, t1, t2"); - self.emit_u32_normalize("t1"); - self.emit("sd t1, 88(sp)"); - - self.emit("ld t0, 48(sp)"); - self.emit("sd t0, 56(sp)"); - self.emit("ld t0, 40(sp)"); - self.emit("sd t0, 48(sp)"); - self.emit("ld t0, 32(sp)"); - self.emit("sd t0, 40(sp)"); - self.emit("ld t0, 24(sp)"); - self.emit("ld t1, 80(sp)"); - self.emit("add t0, t0, t1"); - self.emit_u32_normalize("t0"); - self.emit("sd t0, 32(sp)"); - self.emit("ld t0, 16(sp)"); - self.emit("sd t0, 24(sp)"); - self.emit("ld t0, 8(sp)"); - self.emit("sd t0, 16(sp)"); - self.emit("ld t0, 0(sp)"); - self.emit("sd t0, 8(sp)"); - self.emit("ld t1, 80(sp)"); - self.emit("ld t2, 88(sp)"); - self.emit("add t1, t1, t2"); - self.emit_u32_normalize("t1"); - self.emit("sd t1, 0(sp)"); - } - self.emit("ld a0, 64(sp)"); - for index in 0..8 { - self.emit(format!("ld t0, {}(a0)", index * 8)); - self.emit(format!("ld t1, {}(sp)", index * 8)); - self.emit("add t0, t0, t1"); - self.emit_u32_normalize("t0"); - self.emit(format!("sd t0, {}(a0)", index * 8)); - } - self.emit("li a0, 0"); - self.emit("ld ra, 104(sp)"); - self.emit("addi sp, sp, 112"); - self.emit("ret"); - } - - fn emit_runtime_sha256_fixed(&mut self) { - const H: [u32; 8] = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]; - const STATE_OFFSET: usize = 8; - const W_OFFSET: usize = 72; - const INPUT_OFFSET: usize = 584; - const LEN_OFFSET: usize = 592; - const OUTPUT_OFFSET: usize = 600; - const RA_OFFSET: usize = 616; - const FRAME_SIZE: usize = 624; - self.emit_label("__cellscript_sha256_fixed"); - self.emit("# cellscript abi: bounded SHA-256 for exactly 32 or 64 input bytes; a0=input, a1=len, a2=output[32]"); - let len32 = self.fresh_label("sha256_len32"); - let len64 = self.fresh_label("sha256_len64"); - let after_first = self.fresh_label("sha256_after_first"); - let output = self.fresh_label("sha256_output"); - let invalid = self.fresh_label("sha256_invalid"); - let done = self.fresh_label("sha256_done"); - self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); - self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); - self.emit(format!("sd a0, {}(sp)", INPUT_OFFSET)); - self.emit(format!("sd a1, {}(sp)", LEN_OFFSET)); - self.emit(format!("sd a2, {}(sp)", OUTPUT_OFFSET)); - self.emit(format!("beqz a0, {}", invalid)); - self.emit(format!("beqz a2, {}", invalid)); - self.emit("li t0, 32"); - self.emit("sub t1, a1, t0"); - self.emit(format!("beqz t1, {}", len32)); - self.emit("li t0, 64"); - self.emit("sub t1, a1, t0"); - self.emit(format!("beqz t1, {}", len64)); - self.emit(format!("j {}", invalid)); - self.emit_label(&len32); - for (index, value) in H.iter().enumerate() { - self.emit(format!("li t0, {}", value)); - self.emit(format!("sd t0, {}(sp)", STATE_OFFSET + index * 8)); - } - self.emit("li t6, 8"); - self.emit(format!("j {}", after_first)); - self.emit_label(&len64); - for (index, value) in H.iter().enumerate() { - self.emit(format!("li t0, {}", value)); - self.emit(format!("sd t0, {}(sp)", STATE_OFFSET + index * 8)); - } - self.emit("li t6, 16"); - self.emit_label(&after_first); - self.emit(format!("ld a0, {}(sp)", INPUT_OFFSET)); - for word in 0..16 { - let skip = self.fresh_label("sha256_input_word_skip"); - self.emit(format!("li t5, {}", word + 1)); - self.emit(format!("bltu t6, t5, {}", skip)); - self.emit("li t4, 0"); - for byte in 0..4 { - self.emit(format!("lbu t0, {}(a0)", word * 4 + byte)); - let shift = 24 - byte * 8; - if shift != 0 { - self.emit(format!("slli t0, t0, {}", shift)); - } - self.emit("or t4, t4, t0"); - } - self.emit(format!("sd t4, {}(sp)", W_OFFSET + word * 8)); - self.emit_label(&skip); - } - let first_is64 = self.fresh_label("sha256_first_is64"); - self.emit(format!("ld t0, {}(sp)", LEN_OFFSET)); - self.emit("li t1, 64"); - self.emit("sub t2, t0, t1"); - self.emit(format!("beqz t2, {}", first_is64)); - self.emit("li t0, 2147483648"); - self.emit(format!("sd t0, {}(sp)", W_OFFSET + 8 * 8)); - for word in 9..15 { - self.emit(format!("sd zero, {}(sp)", W_OFFSET + word * 8)); - } - self.emit("li t0, 256"); - self.emit(format!("sd t0, {}(sp)", W_OFFSET + 15 * 8)); - self.emit_label(&first_is64); - self.emit(format!("addi a0, sp, {}", STATE_OFFSET)); - self.emit(format!("addi a1, sp, {}", W_OFFSET)); - self.emit("call __cellscript_sha256_compress"); - self.emit(format!("ld t0, {}(sp)", LEN_OFFSET)); - self.emit("li t1, 32"); - self.emit("sub t2, t0, t1"); - self.emit(format!("beqz t2, {}", output)); - self.emit("li t0, 2147483648"); - self.emit(format!("sd t0, {}(sp)", W_OFFSET)); - for word in 1..15 { - self.emit(format!("sd zero, {}(sp)", W_OFFSET + word * 8)); - } - self.emit("li t0, 512"); - self.emit(format!("sd t0, {}(sp)", W_OFFSET + 15 * 8)); - self.emit(format!("addi a0, sp, {}", STATE_OFFSET)); - self.emit(format!("addi a1, sp, {}", W_OFFSET)); - self.emit("call __cellscript_sha256_compress"); - self.emit_label(&output); - self.emit(format!("ld a0, {}(sp)", OUTPUT_OFFSET)); - for word in 0..8 { - self.emit(format!("ld t0, {}(sp)", STATE_OFFSET + word * 8)); - for byte in 0..4 { - let shift = 24 - byte * 8; - if shift == 0 { - self.emit("addi t1, t0, 0"); - } else { - self.emit(format!("srli t1, t0, {}", shift)); - } - self.emit(format!("sb t1, {}(a0)", word * 4 + byte)); - } - } - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::BoundsCheckFailed.code())); - self.emit_label(&done); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - } - - fn emit_runtime_sha256_wrappers(&mut self) { - self.emit_label("__ckb_hash_sha256"); - self.emit("# cellscript abi: SHA-256 over one 32-byte Hash; a0=input, a1=output"); - self.emit("addi a2, a1, 0"); - self.emit("li a1, 32"); - self.emit("j __cellscript_sha256_fixed"); - - self.emit_label("__ckb_hash_sha256d"); - self.emit("# cellscript abi: SHA256d over one 32-byte Hash; a0=input, a1=output"); - self.emit("addi sp, sp, -80"); - self.emit("sd ra, 72(sp)"); - self.emit("sd a0, 8(sp)"); - self.emit("sd a1, 16(sp)"); - self.emit("ld a0, 8(sp)"); - self.emit("li a1, 32"); - self.emit("addi a2, sp, 32"); - self.emit("call __cellscript_sha256_fixed"); - let sha256d_first_ok = self.fresh_label("sha256d_first_ok"); - let sha256d_done = self.fresh_label("sha256d_done"); - self.emit(format!("beqz a0, {}", sha256d_first_ok)); - self.emit(format!("j {}", sha256d_done)); - self.emit_label(&sha256d_first_ok); - self.emit("addi a0, sp, 32"); - self.emit("li a1, 32"); - self.emit("ld a2, 16(sp)"); - self.emit("call __cellscript_sha256_fixed"); - self.emit_label(&sha256d_done); - self.emit("ld ra, 72(sp)"); - self.emit("addi sp, sp, 80"); - self.emit("ret"); - - self.emit_label("__ckb_hash_sha256_pair"); - self.emit("# cellscript abi: SHA-256 over left[32] || right[32]; a2=output"); - self.emit("addi sp, sp, -112"); - self.emit("sd ra, 104(sp)"); - self.emit("sd a0, 8(sp)"); - self.emit("sd a1, 16(sp)"); - self.emit("sd a2, 24(sp)"); - self.emit("ld a0, 8(sp)"); - self.emit("addi a1, sp, 32"); - self.emit("li a2, 32"); - self.emit("call __cellscript_memcpy_fixed"); - self.emit("ld a0, 16(sp)"); - self.emit("addi a1, sp, 64"); - self.emit("li a2, 32"); - self.emit("call __cellscript_memcpy_fixed"); - self.emit("addi a0, sp, 32"); - self.emit("li a1, 64"); - self.emit("ld a2, 24(sp)"); - self.emit("call __cellscript_sha256_fixed"); - self.emit("ld ra, 104(sp)"); - self.emit("addi sp, sp, 112"); - self.emit("ret"); - - self.emit_label("__ckb_hash_sha256d_pair"); - self.emit("# cellscript abi: SHA256d over left[32] || right[32]; a2=output"); - self.emit("addi sp, sp, -160"); - self.emit("sd ra, 152(sp)"); - self.emit("sd a0, 8(sp)"); - self.emit("sd a1, 16(sp)"); - self.emit("sd a2, 24(sp)"); - self.emit("ld a0, 8(sp)"); - self.emit("addi a1, sp, 32"); - self.emit("li a2, 32"); - self.emit("call __cellscript_memcpy_fixed"); - self.emit("ld a0, 16(sp)"); - self.emit("addi a1, sp, 64"); - self.emit("li a2, 32"); - self.emit("call __cellscript_memcpy_fixed"); - self.emit("addi a0, sp, 32"); - self.emit("li a1, 64"); - self.emit("addi a2, sp, 96"); - self.emit("call __cellscript_sha256_fixed"); - let pair_first_ok = self.fresh_label("sha256d_pair_first_ok"); - let pair_done = self.fresh_label("sha256d_pair_done"); - self.emit(format!("beqz a0, {}", pair_first_ok)); - self.emit(format!("j {}", pair_done)); - self.emit_label(&pair_first_ok); - self.emit("addi a0, sp, 96"); - self.emit("li a1, 32"); - self.emit("ld a2, 24(sp)"); - self.emit("call __cellscript_sha256_fixed"); - self.emit_label(&pair_done); - self.emit("ld ra, 152(sp)"); - self.emit("addi sp, sp, 160"); - self.emit("ret"); - } - - fn emit_runtime_sha256d_merkle_requirement(&mut self) { - self.emit_label("__ckb_require_sha256d_merkle_root"); - self.emit("# cellscript abi: bounded SHA256d Merkle path; siblings is exactly 16 Hash values, depth <= 16"); - self.emit("addi sp, sp, -160"); - self.emit("sd ra, 152(sp)"); - self.emit("sd a1, 8(sp)"); - self.emit("sd a2, 16(sp)"); - self.emit("sd a3, 24(sp)"); - self.emit("sd a4, 32(sp)"); - let invalid = self.fresh_label("sha256d_merkle_invalid"); - let loop_label = self.fresh_label("sha256d_merkle_loop"); - let right_child = self.fresh_label("sha256d_merkle_right_child"); - let hash_ready = self.fresh_label("sha256d_merkle_hash_ready"); - let loop_done = self.fresh_label("sha256d_merkle_loop_done"); - let mismatch = self.fresh_label("sha256d_merkle_mismatch"); - let done = self.fresh_label("sha256d_merkle_done"); - self.emit(format!("beqz a0, {}", invalid)); - self.emit(format!("beqz a1, {}", invalid)); - self.emit(format!("beqz a4, {}", invalid)); - self.emit("li t0, 16"); - self.emit(format!("bltu t0, a2, {}", invalid)); - self.emit("addi a1, sp, 48"); - self.emit("li a2, 32"); - self.emit("call __cellscript_memcpy_fixed"); - self.emit("sd zero, 40(sp)"); - self.emit_label(&loop_label); - self.emit("ld t0, 40(sp)"); - self.emit("ld t1, 16(sp)"); - self.emit(format!("bgeu t0, t1, {}", loop_done)); - self.emit("slli t1, t0, 5"); - self.emit("ld t2, 8(sp)"); - self.emit("add t2, t2, t1"); - self.emit("ld t3, 24(sp)"); - self.emit("li t4, 1"); - self.emit("and t4, t3, t4"); - self.emit(format!("bnez t4, {}", right_child)); - self.emit("addi a0, sp, 48"); - self.emit("addi a1, t2, 0"); - self.emit("addi a2, sp, 80"); - self.emit("call __ckb_hash_sha256d_pair"); - self.emit(format!("j {}", hash_ready)); - self.emit_label(&right_child); - self.emit("addi a0, t2, 0"); - self.emit("addi a1, sp, 48"); - self.emit("addi a2, sp, 80"); - self.emit("call __ckb_hash_sha256d_pair"); - self.emit_label(&hash_ready); - self.emit(format!("bnez a0, {}", invalid)); - self.emit("addi a0, sp, 80"); - self.emit("addi a1, sp, 48"); - self.emit("li a2, 32"); - self.emit("call __cellscript_memcpy_fixed"); - self.emit("ld t0, 24(sp)"); - self.emit("srli t0, t0, 1"); - self.emit("sd t0, 24(sp)"); - self.emit("ld t0, 40(sp)"); - self.emit("addi t0, t0, 1"); - self.emit("sd t0, 40(sp)"); - self.emit(format!("j {}", loop_label)); - self.emit_label(&loop_done); - self.emit("ld t0, 24(sp)"); - self.emit(format!("bnez t0, {}", invalid)); - self.emit("addi a0, sp, 48"); - self.emit("ld a1, 32(sp)"); - self.emit("li a2, 32"); - self.emit("call __cellscript_memcmp_fixed"); - self.emit(format!("bnez a0, {}", mismatch)); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::BoundsCheckFailed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&mismatch); - self.emit("# cellscript runtime error 64 merkle-root-mismatch"); - self.emit(format!("li a0, {}", CellScriptRuntimeError::MerkleRootMismatch.code())); - self.emit_label(&done); - self.emit("ld ra, 152(sp)"); - self.emit("addi sp, sp, 160"); - self.emit("ret"); - } - - fn emit_runtime_blake2b_hash_var(&mut self, enabled: bool) { - self.emit_global("__ckb_hash_blake2b_var"); - self.emit_label("__ckb_hash_blake2b_var"); - self.emit("# cellscript abi: CKB Blake2b-256 variable helper; a0=input, a1=len, a2=output[32], returns a0=0"); - if !enabled { - self.emit_fail(CellScriptRuntimeError::SyscallFailed); - return; - } - - const IV: [u64; 8] = [ - 0x6a09e667f3bcc908, - 0xbb67ae8584caa73b, - 0x3c6ef372fe94f82b, - 0xa54ff53a5f1d36f1, - 0x510e527fade682d1, - 0x9b05688c2b3e6c1f, - 0x1f83d9abfb41bd6b, - 0x5be0cd19137e2179, - ]; - const SIGMA: [[usize; 16]; 12] = [ - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], - [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], - [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4], - [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8], - [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13], - [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9], - [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11], - [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10], - [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5], - [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0], - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], - [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], - ]; - - const H_BASE: usize = 0; - const V_BASE: usize = 64; - const M_BASE: usize = 192; - const PTR: usize = 320; - const LEN: usize = 328; - const OUT: usize = 336; - const POS: usize = 344; - const CHUNK: usize = 352; - const FRAME: usize = 384; - - let personal0 = u64::from_le_bytes(*b"ckb-defa"); - let personal1 = u64::from_le_bytes(*b"ult-hash"); - let h = [IV[0] ^ 0x01010020, IV[1], IV[2], IV[3], IV[4], IV[5], IV[6] ^ personal0, IV[7] ^ personal1]; - - self.emit_large_addi("sp", "sp", -(FRAME as i64)); - self.emit_stack_store("a0", PTR); - self.emit_stack_store("a1", LEN); - self.emit_stack_store("a2", OUT); - self.emit_stack_store("zero", POS); - for (index, value) in h.iter().enumerate() { - self.emit_blake2b_store_const(*value, H_BASE + index * 8); - } - - let block_label = self.fresh_label("blake2b_var_block"); - let done_label = self.fresh_label("blake2b_var_done"); - self.emit_label(&block_label); - self.emit_stack_load("t0", POS); - self.emit_stack_load("t1", LEN); - self.emit("sub t2, t1, t0"); - let empty_first_block_label = self.fresh_label("blake2b_var_empty_first_block"); - self.emit(format!("bnez t2, {}", empty_first_block_label)); - self.emit(format!("beqz t0, {}", empty_first_block_label)); - self.emit(format!("j {}", done_label)); - self.emit_label(&empty_first_block_label); - self.emit("li t3, 128"); - self.emit("sltu t4, t3, t2"); - let chunk_rem_label = self.fresh_label("blake2b_var_chunk_rem"); - let chunk_set_label = self.fresh_label("blake2b_var_chunk_set"); - self.emit(format!("beqz t4, {}", chunk_rem_label)); - self.emit("li t2, 128"); - self.emit(format!("j {}", chunk_set_label)); - self.emit_label(&chunk_rem_label); - self.emit("# chunk already in t2"); - self.emit_label(&chunk_set_label); - self.emit_stack_store("t2", CHUNK); - let zero_loop = self.fresh_label("blake2b_var_zero_loop"); - let zero_done = self.fresh_label("blake2b_var_zero_done"); - self.emit("li t0, 0"); - self.emit_label(&zero_loop); - self.emit("li t1, 128"); - self.emit("sltu t2, t0, t1"); - self.emit(format!("beqz t2, {}", zero_done)); - self.emit(format!("li t3, {}", M_BASE)); - self.emit("add t3, sp, t3"); - self.emit("add t3, t3, t0"); - self.emit("sb zero, 0(t3)"); - self.emit("addi t0, t0, 1"); - self.emit(format!("j {}", zero_loop)); - self.emit_label(&zero_done); - - let copy_loop = self.fresh_label("blake2b_var_copy_loop"); - let copy_done = self.fresh_label("blake2b_var_copy_done"); - self.emit("li t0, 0"); - self.emit_label(©_loop); - self.emit_stack_load("t1", CHUNK); - self.emit("sltu t2, t0, t1"); - self.emit(format!("beqz t2, {}", copy_done)); - self.emit_stack_load("t3", PTR); - self.emit_stack_load("t4", POS); - self.emit("add t3, t3, t4"); - self.emit("add t3, t3, t0"); - self.emit("lbu t5, 0(t3)"); - self.emit(format!("li t6, {}", M_BASE)); - self.emit("add t6, sp, t6"); - self.emit("add t6, t6, t0"); - self.emit("sb t5, 0(t6)"); - self.emit("addi t0, t0, 1"); - self.emit(format!("j {}", copy_loop)); - self.emit_label(©_done); - - for index in 0..8 { - self.emit_stack_load("t0", H_BASE + index * 8); - self.emit_stack_store("t0", V_BASE + index * 8); - } - for (index, value) in IV.iter().enumerate() { - self.emit_blake2b_store_const(*value, V_BASE + (index + 8) * 8); - } - self.emit_stack_load("t0", POS); - self.emit_stack_load("t1", CHUNK); - self.emit("add t0, t0, t1"); - self.emit_stack_load("t2", V_BASE + 12 * 8); - self.emit("xor t2, t2, t0"); - self.emit_stack_store("t2", V_BASE + 12 * 8); - self.emit_stack_load("t2", V_BASE + 13 * 8); - self.emit_stack_store("t2", V_BASE + 13 * 8); - let not_final_label = self.fresh_label("blake2b_var_not_final"); - self.emit_stack_load("t3", LEN); - self.emit("sub t4, t3, t0"); - self.emit(format!("bnez t4, {}", not_final_label)); - self.emit_stack_load("t5", V_BASE + 14 * 8); - self.emit("xori t5, t5, -1"); - self.emit_stack_store("t5", V_BASE + 14 * 8); - self.emit_label(¬_final_label); - - for round in SIGMA { - self.emit_blake2b_g(V_BASE, M_BASE, 0, 4, 8, 12, round[0], round[1]); - self.emit_blake2b_g(V_BASE, M_BASE, 1, 5, 9, 13, round[2], round[3]); - self.emit_blake2b_g(V_BASE, M_BASE, 2, 6, 10, 14, round[4], round[5]); - self.emit_blake2b_g(V_BASE, M_BASE, 3, 7, 11, 15, round[6], round[7]); - self.emit_blake2b_g(V_BASE, M_BASE, 0, 5, 10, 15, round[8], round[9]); - self.emit_blake2b_g(V_BASE, M_BASE, 1, 6, 11, 12, round[10], round[11]); - self.emit_blake2b_g(V_BASE, M_BASE, 2, 7, 8, 13, round[12], round[13]); - self.emit_blake2b_g(V_BASE, M_BASE, 3, 4, 9, 14, round[14], round[15]); - } - for index in 0..8 { - self.emit_stack_load("t0", H_BASE + index * 8); - self.emit_stack_load("t1", V_BASE + index * 8); - self.emit("xor t0, t0, t1"); - self.emit_stack_load("t1", V_BASE + (index + 8) * 8); - self.emit("xor t0, t0, t1"); - self.emit_stack_store("t0", H_BASE + index * 8); - } - self.emit_stack_load("t0", POS); - self.emit_stack_load("t1", CHUNK); - self.emit("add t0, t0, t1"); - self.emit_stack_store("t0", POS); - self.emit(format!("beqz t1, {}", done_label)); - self.emit(format!("j {}", block_label)); - - self.emit_label(&done_label); - self.emit_stack_load("t6", OUT); - for index in 0..4 { - self.emit_stack_load("t0", H_BASE + index * 8); - self.emit(format!("sd t0, {}(t6)", index * 8)); - } - self.emit_large_addi("sp", "sp", FRAME as i64); - self.emit("li a0, 0"); - self.emit("ret"); - } - - fn emit_runtime_blake2b_hash32(&mut self, enabled: bool) { - self.emit_global("__ckb_hash_blake2b"); - self.emit_label("__ckb_hash_blake2b"); - self.emit("# cellscript abi: CKB Blake2b-256 helper; a0=input[32], a1=output[32], returns a0=0"); - if !enabled { - self.emit_fail(CellScriptRuntimeError::SyscallFailed); - return; - } - - const IV: [u64; 8] = [ - 0x6a09e667f3bcc908, - 0xbb67ae8584caa73b, - 0x3c6ef372fe94f82b, - 0xa54ff53a5f1d36f1, - 0x510e527fade682d1, - 0x9b05688c2b3e6c1f, - 0x1f83d9abfb41bd6b, - 0x5be0cd19137e2179, - ]; - const SIGMA: [[usize; 16]; 12] = [ - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], - [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], - [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4], - [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8], - [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13], - [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9], - [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11], - [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10], - [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5], - [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0], - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], - [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], - ]; - - const H_BASE: usize = 0; - const V_BASE: usize = 64; - const M_BASE: usize = 192; - const FRAME: usize = 320; - - let personal0 = u64::from_le_bytes(*b"ckb-defa"); - let personal1 = u64::from_le_bytes(*b"ult-hash"); - let h = [IV[0] ^ 0x01010020, IV[1], IV[2], IV[3], IV[4], IV[5], IV[6] ^ personal0, IV[7] ^ personal1]; - - self.emit_large_addi("sp", "sp", -(FRAME as i64)); - for (index, value) in h.iter().enumerate() { - self.emit_blake2b_store_const(*value, H_BASE + index * 8); - } - for index in 0..4 { - self.emit_blake2b_load_input_word(index, M_BASE + index * 8); - } - for index in 4..16 { - self.emit_stack_store("zero", M_BASE + index * 8); - } - for index in 0..8 { - self.emit_stack_load("t0", H_BASE + index * 8); - self.emit_stack_store("t0", V_BASE + index * 8); - } - for (index, value) in IV.iter().enumerate() { - self.emit_blake2b_store_const(*value, V_BASE + (index + 8) * 8); - } - self.emit_stack_load("t0", V_BASE + 12 * 8); - self.emit("xori t0, t0, 32"); - self.emit_stack_store("t0", V_BASE + 12 * 8); - self.emit_stack_load("t0", V_BASE + 14 * 8); - self.emit("xori t0, t0, -1"); - self.emit_stack_store("t0", V_BASE + 14 * 8); - - for round in SIGMA { - self.emit_blake2b_g(V_BASE, M_BASE, 0, 4, 8, 12, round[0], round[1]); - self.emit_blake2b_g(V_BASE, M_BASE, 1, 5, 9, 13, round[2], round[3]); - self.emit_blake2b_g(V_BASE, M_BASE, 2, 6, 10, 14, round[4], round[5]); - self.emit_blake2b_g(V_BASE, M_BASE, 3, 7, 11, 15, round[6], round[7]); - self.emit_blake2b_g(V_BASE, M_BASE, 0, 5, 10, 15, round[8], round[9]); - self.emit_blake2b_g(V_BASE, M_BASE, 1, 6, 11, 12, round[10], round[11]); - self.emit_blake2b_g(V_BASE, M_BASE, 2, 7, 8, 13, round[12], round[13]); - self.emit_blake2b_g(V_BASE, M_BASE, 3, 4, 9, 14, round[14], round[15]); - } - - for index in 0..8 { - self.emit_stack_load("t0", H_BASE + index * 8); - self.emit_stack_load("t1", V_BASE + index * 8); - self.emit("xor t0, t0, t1"); - self.emit_stack_load("t1", V_BASE + (index + 8) * 8); - self.emit("xor t0, t0, t1"); - self.emit_stack_store("t0", H_BASE + index * 8); - } - for index in 0..4 { - self.emit_stack_load("t0", H_BASE + index * 8); - self.emit(format!("sd t0, {}(a1)", index * 8)); - } - self.emit_large_addi("sp", "sp", FRAME as i64); - self.emit("li a0, 0"); - self.emit("ret"); - } - - fn emit_runtime_blake2b_hash_pair(&mut self, enabled: bool) { - self.emit_global("__ckb_hash_pair"); - self.emit_label("__ckb_hash_pair"); - self.emit("# cellscript abi: hash_pair combines two 32-byte Hash inputs with CKB Blake2b-256; a0=left[32], a1=right[32], a2=output[32]"); - if !enabled { - self.emit_fail(CellScriptRuntimeError::SyscallFailed); - return; - } - - const IV: [u64; 8] = [ - 0x6a09e667f3bcc908, - 0xbb67ae8584caa73b, - 0x3c6ef372fe94f82b, - 0xa54ff53a5f1d36f1, - 0x510e527fade682d1, - 0x9b05688c2b3e6c1f, - 0x1f83d9abfb41bd6b, - 0x5be0cd19137e2179, - ]; - const SIGMA: [[usize; 16]; 12] = [ - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], - [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], - [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4], - [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8], - [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13], - [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9], - [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11], - [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10], - [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5], - [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0], - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], - [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], - ]; - - const H_BASE: usize = 0; - const V_BASE: usize = 64; - const M_BASE: usize = 192; - const FRAME: usize = 320; - - let personal0 = u64::from_le_bytes(*b"ckb-defa"); - let personal1 = u64::from_le_bytes(*b"ult-hash"); - let h = [IV[0] ^ 0x01010020, IV[1], IV[2], IV[3], IV[4], IV[5], IV[6] ^ personal0, IV[7] ^ personal1]; - - self.emit_large_addi("sp", "sp", -(FRAME as i64)); - for (index, value) in h.iter().enumerate() { - self.emit_blake2b_store_const(*value, H_BASE + index * 8); - } - for index in 0..4 { - self.emit_blake2b_load_input_word(index, M_BASE + index * 8); - } - for index in 0..4 { - self.emit_blake2b_load_input_word_from("a1", index, M_BASE + (index + 4) * 8); - } - for index in 8..16 { - self.emit_stack_store("zero", M_BASE + index * 8); - } - for index in 0..8 { - self.emit_stack_load("t0", H_BASE + index * 8); - self.emit_stack_store("t0", V_BASE + index * 8); - } - for (index, value) in IV.iter().enumerate() { - self.emit_blake2b_store_const(*value, V_BASE + (index + 8) * 8); - } - self.emit_stack_load("t0", V_BASE + 12 * 8); - self.emit("xori t0, t0, 64"); - self.emit_stack_store("t0", V_BASE + 12 * 8); - self.emit_stack_load("t0", V_BASE + 14 * 8); - self.emit("xori t0, t0, -1"); - self.emit_stack_store("t0", V_BASE + 14 * 8); - - for round in SIGMA { - self.emit_blake2b_g(V_BASE, M_BASE, 0, 4, 8, 12, round[0], round[1]); - self.emit_blake2b_g(V_BASE, M_BASE, 1, 5, 9, 13, round[2], round[3]); - self.emit_blake2b_g(V_BASE, M_BASE, 2, 6, 10, 14, round[4], round[5]); - self.emit_blake2b_g(V_BASE, M_BASE, 3, 7, 11, 15, round[6], round[7]); - self.emit_blake2b_g(V_BASE, M_BASE, 0, 5, 10, 15, round[8], round[9]); - self.emit_blake2b_g(V_BASE, M_BASE, 1, 6, 11, 12, round[10], round[11]); - self.emit_blake2b_g(V_BASE, M_BASE, 2, 7, 8, 13, round[12], round[13]); - self.emit_blake2b_g(V_BASE, M_BASE, 3, 4, 9, 14, round[14], round[15]); - } - - for index in 0..8 { - self.emit_stack_load("t0", H_BASE + index * 8); - self.emit_stack_load("t1", V_BASE + index * 8); - self.emit("xor t0, t0, t1"); - self.emit_stack_load("t1", V_BASE + (index + 8) * 8); - self.emit("xor t0, t0, t1"); - self.emit_stack_store("t0", H_BASE + index * 8); - } - for index in 0..4 { - self.emit_stack_load("t0", H_BASE + index * 8); - self.emit(format!("sd t0, {}(a2)", index * 8)); - } - self.emit_large_addi("sp", "sp", FRAME as i64); - self.emit("li a0, 0"); - self.emit("ret"); - } - - fn emit_blake2b_store_const(&mut self, value: u64, stack_offset: usize) { - self.emit(format!("li t0, 0x{:016x}", value)); - self.emit_stack_store("t0", stack_offset); - } - - fn emit_blake2b_load_input_word(&mut self, word_index: usize, stack_offset: usize) { - self.emit_blake2b_load_input_word_from("a0", word_index, stack_offset); - } - - fn emit_blake2b_load_input_word_from(&mut self, source_reg: &str, word_index: usize, stack_offset: usize) { - self.emit("li t0, 0"); - for byte_index in 0..8 { - let absolute = word_index * 8 + byte_index; - self.emit(format!("lbu t1, {}({})", absolute, source_reg)); - if byte_index > 0 { - self.emit(format!("slli t1, t1, {}", byte_index * 8)); - } - self.emit("or t0, t0, t1"); - } - self.emit_stack_store("t0", stack_offset); - } - - fn emit_blake2b_rotr(&mut self, register: &str, bits: usize) { - self.emit(format!("srli t1, {}, {}", register, bits)); - self.emit(format!("slli {}, {}, {}", register, register, 64 - bits)); - self.emit(format!("or {}, {}, t1", register, register)); - } - - #[allow(clippy::too_many_arguments)] - fn emit_blake2b_g(&mut self, v_base: usize, m_base: usize, a: usize, b: usize, c: usize, d: usize, mx: usize, my: usize) { - let va = v_base + a * 8; - let vb = v_base + b * 8; - let vc = v_base + c * 8; - let vd = v_base + d * 8; - let vmx = m_base + mx * 8; - let vmy = m_base + my * 8; - - self.emit_stack_load("t0", va); - self.emit_stack_load("t1", vb); - self.emit("add t0, t0, t1"); - self.emit_stack_load("t1", vmx); - self.emit("add t0, t0, t1"); - self.emit_stack_store("t0", va); - self.emit_stack_load("t0", vd); - self.emit_stack_load("t1", va); - self.emit("xor t0, t0, t1"); - self.emit_blake2b_rotr("t0", 32); - self.emit_stack_store("t0", vd); - - self.emit_stack_load("t0", vc); - self.emit_stack_load("t1", vd); - self.emit("add t0, t0, t1"); - self.emit_stack_store("t0", vc); - self.emit_stack_load("t0", vb); - self.emit_stack_load("t1", vc); - self.emit("xor t0, t0, t1"); - self.emit_blake2b_rotr("t0", 24); - self.emit_stack_store("t0", vb); - - self.emit_stack_load("t0", va); - self.emit_stack_load("t1", vb); - self.emit("add t0, t0, t1"); - self.emit_stack_load("t1", vmy); - self.emit("add t0, t0, t1"); - self.emit_stack_store("t0", va); - self.emit_stack_load("t0", vd); - self.emit_stack_load("t1", va); - self.emit("xor t0, t0, t1"); - self.emit_blake2b_rotr("t0", 16); - self.emit_stack_store("t0", vd); - - self.emit_stack_load("t0", vc); - self.emit_stack_load("t1", vd); - self.emit("add t0, t0, t1"); - self.emit_stack_store("t0", vc); - self.emit_stack_load("t0", vb); - self.emit_stack_load("t1", vc); - self.emit("xor t0, t0, t1"); - self.emit_blake2b_rotr("t0", 63); - self.emit_stack_store("t0", vb); - } - - fn emit_runtime_witness_size_helper(&mut self, enabled: bool) { - const SIZE_OFFSET: usize = 8; - const RA_OFFSET: usize = 24; - const FRAME_SIZE: usize = 32; - - self.emit_global("__ckb_witness_size"); - self.emit_label("__ckb_witness_size"); - self.emit("# cellscript abi: witness byte size via LOAD_WITNESS"); - self.emit("# cellscript abi: args a0=SourceView; returns a0=size, a1=0 on success, a1=error_code on failure"); - if !enabled { - self.emit(format!("li a1, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - let invalid = self.fresh_label("witness_size_source_invalid"); - let failed = self.fresh_label("witness_size_load_failed"); - let status_ok = self.fresh_label("witness_size_status_ok"); - let done = self.fresh_label("witness_size_done"); - let abi = self.runtime_abi(); - - self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); - self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); - self.emit_decode_source_view_to_t1_t2(&invalid); - self.emit("li t0, 0"); - self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); - self.emit("li a0, 0"); - self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit("addi a3, t1, 0"); - self.emit("addi a4, t2, 0"); - self.emit(format!("li a7, {}", abi.load_witness)); - self.emit("ecall"); - self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); - self.emit("sub t1, a0, t0"); - self.emit(format!("beqz t1, {}", status_ok)); - self.emit(format!("beqz a0, {}", status_ok)); - self.emit(format!("j {}", failed)); - - self.emit_label(&status_ok); - self.emit(format!("j {}", done)); - - self.emit_label(&invalid); - self.emit(format!("li a1, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - - self.emit_label(&failed); - self.emit(format!("li a1, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - - self.emit_label(&done); - self.emit(format!("ld a0, {}(sp)", SIZE_OFFSET)); - self.emit("li a1, 0"); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - } - - fn emit_runtime_require_witness_size_at_least_helper(&mut self, enabled: bool) { - const SIZE_OFFSET: usize = 8; - const MIN_SIZE_OFFSET: usize = 16; - const RA_OFFSET: usize = 24; - const FRAME_SIZE: usize = 32; - - self.emit_global("__ckb_require_witness_size_at_least"); - self.emit_label("__ckb_require_witness_size_at_least"); - self.emit("# cellscript abi: require witness size >= min_size"); - self.emit("# cellscript abi: args a0=SourceView, a1=min_size; returns a0=status"); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - let invalid = self.fresh_label("witness_req_size_source_invalid"); - let failed = self.fresh_label("witness_req_size_load_failed"); - let too_small = self.fresh_label("witness_req_size_too_small"); - let status_ok = self.fresh_label("witness_req_size_status_ok"); - let done = self.fresh_label("witness_req_size_done"); - let abi = self.runtime_abi(); - - self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); - self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); - self.emit("# cellscript abi: preserve min_size before LOAD_WITNESS size probe"); - self.emit(format!("sd a1, {}(sp)", MIN_SIZE_OFFSET)); - self.emit_decode_source_view_to_t1_t2(&invalid); - self.emit("li t0, 0"); - self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); - self.emit("li a0, 0"); - self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit("addi a3, t1, 0"); - self.emit("addi a4, t2, 0"); - self.emit(format!("li a7, {}", abi.load_witness)); - self.emit("ecall"); - self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); - self.emit("sub t1, a0, t0"); - self.emit(format!("beqz t1, {}", status_ok)); - self.emit(format!("beqz a0, {}", status_ok)); - self.emit(format!("j {}", failed)); - - self.emit_label(&status_ok); - self.emit(format!("ld t0, {}(sp)", SIZE_OFFSET)); - self.emit(format!("ld t1, {}(sp)", MIN_SIZE_OFFSET)); - self.emit("sltu t2, t0, t1"); - self.emit(format!("beqz t2, {}", done)); - - self.emit_label(&too_small); - self.emit(format!("li a0, {}", CellScriptRuntimeError::WitnessMalformed.code())); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - - self.emit_label(&done); - self.emit("li a0, 0"); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - } - - fn emit_runtime_witness_raw_helper(&mut self, enabled: bool) { - const OUTPTR_OFFSET: usize = 8; - const SIZE_OFFSET: usize = 16; - const RA_OFFSET: usize = 24; - const FRAME_SIZE: usize = 32; - - self.emit_global("__ckb_witness_raw"); - self.emit_label("__ckb_witness_raw"); - self.emit("# cellscript abi: load raw witness bytes (first 32) into caller buffer"); - self.emit("# cellscript abi: args a0=SourceView, a1=out32_ptr; returns a0=status"); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - let invalid = self.fresh_label("witness_raw_source_invalid"); - let failed = self.fresh_label("witness_raw_load_failed"); - let status_ok = self.fresh_label("witness_raw_status_ok"); - let done = self.fresh_label("witness_raw_done"); - let abi = self.runtime_abi(); - - self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); - self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); - self.emit(format!("sd a1, {}(sp)", OUTPTR_OFFSET)); - self.emit("# cellscript abi: zero-fill caller witness Hash buffer before raw prefix load"); - self.emit(format!("ld t0, {}(sp)", OUTPTR_OFFSET)); - self.emit("li t1, 0"); - let zero_loop = self.fresh_label("witness_raw_zero_loop"); - let zero_done = self.fresh_label("witness_raw_zero_done"); - self.emit_label(&zero_loop); - self.emit("li t2, 32"); - self.emit("sltu t3, t1, t2"); - self.emit(format!("beqz t3, {}", zero_done)); - self.emit("add t4, t0, t1"); - self.emit("sb zero, 0(t4)"); - self.emit("addi t1, t1, 1"); - self.emit(format!("j {}", zero_loop)); - self.emit_label(&zero_done); - self.emit_decode_source_view_to_t1_t2(&invalid); - self.emit("# cellscript abi: LOAD_WITNESS raw first 32 bytes into caller buffer"); - self.emit("li t0, 32"); - self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); - self.emit(format!("ld a0, {}(sp)", OUTPTR_OFFSET)); - self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit("addi a3, t1, 0"); - self.emit("addi a4, t2, 0"); - self.emit(format!("li a7, {}", abi.load_witness)); - self.emit("ecall"); - self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); - self.emit("sub t1, a0, t0"); - self.emit(format!("beqz t1, {}", status_ok)); - self.emit(format!("beqz a0, {}", status_ok)); - self.emit(format!("j {}", failed)); - - self.emit_label(&status_ok); - self.emit(format!("j {}", done)); - - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - - self.emit_label(&done); - self.emit("li a0, 0"); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - } - - fn emit_runtime_witness_args_field_helper(&mut self, symbol: &str, detail: &str, field_index: u64, enabled: bool) { - const OUTPTR_OFFSET: usize = 0; - const SIZE_OFFSET: usize = 8; - const FULL_BUFFER_OFFSET: usize = 16; - const FULL_BUFFER_SIZE: usize = 512; - const FIELD_BUF_OFFSET: usize = FULL_BUFFER_OFFSET + FULL_BUFFER_SIZE; - const FIELD_BUF_SIZE: usize = 128; - const HEADER_READ_OFFSET: usize = FIELD_BUF_OFFSET + FIELD_BUF_SIZE; - const HEADER_READ_SIZE: usize = 24; - const RA_OFFSET: usize = HEADER_READ_OFFSET + HEADER_READ_SIZE; - const FRAME_SIZE: usize = RA_OFFSET + 8; - - self.emit_global(symbol); - self.emit_label(symbol); - self.emit(format!("# cellscript abi: extract WitnessArgs field {} ({})", field_index, detail)); - self.emit("# cellscript abi: args a0=SourceView, a1=out32_ptr; returns a0=status"); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - let invalid = self.fresh_label("witness_field_source_invalid"); - let failed = self.fresh_label("witness_field_load_failed"); - let malformed = self.fresh_label("witness_field_malformed"); - let truncated = self.fresh_label("witness_field_truncated"); - let field_absent = self.fresh_label("witness_field_absent"); - let ok = self.fresh_label("witness_field_ok"); - let done = self.fresh_label("witness_field_done"); - let abi = self.runtime_abi(); - - self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); - self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); - self.emit(format!("sd a1, {}(sp)", OUTPTR_OFFSET)); - self.emit("# cellscript abi: zero-fill extracted WitnessArgs Hash buffer before parsing"); - self.emit("li t0, 0"); - let zero_field_loop = self.fresh_label("witness_field_prezero_loop"); - let zero_field_done = self.fresh_label("witness_field_prezero_done"); - self.emit_label(&zero_field_loop); - self.emit("li t1, 32"); - self.emit("sltu t2, t0, t1"); - self.emit(format!("beqz t2, {}", zero_field_done)); - self.emit(format!("addi t3, sp, {}", FIELD_BUF_OFFSET)); - self.emit("add t3, t3, t0"); - self.emit("sb zero, 0(t3)"); - self.emit("addi t0, t0, 1"); - self.emit(format!("j {}", zero_field_loop)); - self.emit_label(&zero_field_done); - self.emit_decode_source_view_to_t1_t2(&invalid); - - // Load full witness - self.emit(format!("li t0, {}", FULL_BUFFER_SIZE)); - self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", FULL_BUFFER_OFFSET)); - self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit("addi a3, t1, 0"); - self.emit("addi a4, t2, 0"); - self.emit(format!("li a7, {}", abi.load_witness)); - self.emit("ecall"); - self.emit(format!("beqz a0, {}", ok)); - self.emit(format!("j {}", failed)); - - self.emit_label(&ok); - self.emit(format!("ld t0, {}(sp)", SIZE_OFFSET)); - - // Parse Molecule WitnessArgs table header (minimum 4 + 3*4 = 16 bytes) - // Table encoding: total_size (4 bytes) + offsets[0..N-1] (4 bytes each) - // field_count = (offset0 / 4) - 1 - self.emit("li t1, 16"); - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - - self.emit(format!("addi t3, sp, {}", FULL_BUFFER_OFFSET)); - self.emit("# cellscript abi: WitnessArgs total_size must match loaded witness size"); - self.emit_u32_le_from_base_to("t4", "t3", 0, "t5"); - self.emit("sub t2, t4, t0"); - self.emit(format!("bnez t2, {}", malformed)); - - // For the current 3-field WitnessArgs table, offset0 must be 16. - self.emit_u32_le_from_base_to("t4", "t3", 4, "t5"); - self.emit("li t5, 16"); - self.emit("sub t2, t4, t5"); - self.emit(format!("bnez t2, {}", malformed)); - - // Read field offsets from header (offsets at bytes 4, 8, 12) - self.emit_u32_le_from_base_to("t4", "t3", 4, "t2"); - self.emit(format!("sd t4, {}(sp)", HEADER_READ_OFFSET)); - self.emit_u32_le_from_base_to("t5", "t3", 8, "t2"); - self.emit(format!("sd t5, {}(sp)", HEADER_READ_OFFSET + 8)); - self.emit_u32_le_from_base_to("t6", "t3", 12, "t2"); - self.emit(format!("sd t6, {}(sp)", HEADER_READ_OFFSET + 16)); - - self.emit("# cellscript abi: validate all WitnessArgs field offsets are monotonic and in bounds"); - self.emit(format!("ld t4, {}(sp)", HEADER_READ_OFFSET)); - self.emit(format!("ld t5, {}(sp)", HEADER_READ_OFFSET + 8)); - self.emit("sltu t2, t5, t4"); - self.emit(format!("bnez t2, {}", malformed)); - self.emit(format!("ld t4, {}(sp)", HEADER_READ_OFFSET + 8)); - self.emit(format!("ld t5, {}(sp)", HEADER_READ_OFFSET + 16)); - self.emit("sltu t2, t5, t4"); - self.emit(format!("bnez t2, {}", malformed)); - self.emit("sltu t2, t0, t5"); - self.emit(format!("bnez t2, {}", truncated)); - - // Select field offset and next field offset - let field_offsets_offset = HEADER_READ_OFFSET + (field_index * 8) as usize; - let next_offsets_offset = HEADER_READ_OFFSET + ((field_index + 1) * 8) as usize; - self.emit(format!("ld t4, {}(sp)", field_offsets_offset)); - if field_index < 2 { - self.emit(format!("ld t5, {}(sp)", next_offsets_offset)); - } else { - self.emit("addi t5, t0, 0".to_string()); - } - - // Check field offset bounds: field_offset <= next_offset <= total_size. - // BytesOpt None is an empty span, so adjacent offsets may be equal. - self.emit("sltu t2, t5, t4"); - self.emit(format!("bnez t2, {}", malformed)); - self.emit("sltu t2, t0, t5"); - self.emit(format!("bnez t2, {}", truncated)); - - // Calculate BytesOpt field span. Empty span is None. - self.emit("sub t2, t5, t4"); - self.emit(format!("beqz t2, {}", field_absent)); - self.emit("li t6, 4"); - self.emit("sltu t3, t2, t6"); - self.emit(format!("bnez t3, {}", malformed)); - self.emit("addi t2, t2, -4"); - - // Read Some(Bytes) length at field_offset and require exact Bytes size. - self.emit(format!("addi t3, sp, {}", FULL_BUFFER_OFFSET)); - self.emit("add t6, t3, t4"); - self.emit_u32_le_from_base_to("t1", "t6", 0, "t3"); - self.emit("sub t3, t2, t1"); - self.emit(format!("bnez t3, {}", malformed)); - - // Copy field bytes to output buffer (max 32 bytes for Hash) - self.emit("li t3, 32"); - self.emit("sltu t5, t3, t1"); - let copy_count_ready = self.fresh_label("witness_field_copy_count_ready"); - self.emit(format!("beqz t5, {}", copy_count_ready)); - self.emit("addi t1, t3, 0"); - self.emit_label(©_count_ready); - self.emit(format!("addi t2, sp, {}", FIELD_BUF_OFFSET)); - self.emit("addi t4, t6, 4"); - // Copy loop - self.emit("li t3, 0"); - let copy_loop = self.fresh_label("witness_field_copy_loop"); - let copy_done = self.fresh_label("witness_field_copy_done"); - self.emit_label(©_loop); - self.emit("sltu t5, t3, t1"); - self.emit(format!("beqz t5, {}", copy_done)); - self.emit("add t5, t4, t3"); - self.emit("lbu t6, 0(t5)"); - self.emit("add t5, t2, t3"); - self.emit("sb t6, 0(t5)"); - self.emit("addi t3, t3, 1"); - self.emit(format!("j {}", copy_loop)); - self.emit_label(©_done); - self.emit(format!("j {}", done)); - - self.emit_label(&field_absent); - self.emit("# cellscript abi: BytesOpt None leaves pre-zeroed Hash buffer"); - self.emit(format!("j {}", done)); - - self.emit_label(&malformed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::WitnessMalformed.code())); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - - self.emit_label(&truncated); - self.emit(format!("li a0, {}", CellScriptRuntimeError::WitnessFieldTruncated.code())); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - - self.emit_label(&done); - // Copy 32 bytes from FIELD_BUF_OFFSET to caller's buffer (outptr) - self.emit(format!("ld t0, {}(sp)", OUTPTR_OFFSET)); - self.emit("li t1, 0"); - let copy_out = self.fresh_label("witness_field_copy_out_loop"); - let copy_out_done = self.fresh_label("witness_field_copy_out_done"); - self.emit_label(©_out); - self.emit("li t2, 32"); - self.emit("sltu t3, t1, t2"); - self.emit(format!("beqz t3, {}", copy_out_done)); - self.emit(format!("addi t2, sp, {}", FIELD_BUF_OFFSET)); - self.emit("add t2, t2, t1"); - self.emit("lbu t3, 0(t2)"); - self.emit("add t4, t0, t1"); - self.emit("sb t3, 0(t4)"); - self.emit("addi t1, t1, 1"); - self.emit(format!("j {}", copy_out)); - self.emit_label(©_out_done); - self.emit("li a0, 0"); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - } - - fn emit_runtime_current_script_hash_helper(&mut self, enabled: bool) { - self.emit_global("__ckb_current_script_hash"); - self.emit_label("__ckb_current_script_hash"); - self.emit("# cellscript abi: current script Hash via LOAD_SCRIPT_HASH"); - self.emit("# cellscript abi: args a0=out32_ptr, a1=size_ptr; returns a0=status"); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - let failed = self.fresh_label("current_script_hash_load_failed"); - let malformed = self.fresh_label("current_script_hash_malformed"); - let done = self.fresh_label("current_script_hash_done"); - let abi = self.runtime_abi(); - self.emit("addi sp, sp, -24"); - self.emit("sd ra, 16(sp)"); - self.emit("sd a1, 8(sp)"); - self.emit("li t0, 32"); - self.emit("sd t0, 0(a1)"); - self.emit("li a2, 0"); - self.emit(format!("li a7, {}", abi.load_script_hash)); - self.emit("ecall"); - self.emit(format!("bnez a0, {}", failed)); - self.emit("ld t6, 8(sp)"); - self.emit("ld t0, 0(t6)"); - self.emit("li t1, 32"); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&malformed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); - self.emit_label(&done); - self.emit("ld ra, 16(sp)"); - self.emit("addi sp, sp, 24"); - self.emit("ret"); - } - - fn emit_runtime_source_view_helper(&mut self, symbol: &str, source_view: u64, detail: &str, enabled: bool) { - self.emit_global(symbol); - self.emit_label(symbol); - self.emit(format!("# cellscript abi: CKB SourceView helper ({})", detail)); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("addi a1, a0, 0"); - self.emit("ret"); - return; - } - self.emit(format!("li t0, {}", source_view)); - self.emit(format!("li t1, {}", CKB_SOURCE_VIEW_SHIFT)); - self.emit("mul t0, t0, t1"); - self.emit("add a0, a0, t0"); - self.emit("li a1, 0"); - self.emit("ret"); - } - - fn emit_runtime_ckb_since_epoch_helper(&mut self, symbol: &str, relative: bool, detail: &str, enabled: bool) { - self.emit_global(symbol); - self.emit_label(symbol); - self.emit(format!("# cellscript abi: {}", detail)); - self.emit("# cellscript abi: args a0=number(<2^24), a1=index(<2^16), a2=length(<2^16); requires length>0 and index 53u64, - ScriptHashFieldRead::Args32 => 128u64, - }; - let payload_offset = match read { - ScriptHashFieldRead::CodeHash => SCRIPT_BUFFER_OFFSET + 16, - ScriptHashFieldRead::Args32 => SCRIPT_BUFFER_OFFSET + 53, - }; - let invalid = self.fresh_label("script_ref_hash_source_invalid"); - let failed = self.fresh_label("script_ref_hash_load_failed"); - let loaded = self.fresh_label("script_ref_hash_loaded"); - let malformed = self.fresh_label("script_ref_hash_malformed"); - let args_mismatch = self.fresh_label("script_ref_hash_args_mismatch"); - let copy_loop = self.fresh_label("script_ref_hash_copy"); - let copy_done = self.fresh_label("script_ref_hash_copy_done"); - let done = self.fresh_label("script_ref_hash_done"); - let abi = self.runtime_abi(); - - self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); - self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); - self.emit(format!("sd a1, {}(sp)", OUT_PTR_OFFSET)); - self.emit(format!("sd a2, {}(sp)", SIZE_PTR_OFFSET)); - self.emit_decode_source_view_to_t1_t2(&invalid); - self.emit(format!("li t0, {}", requested_size)); - self.emit(format!("sd t0, {}(sp)", SCRIPT_SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); - self.emit(format!("addi a1, sp, {}", SCRIPT_SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit("addi a3, t1, 0"); - self.emit("addi a4, t2, 0"); - self.emit(format!("li a5, {}", field_id)); - self.emit(format!("li a7, {}", abi.load_cell_by_field)); - self.emit("ecall"); - self.emit(format!("beqz a0, {}", loaded)); - if matches!(read, ScriptHashFieldRead::CodeHash) { - self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); - self.emit("sub t1, a0, t0"); - self.emit(format!("beqz t1, {}", loaded)); - } - self.emit(format!("j {}", failed)); - - self.emit_label(&loaded); - self.emit(format!("ld t3, {}(sp)", SCRIPT_SIZE_OFFSET)); - self.emit("li t1, 49"); - self.emit("sltu t2, t3, t1"); - self.emit(format!("bnez t2, {}", malformed)); - self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET); - self.emit("li t1, 53"); - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - for (offset, expected) in [(4usize, 16u64), (8, 48), (12, 49)] { - self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + offset); - self.emit(format!("li t1, {}", expected)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - } - if matches!(read, ScriptHashFieldRead::Args32) { - self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + 49); - self.emit("li t1, 32"); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", args_mismatch)); - self.emit("li t1, 85"); - self.emit("sub t2, t3, t1"); - self.emit(format!("bnez t2, {}", malformed)); - } - - self.emit("li t1, 0"); - self.emit_label(©_loop); - self.emit("li t2, 32"); - self.emit("sltu t3, t1, t2"); - self.emit(format!("beqz t3, {}", copy_done)); - self.emit(format!("addi t6, sp, {}", payload_offset)); - self.emit("add t6, t6, t1"); - self.emit("lbu t5, 0(t6)"); - self.emit(format!("ld t6, {}(sp)", OUT_PTR_OFFSET)); - self.emit("add t6, t6, t1"); - self.emit("sb t5, 0(t6)"); - self.emit("addi t1, t1, 1"); - self.emit(format!("j {}", copy_loop)); - self.emit_label(©_done); - self.emit(format!("ld t6, {}(sp)", SIZE_PTR_OFFSET)); - self.emit("li t0, 32"); - self.emit("sd t0, 0(t6)"); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&args_mismatch); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptArgsMismatch.code())); - self.emit(format!("j {}", done)); - self.emit_label(&malformed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); - self.emit_label(&done); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - } - - fn emit_runtime_cell_script_scalar_field_helper( - &mut self, - symbol: &str, - detail: &str, - field_id: u64, - read: ScriptScalarFieldRead, - enabled: bool, - ) { - self.emit_global(symbol); - self.emit_label(symbol); - self.emit(format!("# cellscript abi: CKB SourceView read-only ScriptRef scalar field ({})", detail)); - self.emit("# cellscript abi: args a0=SourceView; returns a0=value, a1=status"); - if !enabled { - self.emit("li a0, 0"); - self.emit(format!("li a1, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - - const SCRIPT_SIZE_OFFSET: usize = 8; - const SCRIPT_BUFFER_OFFSET: usize = 16; - const RA_OFFSET: usize = 152; - const FRAME_SIZE: usize = 160; - let requested_size = match read { - ScriptScalarFieldRead::HashType => 53u64, - ScriptScalarFieldRead::ArgsEmpty => 128u64, - }; - let invalid = self.fresh_label("script_ref_scalar_source_invalid"); - let failed = self.fresh_label("script_ref_scalar_load_failed"); - let loaded = self.fresh_label("script_ref_scalar_loaded"); - let malformed = self.fresh_label("script_ref_scalar_malformed"); - let nonempty = self.fresh_label("script_ref_scalar_nonempty"); - let done = self.fresh_label("script_ref_scalar_done"); - let abi = self.runtime_abi(); - - self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); - self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); - self.emit_decode_source_view_to_t1_t2(&invalid); - self.emit(format!("li t0, {}", requested_size)); - self.emit(format!("sd t0, {}(sp)", SCRIPT_SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); - self.emit(format!("addi a1, sp, {}", SCRIPT_SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit("addi a3, t1, 0"); - self.emit("addi a4, t2, 0"); - self.emit(format!("li a5, {}", field_id)); - self.emit(format!("li a7, {}", abi.load_cell_by_field)); - self.emit("ecall"); - self.emit(format!("beqz a0, {}", loaded)); - self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); - self.emit("sub t1, a0, t0"); - self.emit(format!("beqz t1, {}", loaded)); - self.emit(format!("j {}", failed)); - - self.emit_label(&loaded); - self.emit(format!("ld t3, {}(sp)", SCRIPT_SIZE_OFFSET)); - self.emit("li t1, 49"); - self.emit("sltu t2, t3, t1"); - self.emit(format!("bnez t2, {}", malformed)); - self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET); - if matches!(read, ScriptScalarFieldRead::HashType) { - self.emit("li t1, 53"); - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - } else { - self.emit("sub t2, t0, t3"); - self.emit(format!("bnez t2, {}", malformed)); - } - for (offset, expected) in [(4usize, 16u64), (8, 48), (12, 49)] { - self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + offset); - self.emit(format!("li t1, {}", expected)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - } - match read { - ScriptScalarFieldRead::HashType => { - self.emit(format!("lbu a0, {}(sp)", SCRIPT_BUFFER_OFFSET + 48)); - self.emit("li a1, 0"); - self.emit(format!("j {}", done)); - } - ScriptScalarFieldRead::ArgsEmpty => { - self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + 49); - self.emit(format!("bnez t0, {}", nonempty)); - self.emit("li t1, 53"); - self.emit("sub t2, t3, t1"); - self.emit(format!("bnez t2, {}", malformed)); - self.emit("li a0, 1"); - self.emit("li a1, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&nonempty); - self.emit("li a0, 0"); - self.emit("li a1, 0"); - self.emit(format!("j {}", done)); - } - } - - self.emit_label(&invalid); - self.emit("li a0, 0"); - self.emit(format!("li a1, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit("li a0, 0"); - self.emit(format!("li a1, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&malformed); - self.emit("li a0, 0"); - self.emit(format!("li a1, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); - self.emit_label(&done); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - } - - fn emit_runtime_cell_script_args_empty_requirement_helper(&mut self, symbol: &str, detail: &str, field_id: u64, enabled: bool) { - self.emit_global(symbol); - self.emit_label(symbol); - self.emit(format!("# cellscript abi: CKB SourceView Script empty-args requirement ({})", detail)); - self.emit("# cellscript abi: args a0=SourceView; expects Molecule Script args Bytes length == 0"); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - - const SCRIPT_SIZE_OFFSET: usize = 8; - const SCRIPT_BUFFER_OFFSET: usize = 16; - const EMPTY_SCRIPT_SIZE: u64 = 53; - - let invalid = self.fresh_label("script_args_source_invalid"); - let failed = self.fresh_label("script_args_load_failed"); - let nonempty = self.fresh_label("script_args_nonempty"); - let malformed = self.fresh_label("script_args_malformed"); - let done = self.fresh_label("script_args_done"); - let abi = self.runtime_abi(); - - self.emit("addi sp, sp, -160"); - self.emit("sd ra, 152(sp)"); - self.emit_decode_source_view_to_t1_t2(&invalid); - self.emit("li t0, 128"); - self.emit(format!("sd t0, {}(sp)", SCRIPT_SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); - self.emit(format!("addi a1, sp, {}", SCRIPT_SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit("addi a3, t1, 0"); - self.emit("addi a4, t2, 0"); - self.emit(format!("li a5, {}", field_id)); - self.emit(format!("li a7, {}", abi.load_cell_by_field)); - self.emit("ecall"); - self.emit(format!("bnez a0, {}", failed)); - - self.emit(format!("ld t0, {}(sp)", SCRIPT_SIZE_OFFSET)); - self.emit(format!("li t1, {}", EMPTY_SCRIPT_SIZE)); - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", nonempty)); - - for (offset, expected) in [(0usize, EMPTY_SCRIPT_SIZE), (4, 16), (8, 48), (12, 49), (49, 0)] { - self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + offset); - self.emit(format!("li t1, {}", expected)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - } - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&nonempty); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptArgsMismatch.code())); - self.emit(format!("j {}", done)); - self.emit_label(&malformed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); - self.emit_label(&done); - self.emit("ld ra, 152(sp)"); - self.emit("addi sp, sp, 160"); - self.emit("ret"); - } - - fn emit_runtime_cell_script_args_exact_requirement_helper(&mut self, symbol: &str, detail: &str, field_id: u64, enabled: bool) { - self.emit_global(symbol); - self.emit_label(symbol); - self.emit(format!("# cellscript abi: CKB SourceView Script arbitrary exact args requirement ({})", detail)); - self.emit("# cellscript abi: args a0=SourceView, a1=expected_args_ptr, a2=expected_args_len"); - self.emit("# cellscript abi: validates Molecule packed::Script args Bytes exactly, not only 32-byte hash args"); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - - const SCRIPT_SIZE_OFFSET: usize = 8; - const SCRIPT_BUFFER_OFFSET: usize = 16; - const EXPECTED_PTR_OFFSET: usize = 72; - const EXPECTED_LEN_OFFSET: usize = 80; - const ARGS_OFFSET_OFFSET: usize = 88; - const CHUNK_LEN_OFFSET: usize = 96; - const SOURCE_INDEX_OFFSET: usize = 104; - const SOURCE_KIND_OFFSET: usize = 112; - const RA_OFFSET: usize = 120; - const FRAME_SIZE: usize = 128; - const SCRIPT_PREFIX_SIZE: u64 = 53; - const CHUNK_SIZE: u64 = 32; - - let invalid = self.fresh_label("script_args_exact_source_invalid"); - let bad_expected = self.fresh_label("script_args_exact_expected_invalid"); - let prefix_loaded = self.fresh_label("script_args_exact_prefix_loaded"); - let load_failed = self.fresh_label("script_args_exact_load_failed"); - let malformed = self.fresh_label("script_args_exact_malformed"); - let mismatch = self.fresh_label("script_args_exact_mismatch"); - let chunk_loop = self.fresh_label("script_args_exact_chunk_loop"); - let chunk_tail = self.fresh_label("script_args_exact_chunk_tail"); - let chunk_loaded = self.fresh_label("script_args_exact_chunk_loaded"); - let success = self.fresh_label("script_args_exact_success"); - let done = self.fresh_label("script_args_exact_done"); - let abi = self.runtime_abi(); - - self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); - self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); - self.emit(format!("sd a1, {}(sp)", EXPECTED_PTR_OFFSET)); - self.emit(format!("sd a2, {}(sp)", EXPECTED_LEN_OFFSET)); - self.emit(format!("beqz a2, {}", bad_expected)); - self.emit(format!("beqz a1, {}", bad_expected)); - - self.emit_decode_source_view_to_t1_t2(&invalid); - self.emit(format!("sd t1, {}(sp)", SOURCE_INDEX_OFFSET)); - self.emit(format!("sd t2, {}(sp)", SOURCE_KIND_OFFSET)); - - self.emit(format!("li t0, {}", SCRIPT_PREFIX_SIZE)); - self.emit(format!("sd t0, {}(sp)", SCRIPT_SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); - self.emit(format!("addi a1, sp, {}", SCRIPT_SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit(format!("ld a3, {}(sp)", SOURCE_INDEX_OFFSET)); - self.emit(format!("ld a4, {}(sp)", SOURCE_KIND_OFFSET)); - self.emit(format!("li a5, {}", field_id)); - self.emit(format!("li a7, {}", abi.load_cell_by_field)); - self.emit("ecall"); - self.emit(format!("beqz a0, {}", prefix_loaded)); - self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); - self.emit("sub t1, a0, t0"); - self.emit(format!("beqz t1, {}", prefix_loaded)); - self.emit(format!("j {}", load_failed)); - - self.emit_label(&prefix_loaded); - self.emit(format!("ld t3, {}(sp)", SCRIPT_SIZE_OFFSET)); - self.emit(format!("li t1, {}", SCRIPT_PREFIX_SIZE)); - self.emit("sltu t2, t3, t1"); - self.emit(format!("bnez t2, {}", malformed)); - self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET); - self.emit(format!("ld t1, {}(sp)", EXPECTED_LEN_OFFSET)); - self.emit(format!("li t2, {}", SCRIPT_PREFIX_SIZE)); - self.emit("add t1, t1, t2"); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - for (offset, expected) in [(4usize, 16u64), (8, 48), (12, 49)] { - self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + offset); - self.emit(format!("li t1, {}", expected)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - } - self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + 49); - self.emit(format!("ld t1, {}(sp)", EXPECTED_LEN_OFFSET)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", mismatch)); - self.emit(format!("sd zero, {}(sp)", ARGS_OFFSET_OFFSET)); - - self.emit_label(&chunk_loop); - self.emit(format!("ld t0, {}(sp)", ARGS_OFFSET_OFFSET)); - self.emit(format!("ld t1, {}(sp)", EXPECTED_LEN_OFFSET)); - self.emit("sub t2, t1, t0"); - self.emit(format!("beqz t2, {}", success)); - self.emit(format!("li t3, {}", CHUNK_SIZE)); - self.emit("sltu t4, t2, t3"); - self.emit(format!("bnez t4, {}", chunk_tail)); - self.emit(format!("li t2, {}", CHUNK_SIZE)); - self.emit_label(&chunk_tail); - self.emit(format!("sd t2, {}(sp)", SCRIPT_SIZE_OFFSET)); - self.emit(format!("sd t2, {}(sp)", CHUNK_LEN_OFFSET)); - self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); - self.emit(format!("addi a1, sp, {}", SCRIPT_SIZE_OFFSET)); - self.emit(format!("li a2, {}", SCRIPT_PREFIX_SIZE)); - self.emit("add a2, a2, t0"); - self.emit(format!("ld a3, {}(sp)", SOURCE_INDEX_OFFSET)); - self.emit(format!("ld a4, {}(sp)", SOURCE_KIND_OFFSET)); - self.emit(format!("li a5, {}", field_id)); - self.emit(format!("li a7, {}", abi.load_cell_by_field)); - self.emit("ecall"); - self.emit(format!("beqz a0, {}", chunk_loaded)); - self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); - self.emit("sub t1, a0, t0"); - self.emit(format!("beqz t1, {}", chunk_loaded)); - self.emit(format!("j {}", load_failed)); - self.emit_label(&chunk_loaded); - self.emit(format!("ld t2, {}(sp)", CHUNK_LEN_OFFSET)); - self.emit(format!("ld t0, {}(sp)", ARGS_OFFSET_OFFSET)); - self.emit(format!("ld t1, {}(sp)", EXPECTED_PTR_OFFSET)); - self.emit("add a1, t1, t0"); - self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); - self.emit("addi a2, t2, 0"); - self.emit("call __cellscript_memcmp_fixed"); - self.emit(format!("bnez a0, {}", mismatch)); - self.emit(format!("ld t0, {}(sp)", ARGS_OFFSET_OFFSET)); - self.emit(format!("ld t2, {}(sp)", CHUNK_LEN_OFFSET)); - self.emit("add t0, t0, t2"); - self.emit(format!("sd t0, {}(sp)", ARGS_OFFSET_OFFSET)); - self.emit(format!("j {}", chunk_loop)); - - self.emit_label(&success); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit(format!("j {}", done)); - self.emit_label(&bad_expected); - self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); - self.emit(format!("j {}", done)); - self.emit_label(&load_failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&mismatch); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptArgsMismatch.code())); - self.emit(format!("j {}", done)); - self.emit_label(&malformed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); - self.emit_label(&done); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - } - - fn emit_runtime_current_script_args_empty_requirement_helper(&mut self, enabled: bool) { - self.emit_global("__ckb_require_current_script_args_empty"); - self.emit_label("__ckb_require_current_script_args_empty"); - self.emit("# cellscript abi: current-script empty-args requirement via LOAD_SCRIPT plus output lock scan"); - self.emit("# cellscript abi: expects current Script args empty and same-code/hash-type Output locks args empty"); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - - const CURRENT_SIZE_OFFSET: usize = 8; - const CURRENT_BUFFER_OFFSET: usize = 16; - const OUTPUT_INDEX_OFFSET: usize = 144; - const OUTPUT_SIZE_OFFSET: usize = 152; - const OUTPUT_BUFFER_OFFSET: usize = 160; - const OUTPUT_TRUNCATED_OFFSET: usize = 288; - const EMPTY_SCRIPT_SIZE: u64 = 53; - const FRAME_SIZE: usize = 320; - const RA_OFFSET: usize = 312; - - let failed = self.fresh_label("current_script_args_load_failed"); - let current_loaded = self.fresh_label("current_script_args_loaded"); - let nonempty = self.fresh_label("current_script_args_nonempty"); - let malformed = self.fresh_label("current_script_args_malformed"); - let output_loop = self.fresh_label("current_script_args_output_loop"); - let output_loaded = self.fresh_label("current_script_args_output_loaded"); - let output_prefix_loaded = self.fresh_label("current_script_args_output_prefix_loaded"); - let output_advance = self.fresh_label("current_script_args_output_advance"); - let output_done = self.fresh_label("current_script_args_output_done"); - let output_same_hash = self.fresh_label("current_script_args_output_same_hash"); - let output_same_script = self.fresh_label("current_script_args_output_same_script"); - let output_failed = self.fresh_label("current_script_args_output_failed"); - let done = self.fresh_label("current_script_args_done"); - let abi = self.runtime_abi(); - - self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); - self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); - self.emit("li t0, 128"); - self.emit(format!("sd t0, {}(sp)", CURRENT_SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", CURRENT_BUFFER_OFFSET)); - self.emit(format!("addi a1, sp, {}", CURRENT_SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit(format!("li a7, {}", abi.load_script)); - self.emit("ecall"); - self.emit(format!("beqz a0, {}", current_loaded)); - self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); - self.emit("sub t1, a0, t0"); - self.emit(format!("beqz t1, {}", nonempty)); - self.emit(format!("j {}", failed)); - - self.emit_label(¤t_loaded); - self.emit(format!("ld t0, {}(sp)", CURRENT_SIZE_OFFSET)); - self.emit(format!("li t1, {}", EMPTY_SCRIPT_SIZE)); - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", nonempty)); - - for (offset, expected) in [(0usize, EMPTY_SCRIPT_SIZE), (4, 16), (8, 48), (12, 49), (49, 0)] { - self.emit_stack_u32_le_to("t0", CURRENT_BUFFER_OFFSET + offset); - self.emit(format!("li t1, {}", expected)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - } - - self.emit("# cellscript abi: require matching output lock scripts to keep empty args"); - self.emit(format!("sd zero, {}(sp)", OUTPUT_INDEX_OFFSET)); - self.emit_label(&output_loop); - self.emit("li t0, 128"); - self.emit(format!("sd t0, {}(sp)", OUTPUT_SIZE_OFFSET)); - self.emit(format!("sd zero, {}(sp)", OUTPUT_TRUNCATED_OFFSET)); - self.emit(format!("addi a0, sp, {}", OUTPUT_BUFFER_OFFSET)); - self.emit(format!("addi a1, sp, {}", OUTPUT_SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit(format!("ld a3, {}(sp)", OUTPUT_INDEX_OFFSET)); - self.emit(format!("li a4, {}", CKB_SOURCE_OUTPUT)); - self.emit(format!("li a5, {}", CKB_CELL_FIELD_LOCK)); - self.emit(format!("li a7, {}", abi.load_cell_by_field)); - self.emit("ecall"); - self.emit(format!("beqz a0, {}", output_loaded)); - self.emit(format!("li t0, {}", CKB_INDEX_OUT_OF_BOUND)); - self.emit("sub t1, a0, t0"); - self.emit(format!("beqz t1, {}", output_done)); - self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); - self.emit("sub t1, a0, t0"); - self.emit(format!("beqz t1, {}", output_prefix_loaded)); - self.emit(format!("j {}", output_failed)); - - self.emit_label(&output_prefix_loaded); - self.emit("li t0, 1"); - self.emit(format!("sd t0, {}(sp)", OUTPUT_TRUNCATED_OFFSET)); - self.emit_label(&output_loaded); - self.emit(format!("ld t0, {}(sp)", OUTPUT_SIZE_OFFSET)); - self.emit("li t1, 49"); - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - self.emit(format!("addi a0, sp, {}", CURRENT_BUFFER_OFFSET + 16)); - self.emit(format!("addi a1, sp, {}", OUTPUT_BUFFER_OFFSET + 16)); - self.emit("li a2, 32"); - self.emit("call __cellscript_memcmp_fixed"); - self.emit(format!("beqz a0, {}", output_same_hash)); - self.emit(format!("j {}", output_advance)); - - self.emit_label(&output_same_hash); - self.emit(format!("lbu t0, {}(sp)", CURRENT_BUFFER_OFFSET + 48)); - self.emit(format!("lbu t1, {}(sp)", OUTPUT_BUFFER_OFFSET + 48)); - self.emit("sub t2, t0, t1"); - self.emit(format!("beqz t2, {}", output_same_script)); - self.emit(format!("j {}", output_advance)); - - self.emit_label(&output_same_script); - self.emit(format!("ld t0, {}(sp)", OUTPUT_TRUNCATED_OFFSET)); - self.emit(format!("bnez t0, {}", nonempty)); - self.emit(format!("ld t0, {}(sp)", OUTPUT_SIZE_OFFSET)); - self.emit(format!("li t1, {}", EMPTY_SCRIPT_SIZE)); - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", nonempty)); - for (offset, expected) in [(0usize, EMPTY_SCRIPT_SIZE), (4, 16), (8, 48), (12, 49), (49, 0)] { - self.emit_stack_u32_le_to("t0", OUTPUT_BUFFER_OFFSET + offset); - self.emit(format!("li t1, {}", expected)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - } - - self.emit_label(&output_advance); - self.emit(format!("ld t0, {}(sp)", OUTPUT_INDEX_OFFSET)); - self.emit("addi t0, t0, 1"); - self.emit(format!("sd t0, {}(sp)", OUTPUT_INDEX_OFFSET)); - self.emit(format!("j {}", output_loop)); - - self.emit_label(&output_done); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&output_failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&nonempty); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptArgsMismatch.code())); - self.emit(format!("j {}", done)); - self.emit_label(&malformed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); - self.emit_label(&done); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - } - - fn emit_runtime_cell_script_args_hash_requirement_helper( - &mut self, - symbol: &str, - detail: &str, - field_id: u64, - mode: ScriptArgsHashRequirementMode, - enabled: bool, - ) { - self.emit_global(symbol); - self.emit_label(symbol); - self.emit(format!("# cellscript abi: CKB SourceView Script 32-byte args requirement ({})", detail)); - self.emit("# cellscript abi: args a0=SourceView, a1=expected_args_hash_ptr, a2=expected_args_hash_len"); - match mode { - ScriptArgsHashRequirementMode::Exact32 => { - self.emit("# cellscript abi: expects Molecule Script args Bytes length == 32 and payload == expected hash"); - } - ScriptArgsHashRequirementMode::Prefix32 => { - self.emit("# cellscript abi: expects Molecule Script args Bytes length >= 32 and first 32 bytes == expected hash"); - } - ScriptArgsHashRequirementMode::Suffix32 => { - self.emit("# cellscript abi: expects Molecule Script args Bytes length >= 32 and last 32 bytes == expected hash"); - } - } - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - - const SCRIPT_SIZE_OFFSET: usize = 8; - const SCRIPT_BUFFER_OFFSET: usize = 16; - const ARGS_PAYLOAD_OFFSET: usize = SCRIPT_BUFFER_OFFSET + 53; - const SOURCE_INDEX_OFFSET: usize = 152; - const SOURCE_KIND_OFFSET: usize = 160; - const EXPECTED_HASH_LEN_OFFSET: usize = 168; - const EXPECTED_HASH_PTR_OFFSET: usize = 176; - const RA_OFFSET: usize = 184; - const FRAME_SIZE: usize = 192; - const SCRIPT_PREFIX_SIZE: u64 = 53; - const HASH_ARGS_SCRIPT_SIZE: u64 = 85; - - let invalid = self.fresh_label("script_args_hash_source_invalid"); - let bad_expected = self.fresh_label("script_args_hash_expected_invalid"); - let loaded = self.fresh_label("script_args_hash_loaded"); - let suffix_loaded = self.fresh_label("script_args_hash_suffix_loaded"); - let failed = self.fresh_label("script_args_hash_load_failed"); - let mismatch = self.fresh_label("script_args_hash_mismatch"); - let malformed = self.fresh_label("script_args_hash_malformed"); - let done = self.fresh_label("script_args_hash_done"); - let abi = self.runtime_abi(); - - self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); - self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); - self.emit(format!("sd a1, {}(sp)", EXPECTED_HASH_PTR_OFFSET)); - self.emit(format!("sd a2, {}(sp)", EXPECTED_HASH_LEN_OFFSET)); - - self.emit(format!("beqz a1, {}", bad_expected)); - self.emit("li t0, 32"); - self.emit("sub t1, a2, t0"); - self.emit(format!("bnez t1, {}", bad_expected)); - - self.emit_decode_source_view_to_t1_t2(&invalid); - self.emit(format!("sd t1, {}(sp)", SOURCE_INDEX_OFFSET)); - self.emit(format!("sd t2, {}(sp)", SOURCE_KIND_OFFSET)); - let requested_size = match mode { - ScriptArgsHashRequirementMode::Exact32 | ScriptArgsHashRequirementMode::Prefix32 => 128u64, - ScriptArgsHashRequirementMode::Suffix32 => SCRIPT_PREFIX_SIZE, - }; - self.emit(format!("li t0, {}", requested_size)); - self.emit(format!("sd t0, {}(sp)", SCRIPT_SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); - self.emit(format!("addi a1, sp, {}", SCRIPT_SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit("addi a3, t1, 0"); - self.emit("addi a4, t2, 0"); - self.emit(format!("li a5, {}", field_id)); - self.emit(format!("li a7, {}", abi.load_cell_by_field)); - self.emit("ecall"); - self.emit(format!("beqz a0, {}", loaded)); - self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); - self.emit("sub t1, a0, t0"); - self.emit(format!("beqz t1, {}", loaded)); - self.emit(format!("j {}", failed)); - - self.emit_label(&loaded); - self.emit(format!("ld t3, {}(sp)", SCRIPT_SIZE_OFFSET)); - self.emit("li t1, 53"); - self.emit("sltu t2, t3, t1"); - self.emit(format!("bnez t2, {}", malformed)); - - self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET); - self.emit("sub t2, t0, t3"); - self.emit(format!("bnez t2, {}", malformed)); - for (offset, expected) in [(4usize, 16u64), (8, 48), (12, 49)] { - self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + offset); - self.emit(format!("li t1, {}", expected)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - } - - self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + 49); - match mode { - ScriptArgsHashRequirementMode::Exact32 => { - self.emit("li t1, 32"); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", mismatch)); - self.emit(format!("li t1, {}", HASH_ARGS_SCRIPT_SIZE)); - self.emit("sub t2, t3, t1"); - self.emit(format!("bnez t2, {}", malformed)); - self.emit(format!("addi a0, sp, {}", ARGS_PAYLOAD_OFFSET)); - } - ScriptArgsHashRequirementMode::Prefix32 => { - self.emit("li t1, 32"); - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", mismatch)); - self.emit(format!("li t1, {}", SCRIPT_PREFIX_SIZE)); - self.emit("add t1, t1, t0"); - self.emit("sub t2, t3, t1"); - self.emit(format!("bnez t2, {}", malformed)); - self.emit(format!("addi a0, sp, {}", ARGS_PAYLOAD_OFFSET)); - } - ScriptArgsHashRequirementMode::Suffix32 => { - self.emit("li t1, 32"); - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", mismatch)); - self.emit(format!("li t1, {}", SCRIPT_PREFIX_SIZE)); - self.emit("add t1, t1, t0"); - self.emit("sub t2, t3, t1"); - self.emit(format!("bnez t2, {}", malformed)); - self.emit("addi t1, t1, -32"); - self.emit("li t0, 32"); - self.emit(format!("sd t0, {}(sp)", SCRIPT_SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); - self.emit(format!("addi a1, sp, {}", SCRIPT_SIZE_OFFSET)); - self.emit("addi a2, t1, 0"); - self.emit(format!("ld a3, {}(sp)", SOURCE_INDEX_OFFSET)); - self.emit(format!("ld a4, {}(sp)", SOURCE_KIND_OFFSET)); - self.emit(format!("li a5, {}", field_id)); - self.emit(format!("li a7, {}", abi.load_cell_by_field)); - self.emit("ecall"); - self.emit(format!("beqz a0, {}", suffix_loaded)); - self.emit(format!("j {}", failed)); - self.emit_label(&suffix_loaded); - self.emit(format!("ld t0, {}(sp)", SCRIPT_SIZE_OFFSET)); - self.emit("li t1, 32"); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); - } - } - self.emit(format!("ld a1, {}(sp)", EXPECTED_HASH_PTR_OFFSET)); - self.emit("li a2, 32"); - self.emit("call __cellscript_memcmp_fixed"); - self.emit(format!("bnez a0, {}", mismatch)); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit(format!("j {}", done)); - self.emit_label(&bad_expected); - self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&mismatch); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptArgsMismatch.code())); - self.emit(format!("j {}", done)); - self.emit_label(&malformed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); - self.emit_label(&done); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - } - - fn emit_runtime_cell_script_hash_type_requirement_helper(&mut self, symbol: &str, detail: &str, field_id: u64, enabled: bool) { - self.emit_global(symbol); - self.emit_label(symbol); - self.emit(format!("# cellscript abi: CKB SourceView Script code_hash/hash_type requirement ({})", detail)); - self.emit("# cellscript abi: args a0=SourceView, a1=expected_code_hash_ptr, a2=expected_code_hash_len, a3=expected_hash_type"); - self.emit("# cellscript abi: validates Molecule Script table prefix without constraining args length"); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - - const SCRIPT_SIZE_OFFSET: usize = 8; - const SCRIPT_BUFFER_OFFSET: usize = 16; - const EXPECTED_CODE_HASH_PTR_OFFSET: usize = 80; - const EXPECTED_CODE_HASH_LEN_OFFSET: usize = 88; - const EXPECTED_HASH_TYPE_OFFSET: usize = 96; - const RA_OFFSET: usize = 120; - const FRAME_SIZE: usize = 128; - const SCRIPT_PREFIX_SIZE: u64 = 53; - - let invalid = self.fresh_label("script_identity_source_invalid"); - let bad_expected = self.fresh_label("script_identity_expected_invalid"); - let bad_hash_type = self.fresh_label("script_identity_hash_type_invalid"); - let loaded = self.fresh_label("script_identity_loaded"); - let prefix_loaded = self.fresh_label("script_identity_prefix_loaded"); - let failed = self.fresh_label("script_identity_load_failed"); - let malformed = self.fresh_label("script_identity_malformed"); - let mismatch = self.fresh_label("script_identity_mismatch"); - let done = self.fresh_label("script_identity_done"); - let abi = self.runtime_abi(); - - self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); - self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); - self.emit(format!("sd a1, {}(sp)", EXPECTED_CODE_HASH_PTR_OFFSET)); - self.emit(format!("sd a2, {}(sp)", EXPECTED_CODE_HASH_LEN_OFFSET)); - self.emit(format!("sd a3, {}(sp)", EXPECTED_HASH_TYPE_OFFSET)); - - self.emit(format!("beqz a1, {}", bad_expected)); - self.emit("li t0, 32"); - self.emit("sub t1, a2, t0"); - self.emit(format!("bnez t1, {}", bad_expected)); - self.emit("li t0, 256"); - self.emit("sltu t1, a3, t0"); - self.emit(format!("beqz t1, {}", bad_hash_type)); - - self.emit_decode_source_view_to_t1_t2(&invalid); - self.emit(format!("li t0, {}", SCRIPT_PREFIX_SIZE)); - self.emit(format!("sd t0, {}(sp)", SCRIPT_SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); - self.emit(format!("addi a1, sp, {}", SCRIPT_SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit("addi a3, t1, 0"); - self.emit("addi a4, t2, 0"); - self.emit(format!("li a5, {}", field_id)); - self.emit(format!("li a7, {}", abi.load_cell_by_field)); - self.emit("ecall"); - self.emit(format!("beqz a0, {}", loaded)); - self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); - self.emit("sub t1, a0, t0"); - self.emit(format!("beqz t1, {}", prefix_loaded)); - self.emit(format!("j {}", failed)); - - self.emit_label(&loaded); - self.emit(format!("j {}", prefix_loaded)); - self.emit_label(&prefix_loaded); - self.emit(format!("ld t3, {}(sp)", SCRIPT_SIZE_OFFSET)); - self.emit("li t1, 49"); - self.emit("sltu t2, t3, t1"); - self.emit(format!("bnez t2, {}", malformed)); - - self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET); - self.emit(format!("li t1, {}", SCRIPT_PREFIX_SIZE)); - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - for (offset, expected) in [(4usize, 16u64), (8, 48), (12, 49)] { - self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + offset); - self.emit(format!("li t1, {}", expected)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - } - - self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET + 16)); - self.emit(format!("ld a1, {}(sp)", EXPECTED_CODE_HASH_PTR_OFFSET)); - self.emit("li a2, 32"); - self.emit("call __cellscript_memcmp_fixed"); - self.emit(format!("bnez a0, {}", mismatch)); - - self.emit(format!("lbu t0, {}(sp)", SCRIPT_BUFFER_OFFSET + 48)); - self.emit(format!("ld t1, {}(sp)", EXPECTED_HASH_TYPE_OFFSET)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", mismatch)); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit(format!("j {}", done)); - self.emit_label(&bad_expected); - self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); - self.emit(format!("j {}", done)); - self.emit_label(&bad_hash_type); - self.emit(format!("li a0, {}", CellScriptRuntimeError::NumericOrDiscriminantInvalid.code())); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&malformed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&mismatch); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptIdentityMismatch.code())); - self.emit_label(&done); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - } - - fn emit_runtime_load_u64_le_helper(&mut self) { - self.emit_global("__cellscript_load_u64_le"); - self.emit_label("__cellscript_load_u64_le"); - self.emit("# cellscript abi: load unaligned little-endian u64 from pointer a0"); - self.emit("li a1, 0"); - for byte_index in 0..8 { - self.emit(format!("lbu t0, {}(a0)", byte_index)); - if byte_index != 0 { - self.emit(format!("slli t0, t0, {}", byte_index * 8)); - } - self.emit("or a1, a1, t0"); - } - self.emit("addi a0, a1, 0"); - self.emit("ret"); - } - - fn emit_runtime_mul_u128_to_u256_helper(&mut self) { - self.emit_global("__cellscript_mul_u128_to_u256"); - self.emit_label("__cellscript_mul_u128_to_u256"); - self.emit("# cellscript abi: u128*u128 -> u256 limbs; args a0=left_ptr a1=right_ptr a2=out32_ptr"); - self.emit("addi sp, sp, -96"); - self.emit("sd ra, 88(sp)"); - self.emit("sd a0, 0(sp)"); - self.emit("sd a1, 8(sp)"); - self.emit("sd a2, 16(sp)"); - - self.emit("ld a0, 0(sp)"); - self.emit("call __cellscript_load_u64_le"); - self.emit("sd a0, 24(sp)"); - self.emit("ld a0, 0(sp)"); - self.emit("addi a0, a0, 8"); - self.emit("call __cellscript_load_u64_le"); - self.emit("sd a0, 32(sp)"); - self.emit("ld a0, 8(sp)"); - self.emit("call __cellscript_load_u64_le"); - self.emit("sd a0, 40(sp)"); - self.emit("ld a0, 8(sp)"); - self.emit("addi a0, a0, 8"); - self.emit("call __cellscript_load_u64_le"); - self.emit("sd a0, 48(sp)"); - - self.emit("ld t0, 24(sp)"); - self.emit("ld t1, 40(sp)"); - self.emit("mul t2, t0, t1"); - self.emit("mulhu t3, t0, t1"); - self.emit("sd t2, 56(sp)"); - - self.emit("ld t0, 24(sp)"); - self.emit("ld t1, 48(sp)"); - self.emit("mul t4, t0, t1"); - self.emit("mulhu t5, t0, t1"); - - self.emit("ld t0, 32(sp)"); - self.emit("ld t1, 40(sp)"); - self.emit("mul t6, t0, t1"); - self.emit("mulhu a3, t0, t1"); - - self.emit("add t0, t3, t4"); - self.emit("sltu a4, t0, t3"); - self.emit("add t1, t0, t6"); - self.emit("sltu a5, t1, t0"); - self.emit("add a4, a4, a5"); - self.emit("sd t1, 64(sp)"); - - self.emit("ld t0, 32(sp)"); - self.emit("ld t1, 48(sp)"); - self.emit("mul a5, t0, t1"); - self.emit("mulhu a6, t0, t1"); - - self.emit("add t2, t5, a3"); - self.emit("sltu a7, t2, t5"); - self.emit("add t3, t2, a5"); - self.emit("sltu t4, t3, t2"); - self.emit("add t5, t3, a4"); - self.emit("sltu t6, t5, t3"); - self.emit("sd t5, 72(sp)"); - self.emit("add t0, a6, a7"); - self.emit("add t0, t0, t4"); - self.emit("add t0, t0, t6"); - self.emit("sd t0, 80(sp)"); - - self.emit("ld t0, 16(sp)"); - self.emit("ld t1, 56(sp)"); - self.emit("sd t1, 0(t0)"); - self.emit("ld t1, 64(sp)"); - self.emit("sd t1, 8(t0)"); - self.emit("ld t1, 72(sp)"); - self.emit("sd t1, 16(t0)"); - self.emit("ld t1, 80(sp)"); - self.emit("sd t1, 24(t0)"); - self.emit("ld ra, 88(sp)"); - self.emit("addi sp, sp, 96"); - self.emit("ret"); - } - - fn emit_runtime_add_u256_helper(&mut self) { - self.emit_global("__cellscript_add_u256"); - self.emit_label("__cellscript_add_u256"); - self.emit("# cellscript abi: checked u256 addition; args a0=left32_ptr a1=right32_ptr a2=out32_ptr, returns carry in a0"); - self.emit("li a3, 0"); - for limb_offset in [0, 8, 16, 24] { - self.emit(format!("ld t0, {}(a0)", limb_offset)); - self.emit(format!("ld t1, {}(a1)", limb_offset)); - self.emit("add t2, t0, t1"); - self.emit("sltu t3, t2, t0"); - self.emit("add t2, t2, a3"); - self.emit("sltu t4, t2, a3"); - self.emit(format!("sd t2, {}(a2)", limb_offset)); - self.emit("add a3, t3, t4"); - } - self.emit("addi a0, a3, 0"); - self.emit("ret"); - } - - fn emit_runtime_c256_product_requirement_helper(&mut self, symbol: &str, detail: &str, equality: bool) { - self.emit_global(symbol); - self.emit_label(symbol); - self.emit(format!("# cellscript abi: {} with overflow-safe C256 product comparison", detail)); - self.emit("# cellscript abi: args a0..a3 are u128 little-endian pointers"); - let bad_expected = self.fresh_label("c256_operand_invalid"); - let mismatch = self.fresh_label("c256_product_mismatch"); - let success = self.fresh_label("c256_product_ok"); - let done = self.fresh_label("c256_product_done"); - - self.emit("addi sp, sp, -128"); - self.emit("sd ra, 120(sp)"); - self.emit("sd a0, 0(sp)"); - self.emit("sd a1, 8(sp)"); - self.emit("sd a2, 16(sp)"); - self.emit("sd a3, 24(sp)"); - self.emit(format!("beqz a0, {}", bad_expected)); - self.emit(format!("beqz a1, {}", bad_expected)); - self.emit(format!("beqz a2, {}", bad_expected)); - self.emit(format!("beqz a3, {}", bad_expected)); - - self.emit("ld a0, 0(sp)"); - self.emit("ld a1, 8(sp)"); - self.emit("addi a2, sp, 32"); - self.emit("call __cellscript_mul_u128_to_u256"); - self.emit("ld a0, 16(sp)"); - self.emit("ld a1, 24(sp)"); - self.emit("addi a2, sp, 64"); - self.emit("call __cellscript_mul_u128_to_u256"); - - for limb_offset in [24, 16, 8, 0] { - self.emit(format!("ld t0, {}(sp)", 32 + limb_offset)); - self.emit(format!("ld t1, {}(sp)", 64 + limb_offset)); - if equality { - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", mismatch)); - } else { - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", success)); - self.emit("sltu t2, t1, t0"); - self.emit(format!("bnez t2, {}", mismatch)); - } - } - - self.emit_label(&success); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&bad_expected); - self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); - self.emit(format!("j {}", done)); - self.emit_label(&mismatch); - self.emit(format!("li a0, {}", CellScriptRuntimeError::AggregateAmountMismatch.code())); - self.emit_label(&done); - self.emit("ld ra, 120(sp)"); - self.emit("addi sp, sp, 128"); - self.emit("ret"); - } - - fn emit_runtime_c256_sum2_product_requirement_helper(&mut self, symbol: &str, detail: &str, equality: bool) { - self.emit_global(symbol); - self.emit_label(symbol); - self.emit(format!("# cellscript abi: {} with checked u256 product sums", detail)); - self.emit("# cellscript abi: args a0..a7 are u128 little-endian pointers; compares a0*a1+a2*a3 with a4*a5+a6*a7"); - let bad_expected = self.fresh_label("c256_sum_operand_invalid"); - let mismatch = self.fresh_label("c256_sum_mismatch"); - let success = self.fresh_label("c256_sum_ok"); - let done = self.fresh_label("c256_sum_done"); - - self.emit("addi sp, sp, -320"); - self.emit("sd ra, 312(sp)"); - for (index, register) in ["a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7"].into_iter().enumerate() { - self.emit(format!("sd {}, {}(sp)", register, index * 8)); - self.emit(format!("beqz {}, {}", register, bad_expected)); - } - - self.emit("ld a0, 0(sp)"); - self.emit("ld a1, 8(sp)"); - self.emit("addi a2, sp, 64"); - self.emit("call __cellscript_mul_u128_to_u256"); - self.emit("ld a0, 16(sp)"); - self.emit("ld a1, 24(sp)"); - self.emit("addi a2, sp, 96"); - self.emit("call __cellscript_mul_u128_to_u256"); - self.emit("addi a0, sp, 64"); - self.emit("addi a1, sp, 96"); - self.emit("addi a2, sp, 128"); - self.emit("call __cellscript_add_u256"); - self.emit(format!("bnez a0, {}", mismatch)); - - self.emit("ld a0, 32(sp)"); - self.emit("ld a1, 40(sp)"); - self.emit("addi a2, sp, 160"); - self.emit("call __cellscript_mul_u128_to_u256"); - self.emit("ld a0, 48(sp)"); - self.emit("ld a1, 56(sp)"); - self.emit("addi a2, sp, 192"); - self.emit("call __cellscript_mul_u128_to_u256"); - self.emit("addi a0, sp, 160"); - self.emit("addi a1, sp, 192"); - self.emit("addi a2, sp, 224"); - self.emit("call __cellscript_add_u256"); - self.emit(format!("bnez a0, {}", mismatch)); - - for limb_offset in [24, 16, 8, 0] { - self.emit(format!("ld t0, {}(sp)", 128 + limb_offset)); - self.emit(format!("ld t1, {}(sp)", 224 + limb_offset)); - if equality { - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", mismatch)); - } else { - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", success)); - self.emit("sltu t2, t1, t0"); - self.emit(format!("bnez t2, {}", mismatch)); - } - } - - self.emit_label(&success); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&bad_expected); - self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); - self.emit(format!("j {}", done)); - self.emit_label(&mismatch); - self.emit(format!("li a0, {}", CellScriptRuntimeError::AggregateAmountMismatch.code())); - self.emit_label(&done); - self.emit("ld ra, 312(sp)"); - self.emit("addi sp, sp, 320"); - self.emit("ret"); - } - - fn emit_runtime_current_role_helper(&mut self, enabled: bool) { - self.emit_global("__ckb_current_role"); - self.emit_label("__ckb_current_role"); - self.emit("# cellscript abi: current role helper; normal lowering folds role to a compile-time lock/type constant"); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("addi a1, a0, 0"); - } else { - self.emit(format!("li a0, {}", CKB_ROLE_UNKNOWN)); - self.emit("li a1, 0"); - } - self.emit("ret"); - } - - fn emit_runtime_cell_occupied_capacity_helper(&mut self, enabled: bool) { - self.emit_global("__ckb_cell_occupied_capacity"); - self.emit_label("__ckb_cell_occupied_capacity"); - self.emit("# cellscript abi: CKB occupied capacity via LOAD_CELL_BY_FIELD CellField::OccupiedCapacity"); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("addi a1, a0, 0"); - self.emit("ret"); - return; - } - - let invalid = self.fresh_label("occupied_capacity_source_invalid"); - let failed = self.fresh_label("occupied_capacity_load_failed"); - let malformed = self.fresh_label("occupied_capacity_field_malformed"); - let done = self.fresh_label("occupied_capacity_done"); - let abi = self.runtime_abi(); - - self.emit("addi sp, sp, -48"); - self.emit("sd ra, 40(sp)"); - self.emit_decode_source_view_to_t1_t2(&invalid); - self.emit("sd t1, 0(sp)"); - self.emit("sd t2, 8(sp)"); - self.emit("li t0, 8"); - self.emit("sd t0, 16(sp)"); - self.emit("addi a0, sp, 24"); - self.emit("addi a1, sp, 16"); - self.emit("li a2, 0"); - self.emit("ld a3, 0(sp)"); - self.emit("ld a4, 8(sp)"); - self.emit(format!("li a5, {}", CKB_CELL_FIELD_OCCUPIED_CAPACITY)); - self.emit(format!("li a7, {}", abi.load_cell_by_field)); - self.emit("ecall"); - self.emit("ld t0, 16(sp)"); - self.emit(format!("bnez a0, {}", failed)); - self.emit("li t1, 8"); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - self.emit("ld a0, 24(sp)"); - self.emit("li a1, 0"); - self.emit(format!("j {}", done)); - - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit("addi a1, a0, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit("addi a1, a0, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&malformed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); - self.emit("addi a1, a0, 0"); - self.emit_label(&done); - self.emit("ld ra, 40(sp)"); - self.emit("addi sp, sp, 48"); - self.emit("ret"); - } - - fn emit_runtime_cell_unoccupied_capacity_helper(&mut self, enabled: bool) { - self.emit_global("__ckb_cell_unoccupied_capacity"); - self.emit_label("__ckb_cell_unoccupied_capacity"); - self.emit("# cellscript abi: SourceView unoccupied capacity = capacity - occupied_capacity"); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("addi a1, a0, 0"); - self.emit("ret"); - return; - } - - let failed = self.fresh_label("unoccupied_capacity_failed"); - let failed_status_ok = self.fresh_label("unoccupied_capacity_failed_status_ok"); - let underflow = self.fresh_label("unoccupied_capacity_underflow"); - let done = self.fresh_label("unoccupied_capacity_done"); - - self.emit("addi sp, sp, -48"); - self.emit("sd ra, 40(sp)"); - self.emit("sd a0, 32(sp)"); - self.emit("call __ckb_cell_capacity"); - self.emit(format!("bnez a1, {}", failed)); - self.emit("sd a0, 24(sp)"); - self.emit("ld a0, 32(sp)"); - self.emit("call __ckb_cell_occupied_capacity"); - self.emit(format!("bnez a1, {}", failed)); - self.emit("ld t0, 24(sp)"); - self.emit("sltu t1, t0, a0"); - self.emit(format!("bnez t1, {}", underflow)); - self.emit("sub a0, t0, a0"); - self.emit("li a1, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit("addi a0, a1, 0"); - self.emit(format!("bnez a0, {}", failed_status_ok)); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit_label(&failed_status_ok); - self.emit("addi a1, a0, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&underflow); - self.emit(format!("li a0, {}", CellScriptRuntimeError::NumericOrDiscriminantInvalid.code())); - self.emit("addi a1, a0, 0"); - self.emit_label(&done); - self.emit("ld ra, 40(sp)"); - self.emit("addi sp, sp, 48"); - self.emit("ret"); - } - - fn emit_runtime_cell_output_index_helper(&mut self, enabled: bool) { - self.emit_global("__ckb_cell_output_index"); - self.emit_label("__ckb_cell_output_index"); - self.emit("# cellscript abi: SourceView output index extractor"); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("addi a1, a0, 0"); - self.emit("ret"); - return; - } - let invalid = self.fresh_label("source_view_invalid"); - let output = self.fresh_label("source_view_output"); - let done = self.fresh_label("source_view_output_index_done"); - self.emit(format!("li t6, {}", CKB_SOURCE_VIEW_SHIFT)); - self.emit("div t0, a0, t6"); - self.emit("rem t1, a0, t6"); - self.emit(format!("li t5, {}", CKB_SOURCE_VIEW_OUTPUT)); - self.emit("sub t4, t0, t5"); - self.emit(format!("beqz t4, {}", output)); - self.emit(format!("li t5, {}", CKB_SOURCE_VIEW_GROUP_OUTPUT)); - self.emit("sub t4, t0, t5"); - self.emit(format!("beqz t4, {}", output)); - self.emit(format!("j {}", invalid)); - self.emit_label(&output); - self.emit("addi a0, t1, 0"); - self.emit("li a1, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit("addi a1, a0, 0"); - self.emit_label(&done); - self.emit("ret"); - } - - fn emit_runtime_cell_data_size_helper(&mut self, enabled: bool) { - self.emit_global("__ckb_cell_data_size"); - self.emit_label("__ckb_cell_data_size"); - self.emit("# cellscript abi: CKB SourceView LOAD_CELL_DATA size probe"); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("addi a1, a0, 0"); - self.emit("ret"); - return; - } - let invalid = self.fresh_label("source_view_invalid"); - let done = self.fresh_label("cell_data_size_done"); - let failed = self.fresh_label("cell_data_size_failed"); - let status_ok = self.fresh_label("cell_data_size_status_ok"); - let abi = self.runtime_abi(); - self.emit("addi sp, sp, -48"); - self.emit("sd ra, 40(sp)"); - self.emit_decode_source_view_to_t1_t2(&invalid); - self.emit("li t0, 0"); - self.emit("sd t0, 8(sp)"); - self.emit("addi a0, sp, 16"); - self.emit("addi a1, sp, 8"); - self.emit("li a2, 0"); - self.emit("addi a3, t1, 0"); - self.emit("addi a4, t2, 0"); - self.emit(format!("li a7, {}", abi.load_cell_data)); - self.emit("ecall"); - self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); - self.emit("sub t1, a0, t0"); - self.emit(format!("beqz t1, {}", status_ok)); - self.emit(format!("beqz a0, {}", status_ok)); - self.emit(format!("j {}", failed)); - self.emit_label(&status_ok); - self.emit("ld a0, 8(sp)"); - self.emit("li a1, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit("addi a1, a0, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit("addi a1, a0, 0"); - self.emit_label(&done); - self.emit("ld ra, 40(sp)"); - self.emit("addi sp, sp, 48"); - self.emit("ret"); - } - - fn emit_runtime_bounded_cell_dep_data_hash_requirement_helper(&mut self, enabled: bool) { - self.emit_global("__ckb_require_bounded_cell_dep_data_hash"); - self.emit_label("__ckb_require_bounded_cell_dep_data_hash"); - self.emit("# cellscript abi: a0=max_deps(1..=64), a1=expected_data_hash[32]; scan resolved CellDeps with LOAD_CELL_BY_FIELD"); - if !enabled { - self.emit_fail(CellScriptRuntimeError::SyscallFailed); - return; - } - - const EXPECTED_PTR_OFFSET: usize = 8; - const LIMIT_OFFSET: usize = 16; - const INDEX_OFFSET: usize = 24; - const SIZE_OFFSET: usize = 32; - const BUFFER_OFFSET: usize = 40; - const RA_OFFSET: usize = 72; - const FRAME_SIZE: usize = 80; - - let invalid = self.fresh_label("bounded_cell_dep_invalid"); - let scan = self.fresh_label("bounded_cell_dep_scan"); - let not_found = self.fresh_label("bounded_cell_dep_not_found"); - let loaded = self.fresh_label("bounded_cell_dep_loaded"); - let mismatch = self.fresh_label("bounded_cell_dep_mismatch"); - let failed = self.fresh_label("bounded_cell_dep_load_failed"); - let done = self.fresh_label("bounded_cell_dep_done"); - let abi = self.runtime_abi(); - - self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); - self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); - self.emit(format!("sd a1, {}(sp)", EXPECTED_PTR_OFFSET)); - self.emit(format!("sd a0, {}(sp)", LIMIT_OFFSET)); - self.emit(format!("beqz a1, {}", invalid)); - self.emit(format!("beqz a0, {}", invalid)); - self.emit("li t0, 64"); - self.emit(format!("bltu t0, a0, {}", invalid)); - self.emit(format!("sd zero, {}(sp)", INDEX_OFFSET)); - - self.emit_label(&scan); - self.emit(format!("ld t0, {}(sp)", INDEX_OFFSET)); - self.emit(format!("ld t1, {}(sp)", LIMIT_OFFSET)); - self.emit(format!("bgeu t0, t1, {}", not_found)); - self.emit("li t1, 32"); - self.emit(format!("sd t1, {}(sp)", SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", BUFFER_OFFSET)); - self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit(format!("ld a3, {}(sp)", INDEX_OFFSET)); - self.emit(format!("li a4, {}", CKB_SOURCE_CELL_DEP)); - self.emit(format!("li a5, {}", CKB_CELL_FIELD_DATA_HASH)); - self.emit(format!("li a7, {}", abi.load_cell_by_field)); - self.emit("ecall"); - self.emit(format!("beqz a0, {}", loaded)); - self.emit(format!("li t0, {}", CKB_INDEX_OUT_OF_BOUND)); - self.emit("sub t1, a0, t0"); - self.emit(format!("beqz t1, {}", not_found)); - self.emit(format!("j {}", failed)); - - self.emit_label(&loaded); - self.emit(format!("ld t0, {}(sp)", SIZE_OFFSET)); - self.emit("li t1, 32"); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", failed)); - self.emit(format!("addi a0, sp, {}", BUFFER_OFFSET)); - self.emit(format!("ld a1, {}(sp)", EXPECTED_PTR_OFFSET)); - self.emit("li a2, 32"); - self.emit("call __cellscript_memcmp_fixed"); - self.emit(format!("bnez a0, {}", mismatch)); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - - self.emit_label(&mismatch); - self.emit(format!("ld t0, {}(sp)", INDEX_OFFSET)); - self.emit("addi t0, t0, 1"); - self.emit(format!("sd t0, {}(sp)", INDEX_OFFSET)); - self.emit(format!("j {}", scan)); - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::BoundsCheckFailed.code())); - self.emit(format!("j {}", done)); - self.emit_label(¬_found); - self.emit("# cellscript runtime error 63 bounded-cell-dep-not-found"); - self.emit(format!("li a0, {}", CellScriptRuntimeError::BoundedCellDepNotFound.code())); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CellLoadFailed.code())); - self.emit_label(&done); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - } - - fn emit_runtime_cell_data_hash_helper(&mut self, symbol: &str, detail: &str, enabled: bool) { - self.emit_global(symbol); - self.emit_label(symbol); - self.emit(format!("# cellscript abi: CKB SourceView LOAD_CELL_DATA and Blake2b ({})", detail)); - self.emit("# cellscript abi: args a0=SourceView, a1=out32_ptr, a2=size_ptr; returns a0=status"); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - - const SIZE_OFFSET: usize = 8; - const BUFFER_OFFSET: usize = 16; - const OUT_PTR_OFFSET: usize = BUFFER_OFFSET + RUNTIME_CELL_BUFFER_SIZE; - const SIZE_PTR_OFFSET: usize = OUT_PTR_OFFSET + 8; - const RA_OFFSET: usize = SIZE_PTR_OFFSET + 8; - const FRAME_SIZE: usize = RA_OFFSET + 8; - - let invalid = self.fresh_label("cell_data_hash_source_invalid"); - let bad_output = self.fresh_label("cell_data_hash_output_invalid"); - let failed = self.fresh_label("cell_data_hash_load_failed"); - let done = self.fresh_label("cell_data_hash_done"); - let abi = self.runtime_abi(); - - self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); - self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); - self.emit(format!("sd a1, {}(sp)", OUT_PTR_OFFSET)); - self.emit(format!("sd a2, {}(sp)", SIZE_PTR_OFFSET)); - self.emit(format!("beqz a1, {}", bad_output)); - self.emit(format!("beqz a2, {}", bad_output)); - - self.emit_decode_source_view_to_t1_t2(&invalid); - self.emit(format!("li t0, {}", RUNTIME_CELL_BUFFER_SIZE)); - self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", BUFFER_OFFSET)); - self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit("addi a3, t1, 0"); - self.emit("addi a4, t2, 0"); - self.emit(format!("li a7, {}", abi.load_cell_data)); - self.emit("ecall"); - self.emit(format!("bnez a0, {}", failed)); - self.emit(format!("addi a0, sp, {}", BUFFER_OFFSET)); - self.emit(format!("ld a1, {}(sp)", SIZE_OFFSET)); - self.emit(format!("ld a2, {}(sp)", OUT_PTR_OFFSET)); - self.emit("call __ckb_hash_blake2b_var"); - self.emit(format!("bnez a0, {}", failed)); - self.emit(format!("ld t6, {}(sp)", SIZE_PTR_OFFSET)); - self.emit("li t0, 32"); - self.emit("sd t0, 0(t6)"); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit(format!("j {}", done)); - self.emit_label(&bad_output); - self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CellLoadFailed.code())); - self.emit_label(&done); - self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); - self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); - self.emit("ret"); - } - - fn emit_runtime_cell_data_hash_at_helper(&mut self, symbol: &str, detail: &str, enabled: bool) { - self.emit_global(symbol); - self.emit_label(symbol); - self.emit(format!("# cellscript abi: {}; a0=source_view, a1=offset, a2=out[32], a3=size_ptr", detail)); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - let invalid = self.fresh_label("cell_data_hash_at_source_invalid"); - let failed = self.fresh_label("cell_data_hash_at_failed"); - let loaded = self.fresh_label("cell_data_hash_at_loaded"); - let done = self.fresh_label("cell_data_hash_at_done"); - let abi = self.runtime_abi(); - - self.emit("addi sp, sp, -80"); - self.emit("sd ra, 72(sp)"); - self.emit("sd a1, 8(sp)"); - self.emit("sd a2, 16(sp)"); - self.emit("sd a3, 24(sp)"); - self.emit_decode_source_view_to_t1_t2(&invalid); - self.emit("ld a0, 16(sp)"); - self.emit("ld a1, 24(sp)"); - self.emit("ld a2, 8(sp)"); - self.emit("addi a3, t1, 0"); - self.emit("addi a4, t2, 0"); - self.emit(format!("li a7, {}", abi.load_cell_data)); - self.emit("ecall"); - self.emit(format!("beqz a0, {}", loaded)); - self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); - self.emit("sub t1, a0, t0"); - self.emit(format!("bnez t1, {}", failed)); - self.emit_label(&loaded); - self.emit("# cellscript abi: normalize fixed 32-byte slice length after LOAD_CELL_DATA"); - self.emit("ld t0, 24(sp)"); - self.emit("li t1, 32"); - self.emit("sd t1, 0(t0)"); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit_label(&done); - self.emit("ld ra, 72(sp)"); - self.emit("addi sp, sp, 80"); - self.emit("ret"); - } - - fn emit_runtime_cell_data_word_le_helper(&mut self, symbol: &str, detail: &str, width: usize, enabled: bool) { - self.emit_global(symbol); - self.emit_label(symbol); - self.emit(format!("# cellscript abi: {} via LOAD_CELL_DATA offset argument", detail)); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("addi a1, a0, 0"); - self.emit("ret"); - return; - } - let invalid = self.fresh_label("source_view_invalid"); - let done = self.fresh_label("cell_data_u64_done"); - let failed = self.fresh_label("cell_data_u64_failed"); - let loaded = self.fresh_label("cell_data_u64_loaded"); - let ready = self.fresh_label("cell_data_u64_ready"); - let abi = self.runtime_abi(); - self.emit("addi sp, sp, -64"); - self.emit("sd ra, 56(sp)"); - self.emit("# cellscript abi: save requested data offset"); - self.emit("sd a1, 8(sp)"); - self.emit_decode_source_view_to_t1_t2(&invalid); - self.emit(format!("li t0, {}", width)); - self.emit("sd t0, 16(sp)"); - self.emit("addi a0, sp, 24"); - self.emit("addi a1, sp, 16"); - self.emit("ld a2, 8(sp)"); - self.emit("addi a3, t1, 0"); - self.emit("addi a4, t2, 0"); - self.emit(format!("li a7, {}", abi.load_cell_data)); - self.emit("ecall"); - self.emit(format!("beqz a0, {}", loaded)); - self.emit(format!("j {}", failed)); - self.emit_label(&loaded); - self.emit_label(&ready); - if width == 4 { - self.emit("li a0, 0"); - for byte_index in 0..4 { - self.emit(format!("lbu t0, {}(sp)", 24 + byte_index)); - if byte_index != 0 { - self.emit(format!("slli t0, t0, {}", byte_index * 8)); - } - self.emit("or a0, a0, t0"); - } - } else { - self.emit("li a0, 0"); - for byte_index in 0..8 { - self.emit(format!("lbu t0, {}(sp)", 24 + byte_index)); - if byte_index != 0 { - self.emit(format!("slli t0, t0, {}", byte_index * 8)); - } - self.emit("or a0, a0, t0"); - } - } - self.emit("li a1, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit("addi a1, a0, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CellLoadFailed.code())); - self.emit("addi a1, a0, 0"); - self.emit_label(&done); - self.emit("ld ra, 56(sp)"); - self.emit("addi sp, sp, 64"); - self.emit("ret"); - } - - fn emit_runtime_dao_accumulated_rate_helper(&mut self, enabled: bool) { - self.emit_global("__dao_accumulated_rate"); - self.emit_label("__dao_accumulated_rate"); - self.emit( - "# cellscript abi: DAO accumulated-rate HeaderDep SourceView helper via LOAD_HEADER at absolute header offset 160+8", - ); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("addi a1, a0, 0"); - self.emit("ret"); - return; - } - let invalid = self.fresh_label("dao_header_source_invalid"); - let done = self.fresh_label("dao_accumulated_rate_done"); - let failed = self.fresh_label("dao_accumulated_rate_failed"); - let loaded = self.fresh_label("dao_accumulated_rate_loaded"); - let abi = self.runtime_abi(); - self.emit("addi sp, sp, -48"); - self.emit("sd ra, 40(sp)"); - self.emit(format!("li t6, {}", CKB_SOURCE_VIEW_SHIFT)); - self.emit("div t0, a0, t6"); - self.emit("rem t1, a0, t6"); - self.emit(format!("li t5, {}", CKB_SOURCE_VIEW_HEADER_DEP)); - self.emit("sub t4, t0, t5"); - self.emit(format!("bnez t4, {}", invalid)); - self.emit("li t0, 8"); - self.emit("sd t0, 8(sp)"); - self.emit("addi a0, sp, 16"); - self.emit("addi a1, sp, 8"); - self.emit(format!("li a2, {}", CKB_DAO_HEADER_ACCUMULATED_RATE_ABSOLUTE_OFFSET)); - self.emit("addi a3, t1, 0"); - self.emit(format!("li a4, {}", abi.source_header_dep)); - self.emit(format!("li a7, {}", abi.load_header)); - self.emit("ecall"); - self.emit(format!("beqz a0, {}", loaded)); - self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); - self.emit("sub t1, a0, t0"); - self.emit(format!("bnez t1, {}", failed)); - self.emit_label(&loaded); - self.emit("ld t0, 8(sp)"); - self.emit("li t1, 8"); - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", failed)); - self.emit("ld a0, 16(sp)"); - self.emit("li a1, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::HeaderDepMissing.code())); - self.emit("addi a1, a0, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::DaoFieldMalformed.code())); - self.emit("addi a1, a0, 0"); - self.emit_label(&done); - self.emit("ld ra, 40(sp)"); - self.emit("addi sp, sp, 48"); - self.emit("ret"); - } - - fn emit_runtime_dao_input_accumulated_rate_helper(&mut self, enabled: bool) { - self.emit_global("__dao_input_accumulated_rate"); - self.emit_label("__dao_input_accumulated_rate"); - self.emit( - "# cellscript abi: DAO accumulated-rate from Input/GroupInput committed header via LOAD_HEADER at absolute header offset 160+8", - ); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("addi a1, a0, 0"); - self.emit("ret"); - return; - } - - let invalid = self.fresh_label("dao_input_header_source_invalid"); - let done = self.fresh_label("dao_input_accumulated_rate_done"); - let failed = self.fresh_label("dao_input_accumulated_rate_failed"); - let loaded = self.fresh_label("dao_input_accumulated_rate_loaded"); - let abi = self.runtime_abi(); - - self.emit("addi sp, sp, -48"); - self.emit("sd ra, 40(sp)"); - self.emit_decode_input_source_view_to_t1_t2(&invalid); - self.emit("li t0, 8"); - self.emit("sd t0, 8(sp)"); - self.emit("addi a0, sp, 16"); - self.emit("addi a1, sp, 8"); - self.emit(format!("li a2, {}", CKB_DAO_HEADER_ACCUMULATED_RATE_ABSOLUTE_OFFSET)); - self.emit("addi a3, t1, 0"); - self.emit("addi a4, t2, 0"); - self.emit(format!("li a7, {}", abi.load_header)); - self.emit("ecall"); - self.emit(format!("beqz a0, {}", loaded)); - self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); - self.emit("sub t1, a0, t0"); - self.emit(format!("bnez t1, {}", failed)); - self.emit_label(&loaded); - self.emit("ld t0, 8(sp)"); - self.emit("li t1, 8"); - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", failed)); - self.emit("ld a0, 16(sp)"); - self.emit("li a1, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::HeaderDepMissing.code())); - self.emit("addi a1, a0, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::DaoFieldMalformed.code())); - self.emit("addi a1, a0, 0"); - self.emit_label(&done); - self.emit("ld ra, 40(sp)"); - self.emit("addi sp, sp, 48"); - self.emit("ret"); - } - - fn emit_runtime_dao_type_classifier_helper(&mut self, enabled: bool) { - self.emit_global("__dao_has_dao_type"); - self.emit_label("__dao_has_dao_type"); - self.emit("# cellscript abi: NervosDAO type-hash classifier"); - if !enabled { - self.emit("li a0, 0"); - self.emit(format!("li a1, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - - let invalid = self.fresh_label("dao_type_source_invalid"); - let false_label = self.fresh_label("dao_type_false"); - let done = self.fresh_label("dao_type_done"); - let abi = self.runtime_abi(); - - self.emit("addi sp, sp, -64"); - self.emit("sd ra, 56(sp)"); - self.emit_decode_source_view_to_t1_t2(&invalid); - self.emit("li t0, 32"); - self.emit("sd t0, 8(sp)"); - self.emit("addi a0, sp, 16"); - self.emit("addi a1, sp, 8"); - self.emit("li a2, 0"); - self.emit("addi a3, t1, 0"); - self.emit("addi a4, t2, 0"); - self.emit(format!("li a5, {}", CKB_CELL_FIELD_TYPE_HASH)); - self.emit(format!("li a7, {}", abi.load_cell_by_field)); - self.emit("ecall"); - self.emit(format!("bnez a0, {}", false_label)); - self.emit("ld t0, 8(sp)"); - self.emit("li t1, 32"); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", false_label)); - for (word_index, expected) in CKB_DAO_TYPE_HASH_WORDS_LE.iter().enumerate() { - self.emit(format!("ld t0, {}(sp)", 16 + word_index * 8)); - self.emit(format!("li t1, {}", expected)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", false_label)); - } - self.emit("li a0, 1"); - self.emit("li a1, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&false_label); - self.emit("li a0, 0"); - self.emit("li a1, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&invalid); - self.emit("li a0, 0"); - self.emit(format!("li a1, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit_label(&done); - self.emit("ld ra, 56(sp)"); - self.emit("addi sp, sp, 64"); - self.emit("ret"); - } - - fn emit_runtime_dao_cell_data_classifier_helper(&mut self, symbol: &str, detail: &str, deposit: bool, enabled: bool) { - self.emit_global(symbol); - self.emit_label(symbol); - self.emit(format!("# cellscript abi: {} via LOAD_CELL_DATA exact 8-byte DAO data", detail)); - self.emit("# cellscript abi: matches NervosDAO deposit/withdrawal-request 8-byte data convention"); - if !enabled { - self.emit("li a0, 0"); - self.emit(format!("li a1, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - - let invalid = self.fresh_label("dao_data_source_invalid"); - let false_label = self.fresh_label("dao_data_false"); - let true_label = self.fresh_label("dao_data_true"); - let done = self.fresh_label("dao_data_done"); - let abi = self.runtime_abi(); - - self.emit("addi sp, sp, -48"); - self.emit("sd ra, 40(sp)"); - self.emit_decode_source_view_to_t1_t2(&invalid); - self.emit("li t0, 8"); - self.emit("sd t0, 8(sp)"); - self.emit("addi a0, sp, 16"); - self.emit("addi a1, sp, 8"); - self.emit("li a2, 0"); - self.emit("addi a3, t1, 0"); - self.emit("addi a4, t2, 0"); - self.emit(format!("li a7, {}", abi.load_cell_data)); - self.emit("ecall"); - self.emit(format!("bnez a0, {}", false_label)); - self.emit("ld t0, 8(sp)"); - self.emit("li t1, 8"); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", false_label)); - self.emit("ld t0, 16(sp)"); - if deposit { - self.emit(format!("beqz t0, {}", true_label)); - self.emit(format!("j {}", false_label)); - } else { - self.emit(format!("bnez t0, {}", true_label)); - self.emit(format!("j {}", false_label)); - } - - self.emit_label(&true_label); - self.emit("li a0, 1"); - self.emit("li a1, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&false_label); - self.emit("li a0, 0"); - self.emit("li a1, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&invalid); - self.emit("li a0, 0"); - self.emit(format!("li a1, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit_label(&done); - self.emit("ld ra, 40(sp)"); - self.emit("addi sp, sp, 48"); - self.emit("ret"); - } - - fn emit_runtime_dao_require_header_dep_for_input_helper(&mut self, enabled: bool) { - self.emit_global("__dao_require_header_dep_for_input"); - self.emit_label("__dao_require_header_dep_for_input"); - self.emit("# cellscript abi: DAO input header to HeaderDep lineage requirement"); - self.emit("# cellscript abi: args a0=input SourceView, a1=HeaderDep SourceView; compares full 32-byte DAO fields"); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - - const SIZE_OFFSET: usize = 8; - const INPUT_INDEX_OFFSET: usize = 16; - const INPUT_SOURCE_OFFSET: usize = 24; - const HEADER_INDEX_OFFSET: usize = 32; - const INPUT_DAO_OFFSET: usize = 40; - const HEADER_DAO_OFFSET: usize = 72; - const HEADER_VIEW_OFFSET: usize = 104; - - let invalid_input = self.fresh_label("dao_lineage_input_source_invalid"); - let invalid_header = self.fresh_label("dao_lineage_header_source_invalid"); - let input_failed = self.fresh_label("dao_lineage_input_header_missing"); - let header_failed = self.fresh_label("dao_lineage_header_dep_missing"); - let malformed = self.fresh_label("dao_lineage_dao_field_malformed"); - let mismatch = self.fresh_label("dao_lineage_mismatch"); - let done = self.fresh_label("dao_lineage_done"); - let abi = self.runtime_abi(); - - self.emit("addi sp, sp, -128"); - self.emit("sd ra, 120(sp)"); - self.emit(format!("sd a1, {}(sp)", HEADER_VIEW_OFFSET)); - - self.emit_decode_input_source_view_to_t1_t2(&invalid_input); - self.emit(format!("sd t1, {}(sp)", INPUT_INDEX_OFFSET)); - self.emit(format!("sd t2, {}(sp)", INPUT_SOURCE_OFFSET)); - - self.emit(format!("ld a0, {}(sp)", HEADER_VIEW_OFFSET)); - self.emit(format!("li t6, {}", CKB_SOURCE_VIEW_SHIFT)); - self.emit("div t0, a0, t6"); - self.emit("rem t1, a0, t6"); - self.emit(format!("li t5, {}", CKB_SOURCE_VIEW_HEADER_DEP)); - self.emit("sub t4, t0, t5"); - self.emit(format!("bnez t4, {}", invalid_header)); - self.emit(format!("sd t1, {}(sp)", HEADER_INDEX_OFFSET)); - - self.emit("li t0, 32"); - self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", INPUT_DAO_OFFSET)); - self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); - self.emit(format!("li a2, {}", CKB_DAO_HEADER_FIELD_ABSOLUTE_OFFSET)); - self.emit(format!("ld a3, {}(sp)", INPUT_INDEX_OFFSET)); - self.emit(format!("ld a4, {}(sp)", INPUT_SOURCE_OFFSET)); - self.emit(format!("li a7, {}", abi.load_header)); - self.emit("ecall"); - self.emit(format!("bnez a0, {}", input_failed)); - self.emit(format!("ld t0, {}(sp)", SIZE_OFFSET)); - self.emit("li t1, 32"); - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - - self.emit("li t0, 32"); - self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", HEADER_DAO_OFFSET)); - self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); - self.emit(format!("li a2, {}", CKB_DAO_HEADER_FIELD_ABSOLUTE_OFFSET)); - self.emit(format!("ld a3, {}(sp)", HEADER_INDEX_OFFSET)); - self.emit(format!("li a4, {}", CKB_SOURCE_HEADER_DEP)); - self.emit(format!("li a7, {}", abi.load_header)); - self.emit("ecall"); - self.emit(format!("bnez a0, {}", header_failed)); - self.emit(format!("ld t0, {}(sp)", SIZE_OFFSET)); - self.emit("li t1, 32"); - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - - self.emit(format!("addi a0, sp, {}", INPUT_DAO_OFFSET)); - self.emit(format!("addi a1, sp, {}", HEADER_DAO_OFFSET)); - self.emit("li a2, 32"); - self.emit("call __cellscript_memcmp_fixed"); - self.emit(format!("bnez a0, {}", mismatch)); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - - self.emit_label(&invalid_input); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit(format!("j {}", done)); - self.emit_label(&invalid_header); - self.emit(format!("li a0, {}", CellScriptRuntimeError::HeaderDepMissing.code())); - self.emit(format!("j {}", done)); - self.emit_label(&input_failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::HeaderDepMissing.code())); - self.emit(format!("j {}", done)); - self.emit_label(&header_failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::HeaderDepMissing.code())); - self.emit(format!("j {}", done)); - self.emit_label(&malformed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::DaoFieldMalformed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&mismatch); - self.emit(format!("li a0, {}", CellScriptRuntimeError::DaoHeaderLineageMismatch.code())); - self.emit_label(&done); - self.emit("ld ra, 120(sp)"); - self.emit("addi sp, sp, 128"); - self.emit("ret"); - } - - fn emit_runtime_dao_require_input_since_at_least_helper(&mut self, enabled: bool) { - self.emit_global("__dao_require_input_since_at_least"); - self.emit_label("__dao_require_input_since_at_least"); - self.emit("# cellscript abi: DAO input since lower-bound requirement"); - self.emit("# cellscript abi: args a0=input SourceView, a1=required_since; enforces loaded_since >= required_since"); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - - const SIZE_OFFSET: usize = 8; - const REQUIRED_SINCE_OFFSET: usize = 16; - const SINCE_OFFSET: usize = 24; - - let invalid = self.fresh_label("dao_since_input_source_invalid"); - let failed = self.fresh_label("dao_since_load_failed"); - let malformed = self.fresh_label("dao_since_field_malformed"); - let immature = self.fresh_label("dao_since_immature"); - let done = self.fresh_label("dao_since_done"); - let abi = self.runtime_abi(); - - self.emit("addi sp, sp, -48"); - self.emit("sd ra, 40(sp)"); - self.emit(format!("sd a1, {}(sp)", REQUIRED_SINCE_OFFSET)); - - self.emit_decode_input_source_view_to_t1_t2(&invalid); - self.emit("li t0, 8"); - self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", SINCE_OFFSET)); - self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit("addi a3, t1, 0"); - self.emit("addi a4, t2, 0"); - self.emit(format!("li a5, {}", CKB_INPUT_FIELD_SINCE)); - self.emit(format!("li a7, {}", abi.load_input_by_field)); - self.emit("ecall"); - self.emit(format!("bnez a0, {}", failed)); - self.emit(format!("ld t0, {}(sp)", SIZE_OFFSET)); - self.emit("li t1, 8"); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - - self.emit(format!("ld t0, {}(sp)", SINCE_OFFSET)); - self.emit(format!("ld t1, {}(sp)", REQUIRED_SINCE_OFFSET)); - self.emit("sltu t2, t0, t1"); - self.emit(format!("bnez t2, {}", immature)); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CellLoadFailed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&malformed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::DaoFieldMalformed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&immature); - self.emit(format!("li a0, {}", CellScriptRuntimeError::DaoMaturityViolation.code())); - self.emit_label(&done); - self.emit("ld ra, 40(sp)"); - self.emit("addi sp, sp, 48"); - self.emit("ret"); - } - - fn emit_runtime_dao_require_input_relative_epoch_since_at_least_helper(&mut self, enabled: bool) { - self.emit_global("__dao_require_input_relative_epoch_since_at_least"); - self.emit_label("__dao_require_input_relative_epoch_since_at_least"); - self.emit("# cellscript abi: DAO relative epoch since maturity requirement"); - self.emit("# cellscript abi: args a0=input SourceView, a1=epoch_number, a2=epoch_index, a3=epoch_length"); - self.emit("# cellscript abi: loads input since, requires RFC0017 relative epoch flags, and compares epoch fractions"); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - - const SIZE_OFFSET: usize = 8; - const REQUIRED_NUMBER_OFFSET: usize = 16; - const REQUIRED_INDEX_OFFSET: usize = 24; - const REQUIRED_LENGTH_OFFSET: usize = 32; - const SINCE_OFFSET: usize = 40; - const LOADED_NUMBER_OFFSET: usize = 48; - const LOADED_INDEX_OFFSET: usize = 56; - const LOADED_LENGTH_OFFSET: usize = 64; - - let invalid = self.fresh_label("dao_epoch_since_input_source_invalid"); - let failed = self.fresh_label("dao_epoch_since_load_failed"); - let malformed = self.fresh_label("dao_epoch_since_malformed"); - let immature = self.fresh_label("dao_epoch_since_immature"); - let success = self.fresh_label("dao_epoch_since_success"); - let done = self.fresh_label("dao_epoch_since_done"); - let abi = self.runtime_abi(); - - self.emit("addi sp, sp, -80"); - self.emit("sd ra, 72(sp)"); - self.emit(format!("sd a1, {}(sp)", REQUIRED_NUMBER_OFFSET)); - self.emit(format!("sd a2, {}(sp)", REQUIRED_INDEX_OFFSET)); - self.emit(format!("sd a3, {}(sp)", REQUIRED_LENGTH_OFFSET)); - - self.emit(format!("li t0, {}", CKB_EPOCH_NUMBER_BOUND)); - self.emit("sltu t1, a1, t0"); - self.emit(format!("beqz t1, {}", malformed)); - self.emit(format!("li t0, {}", CKB_EPOCH_FRACTION_BOUND)); - self.emit("sltu t1, a2, t0"); - self.emit(format!("beqz t1, {}", malformed)); - self.emit("sltu t1, a3, t0"); - self.emit(format!("beqz t1, {}", malformed)); - self.emit(format!("beqz a3, {}", malformed)); - self.emit("sltu t1, a2, a3"); - self.emit(format!("beqz t1, {}", malformed)); - - self.emit_decode_input_source_view_to_t1_t2(&invalid); - self.emit("li t0, 8"); - self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", SINCE_OFFSET)); - self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit("addi a3, t1, 0"); - self.emit("addi a4, t2, 0"); - self.emit(format!("li a5, {}", CKB_INPUT_FIELD_SINCE)); - self.emit(format!("li a7, {}", abi.load_input_by_field)); - self.emit("ecall"); - self.emit(format!("bnez a0, {}", failed)); - self.emit(format!("ld t0, {}(sp)", SIZE_OFFSET)); - self.emit("li t1, 8"); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - - self.emit(format!("ld t0, {}(sp)", SINCE_OFFSET)); - self.emit("li t1, 1"); - self.emit("slli t1, t1, 63"); - self.emit("and t2, t0, t1"); - self.emit(format!("beqz t2, {}", malformed)); - self.emit(format!("li t1, {}", CKB_SINCE_REMAIN_FLAGS_BITS)); - self.emit("and t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - self.emit(format!("li t1, {}", CKB_SINCE_METRIC_TYPE_FLAG_MASK)); - self.emit("and t2, t0, t1"); - self.emit(format!("li t3, {}", CKB_SINCE_EPOCH_NUMBER_WITH_FRACTION_FLAG)); - self.emit("sub t4, t2, t3"); - self.emit(format!("bnez t4, {}", malformed)); - - self.emit(format!("li t1, {}", CKB_SINCE_VALUE_MASK)); - self.emit("and t0, t0, t1"); - self.emit(format!("li t1, {}", CKB_EPOCH_NUMBER_MASK)); - self.emit("and t2, t0, t1"); - self.emit("srai t3, t0, 24"); - self.emit(format!("li t1, {}", CKB_EPOCH_FRACTION_MASK)); - self.emit("and t3, t3, t1"); - self.emit("srai t4, t0, 40"); - self.emit("and t4, t4, t1"); - self.emit(format!("beqz t4, {}", malformed)); - self.emit("sltu t5, t3, t4"); - self.emit(format!("beqz t5, {}", malformed)); - self.emit(format!("sd t2, {}(sp)", LOADED_NUMBER_OFFSET)); - self.emit(format!("sd t3, {}(sp)", LOADED_INDEX_OFFSET)); - self.emit(format!("sd t4, {}(sp)", LOADED_LENGTH_OFFSET)); - - self.emit(format!("ld t0, {}(sp)", REQUIRED_NUMBER_OFFSET)); - self.emit("sltu t1, t0, t2"); - self.emit(format!("bnez t1, {}", success)); - self.emit("sltu t1, t2, t0"); - self.emit(format!("bnez t1, {}", immature)); - self.emit(format!("ld t0, {}(sp)", LOADED_INDEX_OFFSET)); - self.emit(format!("ld t1, {}(sp)", REQUIRED_LENGTH_OFFSET)); - self.emit("mul t2, t0, t1"); - self.emit(format!("ld t0, {}(sp)", REQUIRED_INDEX_OFFSET)); - self.emit(format!("ld t1, {}(sp)", LOADED_LENGTH_OFFSET)); - self.emit("mul t3, t0, t1"); - self.emit("sltu t4, t2, t3"); - self.emit(format!("bnez t4, {}", immature)); - - self.emit_label(&success); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CellLoadFailed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&malformed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSinceMalformed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&immature); - self.emit(format!("li a0, {}", CellScriptRuntimeError::DaoMaturityViolation.code())); - self.emit_label(&done); - self.emit("ld ra, 72(sp)"); - self.emit("addi sp, sp, 80"); - self.emit("ret"); - } - - fn emit_runtime_xudt_amount_word_helper(&mut self, symbol: &str, detail: &str, offset: u64, enabled: bool) { - self.emit_global(symbol); - self.emit_label(symbol); - self.emit(format!("# cellscript abi: {} via LOAD_CELL_DATA offset={}", detail, offset)); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("addi a1, a0, 0"); - self.emit("ret"); - return; - } - let invalid = self.fresh_label("source_view_invalid"); - let done = self.fresh_label("xudt_amount_done"); - let failed = self.fresh_label("xudt_amount_failed"); - let abi = self.runtime_abi(); - self.emit("addi sp, sp, -48"); - self.emit("sd ra, 40(sp)"); - self.emit_decode_source_view_to_t1_t2(&invalid); - self.emit("li t0, 8"); - self.emit("sd t0, 8(sp)"); - self.emit("addi a0, sp, 16"); - self.emit("addi a1, sp, 8"); - self.emit(format!("li a2, {}", offset)); - self.emit("addi a3, t1, 0"); - self.emit("addi a4, t2, 0"); - self.emit(format!("li a7, {}", abi.load_cell_data)); - self.emit("ecall"); - self.emit(format!("bnez a0, {}", failed)); - self.emit("ld a0, 16(sp)"); - self.emit("li a1, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit("addi a1, a0, 0"); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::XudtBindingMismatch.code())); - self.emit("addi a1, a0, 0"); - self.emit_label(&done); - self.emit("ld ra, 40(sp)"); - self.emit("addi sp, sp, 48"); - self.emit("ret"); - } - - fn emit_runtime_xudt_require_owner_mode_input_type_helper(&mut self, enabled: bool) { - self.emit_runtime_cell_hash_requirement_helper( - "__xudt_require_owner_mode_input_type", - "xUDT owner-mode input-type full 32-byte binding check", - CKB_CELL_FIELD_TYPE_HASH, - CellScriptRuntimeError::XudtBindingMismatch, - enabled, - ); - } - - fn emit_stack_u32_le_to(&mut self, dest: &str, stack_offset: usize) { - self.emit(format!("lbu {}, {}(sp)", dest, stack_offset)); - self.emit(format!("lbu t4, {}(sp)", stack_offset + 1)); - self.emit("slli t4, t4, 8"); - self.emit(format!("or {}, {}, t4", dest, dest)); - self.emit(format!("lbu t4, {}(sp)", stack_offset + 2)); - self.emit("slli t4, t4, 16"); - self.emit(format!("or {}, {}, t4", dest, dest)); - self.emit(format!("lbu t4, {}(sp)", stack_offset + 3)); - self.emit("slli t4, t4, 24"); - self.emit(format!("or {}, {}, t4", dest, dest)); - } - - fn emit_u32_le_from_base_to(&mut self, dest: &str, base: &str, offset: usize, scratch: &str) { - self.emit(format!("lbu {}, {}({})", dest, offset, base)); - self.emit(format!("lbu {}, {}({})", scratch, offset + 1, base)); - self.emit(format!("slli {}, {}, 8", scratch, scratch)); - self.emit(format!("or {}, {}, {}", dest, dest, scratch)); - self.emit(format!("lbu {}, {}({})", scratch, offset + 2, base)); - self.emit(format!("slli {}, {}, 16", scratch, scratch)); - self.emit(format!("or {}, {}, {}", dest, dest, scratch)); - self.emit(format!("lbu {}, {}({})", scratch, offset + 3, base)); - self.emit(format!("slli {}, {}, 24", scratch, scratch)); - self.emit(format!("or {}, {}, {}", dest, dest, scratch)); - } - - fn emit_runtime_xudt_require_owner_mode_type_args_helper(&mut self, enabled: bool) { - self.emit_global("__xudt_require_owner_mode_type_args"); - self.emit_label("__xudt_require_owner_mode_type_args"); - self.emit("# cellscript abi: xUDT owner-mode Type Script args requirement"); - self.emit("# cellscript abi: args a0=SourceView, a1=owner_hash_ptr, a2=owner_hash_len, a3=flags_u32"); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - - const SCRIPT_BUFFER_OFFSET: usize = 16; - const SCRIPT_SIZE_OFFSET: usize = 8; - const OWNER_ARGS_OFFSET: usize = SCRIPT_BUFFER_OFFSET + 53; - const FLAGS_ARGS_OFFSET: usize = OWNER_ARGS_OFFSET + 32; - - let invalid = self.fresh_label("xudt_args_source_invalid"); - let bad_expected = self.fresh_label("xudt_args_expected_invalid"); - let malformed = self.fresh_label("xudt_script_malformed"); - let failed = self.fresh_label("xudt_script_load_failed"); - let mismatch = self.fresh_label("xudt_args_mismatch"); - let done = self.fresh_label("xudt_args_done"); - let abi = self.runtime_abi(); - - self.emit("addi sp, sp, -192"); - self.emit("sd ra, 184(sp)"); - self.emit("sd a1, 176(sp)"); - self.emit("sd a2, 168(sp)"); - self.emit("sd a3, 160(sp)"); - - self.emit(format!("beqz a1, {}", bad_expected)); - self.emit("li t0, 32"); - self.emit("sub t1, a2, t0"); - self.emit(format!("bnez t1, {}", bad_expected)); - self.emit("li t0, 4294967296"); - self.emit("sltu t1, a3, t0"); - self.emit(format!("beqz t1, {}", mismatch)); - - self.emit_decode_source_view_to_t1_t2(&invalid); - self.emit("li t0, 128"); - self.emit(format!("sd t0, {}(sp)", SCRIPT_SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); - self.emit(format!("addi a1, sp, {}", SCRIPT_SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit("addi a3, t1, 0"); - self.emit("addi a4, t2, 0"); - self.emit(format!("li a5, {}", CKB_CELL_FIELD_TYPE)); - self.emit(format!("li a7, {}", abi.load_cell_by_field)); - self.emit("ecall"); - self.emit(format!("bnez a0, {}", failed)); - - self.emit(format!("ld t0, {}(sp)", SCRIPT_SIZE_OFFSET)); - self.emit("li t1, 89"); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - - for (offset, expected) in [(0usize, 89u64), (4, 16), (8, 48), (12, 49), (49, 36)] { - self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + offset); - self.emit(format!("li t1, {}", expected)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", malformed)); - } - - self.emit(format!("addi a0, sp, {}", OWNER_ARGS_OFFSET)); - self.emit("ld a1, 176(sp)"); - self.emit("li a2, 32"); - self.emit("call __cellscript_memcmp_fixed"); - self.emit(format!("bnez a0, {}", mismatch)); - - self.emit_stack_u32_le_to("t0", FLAGS_ARGS_OFFSET); - self.emit("ld t1, 160(sp)"); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", mismatch)); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - - self.emit_label(&invalid); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit(format!("j {}", done)); - self.emit_label(&bad_expected); - self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); - self.emit(format!("j {}", done)); - self.emit_label(&malformed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&mismatch); - self.emit(format!("li a0, {}", CellScriptRuntimeError::XudtBindingMismatch.code())); - self.emit_label(&done); - self.emit("ld ra, 184(sp)"); - self.emit("addi sp, sp, 192"); - self.emit("ret"); - } - - fn emit_runtime_xudt_require_owner_mode_type_args_current_script_helper(&mut self, enabled: bool) { - self.emit_global("__xudt_require_owner_mode_type_args_current_script"); - self.emit_label("__xudt_require_owner_mode_type_args_current_script"); - self.emit("# cellscript abi: xUDT owner-mode Type Script args requirement bound to current script hash"); - self.emit("# cellscript abi: args a0=SourceView, a1=flags_u32; owner hash is LOAD_SCRIPT_HASH(current script)"); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - - const SIZE_OFFSET: usize = 8; - const SOURCE_VIEW_OFFSET: usize = 16; - const FLAGS_OFFSET: usize = 24; - const SCRIPT_HASH_OFFSET: usize = 32; - - let hash_failed = self.fresh_label("xudt_current_script_hash_load_failed"); - let hash_malformed = self.fresh_label("xudt_current_script_hash_malformed"); - let done = self.fresh_label("xudt_current_script_args_done"); - let abi = self.runtime_abi(); - - self.emit("addi sp, sp, -80"); - self.emit("sd ra, 72(sp)"); - self.emit(format!("sd a0, {}(sp)", SOURCE_VIEW_OFFSET)); - self.emit(format!("sd a1, {}(sp)", FLAGS_OFFSET)); - - self.emit("li t0, 32"); - self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", SCRIPT_HASH_OFFSET)); - self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit(format!("li a7, {}", abi.load_script_hash)); - self.emit("ecall"); - self.emit(format!("bnez a0, {}", hash_failed)); - self.emit(format!("ld t0, {}(sp)", SIZE_OFFSET)); - self.emit("li t1, 32"); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", hash_malformed)); - - self.emit(format!("ld a0, {}(sp)", SOURCE_VIEW_OFFSET)); - self.emit(format!("addi a1, sp, {}", SCRIPT_HASH_OFFSET)); - self.emit("li a2, 32"); - self.emit(format!("ld a3, {}(sp)", FLAGS_OFFSET)); - self.emit("call __xudt_require_owner_mode_type_args"); - self.emit(format!("j {}", done)); - - self.emit_label(&hash_failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&hash_malformed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); - self.emit_label(&done); - self.emit("ld ra, 72(sp)"); - self.emit("addi sp, sp, 80"); - self.emit("ret"); - } - - fn emit_runtime_fungible_type_group_conservation_helper(&mut self, symbol: &str, detail: &str, enabled: bool) { - self.emit_global(symbol); - self.emit_label(symbol); - let owner_mode = symbol == FUNGIBLE_TYPE_GROUP_V1_CODEGEN_HELPER; - if owner_mode { - self.emit(format!( - "# cellscript abi: {detail}; owner-authorized issuance or non-empty input/output checked-u128 conservation" - )); - self.emit("# cellscript abi: supply authorization: 32-byte input lock hash or 0x01-tagged 32-byte input Type Script hash"); - } else { - self.emit(format!("# cellscript abi: {detail}; requires non-empty input/output groups and conserves checked u128 sums")); - } - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - - const SIZE_OFFSET: usize = 8; - const BUFFER_OFFSET: usize = 16; - const INPUT_LOW_OFFSET: usize = 32; - const INPUT_HIGH_OFFSET: usize = 40; - const OUTPUT_LOW_OFFSET: usize = 48; - const OUTPUT_HIGH_OFFSET: usize = 56; - const INDEX_OFFSET: usize = 64; - const SOURCE_OFFSET: usize = 72; - const SUM_LOW_OFFSET: usize = 80; - const SUM_HIGH_OFFSET: usize = 88; - const CURRENT_SCRIPT_BUFFER_OFFSET: usize = 96; - const CURRENT_SCRIPT_SIZE_OFFSET: usize = 240; - const OWNER_LOCK_BUFFER_OFFSET: usize = 192; - const OWNER_LOCK_SIZE_OFFSET: usize = 224; - const OWNER_INPUT_INDEX_OFFSET: usize = 232; - const OWNER_AUTHORIZED_OFFSET: usize = 248; - const OWNER_AUTHORITY_FIELD_OFFSET: usize = 256; - const LEGACY_OWNER_SCRIPT_SIZE: u64 = 85; - const TAGGED_TYPE_OWNER_SCRIPT_SIZE: u64 = 86; - const TAGGED_TYPE_AUTHORITY: u64 = 1; - - let frame_size = if owner_mode { 272usize } else { 112usize }; - let ra_offset = frame_size - 8; - - let conservation_start = self.fresh_label("fungible_group_conservation_start"); - let owner_script_loaded = self.fresh_label("fungible_group_owner_script_loaded"); - let owner_legacy_lock_mode = self.fresh_label("fungible_group_owner_legacy_lock_mode"); - let owner_tagged_type_mode = self.fresh_label("fungible_group_owner_tagged_type_mode"); - let owner_authority_mode_ready = self.fresh_label("fungible_group_owner_authority_mode_ready"); - let owner_scan_loop = self.fresh_label("fungible_group_owner_scan_loop"); - let owner_lock_loaded = self.fresh_label("fungible_group_owner_lock_loaded"); - let owner_not_matched = self.fresh_label("fungible_group_owner_not_matched"); - let owner_matched = self.fresh_label("fungible_group_owner_matched"); - let owner_expected_type_hash = self.fresh_label("fungible_group_owner_expected_type_hash"); - let owner_expected_hash_ready = self.fresh_label("fungible_group_owner_expected_hash_ready"); - let owner_authorized = self.fresh_label("fungible_group_owner_authorized"); - let owner_script_failed = self.fresh_label("fungible_group_owner_script_failed"); - let owner_script_malformed = self.fresh_label("fungible_group_owner_script_malformed"); - let owner_scan_failed = self.fresh_label("fungible_group_owner_scan_failed"); - let scan_source = self.fresh_label("xudt_group_scan_source"); - let scan_loop = self.fresh_label("xudt_group_scan_loop"); - let scan_done = self.fresh_label("xudt_group_scan_done"); - let scan_failed = self.fresh_label("xudt_group_scan_failed"); - let scan_malformed = self.fresh_label("xudt_group_scan_malformed"); - let overflow = self.fresh_label("xudt_group_sum_overflow"); - let output_phase = self.fresh_label("xudt_group_output_phase"); - let compare = self.fresh_label("xudt_group_compare"); - let mismatch = self.fresh_label("xudt_group_mismatch"); - let done = self.fresh_label("xudt_group_done"); - let abi = self.runtime_abi(); - - self.emit(format!("addi sp, sp, -{}", frame_size)); - self.emit(format!("sd ra, {}(sp)", ra_offset)); - for offset in [INPUT_LOW_OFFSET, INPUT_HIGH_OFFSET, OUTPUT_LOW_OFFSET, OUTPUT_HIGH_OFFSET] { - self.emit(format!("sd zero, {}(sp)", offset)); - } - - if owner_mode { - self.emit("# cellscript abi: authority args are legacy 32-byte lock hash or 0x01 plus 32-byte policy Type Script hash"); - self.emit("li t0, 96"); - self.emit(format!("sd t0, {}(sp)", CURRENT_SCRIPT_SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", CURRENT_SCRIPT_BUFFER_OFFSET)); - self.emit(format!("addi a1, sp, {}", CURRENT_SCRIPT_SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit(format!("li a7, {}", abi.load_script)); - self.emit("ecall"); - self.emit(format!("beqz a0, {}", owner_script_loaded)); - self.emit(format!("j {}", owner_script_failed)); - - self.emit_label(&owner_script_loaded); - self.emit(format!("ld t0, {}(sp)", CURRENT_SCRIPT_SIZE_OFFSET)); - self.emit(format!("li t1, {}", LEGACY_OWNER_SCRIPT_SIZE)); - self.emit("sub t2, t0, t1"); - self.emit(format!("beqz t2, {}", owner_legacy_lock_mode)); - self.emit(format!("li t1, {}", TAGGED_TYPE_OWNER_SCRIPT_SIZE)); - self.emit("sub t2, t0, t1"); - self.emit(format!("beqz t2, {}", owner_tagged_type_mode)); - self.emit(format!("j {}", owner_script_malformed)); - - self.emit_label(&owner_legacy_lock_mode); - for (offset, expected) in [(0usize, LEGACY_OWNER_SCRIPT_SIZE), (4, 16), (8, 48), (12, 49), (49, 32)] { - self.emit_stack_u32_le_to("t0", CURRENT_SCRIPT_BUFFER_OFFSET + offset); - self.emit(format!("li t1, {}", expected)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", owner_script_malformed)); - } - self.emit(format!("li t0, {}", CKB_CELL_FIELD_LOCK_HASH)); - self.emit(format!("sd t0, {}(sp)", OWNER_AUTHORITY_FIELD_OFFSET)); - self.emit(format!("j {}", owner_authority_mode_ready)); - - self.emit_label(&owner_tagged_type_mode); - for (offset, expected) in [(0usize, TAGGED_TYPE_OWNER_SCRIPT_SIZE), (4, 16), (8, 48), (12, 49), (49, 33)] { - self.emit_stack_u32_le_to("t0", CURRENT_SCRIPT_BUFFER_OFFSET + offset); - self.emit(format!("li t1, {}", expected)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", owner_script_malformed)); - } - self.emit(format!("lbu t0, {}(sp)", CURRENT_SCRIPT_BUFFER_OFFSET + 53)); - self.emit(format!("li t1, {}", TAGGED_TYPE_AUTHORITY)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", owner_script_malformed)); - self.emit(format!("li t0, {}", CKB_CELL_FIELD_TYPE_HASH)); - self.emit(format!("sd t0, {}(sp)", OWNER_AUTHORITY_FIELD_OFFSET)); - - self.emit_label(&owner_authority_mode_ready); - - self.emit("# cellscript abi: supply authority succeeds only when an absolute Input lock/type hash equals Script args"); - self.emit(format!("sd zero, {}(sp)", OWNER_INPUT_INDEX_OFFSET)); - self.emit(format!("sd zero, {}(sp)", OWNER_AUTHORIZED_OFFSET)); - self.emit_label(&owner_scan_loop); - self.emit("li t0, 32"); - self.emit(format!("sd t0, {}(sp)", OWNER_LOCK_SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", OWNER_LOCK_BUFFER_OFFSET)); - self.emit(format!("addi a1, sp, {}", OWNER_LOCK_SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit(format!("ld a3, {}(sp)", OWNER_INPUT_INDEX_OFFSET)); - self.emit(format!("li a4, {}", CKB_SOURCE_INPUT)); - self.emit(format!("ld a5, {}(sp)", OWNER_AUTHORITY_FIELD_OFFSET)); - self.emit(format!("li a7, {}", abi.load_cell_by_field)); - self.emit("ecall"); - self.emit(format!("beqz a0, {}", owner_lock_loaded)); - self.emit(format!("li t0, {}", CKB_INDEX_OUT_OF_BOUND)); - self.emit("sub t1, a0, t0"); - self.emit(format!("beqz t1, {}", conservation_start)); - self.emit(format!("li t0, {}", CKB_ITEM_MISSING)); - self.emit("sub t1, a0, t0"); - self.emit(format!("beqz t1, {}", owner_not_matched)); - self.emit(format!("j {}", owner_scan_failed)); - - self.emit_label(&owner_lock_loaded); - self.emit(format!("ld t0, {}(sp)", OWNER_LOCK_SIZE_OFFSET)); - self.emit("li t1, 32"); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", owner_scan_failed)); - self.emit(format!("addi a0, sp, {}", OWNER_LOCK_BUFFER_OFFSET)); - self.emit(format!("ld t0, {}(sp)", OWNER_AUTHORITY_FIELD_OFFSET)); - self.emit(format!("li t1, {}", CKB_CELL_FIELD_TYPE_HASH)); - self.emit("sub t2, t0, t1"); - self.emit(format!("beqz t2, {}", owner_expected_type_hash)); - self.emit(format!("addi a1, sp, {}", CURRENT_SCRIPT_BUFFER_OFFSET + 53)); - self.emit(format!("j {}", owner_expected_hash_ready)); - self.emit_label(&owner_expected_type_hash); - self.emit(format!("addi a1, sp, {}", CURRENT_SCRIPT_BUFFER_OFFSET + 54)); - self.emit_label(&owner_expected_hash_ready); - self.emit("li a2, 32"); - self.emit("call __cellscript_memcmp_fixed"); - self.emit(format!("beqz a0, {}", owner_matched)); - self.emit(format!("j {}", owner_not_matched)); - - self.emit_label(&owner_not_matched); - self.emit(format!("ld t0, {}(sp)", OWNER_INPUT_INDEX_OFFSET)); - self.emit("addi t0, t0, 1"); - self.emit(format!("sd t0, {}(sp)", OWNER_INPUT_INDEX_OFFSET)); - self.emit(format!("j {}", owner_scan_loop)); - - self.emit_label(&owner_matched); - self.emit("li t0, 1"); - self.emit(format!("sd t0, {}(sp)", OWNER_AUTHORIZED_OFFSET)); - self.emit(format!("j {}", conservation_start)); - - self.emit_label(&owner_script_failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&owner_script_malformed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); - self.emit(format!("j {}", done)); - self.emit_label(&owner_scan_failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); - self.emit(format!("j {}", done)); - - self.emit_label(&conservation_start); - } - - self.emit(format!("li t0, {}", CKB_SOURCE_GROUP_FLAG | CKB_SOURCE_INPUT)); - self.emit(format!("sd t0, {}(sp)", SOURCE_OFFSET)); - self.emit(format!("addi t0, sp, {}", INPUT_LOW_OFFSET)); - self.emit(format!("addi t1, sp, {}", INPUT_HIGH_OFFSET)); - self.emit(format!("j {}", scan_source)); - - self.emit_label(&output_phase); - self.emit(format!("li t0, {}", CKB_SOURCE_GROUP_FLAG | CKB_SOURCE_OUTPUT)); - self.emit(format!("sd t0, {}(sp)", SOURCE_OFFSET)); - self.emit(format!("addi t0, sp, {}", OUTPUT_LOW_OFFSET)); - self.emit(format!("addi t1, sp, {}", OUTPUT_HIGH_OFFSET)); - - self.emit_label(&scan_source); - self.emit(format!("sd t0, {}(sp)", SUM_LOW_OFFSET)); - self.emit(format!("sd t1, {}(sp)", SUM_HIGH_OFFSET)); - self.emit(format!("sd zero, {}(sp)", INDEX_OFFSET)); - - self.emit_label(&scan_loop); - self.emit("li t0, 16"); - self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", BUFFER_OFFSET)); - self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit(format!("ld a3, {}(sp)", INDEX_OFFSET)); - self.emit(format!("ld a4, {}(sp)", SOURCE_OFFSET)); - self.emit(format!("li a7, {}", abi.load_cell_data)); - self.emit("ecall"); - self.emit(format!("beqz a0, {}", scan_done)); - self.emit(format!("li t0, {}", CKB_INDEX_OUT_OF_BOUND)); - self.emit("sub t1, a0, t0"); - self.emit(format!("beqz t1, {}", compare)); - self.emit(format!("j {}", scan_failed)); - - self.emit_label(&scan_done); - self.emit(format!("ld t0, {}(sp)", SIZE_OFFSET)); - self.emit("li t1, 16"); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", scan_malformed)); - self.emit(format!("ld t0, {}(sp)", SUM_LOW_OFFSET)); - self.emit(format!("ld t1, {}(sp)", SUM_HIGH_OFFSET)); - self.emit("ld t2, 16(sp)"); - self.emit("ld t3, 24(sp)"); - self.emit("ld t4, 0(t0)"); - self.emit("ld t5, 0(t1)"); - self.emit("add t6, t4, t2"); - self.emit("sltu t4, t6, t4"); - self.emit("add t5, t5, t3"); - self.emit("sltu t3, t5, t3"); - self.emit(format!("bnez t3, {}", overflow)); - self.emit("add t5, t5, t4"); - self.emit("sltu t4, t5, t4"); - self.emit(format!("bnez t4, {}", overflow)); - self.emit("sd t6, 0(t0)"); - self.emit("sd t5, 0(t1)"); - self.emit(format!("ld t0, {}(sp)", INDEX_OFFSET)); - self.emit("addi t0, t0, 1"); - self.emit(format!("sd t0, {}(sp)", INDEX_OFFSET)); - self.emit(format!("j {}", scan_loop)); - - self.emit_label(&compare); - let non_empty = self.fresh_label("fungible_group_non_empty"); - if owner_mode { - self.emit(format!("ld t4, {}(sp)", OWNER_AUTHORIZED_OFFSET)); - self.emit(format!("bnez t4, {}", non_empty)); - } - self.emit(format!("ld t3, {}(sp)", INDEX_OFFSET)); - self.emit(format!("beqz t3, {}", mismatch)); - self.emit_label(&non_empty); - self.emit(format!("ld t0, {}(sp)", SOURCE_OFFSET)); - self.emit(format!("li t1, {}", CKB_SOURCE_GROUP_FLAG | CKB_SOURCE_INPUT)); - self.emit("sub t2, t0, t1"); - self.emit(format!("beqz t2, {}", output_phase)); - if owner_mode { - self.emit(format!("ld t0, {}(sp)", OWNER_AUTHORIZED_OFFSET)); - self.emit(format!("bnez t0, {}", owner_authorized)); - } - self.emit(format!("ld t0, {}(sp)", INPUT_LOW_OFFSET)); - self.emit(format!("ld t1, {}(sp)", OUTPUT_LOW_OFFSET)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", mismatch)); - self.emit(format!("ld t0, {}(sp)", INPUT_HIGH_OFFSET)); - self.emit(format!("ld t1, {}(sp)", OUTPUT_HIGH_OFFSET)); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", mismatch)); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - if owner_mode { - self.emit_label(&owner_authorized); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - } - - self.emit_label(&scan_failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::XudtBindingMismatch.code())); - self.emit(format!("j {}", done)); - self.emit_label(&scan_malformed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::XudtBindingMismatch.code())); - self.emit(format!("j {}", done)); - self.emit_label(&overflow); - self.emit(format!("li a0, {}", CellScriptRuntimeError::AggregateAmountMismatch.code())); - self.emit(format!("j {}", done)); - self.emit_label(&mismatch); - self.emit(format!("li a0, {}", CellScriptRuntimeError::AggregateAmountMismatch.code())); - self.emit_label(&done); - self.emit(format!("ld ra, {}(sp)", ra_offset)); - self.emit(format!("addi sp, sp, {}", frame_size)); - self.emit("ret"); - } - - fn emit_runtime_xudt_require_group_amount_delta_helper(&mut self, symbol: &str, minted: bool, enabled: bool) { - self.emit_global(symbol); - self.emit_label(symbol); - if minted { - self.emit( - "# cellscript abi: scans current xUDT type group and requires sum(outputs.amount) == sum(inputs.amount) + delta", - ); - } else { - self.emit( - "# cellscript abi: scans current xUDT type group and requires sum(inputs.amount) == sum(outputs.amount) + delta", - ); - } - self.emit("# cellscript abi: args a0=delta_u128_le_ptr"); - if !enabled { - self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); - self.emit("ret"); - return; - } - - const SIZE_OFFSET: usize = 8; - const BUFFER_OFFSET: usize = 16; - const INPUT_LOW_OFFSET: usize = 32; - const INPUT_HIGH_OFFSET: usize = 40; - const OUTPUT_LOW_OFFSET: usize = 48; - const OUTPUT_HIGH_OFFSET: usize = 56; - const INDEX_OFFSET: usize = 64; - const SOURCE_OFFSET: usize = 72; - const SUM_LOW_OFFSET: usize = 80; - const SUM_HIGH_OFFSET: usize = 88; - const DELTA_PTR_OFFSET: usize = 96; - - let bad_delta = self.fresh_label("xudt_group_delta_bad"); - let scan_source = self.fresh_label("xudt_group_delta_scan_source"); - let scan_loop = self.fresh_label("xudt_group_delta_scan_loop"); - let scan_done = self.fresh_label("xudt_group_delta_scan_done"); - let scan_failed = self.fresh_label("xudt_group_delta_scan_failed"); - let scan_malformed = self.fresh_label("xudt_group_delta_scan_malformed"); - let overflow = self.fresh_label("xudt_group_delta_overflow"); - let output_phase = self.fresh_label("xudt_group_delta_output_phase"); - let compare = self.fresh_label("xudt_group_delta_compare"); - let mismatch = self.fresh_label("xudt_group_delta_mismatch"); - let done = self.fresh_label("xudt_group_delta_done"); - let abi = self.runtime_abi(); - - self.emit("addi sp, sp, -128"); - self.emit("sd ra, 120(sp)"); - self.emit(format!("beqz a0, {}", bad_delta)); - self.emit(format!("sd a0, {}(sp)", DELTA_PTR_OFFSET)); - for offset in [INPUT_LOW_OFFSET, INPUT_HIGH_OFFSET, OUTPUT_LOW_OFFSET, OUTPUT_HIGH_OFFSET] { - self.emit(format!("sd zero, {}(sp)", offset)); - } - - self.emit(format!("li t0, {}", CKB_SOURCE_GROUP_FLAG | CKB_SOURCE_INPUT)); - self.emit(format!("sd t0, {}(sp)", SOURCE_OFFSET)); - self.emit(format!("addi t0, sp, {}", INPUT_LOW_OFFSET)); - self.emit(format!("addi t1, sp, {}", INPUT_HIGH_OFFSET)); - self.emit(format!("j {}", scan_source)); - - self.emit_label(&output_phase); - self.emit(format!("li t0, {}", CKB_SOURCE_GROUP_FLAG | CKB_SOURCE_OUTPUT)); - self.emit(format!("sd t0, {}(sp)", SOURCE_OFFSET)); - self.emit(format!("addi t0, sp, {}", OUTPUT_LOW_OFFSET)); - self.emit(format!("addi t1, sp, {}", OUTPUT_HIGH_OFFSET)); - - self.emit_label(&scan_source); - self.emit(format!("sd t0, {}(sp)", SUM_LOW_OFFSET)); - self.emit(format!("sd t1, {}(sp)", SUM_HIGH_OFFSET)); - self.emit(format!("sd zero, {}(sp)", INDEX_OFFSET)); - - self.emit_label(&scan_loop); - self.emit("li t0, 16"); - self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); - self.emit(format!("addi a0, sp, {}", BUFFER_OFFSET)); - self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); - self.emit("li a2, 0"); - self.emit(format!("ld a3, {}(sp)", INDEX_OFFSET)); - self.emit(format!("ld a4, {}(sp)", SOURCE_OFFSET)); - self.emit(format!("li a7, {}", abi.load_cell_data)); - self.emit("ecall"); - self.emit(format!("beqz a0, {}", scan_done)); - self.emit(format!("li t0, {}", CKB_INDEX_OUT_OF_BOUND)); - self.emit("sub t1, a0, t0"); - self.emit(format!("beqz t1, {}", compare)); - self.emit(format!("j {}", scan_failed)); - - self.emit_label(&scan_done); - self.emit(format!("ld t0, {}(sp)", SIZE_OFFSET)); - self.emit("li t1, 16"); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", scan_malformed)); - self.emit(format!("ld t0, {}(sp)", SUM_LOW_OFFSET)); - self.emit(format!("ld t1, {}(sp)", SUM_HIGH_OFFSET)); - self.emit("ld t2, 16(sp)"); - self.emit("ld t3, 24(sp)"); - self.emit("ld t4, 0(t0)"); - self.emit("ld t5, 0(t1)"); - self.emit("add t6, t4, t2"); - self.emit("sltu t4, t6, t4"); - self.emit("add t5, t5, t3"); - self.emit("sltu t3, t5, t3"); - self.emit(format!("bnez t3, {}", overflow)); - self.emit("add t5, t5, t4"); - self.emit("sltu t4, t5, t4"); - self.emit(format!("bnez t4, {}", overflow)); - self.emit("sd t6, 0(t0)"); - self.emit("sd t5, 0(t1)"); - self.emit(format!("ld t0, {}(sp)", INDEX_OFFSET)); - self.emit("addi t0, t0, 1"); - self.emit(format!("sd t0, {}(sp)", INDEX_OFFSET)); - self.emit(format!("j {}", scan_loop)); - - self.emit_label(&compare); - self.emit(format!("ld t0, {}(sp)", SOURCE_OFFSET)); - self.emit(format!("li t1, {}", CKB_SOURCE_GROUP_FLAG | CKB_SOURCE_INPUT)); - self.emit("sub t2, t0, t1"); - self.emit(format!("beqz t2, {}", output_phase)); - - self.emit(format!("ld a0, {}(sp)", DELTA_PTR_OFFSET)); - self.emit("ld t2, 0(a0)"); - self.emit("ld t3, 8(a0)"); - if minted { - self.emit(format!("ld t0, {}(sp)", INPUT_LOW_OFFSET)); - self.emit(format!("ld t1, {}(sp)", INPUT_HIGH_OFFSET)); - self.emit(format!("ld t4, {}(sp)", OUTPUT_LOW_OFFSET)); - self.emit(format!("ld t5, {}(sp)", OUTPUT_HIGH_OFFSET)); - } else { - self.emit(format!("ld t0, {}(sp)", OUTPUT_LOW_OFFSET)); - self.emit(format!("ld t1, {}(sp)", OUTPUT_HIGH_OFFSET)); - self.emit(format!("ld t4, {}(sp)", INPUT_LOW_OFFSET)); - self.emit(format!("ld t5, {}(sp)", INPUT_HIGH_OFFSET)); - } - self.emit("add t6, t0, t2"); - self.emit("sltu t0, t6, t0"); - self.emit("add t1, t1, t3"); - self.emit("sltu t3, t1, t3"); - self.emit(format!("bnez t3, {}", overflow)); - self.emit("add t1, t1, t0"); - self.emit("sltu t0, t1, t0"); - self.emit(format!("bnez t0, {}", overflow)); - self.emit("sub t0, t6, t4"); - self.emit(format!("bnez t0, {}", mismatch)); - self.emit("sub t0, t1, t5"); - self.emit(format!("bnez t0, {}", mismatch)); - self.emit("li a0, 0"); - self.emit(format!("j {}", done)); - - self.emit_label(&bad_delta); - self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); - self.emit(format!("j {}", done)); - self.emit_label(&scan_failed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::XudtBindingMismatch.code())); - self.emit(format!("j {}", done)); - self.emit_label(&scan_malformed); - self.emit(format!("li a0, {}", CellScriptRuntimeError::XudtBindingMismatch.code())); - self.emit(format!("j {}", done)); - self.emit_label(&overflow); - self.emit(format!("li a0, {}", CellScriptRuntimeError::AggregateAmountMismatch.code())); - self.emit(format!("j {}", done)); - self.emit_label(&mismatch); - self.emit(format!("li a0, {}", CellScriptRuntimeError::AggregateAmountMismatch.code())); - self.emit_label(&done); - self.emit("ld ra, 120(sp)"); - self.emit("addi sp, sp, 128"); - self.emit("ret"); - } - - fn emit_runtime_memcmp_fixed(&mut self) { - self.emit_global("__cellscript_memcmp_fixed"); - self.emit_label("__cellscript_memcmp_fixed"); - self.emit("# cellscript abi: fixed-byte helper compares a0/a1 for a2 bytes; returns a0=0 when equal"); - let loop_label = ".L__cellscript_memcmp_fixed_loop"; - let mismatch_label = ".L__cellscript_memcmp_fixed_mismatch"; - let equal_label = ".L__cellscript_memcmp_fixed_equal"; - self.emit(format!("beqz a2, {}", equal_label)); - self.emit_label(loop_label); - self.emit("lbu t0, 0(a0)"); - self.emit("lbu t1, 0(a1)"); - self.emit("sub t2, t0, t1"); - self.emit(format!("bnez t2, {}", mismatch_label)); - self.emit("addi a0, a0, 1"); - self.emit("addi a1, a1, 1"); - self.emit("addi a2, a2, -1"); - self.emit(format!("bnez a2, {}", loop_label)); - self.emit_label(equal_label); - self.emit("li a0, 0"); - self.emit("ret"); - self.emit_label(mismatch_label); - self.emit("li a0, 1"); - self.emit("ret"); - } - - fn emit_runtime_memzero_fixed(&mut self) { - self.emit_global("__cellscript_memzero_fixed"); - self.emit_label("__cellscript_memzero_fixed"); - self.emit("# cellscript abi: fixed-byte helper checks a0 for a1 zero bytes; returns a0=0 when all zero"); - let loop_label = ".L__cellscript_memzero_fixed_loop"; - let mismatch_label = ".L__cellscript_memzero_fixed_mismatch"; - let equal_label = ".L__cellscript_memzero_fixed_equal"; - self.emit(format!("beqz a1, {}", equal_label)); - self.emit_label(loop_label); - self.emit("lbu t0, 0(a0)"); - self.emit(format!("bnez t0, {}", mismatch_label)); - self.emit("addi a0, a0, 1"); - self.emit("addi a1, a1, -1"); - self.emit(format!("bnez a1, {}", loop_label)); - self.emit_label(equal_label); - self.emit("li a0, 0"); - self.emit("ret"); - self.emit_label(mismatch_label); - self.emit("li a0, 1"); - self.emit("ret"); - } - - fn emit_runtime_memcpy_fixed(&mut self) { - self.emit_global("__cellscript_memcpy_fixed"); - self.emit_label("__cellscript_memcpy_fixed"); - self.emit("# cellscript abi: fixed-byte helper copies a0 to a1 for a2 bytes; returns a0=0"); - let loop_label = ".L__cellscript_memcpy_fixed_loop"; - let done_label = ".L__cellscript_memcpy_fixed_done"; - self.emit(format!("beqz a2, {}", done_label)); - self.emit_label(loop_label); - self.emit("lbu t0, 0(a0)"); - self.emit("sb t0, 0(a1)"); - self.emit("addi a0, a0, 1"); - self.emit("addi a1, a1, 1"); - self.emit("addi a2, a2, -1"); - self.emit(format!("bnez a2, {}", loop_label)); - self.emit_label(done_label); - self.emit("li a0, 0"); - self.emit("ret"); - } - - fn emit_runtime_size_guards(&mut self) { - self.emit_global("__cellscript_require_min_size"); - self.emit_label("__cellscript_require_min_size"); - self.emit("# cellscript abi: returns a0=0 when actual size a0 is at least required size a1"); - self.emit("slt a0, a0, a1"); - self.emit("ret"); - - self.emit_global("__cellscript_require_exact_size"); - self.emit_label("__cellscript_require_exact_size"); - self.emit("# cellscript abi: returns a0=0 when actual size a0 equals expected size a1"); - self.emit("sub a0, a0, a1"); - self.emit("ret"); - } - - fn emit_runtime_header_field_u64(&mut self, symbol: &str, field_name: &str, field_id: u64, enabled: bool, disabled_reason: &str) { - self.emit_global(symbol); - self.emit_label(symbol); - if !enabled { - self.emit(format!("# cellscript abi: {}", disabled_reason)); - self.emit_runtime_error_comment(CellScriptRuntimeError::ConsumeInvalidOperand); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ConsumeInvalidOperand.code())); - self.emit("ret"); - return; - } - - let abi = self.runtime_abi(); - self.emit_large_addi("sp", "sp", -32); - self.emit_stack_store("ra", 24); - self.emit(format!("# cellscript abi: LOAD_HEADER_BY_FIELD field={} source=HeaderDep index=0", field_name)); - self.emit("li t0, 8"); - self.emit_stack_store("t0", 8); - self.emit_sp_addi("a0", 16); - self.emit_sp_addi("a1", 8); - self.emit("li a2, 0"); - self.emit("li a3, 0"); - self.emit(format!("li a4, {}", abi.source_header_dep)); - self.emit(format!("li a5, {}", field_id)); - self.emit(format!("li a7, {}", abi.load_header_by_field)); - self.emit("ecall"); - self.emit_stack_load("a0", 16); - self.emit_stack_load("ra", 24); - self.emit_large_addi("sp", "sp", 32); - self.emit("ret"); - } - - fn emit_runtime_input_field_u64(&mut self, symbol: &str, field_name: &str, field_id: u64, enabled: bool, disabled_reason: &str) { - self.emit_global(symbol); - self.emit_label(symbol); - if !enabled { - self.emit(format!("# cellscript abi: {}", disabled_reason)); - self.emit_runtime_error_comment(CellScriptRuntimeError::ConsumeInvalidOperand); - self.emit(format!("li a0, {}", CellScriptRuntimeError::ConsumeInvalidOperand.code())); - self.emit("ret"); - return; - } - - let abi = self.runtime_abi(); - self.emit_large_addi("sp", "sp", -32); - self.emit_stack_store("ra", 24); - self.emit(format!("# cellscript abi: LOAD_INPUT_BY_FIELD field={} source=GroupInput index=0", field_name)); - self.emit("li t0, 8"); - self.emit_stack_store("t0", 8); - self.emit_sp_addi("a0", 16); - self.emit_sp_addi("a1", 8); - self.emit("li a2, 0"); - self.emit("li a3, 0"); - self.emit(format!("li a4, {}", abi.source_group_input)); - self.emit(format!("li a5, {}", field_id)); - self.emit(format!("li a7, {}", abi.load_input_by_field)); - self.emit("ecall"); - self.emit_stack_load("a0", 16); - self.emit_stack_load("ra", 24); - self.emit_large_addi("sp", "sp", 32); - self.emit("ret"); - } - - fn assemble(&self, format: ArtifactFormat) -> Result> { - let assembly_text = self.assembly.join("\n"); - match format { - ArtifactFormat::RiscvAssembly => Ok(assembly_text.into_bytes()), - ArtifactFormat::RiscvElf => { - // All former non-executable runtime paths now have real RISC-V - // lowerings or fail-closed traps with specific error codes. - // ELF emission is always permitted. - assemble_elf(&self.assembly) - } - } - } -} - -pub fn generate(ir: &IrModule, options: &CodegenOptions, format: ArtifactFormat) -> Result> { - let generator = CodeGenerator::new(options.clone()); - generator.generate(ir, format) -} - -fn with_codegen_code(error: CompileError, code: &'static str) -> CompileError { - if error.code.is_some() { - error - } else { - error.with_code(code) - } -} - -pub fn analyze_backend_shape(assembly: &str) -> Result { - let lines = assembly.lines().map(str::to_string).collect::>(); - MachineLayoutPlan::build(&lines).map(|plan| plan.metrics.into()) -} - -fn first_entrypoint(ir: &IrModule) -> Option<(&str, &[IrParam])> { - for item in &ir.items { - if let IrItem::Action(action) = item { - if action.name == "main" { - return Some((&action.name, &action.params)); - } - } - } - for item in &ir.items { - if let IrItem::Action(action) = item { - if action.params.is_empty() { - return Some((&action.name, &action.params)); - } - } - } - for item in &ir.items { - if let IrItem::Action(action) = item { - return Some((&action.name, &action.params)); - } - } - for item in &ir.items { - if let IrItem::Lock(lock) = item { - return Some((&lock.name, &lock.params)); - } - } - None -} - -fn entry_witness_payload_layout( - params: &[IrParam], - runtime_bound_param_indices: &BTreeSet, - enum_layouts: &HashMap, -) -> Vec { - params - .iter() - .enumerate() - .map(|(index, param)| { - if !entry_param_consumes_witness_payload(param, index, runtime_bound_param_indices) { - EntryWitnessPayloadArg { width: 0, schema_dynamic: false, unsupported: false } - } else if let Some(layout) = match ¶m.ty { - IrType::Named(name) => enum_layouts.get(name).filter(|layout| layout.has_payload()), - _ => None, - } { - EntryWitnessPayloadArg { width: layout.encoded_size, schema_dynamic: false, unsupported: false } - } else if entry_witness_dynamic_schema_param(¶m.ty) { - EntryWitnessPayloadArg { width: 4, schema_dynamic: true, unsupported: false } - } else if let Some(width) = - fixed_byte_pointer_param_width(¶m.ty).or_else(|| fixed_aggregate_pointer_param_width(¶m.ty)) - { - EntryWitnessPayloadArg { width, schema_dynamic: false, unsupported: false } - } else if let Some(width) = entry_witness_register_param_width(¶m.ty) { - EntryWitnessPayloadArg { width, schema_dynamic: false, unsupported: false } - } else { - EntryWitnessPayloadArg { width: 0, schema_dynamic: false, unsupported: true } - } - }) - .collect() -} - -fn entry_param_consumes_witness_payload(param: &IrParam, index: usize, runtime_bound_param_indices: &BTreeSet) -> bool { - param.source != ParamSource::LockArgs - && !runtime_bound_param_indices.contains(&index) - && !matches!(param.ty, IrType::Ref(_) | IrType::MutRef(_)) -} - -fn entry_witness_dynamic_schema_param(ty: &IrType) -> bool { - fixed_byte_pointer_param_width(ty).is_none() - && fixed_aggregate_pointer_param_width(ty).is_none() - && entry_witness_register_param_width(ty).is_none() -} - -fn entry_witness_register_param_width(ty: &IrType) -> Option { - fixed_register_width(ty, type_static_length(ty)).or_else(|| match ty { - IrType::Array(_, _) | IrType::Tuple(_) => type_static_length(ty).filter(|width| (1..=8).contains(width)), - IrType::Unit => Some(0), - _ => None, - }) -} - -fn named_type_name(ty: &IrType) -> Option<&str> { - match ty { - IrType::Named(name) => Some(name.as_str()), - IrType::Ref(inner) | IrType::MutRef(inner) => named_type_name(inner), - _ => None, - } -} - -fn consumed_operand_var(instruction: &IrInstruction) -> Option<&IrVar> { - let operand = match instruction { - IrInstruction::Consume { operand } - | IrInstruction::Transfer { operand, .. } - | IrInstruction::Destroy { operand, .. } - | IrInstruction::Settle { operand, .. } - | IrInstruction::ReplaceUnique { operand, .. } => operand, - IrInstruction::Claim { receipt, .. } => receipt, - _ => return None, - }; - match operand { - IrOperand::Var(var) if named_type_name(&var.ty).is_some() => Some(var), - _ => None, - } -} - -const ELF_HEADER_SIZE: usize = 64; -const ELF_PROGRAM_HEADER_SIZE: usize = 56; -const ELF_SEGMENT_ALIGN: usize = 0x1000; -const ELF_PF_X: u32 = 1; -#[cfg(test)] -const ELF_PF_W: u32 = 2; -const ELF_PF_R: u32 = 4; -const ELF_BASE_ADDR: u64 = 0x10000; -const START_TRAMPOLINE_SIZE: usize = 20; -const EXIT_SYSCALL_NUMBER: i64 = 93; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -enum SectionKind { - Text, - Rodata, -} - -#[derive(Debug, Clone)] -enum AsmOp { - Label(String), - Instruction(Instruction), - Word(u32), - Byte(u8), - Ascii(Vec), - Align(usize), -} - -#[derive(Debug, Clone, Copy)] -struct SymbolDef { - section: SectionKind, - offset: usize, -} - -#[derive(Debug, Clone, Copy)] -struct SectionLayout { - text_base: u64, - text_user_base: u64, - rodata_base: u64, -} - -impl SectionLayout { - fn for_text_user_size(text_user_size: usize) -> Self { - let rodata_offset = align_up(START_TRAMPOLINE_SIZE + text_user_size, 8); - Self { - text_base: ELF_BASE_ADDR, - text_user_base: ELF_BASE_ADDR + START_TRAMPOLINE_SIZE as u64, - rodata_base: ELF_BASE_ADDR + rodata_offset as u64, - } - } - - fn rodata_offset(&self) -> Result { - usize::try_from(self.rodata_base - self.text_base) - .map_err(|_| CompileError::new("ELF rodata offset does not fit usize", crate::error::Span::default())) - } -} - -#[derive(Debug)] -struct MachineLayoutPlan { - parsed: ParsedAssembly, - layout: SectionLayout, - cfg: MachineCfg, - order: MachineLayoutOrder, - metrics: BackendLayoutMetrics, -} - -#[derive(Debug, Clone, Copy, Default)] -struct BackendLayoutMetrics { - text_size: usize, - rodata_size: usize, - executable_text_op_count: usize, - covered_text_op_count: usize, - relaxed_branch_count: usize, - max_cond_branch_abs_distance: u64, - machine_block_count: usize, - max_machine_block_size: usize, - conditional_branch_block_count: usize, - labeled_machine_block_count: usize, - machine_cfg_edge_count: usize, - machine_call_edge_count: usize, - unreachable_machine_block_count: usize, - layout_order_block_count: usize, - layout_order_text_size: usize, -} - -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)] -pub struct BackendShapeMetrics { - pub text_size: usize, - pub rodata_size: usize, - pub executable_text_op_count: usize, - pub covered_text_op_count: usize, - pub relaxed_branch_count: usize, - pub max_cond_branch_abs_distance: u64, - pub machine_block_count: usize, - pub max_machine_block_size: usize, - pub conditional_branch_block_count: usize, - pub labeled_machine_block_count: usize, - pub machine_cfg_edge_count: usize, - pub machine_call_edge_count: usize, - pub unreachable_machine_block_count: usize, - pub layout_order_block_count: usize, - pub layout_order_text_size: usize, -} - -impl From for BackendShapeMetrics { - fn from(metrics: BackendLayoutMetrics) -> Self { - Self { - text_size: metrics.text_size, - rodata_size: metrics.rodata_size, - executable_text_op_count: metrics.executable_text_op_count, - covered_text_op_count: metrics.covered_text_op_count, - relaxed_branch_count: metrics.relaxed_branch_count, - max_cond_branch_abs_distance: metrics.max_cond_branch_abs_distance, - machine_block_count: metrics.machine_block_count, - max_machine_block_size: metrics.max_machine_block_size, - conditional_branch_block_count: metrics.conditional_branch_block_count, - labeled_machine_block_count: metrics.labeled_machine_block_count, - machine_cfg_edge_count: metrics.machine_cfg_edge_count, - machine_call_edge_count: metrics.machine_call_edge_count, - unreachable_machine_block_count: metrics.unreachable_machine_block_count, - layout_order_block_count: metrics.layout_order_block_count, - layout_order_text_size: metrics.layout_order_text_size, - } - } -} - -#[derive(Debug, Clone)] -enum Instruction { - Addi { rd: u8, rs1: u8, imm: i64 }, - Add { rd: u8, rs1: u8, rs2: u8 }, - Sub { rd: u8, rs1: u8, rs2: u8 }, - And { rd: u8, rs1: u8, rs2: u8 }, - Or { rd: u8, rs1: u8, rs2: u8 }, - Xor { rd: u8, rs1: u8, rs2: u8 }, - Mul { rd: u8, rs1: u8, rs2: u8 }, - Mulhu { rd: u8, rs1: u8, rs2: u8 }, - Div { rd: u8, rs1: u8, rs2: u8 }, - Divu { rd: u8, rs1: u8, rs2: u8 }, - Rem { rd: u8, rs1: u8, rs2: u8 }, - Remu { rd: u8, rs1: u8, rs2: u8 }, - Slt { rd: u8, rs1: u8, rs2: u8 }, - Sltu { rd: u8, rs1: u8, rs2: u8 }, - Sgt { rd: u8, rs1: u8, rs2: u8 }, - Xori { rd: u8, rs1: u8, imm: i64 }, - Seqz { rd: u8, rs: u8 }, - Snez { rd: u8, rs: u8 }, - Neg { rd: u8, rs: u8 }, - Ld { rd: u8, rs1: u8, imm: i64 }, - Lbu { rd: u8, rs1: u8, imm: i64 }, - Sb { rs2: u8, rs1: u8, imm: i64 }, - Sh { rs2: u8, rs1: u8, imm: i64 }, - Sw { rs2: u8, rs1: u8, imm: i64 }, - Sd { rs2: u8, rs1: u8, imm: i64 }, - Slli { rd: u8, rs1: u8, shamt: i64 }, - Srai { rd: u8, rs1: u8, shamt: i64 }, - Srli { rd: u8, rs1: u8, shamt: i64 }, - Li { rd: u8, imm: i128 }, - La { rd: u8, label: String }, - Call { label: String }, - Jump { label: String }, - Beq { rs1: u8, rs2: u8, label: String }, - Bne { rs1: u8, rs2: u8, label: String }, - Blt { rs1: u8, rs2: u8, label: String }, - Bge { rs1: u8, rs2: u8, label: String }, - Bltu { rs1: u8, rs2: u8, label: String }, - Bgeu { rs1: u8, rs2: u8, label: String }, - Beqz { rs: u8, label: String }, - Bnez { rs: u8, label: String }, - Ret, - Ecall, -} - -fn assemble_elf(lines: &[String]) -> Result> { - reject_unresolved_calls(lines).map_err(|error| with_codegen_code(error, "E2200"))?; - if let Some(external) = try_external_elf_toolchain(lines).map_err(|error| with_codegen_code(error, "E2400"))? { - return Ok(external); - } - assemble_elf_internal(lines) -} - -fn reject_unresolved_calls(lines: &[String]) -> Result<()> { - let mut labels = BTreeSet::new(); - let mut calls = BTreeSet::new(); - - for line in lines { - let Some(clean) = strip_comment(line) else { - continue; - }; - if let Some(label) = clean.strip_suffix(':') { - labels.insert(label.trim().to_string()); - continue; - } - if let Some(target) = clean.strip_prefix("call ") { - let target = target.trim(); - if !target.is_empty() { - calls.insert(target.to_string()); - } - } - } - - let missing = calls.difference(&labels).cloned().collect::>(); - if missing.is_empty() { - return Ok(()); - } - - Err(CompileError::without_span(format!( - "unresolved call target(s) in generated assembly: {}; production ELF emission requires all call targets to be lowered", - missing.join(", ") - ))) -} - -fn assemble_elf_internal(lines: &[String]) -> Result> { - let plan = MachineLayoutPlan::build(lines).map_err(|error| with_codegen_code(error, "E2201"))?; - let parsed = &plan.parsed; - let layout = plan.layout; - let _layout_control_metrics = ( - plan.metrics.executable_text_op_count, - plan.metrics.covered_text_op_count, - plan.metrics.relaxed_branch_count, - plan.metrics.max_cond_branch_abs_distance, - plan.metrics.machine_block_count, - plan.metrics.max_machine_block_size, - plan.metrics.conditional_branch_block_count, - plan.metrics.labeled_machine_block_count, - plan.metrics.machine_cfg_edge_count, - plan.metrics.machine_call_edge_count, - plan.metrics.unreachable_machine_block_count, - plan.metrics.layout_order_block_count, - plan.metrics.layout_order_text_size, - plan.cfg.blocks.len(), - plan.cfg.edges.len(), - plan.order.block_order.len(), - plan.order.placed_blocks.len(), - plan.order.text_size, - ); - let entry_label = parsed.entry_label.as_deref().ok_or_else(|| { - CompileError::new("ELF target requires at least one action or lock entry point", crate::error::Span::default()) - })?; - let text_user_size = plan.metrics.text_size; - let rodata_size = plan.metrics.rodata_size; - let rodata_offset = layout.rodata_offset()?; - let mut text_bytes = Vec::with_capacity(START_TRAMPOLINE_SIZE + text_user_size); - if entry_requires_explicit_parameter_abi(lines, entry_label) { - encode_li_sequence(&mut text_bytes, 10, 25)?; - } else { - let entry_addr = parsed.symbol_address(entry_label, &layout)?; - encode_call_sequence(&mut text_bytes, layout.text_base, entry_addr)?; - } - encode_li_sequence(&mut text_bytes, 17, i128::from(EXIT_SYSCALL_NUMBER))?; - text_bytes.extend_from_slice(&encode_ecall().to_le_bytes()); - debug_assert_eq!(text_bytes.len(), START_TRAMPOLINE_SIZE); - parsed - .encode_section(SectionKind::Text, &mut text_bytes, &layout, START_TRAMPOLINE_SIZE) - .map_err(|error| with_codegen_code(error, "E2202"))?; - - let mut rodata_bytes = Vec::with_capacity(rodata_size); - parsed.encode_section(SectionKind::Rodata, &mut rodata_bytes, &layout, 0).map_err(|error| with_codegen_code(error, "E2202"))?; - - let segment_file_payload_size = rodata_offset + rodata_bytes.len(); - let segment_file_offset = align_up(ELF_HEADER_SIZE + ELF_PROGRAM_HEADER_SIZE, ELF_SEGMENT_ALIGN); - let load_segment_offset = 0u64; - let load_segment_vaddr = layout.text_base.checked_sub(segment_file_offset as u64).ok_or_else(|| { - CompileError::new("ELF text base is smaller than the load segment file offset", crate::error::Span::default()) - })?; - let load_segment_file_size = segment_file_offset + segment_file_payload_size; - let mut elf = vec![0u8; load_segment_file_size]; - write_elf_header(&mut elf[..ELF_HEADER_SIZE], layout.text_base, 1)?; - write_program_header( - &mut elf[ELF_HEADER_SIZE..ELF_HEADER_SIZE + ELF_PROGRAM_HEADER_SIZE], - ELF_PF_R | ELF_PF_X, - load_segment_offset, - load_segment_vaddr, - load_segment_file_size as u64, - load_segment_file_size as u64, - )?; - - let segment = &mut elf[segment_file_offset..segment_file_offset + segment_file_payload_size]; - segment[..text_bytes.len()].copy_from_slice(&text_bytes); - segment[rodata_offset..rodata_offset + rodata_bytes.len()].copy_from_slice(&rodata_bytes); - Ok(elf) -} - -fn try_external_elf_toolchain(lines: &[String]) -> Result>> { - let Some(toolchain) = discover_external_toolchain()? else { - return Ok(None); - }; - let parsed = ParsedAssembly::from_lines(lines)?; - let entry_label = parsed.entry_label.as_deref().ok_or_else(|| { - CompileError::new("ELF target requires at least one action or lock entry point", crate::error::Span::default()) - })?; - - let temp_dir = make_external_toolchain_temp_dir()?; - let _temp_dir_cleanup = TempDirCleanup(temp_dir.clone()); - let asm_path = temp_dir.join("module.s"); - let elf_path = temp_dir.join("module.elf"); - let obj_path = temp_dir.join("module.o"); - fs::write(&asm_path, render_external_assembly(lines, entry_label)).map_err(|err| { - CompileError::new( - format!("failed to write temporary assembly file '{}': {}", asm_path.display(), err), - crate::error::Span::default(), - ) - })?; - - let external_result = match &toolchain.mode { - ExternalToolchainMode::Compiler(compiler) => run_external_command( - Command::new(compiler) - .arg("-nostdlib") - .arg("-march=rv64imac") - .arg("-mabi=lp64") - .arg("-Wl,--strip-all") - .arg("-Wl,-e,_start") - .arg("-Wl,-Ttext=0x10000") - .arg("-o") - .arg(&elf_path) - .arg(&asm_path), - "RISC-V compiler", - ), - ExternalToolchainMode::AssemblerLinker { assembler, linker } => run_external_command( - Command::new(assembler).arg("-march=rv64imac").arg("-mabi=lp64").arg(&asm_path).arg("-o").arg(&obj_path), - "RISC-V assembler", - ) - .and_then(|_| { - run_external_command( - Command::new(linker) - .arg("-m") - .arg("elf64lriscv") - .arg("--strip-all") - .arg("-e") - .arg("_start") - .arg("-Ttext") - .arg("0x10000") - .arg("-o") - .arg(&elf_path) - .arg(&obj_path), - "RISC-V linker", - ) - }), - }; - - let elf = match external_result { - Ok(()) => fs::read(&elf_path).map_err(|err| { - CompileError::new( - format!("failed to read external ELF output '{}': {}", elf_path.display(), err), - crate::error::Span::default(), - ) - }), - Err(err) => Err(err), - }; - - let elf = elf.and_then(|bytes| { - if bytes.starts_with(b"\x7fELF") { - Ok(bytes) - } else { - Err(CompileError::new( - format!("external toolchain output '{}' is not an ELF file", elf_path.display()), - crate::error::Span::default(), - )) - } - })?; - - Ok(Some(elf)) -} - -struct TempDirCleanup(PathBuf); - -impl Drop for TempDirCleanup { - fn drop(&mut self) { - let _ = fs::remove_dir_all(&self.0); - } -} - -fn render_external_assembly(lines: &[String], entry_label: &str) -> String { - let mut rendered = - vec![".section .text".to_string(), ".global _start".to_string(), ".type _start, @function".to_string(), "_start:".to_string()]; - if entry_requires_explicit_parameter_abi(lines, entry_label) { - let error = CellScriptRuntimeError::EntryWitnessAbiInvalid; - rendered.push(format!(" # cellscript runtime error {} {}", error.code(), error.name())); - rendered.push(format!(" li a0, {}", error.code())); - } else { - rendered.push(format!(" call {}", entry_label)); - } - rendered.push(format!(" li a7, {}", EXIT_SYSCALL_NUMBER)); - rendered.push(" ecall".to_string()); - rendered.extend(lines.iter().filter(|line| !line.trim_start().starts_with(".option arch,")).cloned()); - let mut rendered = rendered.join("\n"); - rendered.push('\n'); - rendered -} - -fn entry_requires_explicit_parameter_abi(lines: &[String], entry_label: &str) -> bool { - let marker = format!("# cellscript entry abi: {} requires-explicit-parameter-abi", entry_label); - lines.iter().any(|line| line.trim() == marker) -} - -#[derive(Debug, Clone)] -struct ExternalToolchain { - mode: ExternalToolchainMode, -} - -#[derive(Debug, Clone)] -enum ExternalToolchainMode { - Compiler(PathBuf), - AssemblerLinker { assembler: PathBuf, linker: PathBuf }, -} - -fn discover_external_toolchain() -> Result> { - let explicit_compiler = explicit_toolchain_path("CELLSCRIPT_RISCV_CC")?; - let explicit_assembler = explicit_toolchain_path("CELLSCRIPT_RISCV_AS")?; - let explicit_linker = explicit_toolchain_path("CELLSCRIPT_RISCV_LD")?; - - if let Some(compiler) = explicit_compiler { - if explicit_assembler.is_some() || explicit_linker.is_some() { - return Err(CompileError::new( - "set either CELLSCRIPT_RISCV_CC or CELLSCRIPT_RISCV_AS/CELLSCRIPT_RISCV_LD, not both", - crate::error::Span::default(), - )); - } - return Ok(Some(ExternalToolchain { mode: ExternalToolchainMode::Compiler(compiler) })); - } - - match (explicit_assembler, explicit_linker) { - (Some(assembler), Some(linker)) => { - return Ok(Some(ExternalToolchain { mode: ExternalToolchainMode::AssemblerLinker { assembler, linker } })); - } - (Some(_), None) | (None, Some(_)) => { - return Err(CompileError::new( - "CELLSCRIPT_RISCV_AS and CELLSCRIPT_RISCV_LD must be set together", - crate::error::Span::default(), - )); - } - (None, None) => {} - } - - Ok(None) -} - -fn explicit_toolchain_path(var: &str) -> Result> { - env::var_os(var).map(PathBuf::from).map(|path| validate_explicit_toolchain_path(var, path)).transpose() -} - -fn validate_explicit_toolchain_path(var: &str, path: PathBuf) -> Result { - if !path.is_absolute() { - return Err(CompileError::new( - format!("{} must be an absolute path, got '{}'", var, path.display()), - crate::error::Span::default(), - )); - } - - let metadata = fs::metadata(&path).map_err(|err| { - CompileError::new( - format!("{} points to unreadable toolchain path '{}': {}", var, path.display(), err), - crate::error::Span::default(), - ) - })?; - if !metadata.is_file() { - return Err(CompileError::new( - format!("{} must point to an executable file, got '{}'", var, path.display()), - crate::error::Span::default(), - )); - } - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt as _; - if metadata.permissions().mode() & 0o111 == 0 { - return Err(CompileError::new( - format!("{} path '{}' is not executable", var, path.display()), - crate::error::Span::default(), - )); - } - } - - Ok(path) -} - -fn run_external_command(command: &mut Command, label: &str) -> Result<()> { - let rendered = render_command(command); - let output = command.output().map_err(|err| { - CompileError::new(format!("failed to launch {} ({}): {}", label, rendered, err), crate::error::Span::default()) - })?; - if output.status.success() { - return Ok(()); - } - - let stderr = String::from_utf8_lossy(&output.stderr); - let message = format!("{} failed ({}): {}", label, rendered, stderr.trim()); - Err(CompileError::new(message, crate::error::Span::default())) -} - -fn render_command(command: &Command) -> String { - let program = command.get_program().to_string_lossy(); - let args = command.get_args().map(|arg| arg.to_string_lossy().into_owned()).collect::>().join(" "); - if args.is_empty() { - program.into_owned() - } else { - format!("{} {}", program, args) - } -} - -fn make_external_toolchain_temp_dir() -> Result { - let stamp = SystemTime::now().duration_since(UNIX_EPOCH).map(|duration| duration.as_nanos()).unwrap_or_default(); - let dir = env::temp_dir().join(format!("cellscript-riscv-{}-{}", std::process::id(), stamp)); - fs::create_dir_all(&dir).map_err(|err| { - CompileError::new( - format!("failed to create temporary toolchain directory '{}': {}", dir.display(), err), - crate::error::Span::default(), - ) - })?; - Ok(dir) -} - -#[derive(Debug, Default)] -struct ParsedAssembly { - text_ops: Vec, - rodata_ops: Vec, - text_size: usize, - rodata_size: usize, - symbols: HashMap, - entry_label: Option, - relaxed_text_branches: BTreeSet, -} - -impl ParsedAssembly { - fn from_lines(lines: &[String]) -> Result { - Self::from_lines_with_branch_mode(lines, BranchSizeMode::Exact(&BTreeSet::new())) - } - - fn from_lines_relaxed(lines: &[String], layout: &SectionLayout) -> Result { - let conservative = Self::from_lines_with_branch_mode(lines, BranchSizeMode::Conservative)?; - let relaxed_text_branches = conservative.relaxed_branch_indices(layout)?; - Self::from_lines_with_branch_mode(lines, BranchSizeMode::Exact(&relaxed_text_branches)) - } - - fn from_lines_with_branch_mode(lines: &[String], branch_size_mode: BranchSizeMode<'_>) -> Result { - let mut current_section = SectionKind::Text; - let mut text_size = 0usize; - let mut rodata_size = 0usize; - let mut text_ops = Vec::new(); - let mut rodata_ops = Vec::new(); - let mut symbols = HashMap::new(); - let mut globals = BTreeSet::new(); - let mut entry_label = None; - let mut fallback_entry = None; - - for line in lines { - let Some(clean) = strip_comment(line) else { - continue; - }; - if clean.is_empty() { - continue; - } - - if let Some(section) = parse_section_directive(clean)? { - current_section = section; - continue; - } - if clean.starts_with(".option ") || clean.starts_with(".type ") { - continue; - } - if let Some(symbol) = clean.strip_prefix(".global ") { - globals.insert(symbol.trim().to_string()); - continue; - } - - let (ops, offset) = match current_section { - SectionKind::Text => (&mut text_ops, &mut text_size), - SectionKind::Rodata => (&mut rodata_ops, &mut rodata_size), - }; - let op_index = ops.len(); - - if let Some(label) = clean.strip_suffix(':') { - let label = label.trim().to_string(); - let symbol = SymbolDef { section: current_section, offset: *offset }; - if symbols.insert(label.clone(), symbol).is_some() { - return Err(CompileError::new(format!("duplicate assembly label '{}'", label), crate::error::Span::default())); - } - if current_section == SectionKind::Text && globals.contains(&label) { - if fallback_entry.is_none() { - fallback_entry = Some(label.clone()); - } - if !label.starts_with("__") && entry_label.is_none() { - entry_label = Some(label.clone()); - } - } - ops.push(AsmOp::Label(label)); - continue; - } - - let op = parse_asm_op(clean)?; - *offset += op_size(&op, *offset, current_section, op_index, branch_size_mode); - ops.push(op); - } - - Ok(Self { - text_ops, - rodata_ops, - text_size, - rodata_size, - symbols, - entry_label: entry_label.or(fallback_entry), - relaxed_text_branches: branch_size_mode.relaxed_text_branches().cloned().unwrap_or_default(), - }) - } - - fn relaxed_branch_indices(&self, layout: &SectionLayout) -> Result> { - let mut relaxed = BTreeSet::new(); - let mut offset = 0usize; - for (index, op) in self.text_ops.iter().enumerate() { - if let AsmOp::Instruction(inst) = op { - if conditional_branch_parts(inst).is_some() { - let pc = layout.text_user_base + offset as u64; - let target = branch_target(inst, self, layout)?; - if !signed_bits_fit(relative_offset(pc, target)?, 13) { - relaxed.insert(index); - } - } - } - offset += op_size(op, offset, SectionKind::Text, index, BranchSizeMode::Conservative); - } - Ok(relaxed) - } - - fn section_size(&self, section: SectionKind) -> usize { - match section { - SectionKind::Text => self.text_size, - SectionKind::Rodata => self.rodata_size, - } - } - - fn symbol_address(&self, label: &str, layout: &SectionLayout) -> Result { - let symbol = self - .symbols - .get(label) - .ok_or_else(|| CompileError::new(format!("unknown assembly label '{}'", label), crate::error::Span::default()))?; - Ok(match symbol.section { - SectionKind::Text => layout.text_user_base + symbol.offset as u64, - SectionKind::Rodata => layout.rodata_base + symbol.offset as u64, - }) - } - - fn encode_section(&self, section: SectionKind, out: &mut Vec, layout: &SectionLayout, base_bias: usize) -> Result<()> { - let ops = match section { - SectionKind::Text => &self.text_ops, - SectionKind::Rodata => &self.rodata_ops, - }; - let section_base = match section { - SectionKind::Text => layout.text_user_base, - SectionKind::Rodata => layout.rodata_base, - }; - - for (op_index, op) in ops.iter().enumerate() { - match op { - AsmOp::Label(_) => {} - AsmOp::Word(word) => out.extend_from_slice(&word.to_le_bytes()), - AsmOp::Byte(byte) => out.push(*byte), - AsmOp::Ascii(bytes) => out.extend_from_slice(bytes), - AsmOp::Align(bytes) => pad_to_alignment(out, *bytes), - AsmOp::Instruction(inst) => { - let section_offset = out.len().checked_sub(base_bias).ok_or_else(|| { - CompileError::new("assembly output offset is smaller than section base bias", crate::error::Span::default()) - })?; - let pc = section_base + section_offset as u64; - encode_instruction( - out, - inst, - pc, - self, - layout, - section == SectionKind::Text && self.relaxed_text_branches.contains(&op_index), - )?; - } - } - } - - Ok(()) - } -} - -impl MachineLayoutPlan { - fn build(lines: &[String]) -> Result { - let preliminary = ParsedAssembly::from_lines_with_branch_mode(lines, BranchSizeMode::Conservative)?; - let preliminary_layout = SectionLayout::for_text_user_size(preliminary.section_size(SectionKind::Text)); - let parsed = ParsedAssembly::from_lines_relaxed(lines, &preliminary_layout)?; - let layout = SectionLayout::for_text_user_size(parsed.section_size(SectionKind::Text)); - let cfg = machine_cfg(&parsed)?; - let coverage = validate_machine_block_coverage(&parsed, &cfg)?; - let order = machine_layout_order(&cfg)?; - let metrics = parsed.layout_metrics(&layout, &cfg, &order, coverage)?; - Ok(Self { parsed, layout, cfg, order, metrics }) - } -} - -#[derive(Debug, Clone, Copy)] -struct TextOpLayout { - op_index: usize, - offset: usize, - size: usize, -} - -#[derive(Debug, Clone)] -struct MachineBlock { - label: Option, - op_start: usize, - op_end: usize, - byte_start: usize, - byte_size: usize, - terminator: MachineTerminator, -} - -#[derive(Debug, Clone)] -struct MachineCfg { - blocks: Vec, - edges: Vec, -} - -#[derive(Debug, Clone, Copy, Default)] -struct MachineBlockCoverage { - executable_text_op_count: usize, - covered_text_op_count: usize, -} - -#[derive(Debug, Clone)] -struct MachineLayoutOrder { - block_order: Vec, - placed_blocks: Vec, - text_size: usize, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct MachinePlacedBlock { - block_index: usize, - byte_start: usize, - byte_size: usize, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct MachineCfgEdge { - from: usize, - to: usize, - kind: MachineCfgEdgeKind, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum MachineCfgEdgeKind { - Fallthrough, - Jump, - ConditionalTaken, - ConditionalFallthrough, - Call, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum MachineTerminator { - Fallthrough, - Jump { target: String }, - ConditionalBranch { target: String }, - Return, -} - -fn text_op_layouts(parsed: &ParsedAssembly) -> Vec { - let mut offset = 0usize; - let mut layouts = Vec::with_capacity(parsed.text_ops.len()); - for (op_index, op) in parsed.text_ops.iter().enumerate() { - let size = op_size(op, offset, SectionKind::Text, op_index, BranchSizeMode::Exact(&parsed.relaxed_text_branches)); - layouts.push(TextOpLayout { op_index, offset, size }); - offset += size; - } - layouts -} - -fn machine_blocks(parsed: &ParsedAssembly) -> Vec { - let layouts = text_op_layouts(parsed); - let mut blocks = Vec::new(); - let mut block_start = 0usize; - let mut block_label = None; - - for (op_index, op) in parsed.text_ops.iter().enumerate() { - if let AsmOp::Label(label) = op { - if block_has_executable_ops(&parsed.text_ops[block_start..op_index]) { - blocks.push(build_machine_block(parsed, &layouts, block_start, op_index, block_label.take())); - block_start = op_index; - } - if block_label.is_none() { - block_label = Some(label.clone()); - } - continue; - } - - if instruction_terminator(op).is_some() { - blocks.push(build_machine_block(parsed, &layouts, block_start, op_index + 1, block_label.take())); - block_start = op_index + 1; - } - } - - if block_start < parsed.text_ops.len() && block_has_executable_ops(&parsed.text_ops[block_start..]) { - blocks.push(build_machine_block(parsed, &layouts, block_start, parsed.text_ops.len(), block_label)); - } - - blocks -} - -fn machine_cfg(parsed: &ParsedAssembly) -> Result { - let blocks = machine_blocks(parsed); - let label_to_block = machine_label_to_block(parsed, &blocks); - let mut edges = Vec::new(); - - for (index, block) in blocks.iter().enumerate() { - for target in machine_block_call_targets(parsed, block) { - if let Some(&target_block) = label_to_block.get(&target) { - edges.push(MachineCfgEdge { from: index, to: target_block, kind: MachineCfgEdgeKind::Call }); - } - } - match &block.terminator { - MachineTerminator::Fallthrough => { - if index + 1 < blocks.len() { - edges.push(MachineCfgEdge { from: index, to: index + 1, kind: MachineCfgEdgeKind::Fallthrough }); - } - } - MachineTerminator::Jump { target } => { - edges.push(MachineCfgEdge { - from: index, - to: machine_cfg_target_block(target, &label_to_block)?, - kind: MachineCfgEdgeKind::Jump, - }); - } - MachineTerminator::ConditionalBranch { target } => { - edges.push(MachineCfgEdge { - from: index, - to: machine_cfg_target_block(target, &label_to_block)?, - kind: MachineCfgEdgeKind::ConditionalTaken, - }); - if index + 1 < blocks.len() { - edges.push(MachineCfgEdge { from: index, to: index + 1, kind: MachineCfgEdgeKind::ConditionalFallthrough }); - } - } - MachineTerminator::Return => {} - } - } - - Ok(MachineCfg { blocks, edges }) -} - -fn validate_machine_block_coverage(parsed: &ParsedAssembly, cfg: &MachineCfg) -> Result { - let executable_text_op_count = parsed.text_ops.iter().filter(|op| !matches!(op, AsmOp::Label(_))).count(); - let mut covered = BTreeSet::new(); - - for block in &cfg.blocks { - if block.op_start >= block.op_end || block.op_end > parsed.text_ops.len() { - return Err(CompileError::new( - format!("machine block has invalid op range {}..{}", block.op_start, block.op_end), - crate::error::Span::default(), - )); - } - if !block_has_executable_ops(&parsed.text_ops[block.op_start..block.op_end]) { - return Err(CompileError::new("machine block contains no executable instructions", crate::error::Span::default())); - } - for op_index in block.op_start..block.op_end { - if matches!(parsed.text_ops[op_index], AsmOp::Label(_)) { - continue; - } - if !covered.insert(op_index) { - return Err(CompileError::new( - format!("machine block coverage overlaps text op {}", op_index), - crate::error::Span::default(), - )); - } - } - } - - if covered.len() != executable_text_op_count { - return Err(CompileError::new( - format!("machine blocks cover {} executable text ops but assembly contains {}", covered.len(), executable_text_op_count), - crate::error::Span::default(), - )); - } - - Ok(MachineBlockCoverage { executable_text_op_count, covered_text_op_count: covered.len() }) -} - -fn machine_layout_order(cfg: &MachineCfg) -> Result { - let block_order = (0..cfg.blocks.len()).collect::>(); - build_machine_layout_order(cfg, block_order) -} - -fn build_machine_layout_order(cfg: &MachineCfg, block_order: Vec) -> Result { - validate_machine_layout_order(cfg, &block_order)?; - let mut byte_start = 0usize; - let mut placed_blocks = Vec::with_capacity(block_order.len()); - for &block_index in &block_order { - let block = &cfg.blocks[block_index]; - placed_blocks.push(MachinePlacedBlock { block_index, byte_start, byte_size: block.byte_size }); - byte_start += block.byte_size; - } - Ok(MachineLayoutOrder { block_order, placed_blocks, text_size: byte_start }) -} - -fn validate_machine_layout_order(cfg: &MachineCfg, block_order: &[usize]) -> Result<()> { - if block_order.len() != cfg.blocks.len() { - return Err(CompileError::new( - format!("machine layout order contains {} blocks but CFG contains {}", block_order.len(), cfg.blocks.len()), - crate::error::Span::default(), - )); - } - - let mut seen = BTreeSet::new(); - for &block_index in block_order { - if block_index >= cfg.blocks.len() { - return Err(CompileError::new( - format!("machine layout order references missing block {}", block_index), - crate::error::Span::default(), - )); - } - if !seen.insert(block_index) { - return Err(CompileError::new( - format!("machine layout order repeats block {}", block_index), - crate::error::Span::default(), - )); - } - } - - Ok(()) -} - -fn machine_label_to_block(parsed: &ParsedAssembly, blocks: &[MachineBlock]) -> HashMap { - let mut label_to_block = HashMap::new(); - for (label, symbol) in &parsed.symbols { - if symbol.section != SectionKind::Text { - continue; - } - if let Some((block_index, _)) = blocks.iter().enumerate().find(|(_, block)| block.byte_start == symbol.offset) { - label_to_block.insert(label.clone(), block_index); - } - } - label_to_block -} - -fn machine_cfg_target_block(target: &str, label_to_block: &HashMap) -> Result { - label_to_block.get(target).copied().ok_or_else(|| { - CompileError::new(format!("assembly branch target '{}' does not start a machine block", target), crate::error::Span::default()) - }) -} - -fn machine_block_call_targets(parsed: &ParsedAssembly, block: &MachineBlock) -> Vec { - parsed.text_ops[block.op_start..block.op_end] - .iter() - .filter_map(|op| match op { - AsmOp::Instruction(Instruction::Call { label }) => Some(label.clone()), - _ => None, - }) - .collect() -} - -fn unreachable_machine_block_count(parsed: &ParsedAssembly, cfg: &MachineCfg) -> usize { - if cfg.blocks.is_empty() { - return 0; - } - let label_to_block = machine_label_to_block(parsed, &cfg.blocks); - let mut roots = parsed.entry_label.as_ref().and_then(|label| label_to_block.get(label).copied()).into_iter().collect::>(); - if roots.is_empty() { - roots.push(0); - } - let mut reachable = BTreeSet::new(); - let mut stack = roots; - while let Some(block) = stack.pop() { - if !reachable.insert(block) { - continue; - } - for edge in cfg.edges.iter().filter(|edge| edge.from == block) { - stack.push(edge.to); - } - } - cfg.blocks.len().saturating_sub(reachable.len()) -} - -fn block_has_executable_ops(ops: &[AsmOp]) -> bool { - ops.iter().any(|op| !matches!(op, AsmOp::Label(_))) -} - -fn build_machine_block( - parsed: &ParsedAssembly, - layouts: &[TextOpLayout], - op_start: usize, - op_end: usize, - label: Option, -) -> MachineBlock { - let byte_start = layouts.get(op_start).map(|layout| layout.offset).unwrap_or(0); - let byte_end = - op_end.checked_sub(1).and_then(|last| layouts.get(last).map(|layout| layout.offset + layout.size)).unwrap_or(byte_start); - let terminator = - parsed.text_ops[op_start..op_end].iter().rev().find_map(instruction_terminator).unwrap_or(MachineTerminator::Fallthrough); - MachineBlock { label, op_start, op_end, byte_start, byte_size: byte_end.saturating_sub(byte_start), terminator } -} - -fn instruction_terminator(op: &AsmOp) -> Option { - match op { - AsmOp::Instruction(Instruction::Jump { label }) => Some(MachineTerminator::Jump { target: label.clone() }), - AsmOp::Instruction(Instruction::Ret) => Some(MachineTerminator::Return), - AsmOp::Instruction(inst) => { - conditional_branch_parts(inst).map(|(_, _, label, _)| MachineTerminator::ConditionalBranch { target: label.to_string() }) - } - _ => None, - } -} - -impl ParsedAssembly { - fn layout_metrics( - &self, - layout: &SectionLayout, - machine_cfg: &MachineCfg, - machine_order: &MachineLayoutOrder, - coverage: MachineBlockCoverage, - ) -> Result { - let text_op_layouts = text_op_layouts(self); - let text_size = text_op_layouts.iter().map(|op| op.size).sum(); - let mut max_cond_branch_abs_distance = 0u64; - for op_layout in text_op_layouts { - let AsmOp::Instruction(inst) = &self.text_ops[op_layout.op_index] else { - continue; - }; - if conditional_branch_parts(inst).is_none() { - continue; - }; - let pc = layout.text_user_base + op_layout.offset as u64; - let target = branch_target(inst, self, layout)?; - let distance = relative_offset(pc, target)?.unsigned_abs(); - max_cond_branch_abs_distance = max_cond_branch_abs_distance.max(distance); - } - let machine_block_count = machine_cfg.blocks.len(); - let max_machine_block_size = machine_cfg.blocks.iter().map(|block| block.byte_size).max().unwrap_or_default(); - let conditional_branch_block_count = - machine_cfg.blocks.iter().filter(|block| matches!(block.terminator, MachineTerminator::ConditionalBranch { .. })).count(); - let labeled_machine_block_count = machine_cfg.blocks.iter().filter(|block| block.label.is_some()).count(); - let machine_cfg_edge_count = machine_cfg.edges.len(); - let machine_call_edge_count = machine_cfg.edges.iter().filter(|edge| edge.kind == MachineCfgEdgeKind::Call).count(); - let unreachable_machine_block_count = unreachable_machine_block_count(self, machine_cfg); - let layout_order_block_count = machine_order.block_order.len(); - let layout_order_text_size = machine_order.text_size; - let _covered_text_ops = machine_cfg.blocks.iter().map(|block| block.op_end.saturating_sub(block.op_start)).sum::(); - let _first_block_byte_start = machine_cfg.blocks.first().map(|block| block.byte_start).unwrap_or_default(); - Ok(BackendLayoutMetrics { - text_size, - rodata_size: self.section_size(SectionKind::Rodata), - executable_text_op_count: coverage.executable_text_op_count, - covered_text_op_count: coverage.covered_text_op_count, - relaxed_branch_count: self.relaxed_text_branches.len(), - max_cond_branch_abs_distance, - machine_block_count, - max_machine_block_size, - conditional_branch_block_count, - labeled_machine_block_count, - machine_cfg_edge_count, - machine_call_edge_count, - unreachable_machine_block_count, - layout_order_block_count, - layout_order_text_size, - }) - } -} - -fn parse_section_directive(line: &str) -> Result> { - if let Some(section) = line.strip_prefix(".section ") { - return match section.trim() { - ".text" => Ok(Some(SectionKind::Text)), - ".rodata" => Ok(Some(SectionKind::Rodata)), - other => Err(CompileError::new(format!("unsupported assembly section '{}'", other), crate::error::Span::default())), - }; - } - Ok(None) -} - -fn parse_asm_op(line: &str) -> Result { - if let Some(value) = line.strip_prefix(".word ") { - let value = parse_immediate(value.trim())?; - return Ok(AsmOp::Word( - u32::try_from(value).map_err(|_| { - CompileError::new(format!("'.word' value '{}' does not fit u32", value), crate::error::Span::default()) - })?, - )); - } - if let Some(value) = line.strip_prefix(".byte ") { - let value = parse_immediate(value.trim())?; - return Ok(AsmOp::Byte( - u8::try_from(value) - .map_err(|_| CompileError::new(format!("'.byte' value '{}' does not fit u8", value), crate::error::Span::default()))?, - )); - } - if let Some(value) = line.strip_prefix(".ascii ") { - return Ok(AsmOp::Ascii(parse_ascii_literal(value.trim())?)); - } - if let Some(value) = line.strip_prefix(".align ") { - let align_pow = parse_immediate(value.trim())?; - if !(0..=16).contains(&align_pow) { - return Err(CompileError::new(format!("unsupported .align value '{}'", align_pow), crate::error::Span::default())); - } - return Ok(AsmOp::Align(1usize << (align_pow as usize))); - } - Ok(AsmOp::Instruction(parse_instruction(line)?)) -} - -fn parse_instruction(line: &str) -> Result { - let mut parts = line.splitn(2, char::is_whitespace); - let opcode = parts.next().unwrap().trim(); - let args = parts.next().unwrap_or("").trim(); - let args = if args.is_empty() { Vec::new() } else { args.split(',').map(|arg| arg.trim().to_string()).collect() }; - - match opcode { - "addi" => Ok(Instruction::Addi { - rd: parse_register(arg(&args, 0)?)?, - rs1: parse_register(arg(&args, 1)?)?, - imm: parse_immediate(arg(&args, 2)?)?, - }), - "add" => Ok(Instruction::Add { - rd: parse_register(arg(&args, 0)?)?, - rs1: parse_register(arg(&args, 1)?)?, - rs2: parse_register(arg(&args, 2)?)?, - }), - "sub" => Ok(Instruction::Sub { - rd: parse_register(arg(&args, 0)?)?, - rs1: parse_register(arg(&args, 1)?)?, - rs2: parse_register(arg(&args, 2)?)?, - }), - "and" => Ok(Instruction::And { - rd: parse_register(arg(&args, 0)?)?, - rs1: parse_register(arg(&args, 1)?)?, - rs2: parse_register(arg(&args, 2)?)?, - }), - "or" => Ok(Instruction::Or { - rd: parse_register(arg(&args, 0)?)?, - rs1: parse_register(arg(&args, 1)?)?, - rs2: parse_register(arg(&args, 2)?)?, - }), - "xor" => Ok(Instruction::Xor { - rd: parse_register(arg(&args, 0)?)?, - rs1: parse_register(arg(&args, 1)?)?, - rs2: parse_register(arg(&args, 2)?)?, - }), - "mul" => Ok(Instruction::Mul { - rd: parse_register(arg(&args, 0)?)?, - rs1: parse_register(arg(&args, 1)?)?, - rs2: parse_register(arg(&args, 2)?)?, - }), - "mulhu" => Ok(Instruction::Mulhu { - rd: parse_register(arg(&args, 0)?)?, - rs1: parse_register(arg(&args, 1)?)?, - rs2: parse_register(arg(&args, 2)?)?, - }), - "div" => Ok(Instruction::Div { - rd: parse_register(arg(&args, 0)?)?, - rs1: parse_register(arg(&args, 1)?)?, - rs2: parse_register(arg(&args, 2)?)?, - }), - "divu" => Ok(Instruction::Divu { - rd: parse_register(arg(&args, 0)?)?, - rs1: parse_register(arg(&args, 1)?)?, - rs2: parse_register(arg(&args, 2)?)?, - }), - "rem" => Ok(Instruction::Rem { - rd: parse_register(arg(&args, 0)?)?, - rs1: parse_register(arg(&args, 1)?)?, - rs2: parse_register(arg(&args, 2)?)?, - }), - "remu" => Ok(Instruction::Remu { - rd: parse_register(arg(&args, 0)?)?, - rs1: parse_register(arg(&args, 1)?)?, - rs2: parse_register(arg(&args, 2)?)?, - }), - "slt" => Ok(Instruction::Slt { - rd: parse_register(arg(&args, 0)?)?, - rs1: parse_register(arg(&args, 1)?)?, - rs2: parse_register(arg(&args, 2)?)?, - }), - "sltu" => Ok(Instruction::Sltu { - rd: parse_register(arg(&args, 0)?)?, - rs1: parse_register(arg(&args, 1)?)?, - rs2: parse_register(arg(&args, 2)?)?, - }), - "sgt" => Ok(Instruction::Sgt { - rd: parse_register(arg(&args, 0)?)?, - rs1: parse_register(arg(&args, 1)?)?, - rs2: parse_register(arg(&args, 2)?)?, - }), - "xori" => Ok(Instruction::Xori { - rd: parse_register(arg(&args, 0)?)?, - rs1: parse_register(arg(&args, 1)?)?, - imm: parse_immediate(arg(&args, 2)?)?, - }), - "seqz" => Ok(Instruction::Seqz { rd: parse_register(arg(&args, 0)?)?, rs: parse_register(arg(&args, 1)?)? }), - "snez" => Ok(Instruction::Snez { rd: parse_register(arg(&args, 0)?)?, rs: parse_register(arg(&args, 1)?)? }), - "neg" => Ok(Instruction::Neg { rd: parse_register(arg(&args, 0)?)?, rs: parse_register(arg(&args, 1)?)? }), - "ld" => { - let (imm, rs1) = parse_memory_operand(arg(&args, 1)?)?; - Ok(Instruction::Ld { rd: parse_register(arg(&args, 0)?)?, rs1, imm }) - } - "lbu" => { - let (imm, rs1) = parse_memory_operand(arg(&args, 1)?)?; - Ok(Instruction::Lbu { rd: parse_register(arg(&args, 0)?)?, rs1, imm }) - } - "sb" => { - let (imm, rs1) = parse_memory_operand(arg(&args, 1)?)?; - Ok(Instruction::Sb { rs2: parse_register(arg(&args, 0)?)?, rs1, imm }) - } - "sh" => { - let (imm, rs1) = parse_memory_operand(arg(&args, 1)?)?; - Ok(Instruction::Sh { rs2: parse_register(arg(&args, 0)?)?, rs1, imm }) - } - "sw" => { - let (imm, rs1) = parse_memory_operand(arg(&args, 1)?)?; - Ok(Instruction::Sw { rs2: parse_register(arg(&args, 0)?)?, rs1, imm }) - } - "sd" => { - let (imm, rs1) = parse_memory_operand(arg(&args, 1)?)?; - Ok(Instruction::Sd { rs2: parse_register(arg(&args, 0)?)?, rs1, imm }) - } - "slli" => Ok(Instruction::Slli { - rd: parse_register(arg(&args, 0)?)?, - rs1: parse_register(arg(&args, 1)?)?, - shamt: parse_immediate(arg(&args, 2)?)?, - }), - "srai" => Ok(Instruction::Srai { - rd: parse_register(arg(&args, 0)?)?, - rs1: parse_register(arg(&args, 1)?)?, - shamt: parse_immediate(arg(&args, 2)?)?, - }), - "srli" => Ok(Instruction::Srli { - rd: parse_register(arg(&args, 0)?)?, - rs1: parse_register(arg(&args, 1)?)?, - shamt: parse_immediate(arg(&args, 2)?)?, - }), - "li" => Ok(Instruction::Li { rd: parse_register(arg(&args, 0)?)?, imm: parse_li_immediate(arg(&args, 1)?)? }), - "mv" => Ok(Instruction::Addi { rd: parse_register(arg(&args, 0)?)?, rs1: parse_register(arg(&args, 1)?)?, imm: 0 }), - "la" => Ok(Instruction::La { rd: parse_register(arg(&args, 0)?)?, label: arg(&args, 1)?.to_string() }), - "call" => Ok(Instruction::Call { label: arg(&args, 0)?.to_string() }), - "j" => Ok(Instruction::Jump { label: arg(&args, 0)?.to_string() }), - "bgt" => Ok(Instruction::Blt { - rs1: parse_register(arg(&args, 1)?)?, - rs2: parse_register(arg(&args, 0)?)?, - label: arg(&args, 2)?.to_string(), - }), - "bgez" => Ok(Instruction::Bge { rs1: parse_register(arg(&args, 0)?)?, rs2: 0, label: arg(&args, 1)?.to_string() }), - "beq" | "bne" | "blt" | "bge" | "bltu" | "bgeu" => { - let rs1 = parse_register(arg(&args, 0)?)?; - let rs2 = parse_register(arg(&args, 1)?)?; - let label = arg(&args, 2)?.to_string(); - match opcode { - "beq" => Ok(Instruction::Beq { rs1, rs2, label }), - "bne" => Ok(Instruction::Bne { rs1, rs2, label }), - "blt" => Ok(Instruction::Blt { rs1, rs2, label }), - "bge" => Ok(Instruction::Bge { rs1, rs2, label }), - "bltu" => Ok(Instruction::Bltu { rs1, rs2, label }), - "bgeu" => Ok(Instruction::Bgeu { rs1, rs2, label }), - _ => unreachable!("branch opcode matched above"), - } - } - "beqz" => Ok(Instruction::Beqz { rs: parse_register(arg(&args, 0)?)?, label: arg(&args, 1)?.to_string() }), - "bnez" => Ok(Instruction::Bnez { rs: parse_register(arg(&args, 0)?)?, label: arg(&args, 1)?.to_string() }), - "ret" => Ok(Instruction::Ret), - "ecall" => Ok(Instruction::Ecall), - other => Err(CompileError::new(format!("unsupported assembly instruction '{}'", other), crate::error::Span::default())), - } -} - -#[derive(Debug, Clone, Copy)] -enum BranchSizeMode<'a> { - Conservative, - Exact(&'a BTreeSet), -} - -impl<'a> BranchSizeMode<'a> { - fn relaxed_text_branches(self) -> Option<&'a BTreeSet> { - match self { - Self::Conservative => None, - Self::Exact(branches) => Some(branches), - } - } -} - -fn branch_target(inst: &Instruction, parsed: &ParsedAssembly, layout: &SectionLayout) -> Result { - if let Some((_, _, label, _)) = conditional_branch_parts(inst) { - parsed.symbol_address(label, layout) - } else { - Err(CompileError::new("instruction is not a conditional branch", crate::error::Span::default())) - } -} - -fn conditional_branch_parts(inst: &Instruction) -> Option<(u8, u8, &str, u32)> { - match inst { - Instruction::Beq { rs1, rs2, label } => Some((*rs1, *rs2, label.as_str(), 0b000)), - Instruction::Bne { rs1, rs2, label } => Some((*rs1, *rs2, label.as_str(), 0b001)), - Instruction::Blt { rs1, rs2, label } => Some((*rs1, *rs2, label.as_str(), 0b100)), - Instruction::Bge { rs1, rs2, label } => Some((*rs1, *rs2, label.as_str(), 0b101)), - Instruction::Bltu { rs1, rs2, label } => Some((*rs1, *rs2, label.as_str(), 0b110)), - Instruction::Bgeu { rs1, rs2, label } => Some((*rs1, *rs2, label.as_str(), 0b111)), - Instruction::Beqz { rs, label } => Some((*rs, 0, label.as_str(), 0b000)), - Instruction::Bnez { rs, label } => Some((*rs, 0, label.as_str(), 0b001)), - _ => None, - } -} - -fn inverse_branch_funct3(funct3: u32) -> u32 { - match funct3 { - 0b000 => 0b001, - 0b001 => 0b000, - 0b100 => 0b101, - 0b101 => 0b100, - 0b110 => 0b111, - 0b111 => 0b110, - _ => unreachable!("unsupported branch funct3"), - } -} - -fn encode_instruction( - out: &mut Vec, - inst: &Instruction, - pc: u64, - parsed: &ParsedAssembly, - layout: &SectionLayout, - relaxed_branch: bool, -) -> Result<()> { - match inst { - Instruction::Addi { rd, rs1, imm } => out.extend_from_slice(&encode_i_type(0x13, *rd, 0b000, *rs1, *imm)?.to_le_bytes()), - Instruction::Add { rd, rs1, rs2 } => { - out.extend_from_slice(&encode_r_type(0x33, *rd, 0b000, *rs1, *rs2, 0b0000000).to_le_bytes()) - } - Instruction::Sub { rd, rs1, rs2 } => { - out.extend_from_slice(&encode_r_type(0x33, *rd, 0b000, *rs1, *rs2, 0b0100000).to_le_bytes()) - } - Instruction::And { rd, rs1, rs2 } => { - out.extend_from_slice(&encode_r_type(0x33, *rd, 0b111, *rs1, *rs2, 0b0000000).to_le_bytes()) - } - Instruction::Or { rd, rs1, rs2 } => { - out.extend_from_slice(&encode_r_type(0x33, *rd, 0b110, *rs1, *rs2, 0b0000000).to_le_bytes()) - } - Instruction::Xor { rd, rs1, rs2 } => { - out.extend_from_slice(&encode_r_type(0x33, *rd, 0b100, *rs1, *rs2, 0b0000000).to_le_bytes()) - } - Instruction::Mul { rd, rs1, rs2 } => { - out.extend_from_slice(&encode_r_type(0x33, *rd, 0b000, *rs1, *rs2, 0b0000001).to_le_bytes()) - } - Instruction::Mulhu { rd, rs1, rs2 } => { - out.extend_from_slice(&encode_r_type(0x33, *rd, 0b011, *rs1, *rs2, 0b0000001).to_le_bytes()) - } - Instruction::Div { rd, rs1, rs2 } => { - out.extend_from_slice(&encode_r_type(0x33, *rd, 0b100, *rs1, *rs2, 0b0000001).to_le_bytes()) - } - Instruction::Divu { rd, rs1, rs2 } => { - out.extend_from_slice(&encode_r_type(0x33, *rd, 0b101, *rs1, *rs2, 0b0000001).to_le_bytes()) - } - Instruction::Rem { rd, rs1, rs2 } => { - out.extend_from_slice(&encode_r_type(0x33, *rd, 0b110, *rs1, *rs2, 0b0000001).to_le_bytes()) - } - Instruction::Remu { rd, rs1, rs2 } => { - out.extend_from_slice(&encode_r_type(0x33, *rd, 0b111, *rs1, *rs2, 0b0000001).to_le_bytes()) - } - Instruction::Slt { rd, rs1, rs2 } => { - out.extend_from_slice(&encode_r_type(0x33, *rd, 0b010, *rs1, *rs2, 0b0000000).to_le_bytes()) - } - Instruction::Sltu { rd, rs1, rs2 } => { - out.extend_from_slice(&encode_r_type(0x33, *rd, 0b011, *rs1, *rs2, 0b0000000).to_le_bytes()) - } - Instruction::Sgt { rd, rs1, rs2 } => { - out.extend_from_slice(&encode_r_type(0x33, *rd, 0b010, *rs2, *rs1, 0b0000000).to_le_bytes()) - } - Instruction::Xori { rd, rs1, imm } => out.extend_from_slice(&encode_i_type(0x13, *rd, 0b100, *rs1, *imm)?.to_le_bytes()), - Instruction::Seqz { rd, rs } => out.extend_from_slice(&encode_i_type(0x13, *rd, 0b011, *rs, 1)?.to_le_bytes()), - Instruction::Snez { rd, rs } => out.extend_from_slice(&encode_r_type(0x33, *rd, 0b011, 0, *rs, 0b0000000).to_le_bytes()), - Instruction::Neg { rd, rs } => out.extend_from_slice(&encode_r_type(0x33, *rd, 0b000, 0, *rs, 0b0100000).to_le_bytes()), - Instruction::Ld { rd, rs1, imm } => out.extend_from_slice(&encode_i_type(0x03, *rd, 0b011, *rs1, *imm)?.to_le_bytes()), - Instruction::Lbu { rd, rs1, imm } => out.extend_from_slice(&encode_i_type(0x03, *rd, 0b100, *rs1, *imm)?.to_le_bytes()), - Instruction::Sb { rs2, rs1, imm } => out.extend_from_slice(&encode_s_type(0x23, 0b000, *rs1, *rs2, *imm)?.to_le_bytes()), - Instruction::Sh { rs2, rs1, imm } => out.extend_from_slice(&encode_s_type(0x23, 0b001, *rs1, *rs2, *imm)?.to_le_bytes()), - Instruction::Sw { rs2, rs1, imm } => out.extend_from_slice(&encode_s_type(0x23, 0b010, *rs1, *rs2, *imm)?.to_le_bytes()), - Instruction::Sd { rs2, rs1, imm } => out.extend_from_slice(&encode_s_type(0x23, 0b011, *rs1, *rs2, *imm)?.to_le_bytes()), - Instruction::Slli { rd, rs1, shamt } => { - if !(0..=63).contains(shamt) { - return Err(CompileError::new("slli shift amount must be in 0..=63", crate::error::Span::default())); - } - out.extend_from_slice(&encode_i_type(0x13, *rd, 0b001, *rs1, *shamt)?.to_le_bytes()); - } - Instruction::Srai { rd, rs1, shamt } => { - if !(0..=63).contains(shamt) { - return Err(CompileError::new("srai shift amount must be in 0..=63", crate::error::Span::default())); - } - let imm = (0b0100000_i64 << 5) | *shamt; - out.extend_from_slice(&encode_i_type(0x13, *rd, 0b101, *rs1, imm)?.to_le_bytes()); - } - Instruction::Srli { rd, rs1, shamt } => { - if !(0..=63).contains(shamt) { - return Err(CompileError::new("srli shift amount must be in 0..=63", crate::error::Span::default())); - } - out.extend_from_slice(&encode_i_type(0x13, *rd, 0b101, *rs1, *shamt)?.to_le_bytes()); - } - Instruction::Li { rd, imm } => encode_li_sequence(out, *rd, *imm)?, - Instruction::La { rd, label } => encode_address_sequence(out, *rd, pc, parsed.symbol_address(label, layout)?)?, - Instruction::Call { label } => { - let target = parsed.symbol_address(label, layout)?; - encode_call_sequence(out, pc, target)?; - } - Instruction::Jump { label } => { - let target = parsed.symbol_address(label, layout)?; - out.extend_from_slice(&encode_j_type(0x6f, 0, relative_offset(pc, target)?)?.to_le_bytes()); - } - Instruction::Beq { .. } - | Instruction::Bne { .. } - | Instruction::Blt { .. } - | Instruction::Bge { .. } - | Instruction::Bltu { .. } - | Instruction::Bgeu { .. } - | Instruction::Beqz { .. } - | Instruction::Bnez { .. } => { - let (rs1, rs2, label, funct3) = conditional_branch_parts(inst).expect("conditional branch parts"); - let target = parsed.symbol_address(label, layout)?; - if relaxed_branch { - out.extend_from_slice(&encode_b_type(0x63, inverse_branch_funct3(funct3), rs1, rs2, 8)?.to_le_bytes()); - out.extend_from_slice(&encode_j_type(0x6f, 0, relative_offset(pc + 4, target)?)?.to_le_bytes()); - } else { - out.extend_from_slice(&encode_b_type(0x63, funct3, rs1, rs2, relative_offset(pc, target)?)?.to_le_bytes()); - } - } - Instruction::Ret => out.extend_from_slice(&encode_i_type(0x67, 0, 0b000, 1, 0)?.to_le_bytes()), - Instruction::Ecall => out.extend_from_slice(&encode_ecall().to_le_bytes()), - } - Ok(()) -} - -fn encode_li_sequence(out: &mut Vec, rd: u8, imm: i128) -> Result<()> { - if let Some(signed) = li_signed_i64(imm) { - if li_fits_lui_addi_rv64(signed) { - let (hi, lo) = split_hi_lo(signed)?; - out.extend_from_slice(&encode_u_type(0x37, rd, hi).to_le_bytes()); - out.extend_from_slice(&encode_i_type(0x13, rd, 0b000, rd, lo)?.to_le_bytes()); - return Ok(()); - } - } - encode_large_li_sequence(out, rd, li_bits(imm)?) -} - -fn encode_large_li_sequence(out: &mut Vec, rd: u8, bits: u64) -> Result<()> { - let bytes = bits.to_be_bytes(); - out.extend_from_slice(&encode_i_type(0x13, rd, 0b000, 0, i64::from(bytes[0]))?.to_le_bytes()); - for byte in bytes.iter().skip(1) { - out.extend_from_slice(&encode_i_type(0x13, rd, 0b001, rd, 8)?.to_le_bytes()); - out.extend_from_slice(&encode_i_type(0x13, rd, 0b000, rd, i64::from(*byte))?.to_le_bytes()); - } - Ok(()) -} - -fn li_signed_i64(imm: i128) -> Option { - i64::try_from(imm).ok() -} - -fn li_bits(imm: i128) -> Result { - if imm < i128::from(i64::MIN) || imm > i128::from(u64::MAX) { - return Err(CompileError::new(format!("li immediate '{}' does not fit 64 bits", imm), crate::error::Span::default())); - } - if imm < 0 { - Ok((imm as i64) as u64) - } else { - Ok(imm as u64) - } -} - -fn encode_address_sequence(out: &mut Vec, rd: u8, pc: u64, target: u64) -> Result<()> { - let (hi, lo) = split_hi_lo(relative_offset(pc, target)?)?; - out.extend_from_slice(&encode_u_type(0x17, rd, hi).to_le_bytes()); - out.extend_from_slice(&encode_i_type(0x13, rd, 0b000, rd, lo)?.to_le_bytes()); - Ok(()) -} - -fn encode_call_sequence(out: &mut Vec, pc: u64, target: u64) -> Result<()> { - let (hi, lo) = split_hi_lo(relative_offset(pc, target)?)?; - out.extend_from_slice(&encode_u_type(0x17, 1, hi).to_le_bytes()); - out.extend_from_slice(&encode_i_type(0x67, 1, 0b000, 1, lo)?.to_le_bytes()); - Ok(()) -} - -fn op_size(op: &AsmOp, current_offset: usize, section: SectionKind, op_index: usize, branch_size_mode: BranchSizeMode<'_>) -> usize { - match op { - AsmOp::Label(_) => 0, - AsmOp::Instruction(Instruction::Li { imm, .. }) => li_sequence_size(*imm), - AsmOp::Instruction(Instruction::La { .. }) => 8, - AsmOp::Instruction(Instruction::Call { .. }) => 8, - AsmOp::Instruction( - Instruction::Beq { .. } - | Instruction::Bne { .. } - | Instruction::Blt { .. } - | Instruction::Bge { .. } - | Instruction::Bltu { .. } - | Instruction::Bgeu { .. } - | Instruction::Beqz { .. } - | Instruction::Bnez { .. }, - ) => match branch_size_mode { - BranchSizeMode::Conservative => 8, - BranchSizeMode::Exact(relaxed) if section == SectionKind::Text && relaxed.contains(&op_index) => 8, - BranchSizeMode::Exact(_) => 4, - }, - AsmOp::Instruction(_) => 4, - AsmOp::Word(_) => 4, - AsmOp::Byte(_) => 1, - AsmOp::Ascii(bytes) => bytes.len(), - AsmOp::Align(bytes) => padding_for(current_offset, *bytes), - } -} - -fn li_sequence_size(imm: i128) -> usize { - if li_signed_i64(imm).is_some_and(li_fits_lui_addi_rv64) { - 8 - } else { - 60 - } -} - -fn write_elf_header(out: &mut [u8], entry: u64, program_header_count: u16) -> Result<()> { - if out.len() != ELF_HEADER_SIZE { - return Err(CompileError::new("invalid ELF header buffer size", crate::error::Span::default())); - } - out.fill(0); - out[0..4].copy_from_slice(b"\x7fELF"); - out[4] = 2; - out[5] = 1; - out[6] = 1; - out[16..18].copy_from_slice(&2u16.to_le_bytes()); - out[18..20].copy_from_slice(&243u16.to_le_bytes()); - out[20..24].copy_from_slice(&1u32.to_le_bytes()); - out[24..32].copy_from_slice(&entry.to_le_bytes()); - out[32..40].copy_from_slice(&(ELF_HEADER_SIZE as u64).to_le_bytes()); - out[40..48].copy_from_slice(&0u64.to_le_bytes()); - out[48..52].copy_from_slice(&0u32.to_le_bytes()); - out[52..54].copy_from_slice(&(ELF_HEADER_SIZE as u16).to_le_bytes()); - out[54..56].copy_from_slice(&(ELF_PROGRAM_HEADER_SIZE as u16).to_le_bytes()); - out[56..58].copy_from_slice(&program_header_count.to_le_bytes()); - Ok(()) -} - -fn write_program_header(out: &mut [u8], flags: u32, offset: u64, vaddr: u64, file_size: u64, memory_size: u64) -> Result<()> { - if out.len() != ELF_PROGRAM_HEADER_SIZE { - return Err(CompileError::new("invalid ELF program header buffer size", crate::error::Span::default())); - } - out.fill(0); - out[0..4].copy_from_slice(&1u32.to_le_bytes()); - out[4..8].copy_from_slice(&flags.to_le_bytes()); - out[8..16].copy_from_slice(&offset.to_le_bytes()); - out[16..24].copy_from_slice(&vaddr.to_le_bytes()); - out[24..32].copy_from_slice(&vaddr.to_le_bytes()); - out[32..40].copy_from_slice(&file_size.to_le_bytes()); - out[40..48].copy_from_slice(&memory_size.to_le_bytes()); - out[48..56].copy_from_slice(&(ELF_SEGMENT_ALIGN as u64).to_le_bytes()); - Ok(()) -} - -fn strip_comment(line: &str) -> Option<&str> { - let mut in_string = false; - let mut escape = false; - for (idx, ch) in line.char_indices() { - match ch { - '"' if !escape => in_string = !in_string, - '#' if !in_string => return Some(line[..idx].trim()), - '\\' if in_string => { - escape = !escape; - continue; + match identity { + IrIdentityPolicy::None => {} + IrIdentityPolicy::CkbTypeId => { + self.emit_output_type_hash_present_check(output_index, "create_unique_ckb_type_id_output_type_hash"); } - _ => {} - } - escape = false; - } - let trimmed = line.trim(); - (!trimmed.is_empty()).then_some(trimmed) -} - -fn parse_ascii_literal(value: &str) -> Result> { - let Some(inner) = value.strip_prefix('"').and_then(|value| value.strip_suffix('"')) else { - return Err(CompileError::new(format!("invalid .ascii literal '{}'", value), crate::error::Span::default())); - }; - - let mut out = Vec::new(); - let mut chars = inner.chars(); - while let Some(ch) = chars.next() { - if ch != '\\' { - out.extend_from_slice(ch.to_string().as_bytes()); - continue; - } - - let escaped = chars - .next() - .ok_or_else(|| CompileError::new("unterminated escape sequence in .ascii literal", crate::error::Span::default()))?; - match escaped { - 'n' => out.push(b'\n'), - 'r' => out.push(b'\r'), - 't' => out.push(b'\t'), - '\\' => out.push(b'\\'), - '"' => out.push(b'"'), - 'x' => { - let hi = chars - .next() - .ok_or_else(|| CompileError::new("incomplete hex escape in .ascii literal", crate::error::Span::default()))?; - let lo = chars - .next() - .ok_or_else(|| CompileError::new("incomplete hex escape in .ascii literal", crate::error::Span::default()))?; - let hex = format!("{}{}", hi, lo); - let byte = u8::from_str_radix(&hex, 16) - .map_err(|_| CompileError::new(format!("invalid hex escape '\\x{}'", hex), crate::error::Span::default()))?; - out.push(byte); + IrIdentityPolicy::Field(field) => { + self.emit_create_unique_field_identity_anchor(output_index, pattern, field); } - other => { - return Err(CompileError::new( - format!("unsupported escape sequence '\\{}' in .ascii literal", other), - crate::error::Span::default(), - )); + IrIdentityPolicy::ScriptArgs => { + self.emit_cell_field_hash_equality(CellFieldHashCheck { + left: CellFieldHashLocation { + reason: "create_unique_group_input_lock_hash", + source: CKB_SOURCE_GROUP_INPUT, + index: 0, + }, + right: CellFieldHashLocation { + reason: "create_unique_output_lock_hash", + source: CKB_SOURCE_OUTPUT, + index: output_index, + }, + cell_field: CKB_CELL_FIELD_LOCK_HASH, + field_name: "LockHash", + detail: "create_unique script_args identity anchor", + error: CellScriptRuntimeError::LockHashPreservationMismatch, + }); + } + IrIdentityPolicy::SingletonType => { + self.emit_cell_field_hash_equality(CellFieldHashCheck { + left: CellFieldHashLocation { + reason: "create_unique_group_input_type_hash", + source: CKB_SOURCE_GROUP_INPUT, + index: 0, + }, + right: CellFieldHashLocation { + reason: "create_unique_output_type_hash", + source: CKB_SOURCE_OUTPUT, + index: output_index, + }, + cell_field: CKB_CELL_FIELD_TYPE_HASH, + field_name: "TypeHash", + detail: "create_unique singleton_type identity anchor", + error: CellScriptRuntimeError::TypeHashMismatch, + }); } } } - Ok(out) -} - -fn parse_memory_operand(value: &str) -> Result<(i64, u8)> { - let open = value - .find('(') - .ok_or_else(|| CompileError::new(format!("invalid memory operand '{}'", value), crate::error::Span::default()))?; - let close = value - .rfind(')') - .ok_or_else(|| CompileError::new(format!("invalid memory operand '{}'", value), crate::error::Span::default()))?; - let imm = parse_immediate(value[..open].trim())?; - let rs1 = parse_register(value[open + 1..close].trim())?; - Ok((imm, rs1)) -} - -fn memory_operand_offset_and_base(value: &str) -> Option<(i64, &str)> { - let open = value.find('(')?; - let close = value.rfind(')')?; - let offset = parse_immediate(value[..open].trim()).ok()?; - let base = value[open + 1..close].trim(); - (!base.is_empty()).then_some((offset, base)) -} - -fn small_signed_immediate(value: i64) -> bool { - (-2048..=2047).contains(&value) -} - -fn scratch_register_avoiding(registers: &[&str]) -> &'static str { - for candidate in ["t6", "t5", "t3", "t2", "t1", "t0"] { - let candidate_id = parse_register(candidate).expect("scratch register name should be valid"); - if registers.iter().all(|register| parse_register(register).ok() != Some(candidate_id)) { - return candidate; - } - } - "t6" -} - -fn parse_register(name: &str) -> Result { - let reg = match name { - "zero" | "x0" => 0, - "ra" | "x1" => 1, - "sp" | "x2" => 2, - "gp" | "x3" => 3, - "tp" | "x4" => 4, - "t0" | "x5" => 5, - "t1" | "x6" => 6, - "t2" | "x7" => 7, - "s0" | "fp" | "x8" => 8, - "s1" | "x9" => 9, - "a0" | "x10" => 10, - "a1" | "x11" => 11, - "a2" | "x12" => 12, - "a3" | "x13" => 13, - "a4" | "x14" => 14, - "a5" | "x15" => 15, - "a6" | "x16" => 16, - "a7" | "x17" => 17, - "s2" | "x18" => 18, - "s3" | "x19" => 19, - "s4" | "x20" => 20, - "s5" | "x21" => 21, - "s6" | "x22" => 22, - "s7" | "x23" => 23, - "s8" | "x24" => 24, - "s9" | "x25" => 25, - "s10" | "x26" => 26, - "s11" | "x27" => 27, - "t3" | "x28" => 28, - "t4" | "x29" => 29, - "t5" | "x30" => 30, - "t6" | "x31" => 31, - other => return Err(CompileError::new(format!("unknown register '{}'", other), crate::error::Span::default())), - }; - Ok(reg) -} - -fn parse_immediate(value: &str) -> Result { - if let Some(hex) = value.strip_prefix("-0x") { - return i64::from_str_radix(hex, 16) - .map(|value| -value) - .map_err(|_| CompileError::new(format!("invalid immediate '{}'", value), crate::error::Span::default())); - } - if let Some(hex) = value.strip_prefix("0x").or_else(|| value.strip_prefix("+0x")) { - return i64::from_str_radix(hex, 16) - .map_err(|_| CompileError::new(format!("invalid immediate '{}'", value), crate::error::Span::default())); - } - value.parse::().map_err(|_| CompileError::new(format!("invalid immediate '{}'", value), crate::error::Span::default())) -} - -fn parse_li_immediate(value: &str) -> Result { - if let Some(hex) = value.strip_prefix("-0x") { - let parsed = i128::from_str_radix(hex, 16) - .map_err(|_| CompileError::new(format!("invalid immediate '{}'", value), crate::error::Span::default()))?; - return validate_li_immediate(-parsed, value); - } - if let Some(hex) = value.strip_prefix("0x").or_else(|| value.strip_prefix("+0x")) { - let parsed = u128::from_str_radix(hex, 16) - .map_err(|_| CompileError::new(format!("invalid immediate '{}'", value), crate::error::Span::default()))?; - if parsed <= u128::from(u64::MAX) { - return Ok(parsed as i128); - } - return Err(CompileError::new(format!("li immediate '{}' does not fit 64 bits", value), crate::error::Span::default())); - } - if value.starts_with('-') { - let parsed = value - .parse::() - .map_err(|_| CompileError::new(format!("invalid immediate '{}'", value), crate::error::Span::default()))?; - validate_li_immediate(parsed, value) - } else { - value - .parse::() - .map_err(|_| CompileError::new(format!("invalid immediate '{}'", value), crate::error::Span::default())) - .and_then(|parsed| { - if parsed <= u128::from(u64::MAX) { - Ok(parsed as i128) - } else { - Err(CompileError::new(format!("li immediate '{}' does not fit 64 bits", value), crate::error::Span::default())) - } - }) - } -} - -fn validate_li_immediate(parsed: i128, source: &str) -> Result { - if (i64::MIN as i128..=u64::MAX as i128).contains(&parsed) { - Ok(parsed) - } else { - Err(CompileError::new(format!("li immediate '{}' does not fit 64 bits", source), crate::error::Span::default())) - } -} - -fn arg(args: &[String], index: usize) -> Result<&str> { - args.get(index) - .map(|value| value.as_str()) - .ok_or_else(|| CompileError::new("malformed assembly instruction", crate::error::Span::default())) -} - -fn encode_r_type(opcode: u32, rd: u8, funct3: u32, rs1: u8, rs2: u8, funct7: u32) -> u32 { - (funct7 << 25) | ((rs2 as u32) << 20) | ((rs1 as u32) << 15) | (funct3 << 12) | ((rd as u32) << 7) | opcode -} - -fn encode_i_type(opcode: u32, rd: u8, funct3: u32, rs1: u8, imm: i64) -> Result { - let imm = encode_signed_bits(imm, 12)?; - Ok((imm << 20) | ((rs1 as u32) << 15) | (funct3 << 12) | ((rd as u32) << 7) | opcode) -} - -fn encode_s_type(opcode: u32, funct3: u32, rs1: u8, rs2: u8, imm: i64) -> Result { - let imm = encode_signed_bits(imm, 12)?; - let imm_lo = imm & 0x1f; - let imm_hi = (imm >> 5) & 0x7f; - Ok((imm_hi << 25) | ((rs2 as u32) << 20) | ((rs1 as u32) << 15) | (funct3 << 12) | (imm_lo << 7) | opcode) -} - -fn encode_b_type(opcode: u32, funct3: u32, rs1: u8, rs2: u8, imm: i64) -> Result { - if imm % 2 != 0 { - return Err(CompileError::new("branch target is not 2-byte aligned", crate::error::Span::default())); - } - let imm = encode_signed_bits(imm, 13)?; - let bit12 = (imm >> 12) & 0x1; - let bits10_5 = (imm >> 5) & 0x3f; - let bits4_1 = (imm >> 1) & 0xf; - let bit11 = (imm >> 11) & 0x1; - Ok((bit12 << 31) - | (bits10_5 << 25) - | ((rs2 as u32) << 20) - | ((rs1 as u32) << 15) - | (funct3 << 12) - | (bits4_1 << 8) - | (bit11 << 7) - | opcode) -} - -fn encode_u_type(opcode: u32, rd: u8, imm: i64) -> u32 { - (((imm as i32 as u32) & 0x000f_ffff) << 12) | ((rd as u32) << 7) | opcode -} - -fn encode_j_type(opcode: u32, rd: u8, imm: i64) -> Result { - if imm % 2 != 0 { - return Err(CompileError::new("jump target is not 2-byte aligned", crate::error::Span::default())); - } - let imm = encode_signed_bits(imm, 21)?; - let bit20 = (imm >> 20) & 0x1; - let bits10_1 = (imm >> 1) & 0x3ff; - let bit11 = (imm >> 11) & 0x1; - let bits19_12 = (imm >> 12) & 0xff; - Ok((bit20 << 31) | (bits10_1 << 21) | (bit11 << 20) | (bits19_12 << 12) | ((rd as u32) << 7) | opcode) -} - -fn encode_ecall() -> u32 { - 0x0000_0073 -} - -fn encode_signed_bits(value: i64, bits: u32) -> Result { - if !signed_bits_fit(value, bits) { - return Err(CompileError::new( - format!("immediate '{}' does not fit {}-bit signed field", value, bits), - crate::error::Span::default(), - )); - } - Ok((value as i32 as u32) & ((1u32 << bits) - 1)) -} - -fn signed_bits_fit(value: i64, bits: u32) -> bool { - let min = -(1i64 << (bits - 1)); - let max = (1i64 << (bits - 1)) - 1; - value >= min && value <= max -} - -fn split_hi_lo(value: i64) -> Result<(i64, i64)> { - if !li_fits_lui_addi_rv64(value) { - return Err(CompileError::new( - format!("value '{}' is outside the supported RV64 LUI/ADDI immediate range", value), - crate::error::Span::default(), - )); - } - let adjusted = value.checked_add(0x800).ok_or_else(|| { - CompileError::new(format!("value '{}' overflowed while splitting its immediate", value), crate::error::Span::default()) - })?; - let hi = adjusted >> 12; - let lo = value - (hi << 12); - if !(-2048..=2047).contains(&lo) { - return Err(CompileError::new(format!("low immediate '{}' is out of range after split", lo), crate::error::Span::default())); - } - Ok((hi, lo)) -} - -fn li_fits_lui_addi_rv64(value: i64) -> bool { - if !(i32::MIN as i64..=i32::MAX as i64).contains(&value) { - return false; - } - let hi = (value + 0x800) >> 12; - (-0x80000..=0x7ffff).contains(&hi) -} - -fn relative_offset(pc: u64, target: u64) -> Result { - i64::try_from(target as i128 - pc as i128) - .map_err(|_| CompileError::new("relative offset overflowed i64", crate::error::Span::default())) -} - -fn align_up(value: usize, align: usize) -> usize { - if align <= 1 { - return value; - } - (value + align - 1) & !(align - 1) -} - -fn align_frame(value: usize) -> usize { - align_up(value.max(16), 16) -} - -fn is_min_call(func: &str) -> bool { - matches!(func, "min" | "math_min" | "__math_min") -} - -fn is_void_runtime_requirement_call(func: &str) -> bool { - matches!( - func, - "__ckb_require_maturity" - | "__ckb_require_time" - | "__ckb_require_epoch_after" - | "__ckb_require_epoch_relative" - | "__ckb_require_cell_lock_hash" - | "__ckb_require_cell_type_hash" - | "__ckb_require_current_script_args_empty" - | "__ckb_require_cell_lock_args_empty" - | "__ckb_require_cell_type_args_empty" - | "__ckb_require_cell_lock_args_hash" - | "__ckb_require_cell_type_args_hash" - | "__ckb_require_cell_lock_args_prefix_hash" - | "__ckb_require_cell_type_args_prefix_hash" - | "__ckb_require_cell_lock_args_suffix_hash" - | "__ckb_require_cell_type_args_suffix_hash" - | "__ckb_require_cell_lock_script_hash_type" - | "__ckb_require_cell_type_script_hash_type" - | "__ckb_require_input_out_point_tx_hash" - | "__ckb_require_input_out_point" - | "__ckb_require_metapoint_relative" - | "__ckb_require_lock_type_metapoint_pairs" - | "__ckb_require_type_lock_metapoint_pairs" - | "__ckb_require_lock_type_metapoint_pairs_from_i32_data" - | "__ckb_require_type_lock_metapoint_pairs_from_i32_data" - | "__ckb_require_lock_type_metapoint_pairs_from_i32_data_filtered" - | "__ckb_require_type_lock_metapoint_pairs_from_i32_data_filtered" - | "__ckb_require_lock_match_master_out_point_pairs_from_data" - | "__dao_require_header_dep_for_input" - | "__dao_require_input_since_at_least" - | "__dao_require_input_relative_epoch_since_at_least" - | "__xudt_require_owner_mode_input_type" - | "__xudt_require_owner_mode_type_args" - | "__xudt_require_owner_mode_type_args_current_script" - | "__cellscript_require_fungible_type_group_v1" - | "__xudt_require_group_amount_conserved" - | "__xudt_require_group_amount_minted" - | "__xudt_require_group_amount_burned" - | "__c256_require_u128_product_lte" - | "__c256_require_u128_product_eq" - | "__c256_require_u128_sum2_products_lte" - | "__c256_require_u128_sum2_products_eq" - | "__ckb_require_witness_size_at_least" - ) -} - -fn is_runtime_scalar_failclosed_call(func: &str) -> bool { - matches!( - func, - "__ckb_source_input" - | "__ckb_source_output" - | "__ckb_source_cell_dep" - | "__ckb_source_header_dep" - | "__ckb_source_group_input" - | "__ckb_source_group_output" - | "__ckb_since_epoch_absolute" - | "__ckb_since_epoch_relative" - | "__ckb_current_role" - | "__ckb_cell_capacity" - | "__ckb_cell_occupied_capacity" - | "__ckb_cell_unoccupied_capacity" - | "__ckb_cell_output_index" - | "__ckb_cell_data_size" - | "__ckb_cell_data_u32_le" - | "__ckb_cell_data_u64_le" - | "__ckb_cell_lock_hash_type" - | "__ckb_cell_type_hash_type" - | "__ckb_cell_lock_args_empty" - | "__ckb_cell_type_args_empty" - | "__dao_accumulated_rate" - | "__dao_input_accumulated_rate" - | "__dao_has_dao_type" - | "__dao_is_deposit_data" - | "__dao_is_withdrawal_request_data" - | "__xudt_amount_low" - | "__xudt_amount_high" - | "__xudt_owner_mode_input_type_hash" - | "__ckb_witness_size" - ) -} - -fn is_runtime_header_u64_call(func: &str) -> bool { - matches!( - func, - "__env_current_timepoint" - | "__ckb_header_epoch_number" - | "__ckb_header_epoch_start_block_number" - | "__ckb_header_epoch_length" - | "__ckb_input_since" - ) -} - -fn ckb_source_name(source: u64) -> &'static str { - match source { - CKB_SOURCE_INPUT => "Input", - CKB_SOURCE_OUTPUT => "Output", - CKB_SOURCE_CELL_DEP => "CellDep", - CKB_SOURCE_HEADER_DEP => "HeaderDep", - CKB_SOURCE_GROUP_INPUT => "GroupInput", - CKB_SOURCE_GROUP_OUTPUT => "GroupOutput", - source if source == (CKB_SOURCE_GROUP_FLAG | CKB_SOURCE_INPUT) => "GroupInput", - source if source == (CKB_SOURCE_GROUP_FLAG | CKB_SOURCE_OUTPUT) => "GroupOutput", - source if source == (CKB_SOURCE_GROUP_FLAG | CKB_SOURCE_CELL_DEP) => "GroupCellDep", - source if source == (CKB_SOURCE_GROUP_FLAG | CKB_SOURCE_HEADER_DEP) => "GroupHeaderDep", - _ => "Unknown", - } -} - -fn padding_for(offset: usize, align: usize) -> usize { - align_up(offset, align) - offset -} - -fn pad_to_alignment(out: &mut Vec, align: usize) { - let pad = padding_for(out.len(), align); - out.resize(out.len() + pad, 0); -} - -#[cfg(test)] -mod tests { - use super::*; - - const SUPPORTED_INTERNAL_ASSEMBLER_MNEMONICS: &[(&str, &str)] = &[ - ("add", "add t0, a0, a1"), - ("addi", "addi t0, t0, -1"), - ("and", "and t2, a0, a1"), - ("beq", "beq a0, a1, branch_target"), - ("bge", "bge a0, a1, branch_target"), - ("bgeu", "bgeu a0, a1, branch_target"), - ("bgez", "bgez a0, branch_target"), - ("bgt", "bgt a0, a1, branch_target"), - ("blt", "blt a1, a0, branch_target"), - ("bltu", "bltu a1, a0, branch_target"), - ("bne", "bne a0, a1, branch_target"), - ("bnez", "bnez a0, branch_target"), - ("beqz", "beqz a0, branch_target"), - ("call", "call helper"), - ("div", "div t5, a0, a1"), - ("divu", "divu t5, a0, a1"), - ("ecall", "ecall"), - ("j", "j done"), - ("la", "la t3, data_label"), - ("lbu", "lbu t2, 8(sp)"), - ("ld", "ld t1, 0(sp)"), - ("li", "li a0, 8"), - ("mul", "mul t4, a0, a1"), - ("mv", "mv s9, a0"), - ("neg", "neg s6, a0"), - ("or", "or t3, a0, a1"), - ("rem", "rem t6, a0, a1"), - ("remu", "remu t6, a0, a1"), - ("ret", "ret"), - ("sb", "sb t1, 8(sp)"), - ("sd", "sd t0, 0(sp)"), - ("seqz", "seqz s4, a0"), - ("sgt", "sgt s2, a0, a1"), - ("sh", "sh t1, 10(sp)"), - ("slli", "slli s7, a0, 3"), - ("slt", "slt s0, a1, a0"), - ("sltu", "sltu s1, a1, a0"), - ("snez", "snez s5, a0"), - ("srai", "srai a0, a0, 1"), - ("srli", "srli s8, a0, 1"), - ("sub", "sub t1, a0, a1"), - ("sw", "sw t1, 12(sp)"), - ("xor", "xor a0, a0, a1"), - ("xori", "xori s3, a0, 1"), - ]; - - const INTENTIONALLY_UNSUPPORTED_INTERNAL_ASSEMBLER_MNEMONICS: &[(&str, &str)] = &[ - ("addiw", "addiw a0, a0, 1"), - ("addw", "addw a0, a0, a1"), - ("andi", "andi a0, a0, 1"), - ("amoadd.w", "amoadd.w a0, a1, (a2)"), - ("auipc", "auipc a0, 0"), - ("ble", "ble a0, a1, target"), - ("bleu", "bleu a0, a1, target"), - ("blez", "blez a0, target"), - ("bgtu", "bgtu a0, a1, target"), - ("bgtz", "bgtz a0, target"), - ("bltz", "bltz a0, target"), - ("c.nop", "c.nop"), - ("csrr", "csrr a0, cycle"), - ("fence", "fence"), - ("flw", "flw fa0, 0(sp)"), - ("jal", "jal ra, target"), - ("jalr", "jalr zero, 0(ra)"), - ("jr", "jr ra"), - ("lb", "lb a0, 0(sp)"), - ("lh", "lh a0, 0(sp)"), - ("lhu", "lhu a0, 0(sp)"), - ("lui", "lui a0, 1"), - ("lw", "lw a0, 0(sp)"), - ("lwu", "lwu a0, 0(sp)"), - ("nop", "nop"), - ("not", "not a0, a1"), - ("ori", "ori a0, a0, 1"), - ("sll", "sll a0, a0, a1"), - ("slti", "slti a0, a0, 1"), - ("sltiu", "sltiu a0, a0, 1"), - ("sra", "sra a0, a0, a1"), - ("srl", "srl a0, a0, a1"), - ("subw", "subw a0, a0, a1"), - ("tail", "tail target"), - ]; - - #[derive(Debug)] - struct TestProgramHeader { - p_type: u32, - flags: u32, - offset: u64, - vaddr: u64, - file_size: u64, - memory_size: u64, - } - - fn read_u16_le(bytes: &[u8], offset: usize) -> u16 { - let mut raw = [0u8; 2]; - raw.copy_from_slice(&bytes[offset..offset + 2]); - u16::from_le_bytes(raw) - } - - fn read_u32_le(bytes: &[u8], offset: usize) -> u32 { - let mut raw = [0u8; 4]; - raw.copy_from_slice(&bytes[offset..offset + 4]); - u32::from_le_bytes(raw) - } - - fn read_u64_le(bytes: &[u8], offset: usize) -> u64 { - let mut raw = [0u8; 8]; - raw.copy_from_slice(&bytes[offset..offset + 8]); - u64::from_le_bytes(raw) - } - - fn elf_program_headers(elf: &[u8]) -> Vec { - assert!(elf.starts_with(b"\x7fELF"), "expected ELF magic"); - let phoff = usize::try_from(read_u64_le(elf, 32)).expect("program header offset should fit usize"); - let phentsize = usize::from(read_u16_le(elf, 54)); - let phnum = usize::from(read_u16_le(elf, 56)); - assert_eq!(phentsize, ELF_PROGRAM_HEADER_SIZE); - - (0..phnum) - .map(|index| { - let offset = phoff + index * phentsize; - TestProgramHeader { - p_type: read_u32_le(elf, offset), - flags: read_u32_le(elf, offset + 4), - offset: read_u64_le(elf, offset + 8), - vaddr: read_u64_le(elf, offset + 16), - file_size: read_u64_le(elf, offset + 32), - memory_size: read_u64_le(elf, offset + 40), - } - }) - .collect() - } - - fn elf_text_file_offset(elf: &[u8]) -> usize { - let header = elf_program_headers(elf) - .into_iter() - .find(|header| header.p_type == 1 && header.flags & ELF_PF_X != 0) - .expect("ELF should contain an executable load segment"); - let offset_into_segment = ELF_BASE_ADDR.checked_sub(header.vaddr).expect("text base should be inside load segment"); - usize::try_from(header.offset + offset_into_segment).expect("text file offset should fit usize") - } - - #[test] - fn strict_audit_internal_elf_entry_preserves_ckb_stack_pointer() { - let lines = vec![".section .text".to_string(), ".global entry".to_string(), "entry:".to_string(), "ret".to_string()]; - - let elf = assemble_elf_internal(&lines).expect("internal assembler should emit a CKB-loadable ELF"); - let headers = elf_program_headers(&elf); - assert_eq!(headers.len(), 1, "internal CKB ELF should expose one load segment"); - assert_eq!(headers[0].flags, ELF_PF_R | ELF_PF_X, "code segment should be readable and executable only"); - assert_eq!(headers[0].flags & ELF_PF_W, 0, "code segment must not be writable"); - assert_eq!(headers[0].file_size, headers[0].memory_size, "code segment should not fake stack memory in PT_LOAD"); - - let text_offset = elf_text_file_offset(&elf); - let trampoline = (0..START_TRAMPOLINE_SIZE / 4).map(|index| read_u32_le(&elf, text_offset + index * 4)).collect::>(); - assert_eq!(trampoline, vec![0x0000_0097, 0x0140_80e7, 0x0000_08b7, 0x05d8_8893, 0x0000_0073]); - assert!(trampoline[..4].iter().all(|instruction| (instruction >> 7) & 0x1f != 2), "trampoline must not write sp"); - - let entry_instruction = read_u32_le(&elf, text_offset + START_TRAMPOLINE_SIZE); - assert_eq!(entry_instruction, 0x0000_8067, "entry body should start after the 20-byte trampoline"); - } - - #[test] - fn strict_audit_external_assembly_entry_preserves_ckb_stack_pointer() { - let lines = vec![".section .text".to_string(), ".global entry".to_string(), "entry:".to_string(), "ret".to_string()]; - - let rendered = render_external_assembly(&lines, "entry"); - assert!( - !rendered.lines().any(|line| line.trim_start().starts_with("li sp,")), - "external assembly trampoline must not overwrite the CKB VM stack pointer:\n{rendered}" - ); - assert!(rendered.contains("\n call entry\n"), "external assembly should call the entrypoint:\n{rendered}"); - } - - #[test] - fn internal_assembler_relaxes_out_of_range_conditional_branch() { - let mut lines = vec![ - ".section .text".to_string(), - ".global entry".to_string(), - "entry:".to_string(), - "li a0, 0".to_string(), - "beqz a0, far_target".to_string(), - ]; - for _ in 0..1500 { - lines.push("addi t0, t0, 0".to_string()); - } - lines.push("far_target:".to_string()); - lines.push("ret".to_string()); - - let elf = assemble_elf_internal(&lines).expect("internal assembler should relax long conditional branches"); - assert!(elf.starts_with(b"\x7fELF")); - } - - #[test] - fn internal_assembler_encodes_register_conditional_branches() { - for mnemonic in ["beq", "bne", "blt", "bge", "bltu", "bgeu"] { - let lines = vec![ - ".section .text".to_string(), - ".global entry".to_string(), - "entry:".to_string(), - "li a0, 1".to_string(), - "li a1, 1".to_string(), - format!("{} a0, a1, target", mnemonic), - "li a0, 2".to_string(), - "target:".to_string(), - "ret".to_string(), - ]; - - let elf = assemble_elf_internal(&lines).unwrap_or_else(|err| panic!("internal assembler should encode {mnemonic}: {err}")); - assert!(elf.starts_with(b"\x7fELF"), "expected ELF output for {mnemonic}"); - } - } - - #[test] - fn internal_assembler_encodes_emitted_instruction_surface() { - let lines = supported_instruction_surface_lines(); - - let elf = assemble_elf_internal(&lines).expect("internal assembler should encode the emitted instruction surface"); - assert!(elf.starts_with(b"\x7fELF")); - } - - #[test] - fn internal_assembler_rejects_intentionally_unsupported_mnemonics() { - for (mnemonic, instruction) in INTENTIONALLY_UNSUPPORTED_INTERNAL_ASSEMBLER_MNEMONICS { - let lines = vec![ - ".section .text".to_string(), - ".global entry".to_string(), - "entry:".to_string(), - (*instruction).to_string(), - "target:".to_string(), - "ret".to_string(), - ]; - let err = match assemble_elf_internal(&lines) { - Ok(_) => panic!("internal assembler unexpectedly accepted unsupported mnemonic {mnemonic}"), - Err(err) => err, - }; - assert!( - err.message.contains("unsupported assembly instruction"), - "unexpected error for unsupported mnemonic {mnemonic}: {err}" + fn emit_create_unique_field_identity_anchor(&mut self, output_index: usize, pattern: &CreatePattern, field: &str) { + let Some(layout) = self.type_layouts.get(&pattern.ty).and_then(|fields| fields.get(field)).cloned() else { + self.emit(format!( + "# cellscript abi: fail closed because create_unique identity field {}.{} has no layout", + pattern.ty, field + )); + self.emit_fail(CellScriptRuntimeError::AssertionFailed); + return; + }; + let Some(width) = layout_fixed_byte_width(&layout) else { + self.emit(format!( + "# cellscript abi: fail closed because create_unique identity field {}.{} is not fixed-width", + pattern.ty, field + )); + self.emit_fail(CellScriptRuntimeError::DynamicFieldValueMismatch); + return; + }; + let output_size_offset = self.runtime_scratch_size_offset(); + let output_buffer_offset = self.runtime_scratch_buffer_offset(); + self.emit_load_cell_data_syscall("create_unique_identity_field", CKB_SOURCE_OUTPUT, output_index); + self.emit_return_on_syscall_error(CellScriptRuntimeError::CellLoadFailed); + let output_pointer_offset = self.runtime_expr_temp_offset(0); + let output_len_offset = self.runtime_expr_temp_offset(1); + let context = format!("create_unique identity field {}.{}", pattern.ty, field); + if self.type_fixed_sizes.contains_key(&pattern.ty) { + self.emit_loaded_fixed_field_pointer_to_stack( + output_size_offset, + output_buffer_offset, + &layout, + width, + &context, + output_pointer_offset, + ); + } else if let Some(field_count) = self.type_layouts.get(&pattern.ty).map(|fields| fields.len()) { + self.emit_dynamic_fixed_field_pointer_to_stack( + output_size_offset, + output_buffer_offset, + &layout, + field_count, + width, + &context, + output_pointer_offset, + output_len_offset, ); + } else { + self.emit_fail(CellScriptRuntimeError::AssertionFailed); + return; } + self.emit(format!( + "# cellscript abi: create_unique field identity anchored by verified Output#{} {}.{} size={}", + output_index, pattern.ty, field, width + )); } - #[test] - fn generated_public_assembly_mnemonics_are_declared() { - let surfaces = [ - ("stdlib", crate::stdlib::StdLib::generate_assembly()), - ("collections", crate::stdlib::collections::Collections::generate_assembly()), - ]; - let supported = SUPPORTED_INTERNAL_ASSEMBLER_MNEMONICS.iter().map(|(mnemonic, _)| *mnemonic).collect::>(); - let mut undeclared = Vec::new(); - - for (surface, assembly) in surfaces { - for (line_number, mnemonic) in emitted_mnemonics(&assembly).into_iter() { - if !supported.contains(mnemonic.as_str()) { - undeclared.push(format!("{surface}:{line_number}: {mnemonic}")); - } + fn emit_replace_unique_identity_check( + &mut self, + output_index: usize, + operand: &IrOperand, + pattern: &CreatePattern, + identity: &IrIdentityPolicy, + ) { + self.emit(format!( + "# cellscript abi: replace_unique identity policy {} for Output#{}", + identity_policy_label(identity), + output_index + )); + let input_index = match operand { + IrOperand::Var(var) => self.consume_indices.get(&var.id).copied().unwrap_or(0), + _ => 0, + }; + match identity { + IrIdentityPolicy::None => {} + IrIdentityPolicy::CkbTypeId | IrIdentityPolicy::SingletonType => { + self.emit_cell_field_hash_equality(CellFieldHashCheck { + left: CellFieldHashLocation { + reason: "replace_unique_input_type_hash", + source: CKB_SOURCE_INPUT, + index: input_index, + }, + right: CellFieldHashLocation { + reason: "replace_unique_output_type_hash", + source: CKB_SOURCE_OUTPUT, + index: output_index, + }, + cell_field: CKB_CELL_FIELD_TYPE_HASH, + field_name: "TypeHash", + detail: "replace_unique type identity preservation", + error: CellScriptRuntimeError::TypeHashMismatch, + }); + } + IrIdentityPolicy::ScriptArgs => { + self.emit_cell_field_hash_equality(CellFieldHashCheck { + left: CellFieldHashLocation { + reason: "replace_unique_input_lock_hash", + source: CKB_SOURCE_INPUT, + index: input_index, + }, + right: CellFieldHashLocation { + reason: "replace_unique_output_lock_hash", + source: CKB_SOURCE_OUTPUT, + index: output_index, + }, + cell_field: CKB_CELL_FIELD_LOCK_HASH, + field_name: "LockHash", + detail: "replace_unique script_args identity preservation", + error: CellScriptRuntimeError::LockHashPreservationMismatch, + }); + } + IrIdentityPolicy::Field(field) => { + self.emit_replace_unique_field_identity_check(output_index, operand, pattern, field); } } - - assert!( - undeclared.is_empty(), - "generated public assembly used mnemonics outside the declared internal assembler surface:\n{}", - undeclared.join("\n") - ); } - #[test] - fn bundled_example_codegen_mnemonics_are_declared() { - let examples = ["amm_pool.cell", "launch.cell", "multisig.cell", "nft.cell", "timelock.cell", "token.cell", "vesting.cell"]; - let supported = SUPPORTED_INTERNAL_ASSEMBLER_MNEMONICS.iter().map(|(mnemonic, _)| *mnemonic).collect::>(); - let mut undeclared = Vec::new(); - - for example in examples { - let path = camino::Utf8PathBuf::from(format!("{}/examples/{}", env!("CARGO_MANIFEST_DIR"), example)); - let result = crate::compile_file( - path, - crate::CompileOptions { target: Some("riscv64-asm".to_string()), ..crate::CompileOptions::default() }, - ) - .unwrap_or_else(|err| panic!("{example} should compile to assembly: {}", err.message)); - let assembly = std::str::from_utf8(&result.artifact_bytes) - .unwrap_or_else(|err| panic!("{example} emitted invalid utf-8 assembly: {err}")); - - for (line_number, mnemonic) in emitted_mnemonics(assembly).into_iter() { - if !supported.contains(mnemonic.as_str()) { - undeclared.push(format!("{example}:{line_number}: {mnemonic}")); - } + fn emit_replace_unique_field_identity_check( + &mut self, + output_index: usize, + operand: &IrOperand, + pattern: &CreatePattern, + field: &str, + ) { + let input_var = match operand { + IrOperand::Var(var) => var, + _ => { + self.emit("# cellscript abi: fail closed because replace_unique identity input is not a cell variable"); + self.emit_fail(CellScriptRuntimeError::DestroyInvalidOperand); + return; } - } + }; + let (Some(input_size_offset), Some(input_buffer_offset)) = + (self.cell_buffer_size_offsets.get(&input_var.id).copied(), self.cell_buffer_offsets.get(&input_var.id).copied()) + else { + self.emit("# cellscript abi: fail closed because replace_unique identity input cell data is unavailable"); + self.emit_fail(CellScriptRuntimeError::CellLoadFailed); + return; + }; + let Some(layout) = self.type_layouts.get(&pattern.ty).and_then(|fields| fields.get(field)).cloned() else { + self.emit(format!( + "# cellscript abi: fail closed because replace_unique identity field {}.{} has no layout", + pattern.ty, field + )); + self.emit_fail(CellScriptRuntimeError::AssertionFailed); + return; + }; + let Some(width) = layout_fixed_byte_width(&layout) else { + self.emit(format!( + "# cellscript abi: fail closed because replace_unique identity field {}.{} is not fixed-width", + pattern.ty, field + )); + self.emit_fail(CellScriptRuntimeError::DynamicFieldValueMismatch); + return; + }; - assert!( - undeclared.is_empty(), - "bundled examples used mnemonics outside the declared internal assembler surface:\n{}", - undeclared.join("\n") + let output_size_offset = self.runtime_scratch_size_offset(); + let output_buffer_offset = self.runtime_scratch_buffer_offset(); + self.emit_load_cell_data_syscall("replace_unique_identity_field_output", CKB_SOURCE_OUTPUT, output_index); + self.emit_return_on_syscall_error(CellScriptRuntimeError::CellLoadFailed); + let input_pointer_offset = self.runtime_expr_temp_offset(0); + let input_len_offset = self.runtime_expr_temp_offset(1); + let output_pointer_offset = self.runtime_expr_temp_offset(2); + let output_len_offset = self.runtime_expr_temp_offset(3); + let input_context = format!("replace_unique input identity field {}.{}", pattern.ty, field); + let output_context = format!("replace_unique output identity field {}.{}", pattern.ty, field); + if self.type_fixed_sizes.contains_key(&pattern.ty) { + self.emit_loaded_fixed_field_pointer_to_stack( + input_size_offset, + input_buffer_offset, + &layout, + width, + &input_context, + input_pointer_offset, + ); + self.emit_loaded_fixed_field_pointer_to_stack( + output_size_offset, + output_buffer_offset, + &layout, + width, + &output_context, + output_pointer_offset, + ); + } else if let Some(field_count) = self.type_layouts.get(&pattern.ty).map(|fields| fields.len()) { + self.emit_dynamic_fixed_field_pointer_to_stack( + input_size_offset, + input_buffer_offset, + &layout, + field_count, + width, + &input_context, + input_pointer_offset, + input_len_offset, + ); + self.emit_dynamic_fixed_field_pointer_to_stack( + output_size_offset, + output_buffer_offset, + &layout, + field_count, + width, + &output_context, + output_pointer_offset, + output_len_offset, + ); + } else { + self.emit_fail(CellScriptRuntimeError::AssertionFailed); + return; + } + self.emit_fixed_pointer_equality( + input_pointer_offset, + output_pointer_offset, + width, + &format!("replace_unique identity field {}.{} Input == Output#{}", pattern.ty, field, output_index), + CellScriptRuntimeError::DynamicFieldValueMismatch, ); } - fn supported_instruction_surface_lines() -> Vec { - let mut lines = vec![".section .text".to_string(), ".global entry".to_string(), "entry:".to_string(), "li a1, 4".to_string()]; - for (mnemonic, instruction) in SUPPORTED_INTERNAL_ASSEMBLER_MNEMONICS { - if !matches!(*mnemonic, "ecall" | "ret") { - lines.push((*instruction).to_string()); + /// create_unique + fn emit_create_unique(&mut self, dest: &IrVar, pattern: &CreatePattern, identity: &IrIdentityPolicy) -> Result<()> { + let output_index = self.operation_output_indices.get(&dest.id).copied().unwrap_or(self.next_virtual_output); + self.generate_create(pattern, output_index, false, false)?; + self.emit_create_unique_identity_check(output_index, pattern, identity); + self.emit(format!("# create_unique {} identity={}", pattern.ty, identity_policy_label(identity))); + for (field, value) in &pattern.fields { + match value { + IrOperand::Const(IrConst::U64(n)) => self.emit(format!("# field {} = {}", field, n)), + IrOperand::Const(IrConst::Bool(b)) => self.emit(format!("# field {} = {}", field, b)), + IrOperand::Var(var) => self.emit(format!("# field {} <- {}", field, var.name)), + _ => self.emit(format!("# field {} <- ", field)), } } - lines.extend([ - "branch_target:".to_string(), - "ecall".to_string(), - "helper:".to_string(), - "ret".to_string(), - "done:".to_string(), - "ret".to_string(), - ".section .rodata".to_string(), - "data_label:".to_string(), - ".word 7".to_string(), - ".byte 1".to_string(), - ".ascii \"x\"".to_string(), - ".align 3".to_string(), - ]); - lines - } - - fn emitted_mnemonics(assembly: &str) -> Vec<(usize, String)> { - assembly - .lines() - .enumerate() - .filter_map(|(index, line)| { - let clean = strip_comment(line)?; - if clean.is_empty() || clean.starts_with('.') || clean.ends_with(':') { - return None; - } - let mnemonic = clean.split_whitespace().next()?.trim_end_matches(','); - Some((index + 1, mnemonic.to_string())) - }) - .collect() + if pattern.lock.is_some() { + self.emit("# with_lock "); + } + self.emit(format!("li t0, {}", output_index)); + self.emit_stack_store("t0", dest.id * 8); + self.next_virtual_output = self.next_virtual_output.max(output_index + 1); + Ok(()) } - #[test] - fn internal_assembler_encodes_full_width_li_literals() { - let lines = vec![ - ".section .text".to_string(), - ".global entry".to_string(), - "entry:".to_string(), - "li a0, 9223372036854775808".to_string(), - "li a1, 18446744073709551615".to_string(), - "ret".to_string(), - ]; - - let elf = assemble_elf_internal(&lines).expect("internal assembler should encode u64-width li literals"); - assert!(elf.starts_with(b"\x7fELF")); + /// replace_unique + fn emit_replace_unique( + &mut self, + dest: &IrVar, + operand: &IrOperand, + pattern: &CreatePattern, + identity: &IrIdentityPolicy, + ) -> Result<()> { + let output_index = self.operation_output_indices.get(&dest.id).copied().unwrap_or(self.next_virtual_output); + self.emit(format!("# replace_unique {} identity={}", pattern.ty, identity_policy_label(identity))); + self.emit_operand_comment("input", operand); + for (field, value) in &pattern.fields { + match value { + IrOperand::Const(IrConst::U64(n)) => self.emit(format!("# field {} = {}", field, n)), + IrOperand::Const(IrConst::Bool(b)) => self.emit(format!("# field {} = {}", field, b)), + IrOperand::Var(var) => self.emit(format!("# field {} <- {}", field, var.name)), + _ => self.emit(format!("# field {} <- ", field)), + } + } + // replace_unique is a consume + create with identity preservation. + // The output occupies a virtual output slot, similar to transfer. + self.generate_create(pattern, output_index, false, false)?; + self.emit_replace_unique_identity_check(output_index, operand, pattern, identity); + if self.emit_verified_operation_output_handle(dest, "replace_unique") { + return Ok(()); + } + self.emit(format!("# cellscript abi: replace_unique output handle Output#{}", output_index)); + self.emit(format!("li t0, {}", output_index)); + self.emit_stack_store("t0", dest.id * 8); + self.next_virtual_output = self.next_virtual_output.max(output_index + 1); + Ok(()) } - #[test] - fn li_parser_enforces_the_complete_64_bit_domain() { - assert_eq!(parse_li_immediate("-0x8000000000000000").unwrap(), i64::MIN as i128); - assert_eq!(parse_li_immediate("+0xffffffffffffffff").unwrap(), u64::MAX as i128); - for value in ["-0x8000000000000001", "-9223372036854775809", "0x10000000000000000", "18446744073709551616"] { - let error = parse_li_immediate(value).expect_err("out-of-domain li literal must fail during parsing"); - assert!(error.message.contains("does not fit 64 bits"), "unexpected error for {value}: {}", error.message); + /// transfer + fn emit_transfer(&mut self, dest: &IrVar, operand: &IrOperand, to: &IrOperand) -> Result<()> { + self.emit("# transfer"); + self.emit_operand_comment("asset", operand); + self.emit_operand_comment("to", to); + if self.emit_verified_operation_output_handle(dest, "transfer") { + return Ok(()); + } + if let Some(output_index) = self.operation_output_indices.get(&dest.id).copied() { + self.emit(format!("# cellscript abi: transfer output handle Output#{} (unverified)", output_index)); + self.emit(format!("li t0, {}", output_index)); + self.emit_stack_store("t0", dest.id * 8); + self.next_virtual_output = self.next_virtual_output.max(output_index + 1); + return Ok(()); } + self.emit("# cellscript abi: fail closed because transfer output relation is unknown"); + self.emit_fail(CellScriptRuntimeError::DestroyInvalidOperand); + Ok(()) } - #[test] - fn signed_immediate_parser_accepts_explicit_plus() { - assert_eq!(parse_immediate("+12").unwrap(), 12); - assert_eq!(parse_immediate("+0x7ff").unwrap(), 0x7ff); - } - - #[test] - fn split_hi_lo_rejects_extreme_values_before_arithmetic() { - assert!(split_hi_lo(i64::MIN).is_err()); - assert!(split_hi_lo(i64::MAX).is_err()); - assert_eq!(split_hi_lo(i32::MIN as i64).unwrap(), (-0x80000, 0)); - assert_eq!(split_hi_lo(0x7fff_f7ff).unwrap(), (0x7ffff, 0x7ff)); - assert!(split_hi_lo(0x7fff_f800).is_err()); - } - - #[test] - fn runtime_expression_temp_offsets_are_explicitly_bounded() { - let mut generator = CodeGenerator::new(CodegenOptions::default()); - generator.frame_size = RUNTIME_EXPR_TEMP_SIZE + RUNTIME_SCRATCH_SIZE + 16; - assert!(generator.checked_runtime_expr_temp_offset(0).is_some()); - assert!(generator.checked_runtime_expr_temp_offset(RUNTIME_EXPR_TEMP_SLOTS - 1).is_some()); - assert_eq!(generator.checked_runtime_expr_temp_offset(RUNTIME_EXPR_TEMP_SLOTS), None); - assert_eq!(generator.checked_runtime_expr_temp_offset(usize::MAX), None); - } - - #[test] - fn rv64_li_boundary_values_materialize_correct_bits() { - let cases = [(0x7fff_f7ffi128, 8usize), (0x7fff_f800i128, 60usize), (0x7fff_ffffi128, 60usize), (0x8000_0000i128, 60usize)]; - - for (value, expected_size) in cases { - let mut bytes = Vec::new(); - encode_li_sequence(&mut bytes, 10, value).expect("li should encode"); - assert_eq!(bytes.len(), expected_size, "unexpected li size for {value:#x}"); - assert_eq!(simulate_li_sequence(&bytes, 10), value as u64, "li materialized wrong bits for {value:#x}"); + /// claim + fn emit_claim(&mut self, dest: &IrVar, receipt: &IrOperand) -> Result<()> { + self.emit("# claim"); + self.emit_operand_comment("receipt", receipt); + if self.emit_verified_operation_output_handle(dest, "claim") { + return Ok(()); } - } - - fn simulate_li_sequence(bytes: &[u8], register: usize) -> u64 { - let mut regs = [0u64; 32]; - for chunk in bytes.chunks_exact(4) { - let inst = u32::from_le_bytes(chunk.try_into().expect("instruction chunk should be four bytes")); - let opcode = inst & 0x7f; - let rd = ((inst >> 7) & 0x1f) as usize; - let funct3 = (inst >> 12) & 0x7; - let rs1 = ((inst >> 15) & 0x1f) as usize; - match (opcode, funct3) { - (0x37, _) => { - regs[rd] = ((inst & 0xffff_f000) as i32 as i64) as u64; - } - (0x13, 0b000) => { - let imm = sign_extend(inst >> 20, 12); - regs[rd] = regs[rs1].wrapping_add(imm as u64); - } - (0x13, 0b001) => { - let shamt = (inst >> 20) & 0x3f; - regs[rd] = regs[rs1] << shamt; - } - _ => panic!("unexpected instruction in li sequence: 0x{inst:08x}"), - } - regs[0] = 0; + if let Some(output_index) = self.operation_output_indices.get(&dest.id).copied() { + self.emit(format!("# cellscript abi: claim output handle Output#{} (unverified)", output_index)); + self.emit(format!("li t0, {}", output_index)); + self.emit_stack_store("t0", dest.id * 8); + self.next_virtual_output = self.next_virtual_output.max(output_index + 1); + return Ok(()); } - regs[register] - } - - fn sign_extend(value: u32, bits: u32) -> i64 { - let shift = 64 - bits; - ((u64::from(value) << shift) as i64) >> shift - } - - #[test] - fn stack_pointer_offsets_are_emitted_through_helpers() { - let implementation = include_str!("mod.rs") - .split("\n fn emit_runtime_ckb_v014_surface_helpers") - .next() - .expect("source should contain runtime helper boundary"); - let offenders = implementation - .lines() - .enumerate() - .filter_map(|(index, line)| { - let emits_stack_memory = - (line.contains("self.emit(format!(") || line.contains("self.emit(\"")) && line.contains("(sp)"); - let emits_stack_addi = - (line.contains("self.emit(\"addi ") || line.contains("self.emit(format!(\"addi ")) && line.contains(", sp,"); - let allowed_stack_memory = line.contains("self.emit(format!(\"{} {}, {}(sp)\", opcode, register, offset))"); - let allowed_outgoing_stack_memory = line.contains("self.emit(format!(\"sd {}, {}(sp)\", register, offset))"); - let allowed_stack_addi = line.contains("self.emit(format!(\"addi {}, sp, {}\", rd, offset))"); - ((emits_stack_memory && !allowed_stack_memory && !allowed_outgoing_stack_memory) - || (emits_stack_addi && !allowed_stack_addi)) - .then(|| format!("{}: {}", index + 1, line.trim())) - }) - .collect::>(); - - assert!(offenders.is_empty(), "stack pointer accesses must go through stack helpers:\n{}", offenders.join("\n")); + self.emit("# cellscript abi: fail closed because claim output relation is unknown"); + self.emit_fail(CellScriptRuntimeError::DestroyInvalidOperand); + Ok(()) } - #[test] - fn large_addi_avoids_clobbering_source_register() { - let mut generator = CodeGenerator::new(CodegenOptions::default()); - generator.emit_large_addi("t0", "t6", 2048); - generator.emit_large_addi("t6", "t6", 4096); - - assert_eq!(generator.assembly, vec![" li t5, 2048", " add t0, t6, t5", " li t5, 4096", " add t6, t6, t5",]); + /// settle + fn emit_settle(&mut self, dest: &IrVar, operand: &IrOperand) -> Result<()> { + self.emit("# settle"); + self.emit_operand_comment("value", operand); + if self.emit_verified_operation_output_handle(dest, "settle") { + return Ok(()); + } + if let Some(output_index) = self.operation_output_indices.get(&dest.id).copied() { + self.emit(format!("# cellscript abi: settle output handle Output#{} (unverified)", output_index)); + self.emit(format!("li t0, {}", output_index)); + self.emit_stack_store("t0", dest.id * 8); + self.next_virtual_output = self.next_virtual_output.max(output_index + 1); + return Ok(()); + } + self.emit("# cellscript abi: fail closed because settle output relation is unknown"); + self.emit_fail(CellScriptRuntimeError::DestroyInvalidOperand); + Ok(()) } - #[test] - fn sp_addi_large_offsets_clobber_only_destination_register() { - let mut generator = CodeGenerator::new(CodegenOptions::default()); - generator.emit_sp_addi("t4", 4096); - generator.emit_sp_addi("t6", 8192); - - assert_eq!(generator.assembly, vec![" li t4, 4096", " add t4, sp, t4", " li t6, 8192", " add t6, sp, t6",]); + fn emit_verified_operation_output_handle(&mut self, dest: &IrVar, operation: &str) -> bool { + if !self.verified_operation_outputs.contains(&dest.id) { + return false; + } + let output_index = self.operation_output_indices.get(&dest.id).copied().unwrap_or(self.next_virtual_output); + self.emit(format!("# cellscript abi: {} output relation verified by prelude Output#{}", operation, output_index)); + self.emit(format!("li t0, {}", output_index)); + self.emit_stack_store("t0", dest.id * 8); + self.next_virtual_output = self.next_virtual_output.max(output_index + 1); + true } - #[test] - fn state_transition_edges_use_explicit_consumed_binding() { - let mut generator = CodeGenerator::new(CodegenOptions::default()); - generator.consume_order = vec![1, 2]; - generator.consume_type_names.insert(1, "Offer".to_string()); - generator.consume_type_names.insert(2, "Offer".to_string()); - generator.consume_binding_ids.insert("left".to_string(), 1); - generator.consume_binding_ids.insert("right".to_string(), 2); - - let state_edge = IrStateTransitionEdge { - input_binding: Some("right".to_string()), - output_binding: None, - type_name: "Offer".to_string(), - field_name: "state".to_string(), - from: "Live".to_string(), - to: "Filled".to_string(), - from_index: 1, - to_index: 2, - }; - - assert_eq!(generator.consumed_var_for_state_transition("Offer", &[state_edge]), Some(2)); + /// destroy + fn emit_destroy(&mut self, operand: &IrOperand) -> Result<()> { + self.emit("# destroy"); + if let IrOperand::Var(_) = operand { + self.emit_operand_comment("destroyed input retained for verifier field checks", operand); + self.emit("# cellscript abi: destroy consumed input is checked by Output absence scan"); + self.emit("# cellscript abi: retain consumed input pointer for post-destroy output verification"); + return Ok(()); + } + // Non-Var destroy: this should not happen in valid IR, fail with specific error. + self.emit("# cellscript abi: fail closed because destroy operand is not a variable"); + self.emit_fail(CellScriptRuntimeError::ConsumeInvalidOperand); + Ok(()) } - #[test] - fn consumed_schema_params_use_loaded_cell_size_for_field_checks() { - let mut generator = CodeGenerator::new(CodegenOptions::default()); - let binding = IrVar { id: 0, name: "auth".to_string(), ty: IrType::Named("MintAuthority".to_string()) }; - let params = vec![IrParam { - name: "auth".to_string(), - ty: binding.ty.clone(), - is_mut: false, - is_ref: false, - is_read_ref: false, - source: ParamSource::Default, - binding: binding.clone(), - }]; - let body = IrBody { - consume_set: vec![CellPattern { - operation: "input".to_string(), - type_hash: None, - binding: "auth".to_string(), - fields: Vec::new(), - }], - read_refs: Vec::new(), - create_set: Vec::new(), - mutate_set: Vec::new(), - write_intents: Vec::new(), - bounded_collection_ops: Vec::new(), - borrow_regions: Vec::new(), - blocks: Vec::new(), + fn emit_operand_comment(&mut self, label: &str, operand: &IrOperand) { + let rendered = match operand { + IrOperand::Var(var) => format!("{}: {}", label, var.name), + IrOperand::Const(IrConst::U64(n)) => format!("{}: {}", label, n), + IrOperand::Const(IrConst::Bool(b)) => format!("{}: {}", label, b), + IrOperand::Const(IrConst::Address(_)) => format!("{}:
", label), + IrOperand::Const(IrConst::Hash(_)) => format!("{}: ", label), + IrOperand::Const(IrConst::Array(items)) => format!("{}: ", label, items.len()), + IrOperand::Const(_) => format!("{}: ", label), }; - - generator.prepare_function_layout(&body, ¶ms); - - let loaded_size_offset = - generator.cell_buffer_size_offsets.get(&binding.id).copied().expect("consumed input should have size slot"); - assert_eq!(generator.schema_pointer_size_offsets.get(&binding.id), Some(&loaded_size_offset)); - } - - #[test] - fn unaligned_scalar_load_large_offsets_preserve_live_accumulator() { - let mut generator = CodeGenerator::new(CodegenOptions::default()); - generator.emit_unaligned_scalar_load("t4", "t6", "t2", 2048, 2); - - assert_eq!( - generator.assembly, - vec![ - " li t6, 0", - " li t5, 2048", - " add t5, t4, t5", - " lbu t2, 0(t5)", - " or t6, t6, t2", - " li t5, 2049", - " add t5, t4, t5", - " lbu t2, 0(t5)", - " slli t2, t2, 8", - " or t6, t6, t2", - ] - ); - } - - #[test] - fn generated_large_offsets_are_normalized_before_assembly() { - let mut generator = CodeGenerator::new(CodegenOptions::default()); - generator.emit("sd t0, 2048(sp)"); - generator.emit("ld t6, 2056(sp)"); - generator.emit("lbu t2, 2048(t4)"); - generator.emit("addi t0, t4, 2048"); - generator.emit("sb t0, 4096(t6)"); - - assert_eq!( - generator.assembly, - vec![ - " li t6, 2048", - " add t6, sp, t6", - " sd t0, 0(t6)", - " li t5, 2056", - " add t5, sp, t5", - " ld t6, 0(t5)", - " li t6, 2048", - " add t6, t4, t6", - " lbu t2, 0(t6)", - " li t6, 2048", - " add t0, t4, t6", - " li t5, 4096", - " add t5, t6, t5", - " sb t0, 0(t5)", - ] - ); - } - - #[test] - fn read_ref_runtime_fallback_records_cell_buffer_state() { - let mut generator = CodeGenerator::new(CodegenOptions::default()); - generator.frame_size = align_frame(RUNTIME_EXPR_TEMP_SIZE + RUNTIME_SCRATCH_SIZE + 16); - let dest = IrVar { id: 42, name: "cfg".to_string(), ty: IrType::Named("Config".to_string()) }; - generator.read_ref_indices.insert(dest.id, 0); - - generator.emit_read_ref(&dest, "Config").expect("read_ref fallback should lower"); - - let size_offset = generator.runtime_scratch_size_offset(); - let buffer_offset = generator.runtime_scratch_buffer_offset(); - assert_eq!(generator.schema_pointer_size_offsets.get(&dest.id), Some(&size_offset)); - assert_eq!(generator.cell_buffer_size_offsets.get(&dest.id), Some(&size_offset)); - assert_eq!(generator.cell_buffer_offsets.get(&dest.id), Some(&buffer_offset)); - } - - #[test] - fn explicit_external_toolchain_paths_are_strict() { - let err = validate_explicit_toolchain_path("CELLSCRIPT_RISCV_CC", PathBuf::from("riscv64-unknown-elf-gcc")).unwrap_err(); - assert!(err.message.contains("must be an absolute path"), "unexpected error: {}", err.message); - - let err = validate_explicit_toolchain_path("CELLSCRIPT_RISCV_CC", std::env::temp_dir()).unwrap_err(); - assert!(err.message.contains("must point to an executable file"), "unexpected error: {}", err.message); - - let current_exe = std::env::current_exe().expect("test executable path should be available"); - let validated = - validate_explicit_toolchain_path("CELLSCRIPT_RISCV_CC", current_exe.clone()).expect("current test binary is executable"); - assert_eq!(validated, current_exe); - } - - #[test] - fn generated_stdlib_assembly_is_internal_assembler_clean() { - let lines = crate::stdlib::StdLib::generate_assembly().lines().map(|line| line.to_string()).collect::>(); - - let elf = assemble_elf_internal(&lines).expect("generated stdlib assembly should assemble internally"); - assert!(elf.starts_with(b"\x7fELF")); + self.emit(format!("# {}", rendered)); } - #[test] - fn generated_collection_assembly_is_internal_assembler_clean() { - let lines = - crate::stdlib::collections::Collections::generate_assembly().lines().map(|line| line.to_string()).collect::>(); - - let elf = assemble_elf_internal(&lines).expect("generated collection assembly should assemble internally"); - assert!(elf.starts_with(b"\x7fELF")); + fn static_length(&self, operand: &IrOperand) -> Option { + match operand { + IrOperand::Var(var) => Self::static_length_from_type(&var.ty), + IrOperand::Const(IrConst::Array(items)) => Some(items.len()), + _ => None, + } } - #[test] - fn internal_assembler_rejects_unresolved_call_targets() { - let lines = vec![".section .text".to_string(), ".global main".to_string(), "main:".to_string(), "call missing".to_string()]; - let err = assemble_elf_internal(&lines).unwrap_err(); - - assert!(err.message.contains("unknown assembly label 'missing'"), "unexpected error: {}", err.message); + fn static_length_from_type(ty: &IrType) -> Option { + match ty { + IrType::Array(_, size) => Some(*size), + IrType::Ref(inner) | IrType::MutRef(inner) => Self::static_length_from_type(inner), + _ => None, + } } +} - #[test] - fn elf_assembly_classifies_unresolved_symbols() { - let lines = vec![".section .text".to_string(), ".global main".to_string(), "main:".to_string(), "call missing".to_string()]; - let err = assemble_elf(&lines).unwrap_err(); - - assert_eq!(err.code.as_deref(), Some("E2200")); - assert!(err.message.contains("unresolved call target"), "unexpected error: {}", err.message); +fn with_codegen_code(error: CompileError, code: &'static str) -> CompileError { + if error.code.is_some() { + error + } else { + error.with_code(code) } +} - #[test] - fn internal_assembler_relaxes_out_of_range_register_conditional_branch() { - for mnemonic in ["beq", "bne", "blt", "bge", "bltu", "bgeu"] { - let mut lines = vec![ - ".section .text".to_string(), - ".global entry".to_string(), - "entry:".to_string(), - "li a0, 0".to_string(), - "li a1, 0".to_string(), - format!("{} a0, a1, far_target", mnemonic), - ]; - for _ in 0..1500 { - lines.push("addi t0, t0, 0".to_string()); - } - lines.push("far_target:".to_string()); - lines.push("ret".to_string()); +pub fn analyze_backend_shape(assembly: &str) -> Result { + let lines = assembly.lines().map(str::to_string).collect::>(); + MachineLayoutPlan::build(&lines).map(|plan| plan.metrics.into()) +} - let plan = MachineLayoutPlan::build(&lines).unwrap_or_else(|err| panic!("machine layout should relax {mnemonic}: {err}")); - assert_eq!(plan.metrics.relaxed_branch_count, 1, "expected one relaxed branch for {mnemonic}"); - let elf = assemble_elf_internal(&lines).unwrap_or_else(|err| panic!("internal assembler should relax {mnemonic}: {err}")); - assert!(elf.starts_with(b"\x7fELF"), "expected ELF output for relaxed {mnemonic}"); +fn first_entrypoint(ir: &IrModule) -> Option<(&str, &[IrParam])> { + for item in &ir.items { + if let IrItem::Action(action) = item + && action.name == "main" + { + return Some((&action.name, &action.params)); } } - - #[test] - fn machine_layout_plan_reports_branch_relaxation_metrics() { - let mut lines = vec![ - ".section .text".to_string(), - ".global entry".to_string(), - "entry:".to_string(), - "li a0, 0".to_string(), - "beqz a0, far_target".to_string(), - ]; - for _ in 0..1500 { - lines.push("addi t0, t0, 0".to_string()); + for item in &ir.items { + if let IrItem::Action(action) = item + && action.params.is_empty() + { + return Some((&action.name, &action.params)); } - lines.push("far_target:".to_string()); - lines.push("ret".to_string()); - - let plan = MachineLayoutPlan::build(&lines).expect("machine layout plan"); - assert_eq!(plan.metrics.relaxed_branch_count, 1); - assert!( - plan.metrics.max_cond_branch_abs_distance > 4096, - "synthetic branch should exceed RV64 B-type range: {:?}", - plan.metrics - ); - assert_eq!(plan.metrics.text_size, plan.parsed.section_size(SectionKind::Text)); - assert_eq!(plan.metrics.covered_text_op_count, plan.metrics.executable_text_op_count); - assert!(plan.metrics.executable_text_op_count > 1500, "synthetic text ops should be visible: {:?}", plan.metrics); - assert_eq!(plan.metrics.layout_order_block_count, plan.metrics.machine_block_count); - assert_eq!(plan.metrics.layout_order_text_size, plan.metrics.text_size); - assert_eq!(plan.metrics.conditional_branch_block_count, 1); - assert!(plan.metrics.machine_cfg_edge_count >= 2, "far branch CFG edges should be visible: {:?}", plan.metrics); - assert_eq!(plan.metrics.machine_call_edge_count, 0); - assert_eq!(plan.metrics.unreachable_machine_block_count, 0); - assert!(plan.metrics.machine_block_count >= 2, "far branch should produce multiple machine blocks: {:?}", plan.metrics); - assert!( - plan.metrics.max_machine_block_size > 4096, - "large fallthrough block should be visible in layout metrics: {:?}", - plan.metrics - ); - } - - #[test] - fn machine_layout_plan_builds_explicit_machine_blocks() { - let lines = vec![ - ".section .text".to_string(), - ".global entry".to_string(), - "entry:".to_string(), - "li a0, 0".to_string(), - "beqz a0, done".to_string(), - "li a0, 1".to_string(), - "j done".to_string(), - "done:".to_string(), - "ret".to_string(), - ]; - - let plan = MachineLayoutPlan::build(&lines).expect("machine layout plan"); - let cfg = &plan.cfg; - let blocks = &cfg.blocks; - assert_eq!(blocks.len(), 3, "expected entry, fallthrough, and done blocks: {:?}", blocks); - assert_eq!(blocks[0].label.as_deref(), Some("entry")); - assert_eq!(blocks[0].terminator, MachineTerminator::ConditionalBranch { target: "done".to_string() }); - assert_eq!(blocks[1].terminator, MachineTerminator::Jump { target: "done".to_string() }); - assert_eq!(blocks[2].label.as_deref(), Some("done")); - assert_eq!(blocks[2].terminator, MachineTerminator::Return); - - assert_eq!(cfg.blocks.len(), 3); - assert_eq!(plan.order.block_order, vec![0, 1, 2]); - assert_eq!(plan.order.placed_blocks.len(), 3); - assert_eq!( - plan.order.placed_blocks, - vec![ - MachinePlacedBlock { block_index: 0, byte_start: 0, byte_size: cfg.blocks[0].byte_size }, - MachinePlacedBlock { block_index: 1, byte_start: cfg.blocks[0].byte_size, byte_size: cfg.blocks[1].byte_size }, - MachinePlacedBlock { - block_index: 2, - byte_start: cfg.blocks[0].byte_size + cfg.blocks[1].byte_size, - byte_size: cfg.blocks[2].byte_size - }, - ] - ); - assert_eq!(plan.order.text_size, plan.metrics.text_size); - assert_eq!(plan.metrics.executable_text_op_count, 5); - assert_eq!(plan.metrics.covered_text_op_count, 5); - assert_eq!(plan.metrics.layout_order_block_count, 3); - assert_eq!( - cfg.edges, - vec![ - MachineCfgEdge { from: 0, to: 2, kind: MachineCfgEdgeKind::ConditionalTaken }, - MachineCfgEdge { from: 0, to: 1, kind: MachineCfgEdgeKind::ConditionalFallthrough }, - MachineCfgEdge { from: 1, to: 2, kind: MachineCfgEdgeKind::Jump }, - ] - ); - assert_eq!(unreachable_machine_block_count(&plan.parsed, cfg), 0); } - - #[test] - fn machine_layout_plan_builds_register_conditional_branch_blocks() { - let lines = vec![ - ".section .text".to_string(), - ".global entry".to_string(), - "entry:".to_string(), - "li a0, 0".to_string(), - "li a1, 0".to_string(), - "bgeu a0, a1, done".to_string(), - "li a0, 1".to_string(), - "j done".to_string(), - "done:".to_string(), - "ret".to_string(), - ]; - - let plan = MachineLayoutPlan::build(&lines).expect("machine layout plan"); - let cfg = &plan.cfg; - assert_eq!(cfg.blocks.len(), 3, "expected entry, fallthrough, and done blocks: {:?}", cfg.blocks); - assert_eq!(cfg.blocks[0].label.as_deref(), Some("entry")); - assert_eq!(cfg.blocks[0].terminator, MachineTerminator::ConditionalBranch { target: "done".to_string() }); - assert_eq!( - cfg.edges, - vec![ - MachineCfgEdge { from: 0, to: 2, kind: MachineCfgEdgeKind::ConditionalTaken }, - MachineCfgEdge { from: 0, to: 1, kind: MachineCfgEdgeKind::ConditionalFallthrough }, - MachineCfgEdge { from: 1, to: 2, kind: MachineCfgEdgeKind::Jump }, - ] - ); + for item in &ir.items { + if let IrItem::Action(action) = item { + return Some((&action.name, &action.params)); + } } - - #[test] - fn machine_cfg_tracks_call_edges_to_local_helpers() { - let lines = vec![ - ".section .text".to_string(), - ".global entry".to_string(), - "entry:".to_string(), - "call local_helper".to_string(), - "ret".to_string(), - "local_helper:".to_string(), - "li a0, 0".to_string(), - "ret".to_string(), - ]; - - let plan = MachineLayoutPlan::build(&lines).expect("machine layout plan"); - let cfg = &plan.cfg; - assert_eq!(cfg.blocks.len(), 2, "expected entry and local helper blocks: {:?}", cfg.blocks); - assert_eq!(cfg.blocks[0].label.as_deref(), Some("entry")); - assert_eq!(cfg.blocks[1].label.as_deref(), Some("local_helper")); - assert!( - cfg.edges.contains(&MachineCfgEdge { from: 0, to: 1, kind: MachineCfgEdgeKind::Call }), - "call edge to local helper should be explicit: {:?}", - cfg.edges - ); - assert_eq!(plan.metrics.machine_call_edge_count, 1); - assert_eq!(unreachable_machine_block_count(&plan.parsed, cfg), 0); + for item in &ir.items { + if let IrItem::Lock(lock) = item { + return Some((&lock.name, &lock.params)); + } } + None +} - #[test] - fn machine_reachability_uses_entry_label_not_every_global() { - let lines = vec![ - ".section .text".to_string(), - ".global entry".to_string(), - "entry:".to_string(), - "li a0, 0".to_string(), - "ret".to_string(), - ".global unused_export".to_string(), - "unused_export:".to_string(), - "li a0, 1".to_string(), - "ret".to_string(), - ]; - - let plan = MachineLayoutPlan::build(&lines).expect("machine layout plan"); - assert_eq!(plan.parsed.entry_label.as_deref(), Some("entry")); - assert_eq!(plan.cfg.blocks.len(), 2, "expected entry and unused export blocks: {:?}", plan.cfg.blocks); - assert_eq!(plan.metrics.unreachable_machine_block_count, 1); - assert_eq!(unreachable_machine_block_count(&plan.parsed, &plan.cfg), 1); - } +fn entry_witness_payload_layout( + params: &[IrParam], + runtime_bound_param_indices: &BTreeSet, + enum_layouts: &HashMap, +) -> Vec { + params + .iter() + .enumerate() + .map(|(index, param)| { + if !entry_param_consumes_witness_payload(param, index, runtime_bound_param_indices) { + EntryWitnessPayloadArg { width: 0, schema_dynamic: false, unsupported: false } + } else if let Some(layout) = match ¶m.ty { + IrType::Named(name) => enum_layouts.get(name).filter(|layout| layout.has_payload()), + _ => None, + } { + EntryWitnessPayloadArg { width: layout.encoded_size, schema_dynamic: false, unsupported: false } + } else if entry_witness_dynamic_schema_param(¶m.ty) { + EntryWitnessPayloadArg { width: 4, schema_dynamic: true, unsupported: false } + } else if let Some(width) = + fixed_byte_pointer_param_width(¶m.ty).or_else(|| fixed_aggregate_pointer_param_width(¶m.ty)) + { + EntryWitnessPayloadArg { width, schema_dynamic: false, unsupported: false } + } else if let Some(width) = entry_witness_register_param_width(¶m.ty) { + EntryWitnessPayloadArg { width, schema_dynamic: false, unsupported: false } + } else { + EntryWitnessPayloadArg { width: 0, schema_dynamic: false, unsupported: true } + } + }) + .collect() +} - #[test] - fn machine_layout_order_rejects_missing_duplicate_or_unknown_blocks() { - let lines = vec![ - ".section .text".to_string(), - ".global entry".to_string(), - "entry:".to_string(), - "li a0, 0".to_string(), - "beqz a0, done".to_string(), - "li a0, 1".to_string(), - "j done".to_string(), - "done:".to_string(), - "ret".to_string(), - ]; +fn entry_param_consumes_witness_payload(param: &IrParam, index: usize, runtime_bound_param_indices: &BTreeSet) -> bool { + param.source != ParamSource::LockArgs + && !runtime_bound_param_indices.contains(&index) + && !matches!(param.ty, IrType::Ref(_) | IrType::MutRef(_)) +} - let plan = MachineLayoutPlan::build(&lines).expect("machine layout plan"); - assert!(validate_machine_layout_order(&plan.cfg, &[0, 1]).is_err()); - assert!(validate_machine_layout_order(&plan.cfg, &[0, 1, 1]).is_err()); - assert!(validate_machine_layout_order(&plan.cfg, &[0, 1, 3]).is_err()); - let permuted = build_machine_layout_order(&plan.cfg, vec![2, 0, 1]).expect("permuted layout order should be valid"); - assert_eq!(permuted.block_order, vec![2, 0, 1]); - assert_eq!(permuted.placed_blocks[0].block_index, 2); - assert_eq!(permuted.placed_blocks[0].byte_start, 0); - assert_eq!(permuted.placed_blocks[1].byte_start, plan.cfg.blocks[2].byte_size); - assert_eq!(permuted.text_size, plan.order.text_size); - } +fn entry_witness_dynamic_schema_param(ty: &IrType) -> bool { + fixed_byte_pointer_param_width(ty).is_none() + && fixed_aggregate_pointer_param_width(ty).is_none() + && entry_witness_register_param_width(ty).is_none() +} - #[test] - fn machine_layout_plan_rejects_branch_target_outside_text() { - let lines = vec![ - ".section .text".to_string(), - ".global entry".to_string(), - "entry:".to_string(), - "li a0, 0".to_string(), - "beqz a0, data_label".to_string(), - "ret".to_string(), - ".section .rodata".to_string(), - "data_label:".to_string(), - ".word 1".to_string(), - ]; +fn entry_witness_register_param_width(ty: &IrType) -> Option { + fixed_register_width(ty, type_static_length(ty)).or_else(|| match ty { + IrType::Array(_, _) | IrType::Tuple(_) => type_static_length(ty).filter(|width| (1..=8).contains(width)), + IrType::Unit => Some(0), + _ => None, + }) +} - let err = MachineLayoutPlan::build(&lines).expect_err("branch targets outside text blocks should be rejected"); - assert!(err.message.contains("does not start a machine block"), "unexpected error for invalid CFG target: {}", err.message); +fn named_type_name(ty: &IrType) -> Option<&str> { + match ty { + IrType::Named(name) => Some(name.as_str()), + IrType::Ref(inner) | IrType::MutRef(inner) => named_type_name(inner), + _ => None, } +} - #[test] - fn generated_functions_use_shared_epilogue_tail() { - let ir = IrModule { - name: "shape_test".to_string(), - items: vec![IrItem::Action(IrAction { - name: "shape".to_string(), - params: vec![], - return_type: Some(IrType::U64), - state_transition_edges: vec![], - protocol_role_candidates: vec![], - effect_class: EffectClass::Pure, - scheduler_hints: SchedulerHints::default(), - body: IrBody { - consume_set: vec![], - read_refs: vec![], - create_set: vec![], - mutate_set: vec![], - write_intents: vec![], - bounded_collection_ops: vec![], - borrow_regions: vec![], - blocks: vec![IrBlock { - id: BlockId(0), - instructions: vec![], - terminator: IrTerminator::Return(Some(IrOperand::Const(IrConst::U64(7)))), - }], - }, - })], - external_type_defs: vec![], - external_callable_abis: vec![], - enum_fixed_sizes: HashMap::new(), - enum_layouts: HashMap::new(), - }; - let assembly = CodeGenerator::new(CodegenOptions::default()).generate(&ir, ArtifactFormat::RiscvAssembly).unwrap(); - let assembly = String::from_utf8(assembly).unwrap(); - let shape_start = assembly.find("shape:\n").expect("shape function label"); - let runtime_start = - assembly[shape_start..].find(".section .text").map(|offset| shape_start + offset).unwrap_or(assembly.len()); - let shape_assembly = &assembly[shape_start..runtime_start]; - - assert!(shape_assembly.contains("j .Lshape_epilogue"), "return sites should jump to the shared epilogue:\n{}", shape_assembly); - assert_eq!( - shape_assembly.matches(".Lshape_epilogue:").count(), - 1, - "a function should emit one shared epilogue label:\n{}", - shape_assembly - ); - assert_eq!( - shape_assembly.matches("ret").count(), - 1, - "a function should emit one physical return in its shared epilogue:\n{}", - shape_assembly - ); +fn consumed_operand_var(instruction: &IrInstruction) -> Option<&IrVar> { + let operand = match instruction { + IrInstruction::Consume { operand } + | IrInstruction::Transfer { operand, .. } + | IrInstruction::Destroy { operand, .. } + | IrInstruction::Settle { operand, .. } + | IrInstruction::ReplaceUnique { operand, .. } => operand, + IrInstruction::Claim { receipt, .. } => receipt, + _ => return None, + }; + match operand { + IrOperand::Var(var) if named_type_name(&var.ty).is_some() => Some(var), + _ => None, } } diff --git a/src/codegen/runtime.rs b/src/codegen/runtime.rs new file mode 100644 index 00000000..e7dd2b06 --- /dev/null +++ b/src/codegen/runtime.rs @@ -0,0 +1,6941 @@ +use super::*; + +#[derive(Clone, Copy)] +struct OrderMasterDataOffsets { + source: usize, + cell_index: usize, + action_offset: usize, + tx_hash_offset: usize, + index_offset: usize, + tx_dest: usize, + index_dest: usize, + data_buffer: usize, + size: usize, +} + +#[derive(Clone, Copy)] +struct OrderMasterFailureLabels<'a> { + invalid_action: &'a str, + malformed: &'a str, + out_point_failed: &'a str, +} + +impl CodeGenerator { + pub(super) fn generate_runtime_support(&mut self, ir: &IrModule) { + self.emit_section(".text"); + self.emit_runtime_memcmp_fixed(); + self.emit_runtime_memzero_fixed(); + self.emit_runtime_memcpy_fixed(); + self.emit_runtime_size_guards(); + // CKB exposes epoch-number based timepoints here, not Unix timestamps. + self.emit_runtime_header_field_u64( + "__env_current_timepoint", + "ckb_epoch_number", + CKB_HEADER_FIELD_EPOCH_NUMBER, + true, + "env::current_timepoint is required for CKB profile", + ); + self.emit_runtime_header_field_u64( + "__ckb_header_epoch_number", + "ckb_epoch_number", + CKB_HEADER_FIELD_EPOCH_NUMBER, + self.options.target_profile == TargetProfile::Ckb, + "ckb::header_epoch_number is rejected outside the ckb target profile", + ); + self.emit_runtime_header_field_u64( + "__ckb_header_epoch_start_block_number", + "ckb_epoch_start_block_number", + CKB_HEADER_FIELD_EPOCH_START_BLOCK_NUMBER, + self.options.target_profile == TargetProfile::Ckb, + "ckb::header_epoch_start_block_number is rejected outside the ckb target profile", + ); + self.emit_runtime_header_field_u64( + "__ckb_header_epoch_length", + "ckb_epoch_length", + CKB_HEADER_FIELD_EPOCH_LENGTH, + self.options.target_profile == TargetProfile::Ckb, + "ckb::header_epoch_length is rejected outside the ckb target profile", + ); + self.emit_runtime_input_field_u64( + "__ckb_input_since", + "ckb_input_since", + CKB_INPUT_FIELD_SINCE, + self.options.target_profile == TargetProfile::Ckb, + "ckb::input_since is rejected outside the ckb target profile", + ); + let v014_helpers = referenced_v014_runtime_helpers(ir); + self.emit_runtime_ckb_v014_surface_helpers(&v014_helpers); + } + + fn emit_runtime_ckb_v014_surface_helpers(&mut self, referenced_helpers: &BTreeSet) { + let enabled = self.options.target_profile == TargetProfile::Ckb; + for (name, syscall, detail) in [ + ("__ckb_spawn", ckb_abi::syscall::SPAWN, "spawn bounded verifier child"), + ("__ckb_wait", ckb_abi::syscall::WAIT, "wait for bounded verifier child"), + ("__ckb_process_id", ckb_abi::syscall::PROCESS_ID, "current process id"), + ("__ckb_pipe", ckb_abi::syscall::PIPE, "create IPC pipe; returns read fd in a0 and write fd in a1"), + ("__ckb_pipe_write", ckb_abi::syscall::WRITE, "write u64 payload to IPC pipe"), + ("__ckb_pipe_read", ckb_abi::syscall::READ, "read u64 payload from IPC pipe"), + ("__ckb_inherited_fd", ckb_abi::syscall::INHERITED_FDS, "resolve inherited fd"), + ("__ckb_close", ckb_abi::syscall::CLOSE, "close fd"), + ] { + if !referenced_helpers.contains(name) { + continue; + } + self.emit_global(name); + self.emit_label(name); + self.emit(format!("# cellscript abi: CKB VM v2 syscall {} ({})", syscall, detail)); + if !enabled { + self.emit_fail(CellScriptRuntimeError::SyscallFailed); + } else { + match name { + "__ckb_pipe" => { + self.emit("addi sp, sp, -32"); + self.emit("sd ra, 24(sp)"); + self.emit("addi a0, sp, 8"); + self.emit(format!("li a7, {}", syscall)); + self.emit("ecall"); + let failed = self.fresh_label("ckb_pipe_failed"); + let done = self.fresh_label("ckb_pipe_done"); + self.emit(format!("bnez a0, {}", failed)); + self.emit("ld a1, 8(sp)"); + self.emit("ld a2, 16(sp)"); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit_label(&done); + self.emit("ld ra, 24(sp)"); + self.emit("addi sp, sp, 32"); + self.emit("ret"); + } + "__ckb_pipe_write" => { + self.emit("addi sp, sp, -32"); + self.emit("sd ra, 24(sp)"); + self.emit("sd a1, 8(sp)"); + self.emit("li t0, 8"); + self.emit("sd t0, 16(sp)"); + self.emit("addi a1, sp, 8"); + self.emit("addi a2, sp, 16"); + self.emit(format!("li a7, {}", syscall)); + self.emit("ecall"); + let done = self.fresh_label("ckb_pipe_write_done"); + self.emit(format!("beqz a0, {}", done)); + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit_label(&done); + self.emit("ld ra, 24(sp)"); + self.emit("addi sp, sp, 32"); + self.emit("ret"); + } + "__ckb_close" => { + self.emit(format!("li a7, {}", syscall)); + self.emit("ecall"); + let done = self.fresh_label("ckb_close_done"); + self.emit(format!("beqz a0, {}", done)); + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit_label(&done); + self.emit("ret"); + } + "__ckb_wait" => { + self.emit("addi sp, sp, -32"); + self.emit("sd ra, 24(sp)"); + self.emit("sd zero, 8(sp)"); + self.emit("addi a1, sp, 8"); + self.emit(format!("li a7, {}", syscall)); + self.emit("ecall"); + let failed = self.fresh_label("ckb_wait_failed"); + let exit_ok = self.fresh_label("ckb_wait_exit_ok"); + let child_failed = self.fresh_label("ckb_wait_child_failed"); + let done = self.fresh_label("ckb_wait_done"); + self.emit(format!("bnez a0, {}", failed)); + self.emit("lbu t0, 8(sp)"); + self.emit(format!("beqz t0, {}", exit_ok)); + self.emit_label(&child_failed); + self.emit("addi a0, t0, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&exit_ok); + self.emit("li a0, 0"); + self.emit_label(&done); + self.emit("ld ra, 24(sp)"); + self.emit("addi sp, sp, 32"); + self.emit("ret"); + } + "__ckb_spawn" => { + self.emit("li a0, 0"); + self.emit("ret"); + } + _ => { + self.emit(format!("li a7, {}", syscall)); + self.emit("ecall"); + self.emit("ret"); + } + } + } + } + if referenced_helpers.contains("__ckb_spawn_with_fd1") { + self.emit_global("__ckb_spawn_with_fd1"); + self.emit_label("__ckb_spawn_with_fd1"); + self.emit("# cellscript abi: CKB VM v2 spawn CellDep index a0/code with one inherited fd from a1"); + if !enabled { + self.emit_fail(CellScriptRuntimeError::SyscallFailed); + } else { + self.emit("addi sp, sp, -96"); + self.emit("sd ra, 88(sp)"); + self.emit("sd a1, 8(sp)"); + self.emit("sd a0, 64(sp)"); + self.emit("sd zero, 16(sp)"); + self.emit("sd zero, 32(sp)"); + self.emit("sd zero, 40(sp)"); + self.emit("addi t0, sp, 24"); + self.emit("sd t0, 48(sp)"); + self.emit("addi t0, sp, 8"); + self.emit("sd t0, 56(sp)"); + self.emit("ld a0, 64(sp)"); + self.emit(format!("li a1, {}", ckb_abi::source::CELL_DEP)); + self.emit("li a2, 0"); + self.emit(format!("li a3, {}", ckb_abi::place::CELL)); + self.emit("addi a4, sp, 32"); + self.emit(format!("li a7, {}", ckb_abi::syscall::SPAWN)); + self.emit("ecall"); + let failed = self.fresh_label("ckb_spawn_with_fd_failed"); + let done = self.fresh_label("ckb_spawn_with_fd_done"); + self.emit(format!("bnez a0, {}", failed)); + self.emit("ld a1, 24(sp)"); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit_label(&done); + self.emit("ld ra, 88(sp)"); + self.emit("addi sp, sp, 96"); + self.emit("ret"); + } + } + + for (name, source_view, detail) in [ + ("__ckb_source_input", CKB_SOURCE_VIEW_INPUT, "Source::Input"), + ("__ckb_source_output", CKB_SOURCE_VIEW_OUTPUT, "Source::Output"), + ("__ckb_source_cell_dep", CKB_SOURCE_VIEW_CELL_DEP, "Source::CellDep"), + ("__ckb_source_header_dep", CKB_SOURCE_VIEW_HEADER_DEP, "Source::HeaderDep"), + ("__ckb_source_group_input", CKB_SOURCE_VIEW_GROUP_INPUT, "Source::GroupInput"), + ("__ckb_source_group_output", CKB_SOURCE_VIEW_GROUP_OUTPUT, "Source::GroupOutput"), + ] { + if !referenced_helpers.contains(name) { + continue; + } + self.emit_runtime_source_view_helper(name, source_view, detail, enabled); + } + + for (name, relative, detail) in [ + ("__ckb_since_epoch_absolute", false, "CKB RFC0017 absolute epoch since encoder"), + ("__ckb_since_epoch_relative", true, "CKB RFC0017 relative epoch since encoder"), + ] { + if !referenced_helpers.contains(name) { + continue; + } + self.emit_runtime_ckb_since_epoch_helper(name, relative, detail, enabled); + } + + let needs_c256_product = referenced_helpers.contains("__c256_require_u128_product_lte") + || referenced_helpers.contains("__c256_require_u128_product_eq") + || referenced_helpers.contains("__c256_require_u128_sum2_products_lte") + || referenced_helpers.contains("__c256_require_u128_sum2_products_eq"); + let needs_c256_sum = referenced_helpers.contains("__c256_require_u128_sum2_products_lte") + || referenced_helpers.contains("__c256_require_u128_sum2_products_eq"); + if needs_c256_product { + self.emit_runtime_load_u64_le_helper(); + self.emit_runtime_mul_u128_to_u256_helper(); + if needs_c256_sum { + self.emit_runtime_add_u256_helper(); + } + } + if referenced_helpers.contains("__ckb_require_lock_type_metapoint_pairs") + || referenced_helpers.contains("__ckb_require_type_lock_metapoint_pairs") + || referenced_helpers.contains("__ckb_require_lock_type_metapoint_pairs_from_i32_data") + || referenced_helpers.contains("__ckb_require_type_lock_metapoint_pairs_from_i32_data") + || referenced_helpers.contains("__ckb_require_lock_type_metapoint_pairs_from_i32_data_filtered") + || referenced_helpers.contains("__ckb_require_type_lock_metapoint_pairs_from_i32_data_filtered") + || referenced_helpers.contains("__ckb_require_lock_match_master_out_point_pairs_from_data") + { + self.emit_runtime_current_script_role_at_helper(enabled); + } + + for (name, detail) in [ + ("__ckb_current_role", "current script role inferred from group input lock/type hashes"), + ("__ckb_current_script_hash", "current script hash loaded via LOAD_SCRIPT_HASH"), + ("__ckb_cell_capacity", "SourceView cell capacity field"), + ("__ckb_cell_occupied_capacity", "SourceView occupied capacity from CellOutput scripts and data bytes"), + ("__ckb_cell_unoccupied_capacity", "SourceView capacity minus occupied capacity"), + ("__ckb_cell_output_index", "SourceView output index"), + ("__ckb_input_out_point_index", "SourceView input OutPoint index"), + ("__ckb_input_out_point_tx_hash_low", "SourceView input OutPoint tx hash low word"), + ("__ckb_input_out_point_tx_hash", "SourceView input OutPoint full tx hash read"), + ("__ckb_require_input_out_point_tx_hash", "SourceView input OutPoint full tx-hash binding check"), + ("__ckb_require_input_out_point", "SourceView input OutPoint full tx-hash and index binding check"), + ("__ckb_require_metapoint_relative", "SourceView MetaPoint relative-distance binding check"), + ("__ckb_require_lock_type_metapoint_pairs", "current-script lock-only to type-only MetaPoint pair cardinality check"), + ("__ckb_require_type_lock_metapoint_pairs", "current-script type-only to lock-only MetaPoint pair cardinality check"), + ( + "__ckb_require_lock_type_metapoint_pairs_from_i32_data", + "current-script lock-only to type-only MetaPoint pair cardinality check using signed i32 distance loaded from base cell data", + ), + ( + "__ckb_require_type_lock_metapoint_pairs_from_i32_data", + "current-script type-only to lock-only MetaPoint pair cardinality check using signed i32 distance loaded from base cell data", + ), + ( + "__ckb_require_lock_type_metapoint_pairs_from_i32_data_filtered", + "current-script lock-only to type-only filtered MetaPoint pair cardinality check using signed i32 distance loaded from base cell data", + ), + ( + "__ckb_require_type_lock_metapoint_pairs_from_i32_data_filtered", + "current-script type-only to lock-only filtered MetaPoint pair cardinality check using signed i32 distance loaded from base cell data", + ), + ( + "__ckb_require_lock_match_master_out_point_pairs_from_data", + "current-script lock-only match order input/output pairing using master OutPoint loaded from order data", + ), + ("__ckb_cell_lock_hash_low", "SourceView lock hash low word"), + ("__ckb_cell_type_hash_low", "SourceView type hash low word"), + ("__ckb_cell_lock_hash", "SourceView lock hash full 32-byte read"), + ("__ckb_cell_type_hash", "SourceView type hash full 32-byte read"), + ("__ckb_cell_data_hash", "SourceView data hash full 32-byte read"), + ("__ckb_cell_data_hash_at", "SourceView cell data 32-byte read at byte offset"), + ("__ckb_cell_lock_code_hash", "SourceView lock Script code_hash read"), + ("__ckb_cell_type_code_hash", "SourceView type Script code_hash read"), + ("__ckb_cell_lock_hash_type", "SourceView lock Script hash_type read"), + ("__ckb_cell_type_hash_type", "SourceView type Script hash_type read"), + ("__ckb_cell_lock_args_empty", "SourceView lock Script args_empty read"), + ("__ckb_cell_type_args_empty", "SourceView type Script args_empty read"), + ("__ckb_cell_lock_args_hash", "SourceView lock Script 32-byte args read"), + ("__ckb_cell_type_args_hash", "SourceView type Script 32-byte args read"), + ("__ckb_require_cell_lock_hash", "SourceView lock hash full 32-byte binding check"), + ("__ckb_require_cell_type_hash", "SourceView type hash full 32-byte binding check"), + ("__ckb_require_cell_data_hash", "SourceView data hash full 32-byte binding check"), + ( + "__ckb_require_bounded_cell_dep_data_hash", + "bounded resolved CellDep data-hash membership check", + ), + ("__ckb_require_current_script_args_empty", "current Script empty args requirement"), + ("__ckb_require_cell_lock_args_empty", "SourceView lock Script empty args requirement"), + ("__ckb_require_cell_type_args_empty", "SourceView type Script empty args requirement"), + ("__ckb_require_cell_lock_args_hash", "SourceView lock Script 32-byte args binding check"), + ("__ckb_require_cell_type_args_hash", "SourceView type Script 32-byte args binding check"), + ("__ckb_require_cell_lock_args_exact", "SourceView lock Script arbitrary exact args binding check"), + ("__ckb_require_cell_type_args_exact", "SourceView type Script arbitrary exact args binding check"), + ("__ckb_require_cell_lock_args_prefix_hash", "SourceView lock Script 32-byte args prefix binding check"), + ("__ckb_require_cell_type_args_prefix_hash", "SourceView type Script 32-byte args prefix binding check"), + ("__ckb_require_cell_lock_args_suffix_hash", "SourceView lock Script 32-byte args suffix binding check"), + ("__ckb_require_cell_type_args_suffix_hash", "SourceView type Script 32-byte args suffix binding check"), + ("__ckb_require_cell_lock_script_hash_type", "SourceView lock Script code_hash/hash_type binding check"), + ("__ckb_require_cell_type_script_hash_type", "SourceView type Script code_hash/hash_type binding check"), + ("__c256_require_u128_product_lte", "C256 u128 product <= requirement"), + ("__c256_require_u128_product_eq", "C256 u128 product == requirement"), + ("__c256_require_u128_sum2_products_lte", "C256 u128 product-sum <= requirement"), + ("__c256_require_u128_sum2_products_eq", "C256 u128 product-sum == requirement"), + ("__ckb_cell_data_size", "SourceView cell data byte length"), + ("__ckb_cell_data_u32_le", "SourceView cell data little-endian u32 read"), + ("__ckb_cell_data_u64_le", "SourceView cell data little-endian u64 read"), + ("__dao_accumulated_rate", "DAO accumulated rate from HeaderDep SourceView"), + ( + "__dao_input_accumulated_rate", + "DAO accumulated rate from an Input/GroupInput committed header", + ), + ("__dao_has_dao_type", "DAO type hash classifier"), + ("__dao_is_deposit_data", "DAO deposit data classifier"), + ("__dao_is_withdrawal_request_data", "DAO withdrawal request data classifier"), + ("__dao_require_header_dep_for_input", "DAO input header to HeaderDep lineage requirement"), + ("__dao_require_input_since_at_least", "DAO input since lower-bound requirement"), + ("__dao_require_input_relative_epoch_since_at_least", "DAO relative epoch since maturity requirement"), + ("__xudt_amount_low", "xUDT amount low 64 bits"), + ("__xudt_amount_high", "xUDT amount high 64 bits"), + ("__xudt_owner_mode_input_type_hash", "xUDT owner-mode input-type hash low word"), + ("__xudt_require_owner_mode_input_type", "xUDT owner-mode input-type binding check"), + ("__xudt_require_owner_mode_type_args", "xUDT owner-mode type args binding check"), + ( + "__xudt_require_owner_mode_type_args_current_script", + "xUDT owner-mode type args binding check against current script hash", + ), + ( + FUNGIBLE_TYPE_GROUP_V1_CODEGEN_HELPER, + "chain-neutral fungible type-group v1 conservation", + ), + ("__xudt_require_group_amount_conserved", "xUDT group input/output amount conservation"), + ("__xudt_require_group_amount_minted", "xUDT group output-input amount delta check"), + ("__xudt_require_group_amount_burned", "xUDT group input-output amount delta check"), + ("__ckb_witness_raw", "raw witness bytes"), + ("__ckb_witness_lock", "WitnessArgs.lock"), + ("__ckb_witness_input_type", "WitnessArgs.input_type"), + ("__ckb_witness_output_type", "WitnessArgs.output_type"), + ("__ckb_witness_size", "witness byte size"), + ("__ckb_require_witness_size_at_least", "require witness size lower bound"), + ("__ckb_sighash_all", "CKB sighash-all digest"), + ("__ckb_require_maturity", "CKB block-number since maturity"), + ("__ckb_require_time", "CKB timestamp since"), + ("__ckb_require_epoch_after", "CKB absolute epoch since"), + ("__ckb_require_epoch_relative", "CKB relative epoch since"), + ("__ckb_occupied_capacity", "compile-visible occupied capacity floor"), + ] { + if !referenced_helpers.contains(name) { + continue; + } + match name { + "__ckb_current_role" => self.emit_runtime_current_role_helper(enabled), + "__ckb_current_script_hash" => self.emit_runtime_current_script_hash_helper(enabled), + "__ckb_cell_capacity" => { + self.emit_runtime_cell_field_u64_helper(name, detail, CKB_CELL_FIELD_CAPACITY, enabled); + } + "__ckb_cell_occupied_capacity" => self.emit_runtime_cell_occupied_capacity_helper(enabled), + "__ckb_cell_unoccupied_capacity" => self.emit_runtime_cell_unoccupied_capacity_helper(enabled), + "__ckb_cell_output_index" => self.emit_runtime_cell_output_index_helper(enabled), + "__ckb_input_out_point_index" => self.emit_runtime_input_out_point_word_helper(name, detail, 32, 4, enabled), + "__ckb_input_out_point_tx_hash_low" => self.emit_runtime_input_out_point_word_helper(name, detail, 0, 8, enabled), + "__ckb_input_out_point_tx_hash" => self.emit_runtime_input_out_point_tx_hash_helper(enabled), + "__ckb_require_input_out_point_tx_hash" => self.emit_runtime_input_out_point_tx_hash_requirement_helper(enabled), + "__ckb_require_input_out_point" => self.emit_runtime_input_out_point_requirement_helper(enabled), + "__ckb_require_metapoint_relative" => self.emit_runtime_metapoint_relative_requirement_helper(enabled), + "__ckb_require_lock_type_metapoint_pairs" => { + self.emit_runtime_metapoint_pair_cardinality_helper(name, detail, true, false, false, enabled) + } + "__ckb_require_type_lock_metapoint_pairs" => { + self.emit_runtime_metapoint_pair_cardinality_helper(name, detail, false, false, false, enabled) + } + "__ckb_require_lock_type_metapoint_pairs_from_i32_data" => { + self.emit_runtime_metapoint_pair_cardinality_helper(name, detail, true, true, false, enabled) + } + "__ckb_require_type_lock_metapoint_pairs_from_i32_data" => { + self.emit_runtime_metapoint_pair_cardinality_helper(name, detail, false, true, false, enabled) + } + "__ckb_require_lock_type_metapoint_pairs_from_i32_data_filtered" => { + self.emit_runtime_metapoint_pair_cardinality_helper(name, detail, true, true, true, enabled) + } + "__ckb_require_type_lock_metapoint_pairs_from_i32_data_filtered" => { + self.emit_runtime_metapoint_pair_cardinality_helper(name, detail, false, true, true, enabled) + } + "__ckb_require_lock_match_master_out_point_pairs_from_data" => { + self.emit_runtime_lock_match_master_out_point_pairs_from_data_helper(enabled) + } + "__ckb_cell_lock_hash_low" => { + self.emit_runtime_cell_field_low_word_helper(name, detail, CKB_CELL_FIELD_LOCK_HASH, enabled); + } + "__ckb_cell_type_hash_low" => { + self.emit_runtime_cell_field_low_word_helper(name, detail, CKB_CELL_FIELD_TYPE_HASH, enabled); + } + "__ckb_cell_lock_hash" => { + self.emit_runtime_cell_hash_field_helper(name, detail, CKB_CELL_FIELD_LOCK_HASH, enabled); + } + "__ckb_cell_type_hash" => { + self.emit_runtime_cell_hash_field_helper(name, detail, CKB_CELL_FIELD_TYPE_HASH, enabled); + } + "__ckb_cell_data_hash" => { + self.emit_runtime_cell_data_hash_helper(name, detail, enabled); + } + "__ckb_cell_data_hash_at" => { + self.emit_runtime_cell_data_hash_at_helper(name, detail, enabled); + } + "__ckb_cell_lock_code_hash" => { + self.emit_runtime_cell_script_hash_field_helper(name, detail, CKB_CELL_FIELD_LOCK, ScriptHashFieldRead::CodeHash, enabled); + } + "__ckb_cell_type_code_hash" => { + self.emit_runtime_cell_script_hash_field_helper(name, detail, CKB_CELL_FIELD_TYPE, ScriptHashFieldRead::CodeHash, enabled); + } + "__ckb_cell_lock_args_hash" => { + self.emit_runtime_cell_script_hash_field_helper(name, detail, CKB_CELL_FIELD_LOCK, ScriptHashFieldRead::Args32, enabled); + } + "__ckb_cell_type_args_hash" => { + self.emit_runtime_cell_script_hash_field_helper(name, detail, CKB_CELL_FIELD_TYPE, ScriptHashFieldRead::Args32, enabled); + } + "__ckb_cell_lock_hash_type" => { + self.emit_runtime_cell_script_scalar_field_helper(name, detail, CKB_CELL_FIELD_LOCK, ScriptScalarFieldRead::HashType, enabled); + } + "__ckb_cell_type_hash_type" => { + self.emit_runtime_cell_script_scalar_field_helper(name, detail, CKB_CELL_FIELD_TYPE, ScriptScalarFieldRead::HashType, enabled); + } + "__ckb_cell_lock_args_empty" => { + self.emit_runtime_cell_script_scalar_field_helper(name, detail, CKB_CELL_FIELD_LOCK, ScriptScalarFieldRead::ArgsEmpty, enabled); + } + "__ckb_cell_type_args_empty" => { + self.emit_runtime_cell_script_scalar_field_helper(name, detail, CKB_CELL_FIELD_TYPE, ScriptScalarFieldRead::ArgsEmpty, enabled); + } + "__ckb_require_cell_lock_hash" => self.emit_runtime_cell_hash_requirement_helper( + name, + detail, + CKB_CELL_FIELD_LOCK_HASH, + CellScriptRuntimeError::ScriptRoleMismatch, + enabled, + ), + "__ckb_require_cell_type_hash" => self.emit_runtime_cell_hash_requirement_helper( + name, + detail, + CKB_CELL_FIELD_TYPE_HASH, + CellScriptRuntimeError::TypeHashMismatch, + enabled, + ), + "__ckb_require_cell_data_hash" => self.emit_runtime_cell_hash_requirement_helper( + name, + detail, + CKB_CELL_FIELD_DATA_HASH, + CellScriptRuntimeError::ScriptIdentityMismatch, + enabled, + ), + "__ckb_require_bounded_cell_dep_data_hash" => { + self.emit_runtime_bounded_cell_dep_data_hash_requirement_helper(enabled) + } + "__ckb_require_current_script_args_empty" => self.emit_runtime_current_script_args_empty_requirement_helper(enabled), + "__ckb_require_cell_lock_args_empty" => { + self.emit_runtime_cell_script_args_empty_requirement_helper(name, detail, CKB_CELL_FIELD_LOCK, enabled) + } + "__ckb_require_cell_type_args_empty" => { + self.emit_runtime_cell_script_args_empty_requirement_helper(name, detail, CKB_CELL_FIELD_TYPE, enabled) + } + "__ckb_require_cell_lock_args_hash" => { + self.emit_runtime_cell_script_args_hash_requirement_helper( + name, + detail, + CKB_CELL_FIELD_LOCK, + ScriptArgsHashRequirementMode::Exact32, + enabled, + ) + } + "__ckb_require_cell_type_args_hash" => { + self.emit_runtime_cell_script_args_hash_requirement_helper( + name, + detail, + CKB_CELL_FIELD_TYPE, + ScriptArgsHashRequirementMode::Exact32, + enabled, + ) + } + "__ckb_require_cell_lock_args_exact" => { + self.emit_runtime_cell_script_args_exact_requirement_helper(name, detail, CKB_CELL_FIELD_LOCK, enabled) + } + "__ckb_require_cell_type_args_exact" => { + self.emit_runtime_cell_script_args_exact_requirement_helper(name, detail, CKB_CELL_FIELD_TYPE, enabled) + } + "__ckb_require_cell_lock_args_prefix_hash" => { + self.emit_runtime_cell_script_args_hash_requirement_helper( + name, + detail, + CKB_CELL_FIELD_LOCK, + ScriptArgsHashRequirementMode::Prefix32, + enabled, + ) + } + "__ckb_require_cell_type_args_prefix_hash" => { + self.emit_runtime_cell_script_args_hash_requirement_helper( + name, + detail, + CKB_CELL_FIELD_TYPE, + ScriptArgsHashRequirementMode::Prefix32, + enabled, + ) + } + "__ckb_require_cell_lock_args_suffix_hash" => { + self.emit_runtime_cell_script_args_hash_requirement_helper( + name, + detail, + CKB_CELL_FIELD_LOCK, + ScriptArgsHashRequirementMode::Suffix32, + enabled, + ) + } + "__ckb_require_cell_type_args_suffix_hash" => { + self.emit_runtime_cell_script_args_hash_requirement_helper( + name, + detail, + CKB_CELL_FIELD_TYPE, + ScriptArgsHashRequirementMode::Suffix32, + enabled, + ) + } + "__ckb_require_cell_lock_script_hash_type" => { + self.emit_runtime_cell_script_hash_type_requirement_helper(name, detail, CKB_CELL_FIELD_LOCK, enabled) + } + "__ckb_require_cell_type_script_hash_type" => { + self.emit_runtime_cell_script_hash_type_requirement_helper(name, detail, CKB_CELL_FIELD_TYPE, enabled) + } + "__c256_require_u128_product_lte" => self.emit_runtime_c256_product_requirement_helper(name, detail, false), + "__c256_require_u128_product_eq" => self.emit_runtime_c256_product_requirement_helper(name, detail, true), + "__c256_require_u128_sum2_products_lte" => self.emit_runtime_c256_sum2_product_requirement_helper(name, detail, false), + "__c256_require_u128_sum2_products_eq" => self.emit_runtime_c256_sum2_product_requirement_helper(name, detail, true), + "__ckb_cell_data_size" => self.emit_runtime_cell_data_size_helper(enabled), + "__ckb_cell_data_u32_le" => self.emit_runtime_cell_data_word_le_helper(name, detail, 4, enabled), + "__ckb_cell_data_u64_le" => self.emit_runtime_cell_data_word_le_helper(name, detail, 8, enabled), + "__dao_accumulated_rate" => self.emit_runtime_dao_accumulated_rate_helper(enabled), + "__dao_input_accumulated_rate" => self.emit_runtime_dao_input_accumulated_rate_helper(enabled), + "__dao_has_dao_type" => self.emit_runtime_dao_type_classifier_helper(enabled), + "__dao_is_deposit_data" => self.emit_runtime_dao_cell_data_classifier_helper(name, detail, true, enabled), + "__dao_is_withdrawal_request_data" => { + self.emit_runtime_dao_cell_data_classifier_helper(name, detail, false, enabled); + } + "__dao_require_header_dep_for_input" => self.emit_runtime_dao_require_header_dep_for_input_helper(enabled), + "__dao_require_input_since_at_least" => self.emit_runtime_dao_require_input_since_at_least_helper(enabled), + "__dao_require_input_relative_epoch_since_at_least" => { + self.emit_runtime_dao_require_input_relative_epoch_since_at_least_helper(enabled); + } + "__xudt_amount_low" => self.emit_runtime_xudt_amount_word_helper(name, detail, 0, enabled), + "__xudt_amount_high" => self.emit_runtime_xudt_amount_word_helper(name, detail, 8, enabled), + "__xudt_owner_mode_input_type_hash" => { + self.emit_runtime_cell_field_low_word_helper(name, detail, CKB_CELL_FIELD_TYPE_HASH, enabled); + } + "__xudt_require_owner_mode_input_type" => self.emit_runtime_xudt_require_owner_mode_input_type_helper(enabled), + "__xudt_require_owner_mode_type_args" => self.emit_runtime_xudt_require_owner_mode_type_args_helper(enabled), + "__xudt_require_owner_mode_type_args_current_script" => { + self.emit_runtime_xudt_require_owner_mode_type_args_current_script_helper(enabled) + } + FUNGIBLE_TYPE_GROUP_V1_CODEGEN_HELPER | "__xudt_require_group_amount_conserved" => { + self.emit_runtime_fungible_type_group_conservation_helper(name, detail, enabled) + } + "__xudt_require_group_amount_minted" => { + self.emit_runtime_xudt_require_group_amount_delta_helper(name, true, enabled); + } + "__xudt_require_group_amount_burned" => { + self.emit_runtime_xudt_require_group_amount_delta_helper(name, false, enabled); + } + "__ckb_witness_size" => self.emit_runtime_witness_size_helper(enabled), + "__ckb_require_witness_size_at_least" => { + self.emit_runtime_require_witness_size_at_least_helper(enabled) + } + "__ckb_witness_raw" => self.emit_runtime_witness_raw_helper(enabled), + "__ckb_witness_lock" => self.emit_runtime_witness_args_field_helper(name, detail, 0, enabled), + "__ckb_witness_input_type" => self.emit_runtime_witness_args_field_helper(name, detail, 1, enabled), + "__ckb_witness_output_type" => self.emit_runtime_witness_args_field_helper(name, detail, 2, enabled), + _ => { + self.emit_global(name); + self.emit_label(name); + self.emit(format!("# cellscript abi: v0.14 CKB semantic helper ({})", detail)); + if !enabled { + self.emit_fail(CellScriptRuntimeError::SyscallFailed); + } else { + self.emit("li a0, 0"); + self.emit("ret"); + } + } + } + } + + if referenced_helpers.contains("__ckb_hash_chain") { + self.emit_global("__ckb_hash_chain"); + self.emit_label("__ckb_hash_chain"); + self.emit("# cellscript abi: hash_chain aliases CKB Blake2b-256 over one 32-byte Hash input"); + if !enabled { + self.emit_fail(CellScriptRuntimeError::SyscallFailed); + } else { + self.emit("j __ckb_hash_blake2b"); + } + } + if referenced_helpers.contains("__ckb_hash_pair") { + self.emit_runtime_blake2b_hash_pair(enabled); + } + if referenced_helpers.contains("__ckb_hash_chain") + || referenced_helpers.contains("__ckb_hash_blake2b") + || referenced_helpers.contains("__ckb_hash_data_packed") + { + self.emit_runtime_blake2b_hash32(enabled); + } + if referenced_helpers.contains("__ckb_hash_blake2b_var") + || referenced_helpers.contains("__ckb_hash_data_packed") + || referenced_helpers.contains("__ckb_hash_blake2b_packed") + || referenced_helpers.contains("__ckb_cell_data_hash") + { + self.emit_runtime_blake2b_hash_var(enabled); + } + if referenced_helpers.iter().any(|helper| { + matches!( + helper.as_str(), + "__ckb_hash_sha256" + | "__ckb_hash_sha256d" + | "__ckb_hash_sha256_pair" + | "__ckb_hash_sha256d_pair" + | "__ckb_require_sha256d_merkle_root" + ) + }) { + self.emit_runtime_sha256_surface(enabled); + } + } + + fn emit_u32_normalize(&mut self, register: &str) { + self.emit(format!("slli {0}, {0}, 32", register)); + self.emit(format!("srli {0}, {0}, 32", register)); + } + + fn emit_sha256_rotr(&mut self, dest: &str, source: &str, shift: u8, scratch: &str) { + self.emit(format!("srli {}, {}, {}", dest, source, shift)); + self.emit(format!("slli {}, {}, {}", scratch, source, 32 - shift)); + self.emit_u32_normalize(scratch); + self.emit(format!("or {}, {}, {}", dest, dest, scratch)); + } + + fn emit_runtime_sha256_surface(&mut self, enabled: bool) { + for symbol in [ + "__cellscript_sha256_compress", + "__cellscript_sha256_fixed", + "__ckb_hash_sha256", + "__ckb_hash_sha256d", + "__ckb_hash_sha256_pair", + "__ckb_hash_sha256d_pair", + "__ckb_require_sha256d_merkle_root", + ] { + self.emit_global(symbol); + } + if !enabled { + for symbol in [ + "__cellscript_sha256_compress", + "__cellscript_sha256_fixed", + "__ckb_hash_sha256", + "__ckb_hash_sha256d", + "__ckb_hash_sha256_pair", + "__ckb_hash_sha256d_pair", + "__ckb_require_sha256d_merkle_root", + ] { + self.emit_label(symbol); + self.emit_fail(CellScriptRuntimeError::SyscallFailed); + } + return; + } + self.emit_runtime_sha256_compress(); + self.emit_runtime_sha256_fixed(); + self.emit_runtime_sha256_wrappers(); + self.emit_runtime_sha256d_merkle_requirement(); + } + + fn emit_runtime_sha256_compress(&mut self) { + const K: [u32; 64] = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, + 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, + 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, + 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, + ]; + self.emit_label("__cellscript_sha256_compress"); + self.emit("# cellscript abi: SHA-256 compression; a0=state[8] u32-in-u64, a1=schedule[64] u32-in-u64"); + self.emit("addi sp, sp, -112"); + self.emit("sd ra, 104(sp)"); + self.emit("sd a0, 64(sp)"); + self.emit("sd a1, 72(sp)"); + for index in 0..8 { + self.emit(format!("ld t0, {}(a0)", index * 8)); + self.emit(format!("sd t0, {}(sp)", index * 8)); + } + for index in 16..64 { + self.emit(format!("ld t0, {}(a1)", (index - 15) * 8)); + self.emit_sha256_rotr("t1", "t0", 7, "t2"); + self.emit_sha256_rotr("t3", "t0", 18, "t2"); + self.emit("xor t1, t1, t3"); + self.emit("srli t3, t0, 3"); + self.emit("xor t1, t1, t3"); + self.emit(format!("ld t3, {}(a1)", (index - 2) * 8)); + self.emit_sha256_rotr("t4", "t3", 17, "t5"); + self.emit_sha256_rotr("t6", "t3", 19, "t5"); + self.emit("xor t4, t4, t6"); + self.emit("srli t6, t3, 10"); + self.emit("xor t4, t4, t6"); + self.emit(format!("ld t0, {}(a1)", (index - 16) * 8)); + self.emit("add t1, t1, t0"); + self.emit(format!("ld t0, {}(a1)", (index - 7) * 8)); + self.emit("add t1, t1, t0"); + self.emit("add t1, t1, t4"); + self.emit_u32_normalize("t1"); + self.emit(format!("sd t1, {}(a1)", index * 8)); + } + for (round, constant) in K.iter().enumerate() { + self.emit("ld t0, 32(sp)"); + self.emit_sha256_rotr("t1", "t0", 6, "t2"); + self.emit_sha256_rotr("t3", "t0", 11, "t2"); + self.emit("xor t1, t1, t3"); + self.emit_sha256_rotr("t3", "t0", 25, "t2"); + self.emit("xor t1, t1, t3"); + self.emit("ld t2, 40(sp)"); + self.emit("and t2, t0, t2"); + self.emit("xori t3, t0, -1"); + self.emit_u32_normalize("t3"); + self.emit("ld t4, 48(sp)"); + self.emit("and t3, t3, t4"); + self.emit("xor t2, t2, t3"); + self.emit("ld t3, 56(sp)"); + self.emit("add t3, t3, t1"); + self.emit("add t3, t3, t2"); + self.emit(format!("li t1, {}", constant)); + self.emit("add t3, t3, t1"); + self.emit(format!("ld t1, {}(a1)", round * 8)); + self.emit("add t3, t3, t1"); + self.emit_u32_normalize("t3"); + self.emit("sd t3, 80(sp)"); + + self.emit("ld t0, 0(sp)"); + self.emit_sha256_rotr("t1", "t0", 2, "t2"); + self.emit_sha256_rotr("t3", "t0", 13, "t2"); + self.emit("xor t1, t1, t3"); + self.emit_sha256_rotr("t3", "t0", 22, "t2"); + self.emit("xor t1, t1, t3"); + self.emit("ld t3, 8(sp)"); + self.emit("and t2, t0, t3"); + self.emit("ld t4, 16(sp)"); + self.emit("and t5, t0, t4"); + self.emit("xor t2, t2, t5"); + self.emit("and t5, t3, t4"); + self.emit("xor t2, t2, t5"); + self.emit("add t1, t1, t2"); + self.emit_u32_normalize("t1"); + self.emit("sd t1, 88(sp)"); + + self.emit("ld t0, 48(sp)"); + self.emit("sd t0, 56(sp)"); + self.emit("ld t0, 40(sp)"); + self.emit("sd t0, 48(sp)"); + self.emit("ld t0, 32(sp)"); + self.emit("sd t0, 40(sp)"); + self.emit("ld t0, 24(sp)"); + self.emit("ld t1, 80(sp)"); + self.emit("add t0, t0, t1"); + self.emit_u32_normalize("t0"); + self.emit("sd t0, 32(sp)"); + self.emit("ld t0, 16(sp)"); + self.emit("sd t0, 24(sp)"); + self.emit("ld t0, 8(sp)"); + self.emit("sd t0, 16(sp)"); + self.emit("ld t0, 0(sp)"); + self.emit("sd t0, 8(sp)"); + self.emit("ld t1, 80(sp)"); + self.emit("ld t2, 88(sp)"); + self.emit("add t1, t1, t2"); + self.emit_u32_normalize("t1"); + self.emit("sd t1, 0(sp)"); + } + self.emit("ld a0, 64(sp)"); + for index in 0..8 { + self.emit(format!("ld t0, {}(a0)", index * 8)); + self.emit(format!("ld t1, {}(sp)", index * 8)); + self.emit("add t0, t0, t1"); + self.emit_u32_normalize("t0"); + self.emit(format!("sd t0, {}(a0)", index * 8)); + } + self.emit("li a0, 0"); + self.emit("ld ra, 104(sp)"); + self.emit("addi sp, sp, 112"); + self.emit("ret"); + } + + fn emit_runtime_sha256_fixed(&mut self) { + const H: [u32; 8] = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]; + const STATE_OFFSET: usize = 8; + const W_OFFSET: usize = 72; + const INPUT_OFFSET: usize = 584; + const LEN_OFFSET: usize = 592; + const OUTPUT_OFFSET: usize = 600; + const RA_OFFSET: usize = 616; + const FRAME_SIZE: usize = 624; + self.emit_label("__cellscript_sha256_fixed"); + self.emit("# cellscript abi: bounded SHA-256 for exactly 32 or 64 input bytes; a0=input, a1=len, a2=output[32]"); + let len32 = self.fresh_label("sha256_len32"); + let len64 = self.fresh_label("sha256_len64"); + let after_first = self.fresh_label("sha256_after_first"); + let output = self.fresh_label("sha256_output"); + let invalid = self.fresh_label("sha256_invalid"); + let done = self.fresh_label("sha256_done"); + self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); + self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); + self.emit(format!("sd a0, {}(sp)", INPUT_OFFSET)); + self.emit(format!("sd a1, {}(sp)", LEN_OFFSET)); + self.emit(format!("sd a2, {}(sp)", OUTPUT_OFFSET)); + self.emit(format!("beqz a0, {}", invalid)); + self.emit(format!("beqz a2, {}", invalid)); + self.emit("li t0, 32"); + self.emit("sub t1, a1, t0"); + self.emit(format!("beqz t1, {}", len32)); + self.emit("li t0, 64"); + self.emit("sub t1, a1, t0"); + self.emit(format!("beqz t1, {}", len64)); + self.emit(format!("j {}", invalid)); + self.emit_label(&len32); + for (index, value) in H.iter().enumerate() { + self.emit(format!("li t0, {}", value)); + self.emit(format!("sd t0, {}(sp)", STATE_OFFSET + index * 8)); + } + self.emit("li t6, 8"); + self.emit(format!("j {}", after_first)); + self.emit_label(&len64); + for (index, value) in H.iter().enumerate() { + self.emit(format!("li t0, {}", value)); + self.emit(format!("sd t0, {}(sp)", STATE_OFFSET + index * 8)); + } + self.emit("li t6, 16"); + self.emit_label(&after_first); + self.emit(format!("ld a0, {}(sp)", INPUT_OFFSET)); + for word in 0..16 { + let skip = self.fresh_label("sha256_input_word_skip"); + self.emit(format!("li t5, {}", word + 1)); + self.emit(format!("bltu t6, t5, {}", skip)); + self.emit("li t4, 0"); + for byte in 0..4 { + self.emit(format!("lbu t0, {}(a0)", word * 4 + byte)); + let shift = 24 - byte * 8; + if shift != 0 { + self.emit(format!("slli t0, t0, {}", shift)); + } + self.emit("or t4, t4, t0"); + } + self.emit(format!("sd t4, {}(sp)", W_OFFSET + word * 8)); + self.emit_label(&skip); + } + let first_is64 = self.fresh_label("sha256_first_is64"); + self.emit(format!("ld t0, {}(sp)", LEN_OFFSET)); + self.emit("li t1, 64"); + self.emit("sub t2, t0, t1"); + self.emit(format!("beqz t2, {}", first_is64)); + self.emit("li t0, 2147483648"); + self.emit(format!("sd t0, {}(sp)", W_OFFSET + 8 * 8)); + for word in 9..15 { + self.emit(format!("sd zero, {}(sp)", W_OFFSET + word * 8)); + } + self.emit("li t0, 256"); + self.emit(format!("sd t0, {}(sp)", W_OFFSET + 15 * 8)); + self.emit_label(&first_is64); + self.emit(format!("addi a0, sp, {}", STATE_OFFSET)); + self.emit(format!("addi a1, sp, {}", W_OFFSET)); + self.emit("call __cellscript_sha256_compress"); + self.emit(format!("ld t0, {}(sp)", LEN_OFFSET)); + self.emit("li t1, 32"); + self.emit("sub t2, t0, t1"); + self.emit(format!("beqz t2, {}", output)); + self.emit("li t0, 2147483648"); + self.emit(format!("sd t0, {}(sp)", W_OFFSET)); + for word in 1..15 { + self.emit(format!("sd zero, {}(sp)", W_OFFSET + word * 8)); + } + self.emit("li t0, 512"); + self.emit(format!("sd t0, {}(sp)", W_OFFSET + 15 * 8)); + self.emit(format!("addi a0, sp, {}", STATE_OFFSET)); + self.emit(format!("addi a1, sp, {}", W_OFFSET)); + self.emit("call __cellscript_sha256_compress"); + self.emit_label(&output); + self.emit(format!("ld a0, {}(sp)", OUTPUT_OFFSET)); + for word in 0..8 { + self.emit(format!("ld t0, {}(sp)", STATE_OFFSET + word * 8)); + for byte in 0..4 { + let shift = 24 - byte * 8; + if shift == 0 { + self.emit("addi t1, t0, 0"); + } else { + self.emit(format!("srli t1, t0, {}", shift)); + } + self.emit(format!("sb t1, {}(a0)", word * 4 + byte)); + } + } + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::BoundsCheckFailed.code())); + self.emit_label(&done); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + } + + fn emit_runtime_sha256_wrappers(&mut self) { + self.emit_label("__ckb_hash_sha256"); + self.emit("# cellscript abi: SHA-256 over one 32-byte Hash; a0=input, a1=output"); + self.emit("addi a2, a1, 0"); + self.emit("li a1, 32"); + self.emit("j __cellscript_sha256_fixed"); + + self.emit_label("__ckb_hash_sha256d"); + self.emit("# cellscript abi: SHA256d over one 32-byte Hash; a0=input, a1=output"); + self.emit("addi sp, sp, -80"); + self.emit("sd ra, 72(sp)"); + self.emit("sd a0, 8(sp)"); + self.emit("sd a1, 16(sp)"); + self.emit("ld a0, 8(sp)"); + self.emit("li a1, 32"); + self.emit("addi a2, sp, 32"); + self.emit("call __cellscript_sha256_fixed"); + let sha256d_first_ok = self.fresh_label("sha256d_first_ok"); + let sha256d_done = self.fresh_label("sha256d_done"); + self.emit(format!("beqz a0, {}", sha256d_first_ok)); + self.emit(format!("j {}", sha256d_done)); + self.emit_label(&sha256d_first_ok); + self.emit("addi a0, sp, 32"); + self.emit("li a1, 32"); + self.emit("ld a2, 16(sp)"); + self.emit("call __cellscript_sha256_fixed"); + self.emit_label(&sha256d_done); + self.emit("ld ra, 72(sp)"); + self.emit("addi sp, sp, 80"); + self.emit("ret"); + + self.emit_label("__ckb_hash_sha256_pair"); + self.emit("# cellscript abi: SHA-256 over left[32] || right[32]; a2=output"); + self.emit("addi sp, sp, -112"); + self.emit("sd ra, 104(sp)"); + self.emit("sd a0, 8(sp)"); + self.emit("sd a1, 16(sp)"); + self.emit("sd a2, 24(sp)"); + self.emit("ld a0, 8(sp)"); + self.emit("addi a1, sp, 32"); + self.emit("li a2, 32"); + self.emit("call __cellscript_memcpy_fixed"); + self.emit("ld a0, 16(sp)"); + self.emit("addi a1, sp, 64"); + self.emit("li a2, 32"); + self.emit("call __cellscript_memcpy_fixed"); + self.emit("addi a0, sp, 32"); + self.emit("li a1, 64"); + self.emit("ld a2, 24(sp)"); + self.emit("call __cellscript_sha256_fixed"); + self.emit("ld ra, 104(sp)"); + self.emit("addi sp, sp, 112"); + self.emit("ret"); + + self.emit_label("__ckb_hash_sha256d_pair"); + self.emit("# cellscript abi: SHA256d over left[32] || right[32]; a2=output"); + self.emit("addi sp, sp, -160"); + self.emit("sd ra, 152(sp)"); + self.emit("sd a0, 8(sp)"); + self.emit("sd a1, 16(sp)"); + self.emit("sd a2, 24(sp)"); + self.emit("ld a0, 8(sp)"); + self.emit("addi a1, sp, 32"); + self.emit("li a2, 32"); + self.emit("call __cellscript_memcpy_fixed"); + self.emit("ld a0, 16(sp)"); + self.emit("addi a1, sp, 64"); + self.emit("li a2, 32"); + self.emit("call __cellscript_memcpy_fixed"); + self.emit("addi a0, sp, 32"); + self.emit("li a1, 64"); + self.emit("addi a2, sp, 96"); + self.emit("call __cellscript_sha256_fixed"); + let pair_first_ok = self.fresh_label("sha256d_pair_first_ok"); + let pair_done = self.fresh_label("sha256d_pair_done"); + self.emit(format!("beqz a0, {}", pair_first_ok)); + self.emit(format!("j {}", pair_done)); + self.emit_label(&pair_first_ok); + self.emit("addi a0, sp, 96"); + self.emit("li a1, 32"); + self.emit("ld a2, 24(sp)"); + self.emit("call __cellscript_sha256_fixed"); + self.emit_label(&pair_done); + self.emit("ld ra, 152(sp)"); + self.emit("addi sp, sp, 160"); + self.emit("ret"); + } + + fn emit_runtime_sha256d_merkle_requirement(&mut self) { + self.emit_label("__ckb_require_sha256d_merkle_root"); + self.emit("# cellscript abi: bounded SHA256d Merkle path; siblings is exactly 16 Hash values, depth <= 16"); + self.emit("addi sp, sp, -160"); + self.emit("sd ra, 152(sp)"); + self.emit("sd a1, 8(sp)"); + self.emit("sd a2, 16(sp)"); + self.emit("sd a3, 24(sp)"); + self.emit("sd a4, 32(sp)"); + let invalid = self.fresh_label("sha256d_merkle_invalid"); + let loop_label = self.fresh_label("sha256d_merkle_loop"); + let right_child = self.fresh_label("sha256d_merkle_right_child"); + let hash_ready = self.fresh_label("sha256d_merkle_hash_ready"); + let loop_done = self.fresh_label("sha256d_merkle_loop_done"); + let mismatch = self.fresh_label("sha256d_merkle_mismatch"); + let done = self.fresh_label("sha256d_merkle_done"); + self.emit(format!("beqz a0, {}", invalid)); + self.emit(format!("beqz a1, {}", invalid)); + self.emit(format!("beqz a4, {}", invalid)); + self.emit("li t0, 16"); + self.emit(format!("bltu t0, a2, {}", invalid)); + self.emit("addi a1, sp, 48"); + self.emit("li a2, 32"); + self.emit("call __cellscript_memcpy_fixed"); + self.emit("sd zero, 40(sp)"); + self.emit_label(&loop_label); + self.emit("ld t0, 40(sp)"); + self.emit("ld t1, 16(sp)"); + self.emit(format!("bgeu t0, t1, {}", loop_done)); + self.emit("slli t1, t0, 5"); + self.emit("ld t2, 8(sp)"); + self.emit("add t2, t2, t1"); + self.emit("ld t3, 24(sp)"); + self.emit("li t4, 1"); + self.emit("and t4, t3, t4"); + self.emit(format!("bnez t4, {}", right_child)); + self.emit("addi a0, sp, 48"); + self.emit("addi a1, t2, 0"); + self.emit("addi a2, sp, 80"); + self.emit("call __ckb_hash_sha256d_pair"); + self.emit(format!("j {}", hash_ready)); + self.emit_label(&right_child); + self.emit("addi a0, t2, 0"); + self.emit("addi a1, sp, 48"); + self.emit("addi a2, sp, 80"); + self.emit("call __ckb_hash_sha256d_pair"); + self.emit_label(&hash_ready); + self.emit(format!("bnez a0, {}", invalid)); + self.emit("addi a0, sp, 80"); + self.emit("addi a1, sp, 48"); + self.emit("li a2, 32"); + self.emit("call __cellscript_memcpy_fixed"); + self.emit("ld t0, 24(sp)"); + self.emit("srli t0, t0, 1"); + self.emit("sd t0, 24(sp)"); + self.emit("ld t0, 40(sp)"); + self.emit("addi t0, t0, 1"); + self.emit("sd t0, 40(sp)"); + self.emit(format!("j {}", loop_label)); + self.emit_label(&loop_done); + self.emit("ld t0, 24(sp)"); + self.emit(format!("bnez t0, {}", invalid)); + self.emit("addi a0, sp, 48"); + self.emit("ld a1, 32(sp)"); + self.emit("li a2, 32"); + self.emit("call __cellscript_memcmp_fixed"); + self.emit(format!("bnez a0, {}", mismatch)); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::BoundsCheckFailed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&mismatch); + self.emit("# cellscript runtime error 64 merkle-root-mismatch"); + self.emit(format!("li a0, {}", CellScriptRuntimeError::MerkleRootMismatch.code())); + self.emit_label(&done); + self.emit("ld ra, 152(sp)"); + self.emit("addi sp, sp, 160"); + self.emit("ret"); + } + + fn emit_runtime_blake2b_hash_var(&mut self, enabled: bool) { + self.emit_global("__ckb_hash_blake2b_var"); + self.emit_label("__ckb_hash_blake2b_var"); + self.emit("# cellscript abi: CKB Blake2b-256 variable helper; a0=input, a1=len, a2=output[32], returns a0=0"); + if !enabled { + self.emit_fail(CellScriptRuntimeError::SyscallFailed); + return; + } + + const IV: [u64; 8] = [ + 0x6a09e667f3bcc908, + 0xbb67ae8584caa73b, + 0x3c6ef372fe94f82b, + 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, + 0x9b05688c2b3e6c1f, + 0x1f83d9abfb41bd6b, + 0x5be0cd19137e2179, + ]; + const SIGMA: [[usize; 16]; 12] = [ + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], + [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4], + [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8], + [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13], + [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9], + [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11], + [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10], + [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5], + [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], + ]; + + const H_BASE: usize = 0; + const V_BASE: usize = 64; + const M_BASE: usize = 192; + const PTR: usize = 320; + const LEN: usize = 328; + const OUT: usize = 336; + const POS: usize = 344; + const CHUNK: usize = 352; + const FRAME: usize = 384; + + let personal0 = u64::from_le_bytes(*b"ckb-defa"); + let personal1 = u64::from_le_bytes(*b"ult-hash"); + let h = [IV[0] ^ 0x01010020, IV[1], IV[2], IV[3], IV[4], IV[5], IV[6] ^ personal0, IV[7] ^ personal1]; + + self.emit_large_addi("sp", "sp", -(FRAME as i64)); + self.emit_stack_store("a0", PTR); + self.emit_stack_store("a1", LEN); + self.emit_stack_store("a2", OUT); + self.emit_stack_store("zero", POS); + for (index, value) in h.iter().enumerate() { + self.emit_blake2b_store_const(*value, H_BASE + index * 8); + } + + let block_label = self.fresh_label("blake2b_var_block"); + let done_label = self.fresh_label("blake2b_var_done"); + self.emit_label(&block_label); + self.emit_stack_load("t0", POS); + self.emit_stack_load("t1", LEN); + self.emit("sub t2, t1, t0"); + let empty_first_block_label = self.fresh_label("blake2b_var_empty_first_block"); + self.emit(format!("bnez t2, {}", empty_first_block_label)); + self.emit(format!("beqz t0, {}", empty_first_block_label)); + self.emit(format!("j {}", done_label)); + self.emit_label(&empty_first_block_label); + self.emit("li t3, 128"); + self.emit("sltu t4, t3, t2"); + let chunk_rem_label = self.fresh_label("blake2b_var_chunk_rem"); + let chunk_set_label = self.fresh_label("blake2b_var_chunk_set"); + self.emit(format!("beqz t4, {}", chunk_rem_label)); + self.emit("li t2, 128"); + self.emit(format!("j {}", chunk_set_label)); + self.emit_label(&chunk_rem_label); + self.emit("# chunk already in t2"); + self.emit_label(&chunk_set_label); + self.emit_stack_store("t2", CHUNK); + let zero_loop = self.fresh_label("blake2b_var_zero_loop"); + let zero_done = self.fresh_label("blake2b_var_zero_done"); + self.emit("li t0, 0"); + self.emit_label(&zero_loop); + self.emit("li t1, 128"); + self.emit("sltu t2, t0, t1"); + self.emit(format!("beqz t2, {}", zero_done)); + self.emit(format!("li t3, {}", M_BASE)); + self.emit("add t3, sp, t3"); + self.emit("add t3, t3, t0"); + self.emit("sb zero, 0(t3)"); + self.emit("addi t0, t0, 1"); + self.emit(format!("j {}", zero_loop)); + self.emit_label(&zero_done); + + let copy_loop = self.fresh_label("blake2b_var_copy_loop"); + let copy_done = self.fresh_label("blake2b_var_copy_done"); + self.emit("li t0, 0"); + self.emit_label(©_loop); + self.emit_stack_load("t1", CHUNK); + self.emit("sltu t2, t0, t1"); + self.emit(format!("beqz t2, {}", copy_done)); + self.emit_stack_load("t3", PTR); + self.emit_stack_load("t4", POS); + self.emit("add t3, t3, t4"); + self.emit("add t3, t3, t0"); + self.emit("lbu t5, 0(t3)"); + self.emit(format!("li t6, {}", M_BASE)); + self.emit("add t6, sp, t6"); + self.emit("add t6, t6, t0"); + self.emit("sb t5, 0(t6)"); + self.emit("addi t0, t0, 1"); + self.emit(format!("j {}", copy_loop)); + self.emit_label(©_done); + + for index in 0..8 { + self.emit_stack_load("t0", H_BASE + index * 8); + self.emit_stack_store("t0", V_BASE + index * 8); + } + for (index, value) in IV.iter().enumerate() { + self.emit_blake2b_store_const(*value, V_BASE + (index + 8) * 8); + } + self.emit_stack_load("t0", POS); + self.emit_stack_load("t1", CHUNK); + self.emit("add t0, t0, t1"); + self.emit_stack_load("t2", V_BASE + 12 * 8); + self.emit("xor t2, t2, t0"); + self.emit_stack_store("t2", V_BASE + 12 * 8); + self.emit_stack_load("t2", V_BASE + 13 * 8); + self.emit_stack_store("t2", V_BASE + 13 * 8); + let not_final_label = self.fresh_label("blake2b_var_not_final"); + self.emit_stack_load("t3", LEN); + self.emit("sub t4, t3, t0"); + self.emit(format!("bnez t4, {}", not_final_label)); + self.emit_stack_load("t5", V_BASE + 14 * 8); + self.emit("xori t5, t5, -1"); + self.emit_stack_store("t5", V_BASE + 14 * 8); + self.emit_label(¬_final_label); + + for round in SIGMA { + self.emit_blake2b_g(V_BASE, M_BASE, 0, 4, 8, 12, round[0], round[1]); + self.emit_blake2b_g(V_BASE, M_BASE, 1, 5, 9, 13, round[2], round[3]); + self.emit_blake2b_g(V_BASE, M_BASE, 2, 6, 10, 14, round[4], round[5]); + self.emit_blake2b_g(V_BASE, M_BASE, 3, 7, 11, 15, round[6], round[7]); + self.emit_blake2b_g(V_BASE, M_BASE, 0, 5, 10, 15, round[8], round[9]); + self.emit_blake2b_g(V_BASE, M_BASE, 1, 6, 11, 12, round[10], round[11]); + self.emit_blake2b_g(V_BASE, M_BASE, 2, 7, 8, 13, round[12], round[13]); + self.emit_blake2b_g(V_BASE, M_BASE, 3, 4, 9, 14, round[14], round[15]); + } + for index in 0..8 { + self.emit_stack_load("t0", H_BASE + index * 8); + self.emit_stack_load("t1", V_BASE + index * 8); + self.emit("xor t0, t0, t1"); + self.emit_stack_load("t1", V_BASE + (index + 8) * 8); + self.emit("xor t0, t0, t1"); + self.emit_stack_store("t0", H_BASE + index * 8); + } + self.emit_stack_load("t0", POS); + self.emit_stack_load("t1", CHUNK); + self.emit("add t0, t0, t1"); + self.emit_stack_store("t0", POS); + self.emit(format!("beqz t1, {}", done_label)); + self.emit(format!("j {}", block_label)); + + self.emit_label(&done_label); + self.emit_stack_load("t6", OUT); + for index in 0..4 { + self.emit_stack_load("t0", H_BASE + index * 8); + self.emit(format!("sd t0, {}(t6)", index * 8)); + } + self.emit_large_addi("sp", "sp", FRAME as i64); + self.emit("li a0, 0"); + self.emit("ret"); + } + + fn emit_runtime_blake2b_hash32(&mut self, enabled: bool) { + self.emit_global("__ckb_hash_blake2b"); + self.emit_label("__ckb_hash_blake2b"); + self.emit("# cellscript abi: CKB Blake2b-256 helper; a0=input[32], a1=output[32], returns a0=0"); + if !enabled { + self.emit_fail(CellScriptRuntimeError::SyscallFailed); + return; + } + + const IV: [u64; 8] = [ + 0x6a09e667f3bcc908, + 0xbb67ae8584caa73b, + 0x3c6ef372fe94f82b, + 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, + 0x9b05688c2b3e6c1f, + 0x1f83d9abfb41bd6b, + 0x5be0cd19137e2179, + ]; + const SIGMA: [[usize; 16]; 12] = [ + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], + [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4], + [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8], + [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13], + [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9], + [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11], + [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10], + [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5], + [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], + ]; + + const H_BASE: usize = 0; + const V_BASE: usize = 64; + const M_BASE: usize = 192; + const FRAME: usize = 320; + + let personal0 = u64::from_le_bytes(*b"ckb-defa"); + let personal1 = u64::from_le_bytes(*b"ult-hash"); + let h = [IV[0] ^ 0x01010020, IV[1], IV[2], IV[3], IV[4], IV[5], IV[6] ^ personal0, IV[7] ^ personal1]; + + self.emit_large_addi("sp", "sp", -(FRAME as i64)); + for (index, value) in h.iter().enumerate() { + self.emit_blake2b_store_const(*value, H_BASE + index * 8); + } + for index in 0..4 { + self.emit_blake2b_load_input_word(index, M_BASE + index * 8); + } + for index in 4..16 { + self.emit_stack_store("zero", M_BASE + index * 8); + } + for index in 0..8 { + self.emit_stack_load("t0", H_BASE + index * 8); + self.emit_stack_store("t0", V_BASE + index * 8); + } + for (index, value) in IV.iter().enumerate() { + self.emit_blake2b_store_const(*value, V_BASE + (index + 8) * 8); + } + self.emit_stack_load("t0", V_BASE + 12 * 8); + self.emit("xori t0, t0, 32"); + self.emit_stack_store("t0", V_BASE + 12 * 8); + self.emit_stack_load("t0", V_BASE + 14 * 8); + self.emit("xori t0, t0, -1"); + self.emit_stack_store("t0", V_BASE + 14 * 8); + + for round in SIGMA { + self.emit_blake2b_g(V_BASE, M_BASE, 0, 4, 8, 12, round[0], round[1]); + self.emit_blake2b_g(V_BASE, M_BASE, 1, 5, 9, 13, round[2], round[3]); + self.emit_blake2b_g(V_BASE, M_BASE, 2, 6, 10, 14, round[4], round[5]); + self.emit_blake2b_g(V_BASE, M_BASE, 3, 7, 11, 15, round[6], round[7]); + self.emit_blake2b_g(V_BASE, M_BASE, 0, 5, 10, 15, round[8], round[9]); + self.emit_blake2b_g(V_BASE, M_BASE, 1, 6, 11, 12, round[10], round[11]); + self.emit_blake2b_g(V_BASE, M_BASE, 2, 7, 8, 13, round[12], round[13]); + self.emit_blake2b_g(V_BASE, M_BASE, 3, 4, 9, 14, round[14], round[15]); + } + + for index in 0..8 { + self.emit_stack_load("t0", H_BASE + index * 8); + self.emit_stack_load("t1", V_BASE + index * 8); + self.emit("xor t0, t0, t1"); + self.emit_stack_load("t1", V_BASE + (index + 8) * 8); + self.emit("xor t0, t0, t1"); + self.emit_stack_store("t0", H_BASE + index * 8); + } + for index in 0..4 { + self.emit_stack_load("t0", H_BASE + index * 8); + self.emit(format!("sd t0, {}(a1)", index * 8)); + } + self.emit_large_addi("sp", "sp", FRAME as i64); + self.emit("li a0, 0"); + self.emit("ret"); + } + + fn emit_runtime_blake2b_hash_pair(&mut self, enabled: bool) { + self.emit_global("__ckb_hash_pair"); + self.emit_label("__ckb_hash_pair"); + self.emit("# cellscript abi: hash_pair combines two 32-byte Hash inputs with CKB Blake2b-256; a0=left[32], a1=right[32], a2=output[32]"); + if !enabled { + self.emit_fail(CellScriptRuntimeError::SyscallFailed); + return; + } + + const IV: [u64; 8] = [ + 0x6a09e667f3bcc908, + 0xbb67ae8584caa73b, + 0x3c6ef372fe94f82b, + 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, + 0x9b05688c2b3e6c1f, + 0x1f83d9abfb41bd6b, + 0x5be0cd19137e2179, + ]; + const SIGMA: [[usize; 16]; 12] = [ + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], + [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4], + [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8], + [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13], + [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9], + [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11], + [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10], + [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5], + [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], + ]; + + const H_BASE: usize = 0; + const V_BASE: usize = 64; + const M_BASE: usize = 192; + const FRAME: usize = 320; + + let personal0 = u64::from_le_bytes(*b"ckb-defa"); + let personal1 = u64::from_le_bytes(*b"ult-hash"); + let h = [IV[0] ^ 0x01010020, IV[1], IV[2], IV[3], IV[4], IV[5], IV[6] ^ personal0, IV[7] ^ personal1]; + + self.emit_large_addi("sp", "sp", -(FRAME as i64)); + for (index, value) in h.iter().enumerate() { + self.emit_blake2b_store_const(*value, H_BASE + index * 8); + } + for index in 0..4 { + self.emit_blake2b_load_input_word(index, M_BASE + index * 8); + } + for index in 0..4 { + self.emit_blake2b_load_input_word_from("a1", index, M_BASE + (index + 4) * 8); + } + for index in 8..16 { + self.emit_stack_store("zero", M_BASE + index * 8); + } + for index in 0..8 { + self.emit_stack_load("t0", H_BASE + index * 8); + self.emit_stack_store("t0", V_BASE + index * 8); + } + for (index, value) in IV.iter().enumerate() { + self.emit_blake2b_store_const(*value, V_BASE + (index + 8) * 8); + } + self.emit_stack_load("t0", V_BASE + 12 * 8); + self.emit("xori t0, t0, 64"); + self.emit_stack_store("t0", V_BASE + 12 * 8); + self.emit_stack_load("t0", V_BASE + 14 * 8); + self.emit("xori t0, t0, -1"); + self.emit_stack_store("t0", V_BASE + 14 * 8); + + for round in SIGMA { + self.emit_blake2b_g(V_BASE, M_BASE, 0, 4, 8, 12, round[0], round[1]); + self.emit_blake2b_g(V_BASE, M_BASE, 1, 5, 9, 13, round[2], round[3]); + self.emit_blake2b_g(V_BASE, M_BASE, 2, 6, 10, 14, round[4], round[5]); + self.emit_blake2b_g(V_BASE, M_BASE, 3, 7, 11, 15, round[6], round[7]); + self.emit_blake2b_g(V_BASE, M_BASE, 0, 5, 10, 15, round[8], round[9]); + self.emit_blake2b_g(V_BASE, M_BASE, 1, 6, 11, 12, round[10], round[11]); + self.emit_blake2b_g(V_BASE, M_BASE, 2, 7, 8, 13, round[12], round[13]); + self.emit_blake2b_g(V_BASE, M_BASE, 3, 4, 9, 14, round[14], round[15]); + } + + for index in 0..8 { + self.emit_stack_load("t0", H_BASE + index * 8); + self.emit_stack_load("t1", V_BASE + index * 8); + self.emit("xor t0, t0, t1"); + self.emit_stack_load("t1", V_BASE + (index + 8) * 8); + self.emit("xor t0, t0, t1"); + self.emit_stack_store("t0", H_BASE + index * 8); + } + for index in 0..4 { + self.emit_stack_load("t0", H_BASE + index * 8); + self.emit(format!("sd t0, {}(a2)", index * 8)); + } + self.emit_large_addi("sp", "sp", FRAME as i64); + self.emit("li a0, 0"); + self.emit("ret"); + } + + fn emit_blake2b_store_const(&mut self, value: u64, stack_offset: usize) { + self.emit(format!("li t0, 0x{:016x}", value)); + self.emit_stack_store("t0", stack_offset); + } + + fn emit_blake2b_load_input_word(&mut self, word_index: usize, stack_offset: usize) { + self.emit_blake2b_load_input_word_from("a0", word_index, stack_offset); + } + + fn emit_blake2b_load_input_word_from(&mut self, source_reg: &str, word_index: usize, stack_offset: usize) { + self.emit("li t0, 0"); + for byte_index in 0..8 { + let absolute = word_index * 8 + byte_index; + self.emit(format!("lbu t1, {}({})", absolute, source_reg)); + if byte_index > 0 { + self.emit(format!("slli t1, t1, {}", byte_index * 8)); + } + self.emit("or t0, t0, t1"); + } + self.emit_stack_store("t0", stack_offset); + } + + fn emit_blake2b_rotr(&mut self, register: &str, bits: usize) { + self.emit(format!("srli t1, {}, {}", register, bits)); + self.emit(format!("slli {}, {}, {}", register, register, 64 - bits)); + self.emit(format!("or {}, {}, t1", register, register)); + } + + #[allow(clippy::too_many_arguments)] + fn emit_blake2b_g(&mut self, v_base: usize, m_base: usize, a: usize, b: usize, c: usize, d: usize, mx: usize, my: usize) { + let va = v_base + a * 8; + let vb = v_base + b * 8; + let vc = v_base + c * 8; + let vd = v_base + d * 8; + let vmx = m_base + mx * 8; + let vmy = m_base + my * 8; + + self.emit_stack_load("t0", va); + self.emit_stack_load("t1", vb); + self.emit("add t0, t0, t1"); + self.emit_stack_load("t1", vmx); + self.emit("add t0, t0, t1"); + self.emit_stack_store("t0", va); + self.emit_stack_load("t0", vd); + self.emit_stack_load("t1", va); + self.emit("xor t0, t0, t1"); + self.emit_blake2b_rotr("t0", 32); + self.emit_stack_store("t0", vd); + + self.emit_stack_load("t0", vc); + self.emit_stack_load("t1", vd); + self.emit("add t0, t0, t1"); + self.emit_stack_store("t0", vc); + self.emit_stack_load("t0", vb); + self.emit_stack_load("t1", vc); + self.emit("xor t0, t0, t1"); + self.emit_blake2b_rotr("t0", 24); + self.emit_stack_store("t0", vb); + + self.emit_stack_load("t0", va); + self.emit_stack_load("t1", vb); + self.emit("add t0, t0, t1"); + self.emit_stack_load("t1", vmy); + self.emit("add t0, t0, t1"); + self.emit_stack_store("t0", va); + self.emit_stack_load("t0", vd); + self.emit_stack_load("t1", va); + self.emit("xor t0, t0, t1"); + self.emit_blake2b_rotr("t0", 16); + self.emit_stack_store("t0", vd); + + self.emit_stack_load("t0", vc); + self.emit_stack_load("t1", vd); + self.emit("add t0, t0, t1"); + self.emit_stack_store("t0", vc); + self.emit_stack_load("t0", vb); + self.emit_stack_load("t1", vc); + self.emit("xor t0, t0, t1"); + self.emit_blake2b_rotr("t0", 63); + self.emit_stack_store("t0", vb); + } + + fn emit_runtime_witness_size_helper(&mut self, enabled: bool) { + const SIZE_OFFSET: usize = 8; + const RA_OFFSET: usize = 24; + const FRAME_SIZE: usize = 32; + + self.emit_global("__ckb_witness_size"); + self.emit_label("__ckb_witness_size"); + self.emit("# cellscript abi: witness byte size via LOAD_WITNESS"); + self.emit("# cellscript abi: args a0=SourceView; returns a0=size, a1=0 on success, a1=error_code on failure"); + if !enabled { + self.emit(format!("li a1, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + let invalid = self.fresh_label("witness_size_source_invalid"); + let failed = self.fresh_label("witness_size_load_failed"); + let status_ok = self.fresh_label("witness_size_status_ok"); + let done = self.fresh_label("witness_size_done"); + let abi = self.runtime_abi(); + + self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); + self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); + self.emit_decode_source_view_to_t1_t2(&invalid); + self.emit("li t0, 0"); + self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); + self.emit("li a0, 0"); + self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a7, {}", abi.load_witness)); + self.emit("ecall"); + self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", status_ok)); + self.emit(format!("beqz a0, {}", status_ok)); + self.emit(format!("j {}", failed)); + + self.emit_label(&status_ok); + self.emit(format!("j {}", done)); + + self.emit_label(&invalid); + self.emit(format!("li a1, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + + self.emit_label(&failed); + self.emit(format!("li a1, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + + self.emit_label(&done); + self.emit(format!("ld a0, {}(sp)", SIZE_OFFSET)); + self.emit("li a1, 0"); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + } + + fn emit_runtime_require_witness_size_at_least_helper(&mut self, enabled: bool) { + const SIZE_OFFSET: usize = 8; + const MIN_SIZE_OFFSET: usize = 16; + const RA_OFFSET: usize = 24; + const FRAME_SIZE: usize = 32; + + self.emit_global("__ckb_require_witness_size_at_least"); + self.emit_label("__ckb_require_witness_size_at_least"); + self.emit("# cellscript abi: require witness size >= min_size"); + self.emit("# cellscript abi: args a0=SourceView, a1=min_size; returns a0=status"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + let invalid = self.fresh_label("witness_req_size_source_invalid"); + let failed = self.fresh_label("witness_req_size_load_failed"); + let too_small = self.fresh_label("witness_req_size_too_small"); + let status_ok = self.fresh_label("witness_req_size_status_ok"); + let done = self.fresh_label("witness_req_size_done"); + let abi = self.runtime_abi(); + + self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); + self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); + self.emit("# cellscript abi: preserve min_size before LOAD_WITNESS size probe"); + self.emit(format!("sd a1, {}(sp)", MIN_SIZE_OFFSET)); + self.emit_decode_source_view_to_t1_t2(&invalid); + self.emit("li t0, 0"); + self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); + self.emit("li a0, 0"); + self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a7, {}", abi.load_witness)); + self.emit("ecall"); + self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", status_ok)); + self.emit(format!("beqz a0, {}", status_ok)); + self.emit(format!("j {}", failed)); + + self.emit_label(&status_ok); + self.emit(format!("ld t0, {}(sp)", SIZE_OFFSET)); + self.emit(format!("ld t1, {}(sp)", MIN_SIZE_OFFSET)); + self.emit("sltu t2, t0, t1"); + self.emit(format!("beqz t2, {}", done)); + + self.emit_label(&too_small); + self.emit(format!("li a0, {}", CellScriptRuntimeError::WitnessMalformed.code())); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + + self.emit_label(&done); + self.emit("li a0, 0"); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + } + + fn emit_runtime_witness_raw_helper(&mut self, enabled: bool) { + const OUTPTR_OFFSET: usize = 8; + const SIZE_OFFSET: usize = 16; + const RA_OFFSET: usize = 24; + const FRAME_SIZE: usize = 32; + + self.emit_global("__ckb_witness_raw"); + self.emit_label("__ckb_witness_raw"); + self.emit("# cellscript abi: load raw witness bytes (first 32) into caller buffer"); + self.emit("# cellscript abi: args a0=SourceView, a1=out32_ptr; returns a0=status"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + let invalid = self.fresh_label("witness_raw_source_invalid"); + let failed = self.fresh_label("witness_raw_load_failed"); + let status_ok = self.fresh_label("witness_raw_status_ok"); + let done = self.fresh_label("witness_raw_done"); + let abi = self.runtime_abi(); + + self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); + self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); + self.emit(format!("sd a1, {}(sp)", OUTPTR_OFFSET)); + self.emit("# cellscript abi: zero-fill caller witness Hash buffer before raw prefix load"); + self.emit(format!("ld t0, {}(sp)", OUTPTR_OFFSET)); + self.emit("li t1, 0"); + let zero_loop = self.fresh_label("witness_raw_zero_loop"); + let zero_done = self.fresh_label("witness_raw_zero_done"); + self.emit_label(&zero_loop); + self.emit("li t2, 32"); + self.emit("sltu t3, t1, t2"); + self.emit(format!("beqz t3, {}", zero_done)); + self.emit("add t4, t0, t1"); + self.emit("sb zero, 0(t4)"); + self.emit("addi t1, t1, 1"); + self.emit(format!("j {}", zero_loop)); + self.emit_label(&zero_done); + self.emit_decode_source_view_to_t1_t2(&invalid); + self.emit("# cellscript abi: LOAD_WITNESS raw first 32 bytes into caller buffer"); + self.emit("li t0, 32"); + self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); + self.emit(format!("ld a0, {}(sp)", OUTPTR_OFFSET)); + self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a7, {}", abi.load_witness)); + self.emit("ecall"); + self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", status_ok)); + self.emit(format!("beqz a0, {}", status_ok)); + self.emit(format!("j {}", failed)); + + self.emit_label(&status_ok); + self.emit(format!("j {}", done)); + + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + + self.emit_label(&done); + self.emit("li a0, 0"); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + } + + fn emit_runtime_witness_args_field_helper(&mut self, symbol: &str, detail: &str, field_index: u64, enabled: bool) { + const OUTPTR_OFFSET: usize = 0; + const SIZE_OFFSET: usize = 8; + const FULL_BUFFER_OFFSET: usize = 16; + const FULL_BUFFER_SIZE: usize = 512; + const FIELD_BUF_OFFSET: usize = FULL_BUFFER_OFFSET + FULL_BUFFER_SIZE; + const FIELD_BUF_SIZE: usize = 128; + const HEADER_READ_OFFSET: usize = FIELD_BUF_OFFSET + FIELD_BUF_SIZE; + const HEADER_READ_SIZE: usize = 24; + const RA_OFFSET: usize = HEADER_READ_OFFSET + HEADER_READ_SIZE; + const FRAME_SIZE: usize = RA_OFFSET + 8; + + self.emit_global(symbol); + self.emit_label(symbol); + self.emit(format!("# cellscript abi: extract WitnessArgs field {} ({})", field_index, detail)); + self.emit("# cellscript abi: args a0=SourceView, a1=out32_ptr; returns a0=status"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + let invalid = self.fresh_label("witness_field_source_invalid"); + let failed = self.fresh_label("witness_field_load_failed"); + let malformed = self.fresh_label("witness_field_malformed"); + let truncated = self.fresh_label("witness_field_truncated"); + let field_absent = self.fresh_label("witness_field_absent"); + let ok = self.fresh_label("witness_field_ok"); + let done = self.fresh_label("witness_field_done"); + let abi = self.runtime_abi(); + + self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); + self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); + self.emit(format!("sd a1, {}(sp)", OUTPTR_OFFSET)); + self.emit("# cellscript abi: zero-fill extracted WitnessArgs Hash buffer before parsing"); + self.emit("li t0, 0"); + let zero_field_loop = self.fresh_label("witness_field_prezero_loop"); + let zero_field_done = self.fresh_label("witness_field_prezero_done"); + self.emit_label(&zero_field_loop); + self.emit("li t1, 32"); + self.emit("sltu t2, t0, t1"); + self.emit(format!("beqz t2, {}", zero_field_done)); + self.emit(format!("addi t3, sp, {}", FIELD_BUF_OFFSET)); + self.emit("add t3, t3, t0"); + self.emit("sb zero, 0(t3)"); + self.emit("addi t0, t0, 1"); + self.emit(format!("j {}", zero_field_loop)); + self.emit_label(&zero_field_done); + self.emit_decode_source_view_to_t1_t2(&invalid); + + // Load full witness + self.emit(format!("li t0, {}", FULL_BUFFER_SIZE)); + self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", FULL_BUFFER_OFFSET)); + self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a7, {}", abi.load_witness)); + self.emit("ecall"); + self.emit(format!("beqz a0, {}", ok)); + self.emit(format!("j {}", failed)); + + self.emit_label(&ok); + self.emit(format!("ld t0, {}(sp)", SIZE_OFFSET)); + + // Parse Molecule WitnessArgs table header (minimum 4 + 3*4 = 16 bytes) + // Table encoding: total_size (4 bytes) + offsets[0..N-1] (4 bytes each) + // field_count = (offset0 / 4) - 1 + self.emit("li t1, 16"); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + + self.emit(format!("addi t3, sp, {}", FULL_BUFFER_OFFSET)); + self.emit("# cellscript abi: WitnessArgs total_size must match loaded witness size"); + self.emit_u32_le_from_base_to("t4", "t3", 0, "t5"); + self.emit("sub t2, t4, t0"); + self.emit(format!("bnez t2, {}", malformed)); + + // For the current 3-field WitnessArgs table, offset0 must be 16. + self.emit_u32_le_from_base_to("t4", "t3", 4, "t5"); + self.emit("li t5, 16"); + self.emit("sub t2, t4, t5"); + self.emit(format!("bnez t2, {}", malformed)); + + // Read field offsets from header (offsets at bytes 4, 8, 12) + self.emit_u32_le_from_base_to("t4", "t3", 4, "t2"); + self.emit(format!("sd t4, {}(sp)", HEADER_READ_OFFSET)); + self.emit_u32_le_from_base_to("t5", "t3", 8, "t2"); + self.emit(format!("sd t5, {}(sp)", HEADER_READ_OFFSET + 8)); + self.emit_u32_le_from_base_to("t6", "t3", 12, "t2"); + self.emit(format!("sd t6, {}(sp)", HEADER_READ_OFFSET + 16)); + + self.emit("# cellscript abi: validate all WitnessArgs field offsets are monotonic and in bounds"); + self.emit(format!("ld t4, {}(sp)", HEADER_READ_OFFSET)); + self.emit(format!("ld t5, {}(sp)", HEADER_READ_OFFSET + 8)); + self.emit("sltu t2, t5, t4"); + self.emit(format!("bnez t2, {}", malformed)); + self.emit(format!("ld t4, {}(sp)", HEADER_READ_OFFSET + 8)); + self.emit(format!("ld t5, {}(sp)", HEADER_READ_OFFSET + 16)); + self.emit("sltu t2, t5, t4"); + self.emit(format!("bnez t2, {}", malformed)); + self.emit("sltu t2, t0, t5"); + self.emit(format!("bnez t2, {}", truncated)); + + // Select field offset and next field offset + let field_offsets_offset = HEADER_READ_OFFSET + (field_index * 8) as usize; + let next_offsets_offset = HEADER_READ_OFFSET + ((field_index + 1) * 8) as usize; + self.emit(format!("ld t4, {}(sp)", field_offsets_offset)); + if field_index < 2 { + self.emit(format!("ld t5, {}(sp)", next_offsets_offset)); + } else { + self.emit("addi t5, t0, 0".to_string()); + } + + // Check field offset bounds: field_offset <= next_offset <= total_size. + // BytesOpt None is an empty span, so adjacent offsets may be equal. + self.emit("sltu t2, t5, t4"); + self.emit(format!("bnez t2, {}", malformed)); + self.emit("sltu t2, t0, t5"); + self.emit(format!("bnez t2, {}", truncated)); + + // Calculate BytesOpt field span. Empty span is None. + self.emit("sub t2, t5, t4"); + self.emit(format!("beqz t2, {}", field_absent)); + self.emit("li t6, 4"); + self.emit("sltu t3, t2, t6"); + self.emit(format!("bnez t3, {}", malformed)); + self.emit("addi t2, t2, -4"); + + // Read Some(Bytes) length at field_offset and require exact Bytes size. + self.emit(format!("addi t3, sp, {}", FULL_BUFFER_OFFSET)); + self.emit("add t6, t3, t4"); + self.emit_u32_le_from_base_to("t1", "t6", 0, "t3"); + self.emit("sub t3, t2, t1"); + self.emit(format!("bnez t3, {}", malformed)); + + // Copy field bytes to output buffer (max 32 bytes for Hash) + self.emit("li t3, 32"); + self.emit("sltu t5, t3, t1"); + let copy_count_ready = self.fresh_label("witness_field_copy_count_ready"); + self.emit(format!("beqz t5, {}", copy_count_ready)); + self.emit("addi t1, t3, 0"); + self.emit_label(©_count_ready); + self.emit(format!("addi t2, sp, {}", FIELD_BUF_OFFSET)); + self.emit("addi t4, t6, 4"); + // Copy loop + self.emit("li t3, 0"); + let copy_loop = self.fresh_label("witness_field_copy_loop"); + let copy_done = self.fresh_label("witness_field_copy_done"); + self.emit_label(©_loop); + self.emit("sltu t5, t3, t1"); + self.emit(format!("beqz t5, {}", copy_done)); + self.emit("add t5, t4, t3"); + self.emit("lbu t6, 0(t5)"); + self.emit("add t5, t2, t3"); + self.emit("sb t6, 0(t5)"); + self.emit("addi t3, t3, 1"); + self.emit(format!("j {}", copy_loop)); + self.emit_label(©_done); + self.emit(format!("j {}", done)); + + self.emit_label(&field_absent); + self.emit("# cellscript abi: BytesOpt None leaves pre-zeroed Hash buffer"); + self.emit(format!("j {}", done)); + + self.emit_label(&malformed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::WitnessMalformed.code())); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + + self.emit_label(&truncated); + self.emit(format!("li a0, {}", CellScriptRuntimeError::WitnessFieldTruncated.code())); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + + self.emit_label(&done); + // Copy 32 bytes from FIELD_BUF_OFFSET to caller's buffer (outptr) + self.emit(format!("ld t0, {}(sp)", OUTPTR_OFFSET)); + self.emit("li t1, 0"); + let copy_out = self.fresh_label("witness_field_copy_out_loop"); + let copy_out_done = self.fresh_label("witness_field_copy_out_done"); + self.emit_label(©_out); + self.emit("li t2, 32"); + self.emit("sltu t3, t1, t2"); + self.emit(format!("beqz t3, {}", copy_out_done)); + self.emit(format!("addi t2, sp, {}", FIELD_BUF_OFFSET)); + self.emit("add t2, t2, t1"); + self.emit("lbu t3, 0(t2)"); + self.emit("add t4, t0, t1"); + self.emit("sb t3, 0(t4)"); + self.emit("addi t1, t1, 1"); + self.emit(format!("j {}", copy_out)); + self.emit_label(©_out_done); + self.emit("li a0, 0"); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + } + + fn emit_runtime_current_script_hash_helper(&mut self, enabled: bool) { + self.emit_global("__ckb_current_script_hash"); + self.emit_label("__ckb_current_script_hash"); + self.emit("# cellscript abi: current script Hash via LOAD_SCRIPT_HASH"); + self.emit("# cellscript abi: args a0=out32_ptr, a1=size_ptr; returns a0=status"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + let failed = self.fresh_label("current_script_hash_load_failed"); + let malformed = self.fresh_label("current_script_hash_malformed"); + let done = self.fresh_label("current_script_hash_done"); + let abi = self.runtime_abi(); + self.emit("addi sp, sp, -24"); + self.emit("sd ra, 16(sp)"); + self.emit("sd a1, 8(sp)"); + self.emit("li t0, 32"); + self.emit("sd t0, 0(a1)"); + self.emit("li a2, 0"); + self.emit(format!("li a7, {}", abi.load_script_hash)); + self.emit("ecall"); + self.emit(format!("bnez a0, {}", failed)); + self.emit("ld t6, 8(sp)"); + self.emit("ld t0, 0(t6)"); + self.emit("li t1, 32"); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&malformed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); + self.emit_label(&done); + self.emit("ld ra, 16(sp)"); + self.emit("addi sp, sp, 24"); + self.emit("ret"); + } + + fn emit_runtime_source_view_helper(&mut self, symbol: &str, source_view: u64, detail: &str, enabled: bool) { + self.emit_global(symbol); + self.emit_label(symbol); + self.emit(format!("# cellscript abi: CKB SourceView helper ({})", detail)); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("addi a1, a0, 0"); + self.emit("ret"); + return; + } + self.emit(format!("li t0, {}", source_view)); + self.emit(format!("li t1, {}", CKB_SOURCE_VIEW_SHIFT)); + self.emit("mul t0, t0, t1"); + self.emit("add a0, a0, t0"); + self.emit("li a1, 0"); + self.emit("ret"); + } + + fn emit_runtime_ckb_since_epoch_helper(&mut self, symbol: &str, relative: bool, detail: &str, enabled: bool) { + self.emit_global(symbol); + self.emit_label(symbol); + self.emit(format!("# cellscript abi: {}", detail)); + self.emit("# cellscript abi: args a0=number(<2^24), a1=index(<2^16), a2=length(<2^16); requires length>0 and index, + ) { + let OrderMasterDataOffsets { + source: source_offset, + cell_index: cell_index_offset, + action_offset: action_offset_offset, + tx_hash_offset: tx_hash_offset_offset, + index_offset: index_offset_offset, + tx_dest: tx_dest_offset, + index_dest: index_dest_offset, + data_buffer: data_buffer_offset, + size: size_offset, + } = offsets; + let OrderMasterFailureLabels { invalid_action, malformed, out_point_failed } = failures; + let action_match = self.fresh_label("order_master_action_match"); + let action_mint = self.fresh_label("order_master_action_mint"); + let size_status_ok = self.fresh_label("order_master_data_size_status_ok"); + let done = self.fresh_label("order_master_loaded"); + let abi = self.runtime_abi(); + + self.emit("# cellscript abi: iCKB Limit Order data is exact-length order fields; trailing bytes are malformed"); + self.emit(format!("ld t0, {}(sp)", index_offset_offset)); + self.emit("li t1, 37"); + self.emit("add t0, t0, t1"); + self.emit(format!("sd t0, {}(sp)", data_buffer_offset)); + self.emit("li t1, 0"); + self.emit(format!("sd t1, {}(sp)", size_offset)); + self.emit(format!("addi a0, sp, {}", data_buffer_offset + 8)); + self.emit(format!("addi a1, sp, {}", size_offset)); + self.emit("li a2, 0"); + self.emit(format!("ld a3, {}(sp)", cell_index_offset)); + self.emit(format!("ld a4, {}(sp)", source_offset)); + self.emit(format!("li a7, {}", abi.load_cell_data)); + self.emit("ecall"); + self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", size_status_ok)); + self.emit(format!("beqz a0, {}", size_status_ok)); + self.emit(format!("j {}", malformed)); + self.emit_label(&size_status_ok); + self.emit(format!("ld t0, {}(sp)", data_buffer_offset)); + self.emit(format!("ld t1, {}(sp)", size_offset)); + self.emit("sub t2, t1, t0"); + self.emit(format!("bnez t2, {}", malformed)); + + self.emit_load_cell_data_prefix_to_stack( + source_offset, + cell_index_offset, + action_offset_offset, + data_buffer_offset, + 4, + size_offset, + malformed, + ); + self.emit_stack_u32_le_to("t0", data_buffer_offset); + self.emit("li t1, 1"); + self.emit("sub t2, t0, t1"); + self.emit(format!("beqz t2, {}", action_match)); + if allow_mint_relative { + self.emit(format!("beqz t0, {}", action_mint)); + } + self.emit(format!("j {}", invalid_action)); + + self.emit_label(&action_match); + self.emit_load_cell_data_prefix_to_stack( + source_offset, + cell_index_offset, + tx_hash_offset_offset, + tx_dest_offset, + 32, + size_offset, + malformed, + ); + self.emit_load_cell_data_prefix_to_stack( + source_offset, + cell_index_offset, + index_offset_offset, + data_buffer_offset, + 4, + size_offset, + malformed, + ); + self.emit_stack_u32_le_to("t0", data_buffer_offset); + self.emit(format!("sd t0, {}(sp)", index_dest_offset)); + self.emit(format!("j {}", done)); + + if allow_mint_relative { + self.emit_label(&action_mint); + self.emit_load_cell_data_prefix_to_stack( + source_offset, + cell_index_offset, + tx_hash_offset_offset, + tx_dest_offset, + 32, + size_offset, + malformed, + ); + for word in 0..4 { + self.emit(format!("ld t0, {}(sp)", tx_dest_offset + word * 8)); + self.emit(format!("bnez t0, {}", malformed)); + } + self.emit_load_cell_data_prefix_to_stack( + source_offset, + cell_index_offset, + index_offset_offset, + data_buffer_offset, + 4, + size_offset, + malformed, + ); + self.emit_stack_u32_le_to("t3", data_buffer_offset); + self.emit_sign_extend_i32("t3"); + self.emit(format!("sd t3, {}(sp)", data_buffer_offset)); + self.emit_load_input_out_point_to_stack( + source_offset, + cell_index_offset, + tx_dest_offset, + index_dest_offset, + size_offset, + out_point_failed, + ); + self.emit(format!("ld t3, {}(sp)", data_buffer_offset)); + self.emit(format!("ld t0, {}(sp)", index_dest_offset)); + self.emit("add t0, t0, t3"); + self.emit("slt t1, t0, zero"); + self.emit(format!("bnez t1, {}", out_point_failed)); + self.emit(format!("sd t0, {}(sp)", index_dest_offset)); + } + + self.emit_label(&done); + } + + fn emit_load_cell_data_prefix_to_stack( + &mut self, + source_offset: usize, + cell_index_offset: usize, + data_offset_offset: usize, + dest_offset: usize, + width: usize, + size_offset: usize, + malformed: &str, + ) { + let loaded = self.fresh_label("cell_data_prefix_loaded"); + let len_enough = self.fresh_label("cell_data_prefix_len_enough"); + let ready = self.fresh_label("cell_data_prefix_ready"); + let abi = self.runtime_abi(); + + self.emit(format!("li t0, {}", width)); + self.emit(format!("sd t0, {}(sp)", size_offset)); + self.emit(format!("addi a0, sp, {}", dest_offset)); + self.emit(format!("addi a1, sp, {}", size_offset)); + self.emit(format!("ld a2, {}(sp)", data_offset_offset)); + self.emit(format!("ld a3, {}(sp)", cell_index_offset)); + self.emit(format!("ld a4, {}(sp)", source_offset)); + self.emit(format!("li a7, {}", abi.load_cell_data)); + self.emit("ecall"); + self.emit(format!("beqz a0, {}", loaded)); + self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", len_enough)); + self.emit(format!("j {}", malformed)); + self.emit_label(&loaded); + self.emit(format!("ld t0, {}(sp)", size_offset)); + self.emit(format!("li t1, {}", width)); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + self.emit(format!("j {}", ready)); + self.emit_label(&len_enough); + self.emit(format!("ld t0, {}(sp)", size_offset)); + self.emit(format!("li t1, {}", width)); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + self.emit_label(&ready); + } + + fn emit_load_input_out_point_to_stack( + &mut self, + source_offset: usize, + cell_index_offset: usize, + tx_dest_offset: usize, + index_dest_offset: usize, + size_offset: usize, + failed: &str, + ) { + let abi = self.runtime_abi(); + + self.emit("li t0, 36"); + self.emit(format!("sd t0, {}(sp)", size_offset)); + self.emit(format!("addi a0, sp, {}", tx_dest_offset)); + self.emit(format!("addi a1, sp, {}", size_offset)); + self.emit("li a2, 0"); + self.emit(format!("ld a3, {}(sp)", cell_index_offset)); + self.emit(format!("ld a4, {}(sp)", source_offset)); + self.emit(format!("li a5, {}", CKB_INPUT_FIELD_OUT_POINT)); + self.emit(format!("li a7, {}", abi.load_input_by_field)); + self.emit("ecall"); + self.emit(format!("bnez a0, {}", failed)); + self.emit(format!("ld t0, {}(sp)", size_offset)); + self.emit("li t1, 36"); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", failed)); + self.emit_stack_u32_le_to("t0", tx_dest_offset + 32); + self.emit(format!("sd t0, {}(sp)", index_dest_offset)); + } + + fn emit_runtime_cell_hash_requirement_helper( + &mut self, + symbol: &str, + detail: &str, + field_id: u64, + mismatch_error: CellScriptRuntimeError, + enabled: bool, + ) { + self.emit_global(symbol); + self.emit_label(symbol); + self.emit(format!("# cellscript abi: CKB SourceView full-hash requirement ({})", detail)); + self.emit("# cellscript abi: args a0=SourceView, a1=expected_hash_ptr, a2=expected_hash_len"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + + let invalid = self.fresh_label("source_view_invalid"); + let bad_expected = self.fresh_label("expected_hash_invalid"); + let failed = self.fresh_label("cell_hash_load_failed"); + let mismatch = self.fresh_label("cell_hash_mismatch"); + let done = self.fresh_label("cell_hash_done"); + let abi = self.runtime_abi(); + + self.emit("addi sp, sp, -80"); + self.emit("sd ra, 72(sp)"); + self.emit("sd a1, 64(sp)"); + self.emit("sd a2, 56(sp)"); + + self.emit(format!("beqz a1, {}", bad_expected)); + self.emit("li t0, 32"); + self.emit("sub t1, a2, t0"); + self.emit(format!("bnez t1, {}", bad_expected)); + + self.emit_decode_source_view_to_t1_t2(&invalid); + self.emit("li t0, 32"); + self.emit("sd t0, 8(sp)"); + self.emit("addi a0, sp, 16"); + self.emit("addi a1, sp, 8"); + self.emit("li a2, 0"); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a5, {}", field_id)); + self.emit(format!("li a7, {}", abi.load_cell_by_field)); + self.emit("ecall"); + self.emit(format!("bnez a0, {}", failed)); + self.emit("ld t0, 8(sp)"); + self.emit("li t1, 32"); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", failed)); + self.emit("addi a0, sp, 16"); + self.emit("ld a1, 64(sp)"); + self.emit("li a2, 32"); + self.emit("call __cellscript_memcmp_fixed"); + self.emit(format!("bnez a0, {}", mismatch)); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit(format!("j {}", done)); + self.emit_label(&bad_expected); + self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit(format!("j {}", done)); + self.emit_label(&mismatch); + self.emit(format!("li a0, {}", mismatch_error.code())); + self.emit_label(&done); + self.emit("ld ra, 72(sp)"); + self.emit("addi sp, sp, 80"); + self.emit("ret"); + } + + fn emit_runtime_cell_script_hash_field_helper( + &mut self, + symbol: &str, + detail: &str, + field_id: u64, + read: ScriptHashFieldRead, + enabled: bool, + ) { + self.emit_global(symbol); + self.emit_label(symbol); + self.emit(format!("# cellscript abi: CKB SourceView read-only ScriptRef Hash field ({})", detail)); + self.emit("# cellscript abi: args a0=SourceView, a1=out32_ptr, a2=size_ptr; returns a0=status"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + + const SCRIPT_SIZE_OFFSET: usize = 8; + const SCRIPT_BUFFER_OFFSET: usize = 16; + const OUT_PTR_OFFSET: usize = 152; + const SIZE_PTR_OFFSET: usize = 160; + const RA_OFFSET: usize = 184; + const FRAME_SIZE: usize = 192; + + let requested_size = match read { + ScriptHashFieldRead::CodeHash => 53u64, + ScriptHashFieldRead::Args32 => 128u64, + }; + let payload_offset = match read { + ScriptHashFieldRead::CodeHash => SCRIPT_BUFFER_OFFSET + 16, + ScriptHashFieldRead::Args32 => SCRIPT_BUFFER_OFFSET + 53, + }; + let invalid = self.fresh_label("script_ref_hash_source_invalid"); + let failed = self.fresh_label("script_ref_hash_load_failed"); + let loaded = self.fresh_label("script_ref_hash_loaded"); + let malformed = self.fresh_label("script_ref_hash_malformed"); + let args_mismatch = self.fresh_label("script_ref_hash_args_mismatch"); + let copy_loop = self.fresh_label("script_ref_hash_copy"); + let copy_done = self.fresh_label("script_ref_hash_copy_done"); + let done = self.fresh_label("script_ref_hash_done"); + let abi = self.runtime_abi(); + + self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); + self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); + self.emit(format!("sd a1, {}(sp)", OUT_PTR_OFFSET)); + self.emit(format!("sd a2, {}(sp)", SIZE_PTR_OFFSET)); + self.emit_decode_source_view_to_t1_t2(&invalid); + self.emit(format!("li t0, {}", requested_size)); + self.emit(format!("sd t0, {}(sp)", SCRIPT_SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); + self.emit(format!("addi a1, sp, {}", SCRIPT_SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a5, {}", field_id)); + self.emit(format!("li a7, {}", abi.load_cell_by_field)); + self.emit("ecall"); + self.emit(format!("beqz a0, {}", loaded)); + if matches!(read, ScriptHashFieldRead::CodeHash) { + self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", loaded)); + } + self.emit(format!("j {}", failed)); + + self.emit_label(&loaded); + self.emit(format!("ld t3, {}(sp)", SCRIPT_SIZE_OFFSET)); + self.emit("li t1, 49"); + self.emit("sltu t2, t3, t1"); + self.emit(format!("bnez t2, {}", malformed)); + self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET); + self.emit("li t1, 53"); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + for (offset, expected) in [(4usize, 16u64), (8, 48), (12, 49)] { + self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + offset); + self.emit(format!("li t1, {}", expected)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + } + if matches!(read, ScriptHashFieldRead::Args32) { + self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + 49); + self.emit("li t1, 32"); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", args_mismatch)); + self.emit("li t1, 85"); + self.emit("sub t2, t3, t1"); + self.emit(format!("bnez t2, {}", malformed)); + } + + self.emit("li t1, 0"); + self.emit_label(©_loop); + self.emit("li t2, 32"); + self.emit("sltu t3, t1, t2"); + self.emit(format!("beqz t3, {}", copy_done)); + self.emit(format!("addi t6, sp, {}", payload_offset)); + self.emit("add t6, t6, t1"); + self.emit("lbu t5, 0(t6)"); + self.emit(format!("ld t6, {}(sp)", OUT_PTR_OFFSET)); + self.emit("add t6, t6, t1"); + self.emit("sb t5, 0(t6)"); + self.emit("addi t1, t1, 1"); + self.emit(format!("j {}", copy_loop)); + self.emit_label(©_done); + self.emit(format!("ld t6, {}(sp)", SIZE_PTR_OFFSET)); + self.emit("li t0, 32"); + self.emit("sd t0, 0(t6)"); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&args_mismatch); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptArgsMismatch.code())); + self.emit(format!("j {}", done)); + self.emit_label(&malformed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); + self.emit_label(&done); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + } + + fn emit_runtime_cell_script_scalar_field_helper( + &mut self, + symbol: &str, + detail: &str, + field_id: u64, + read: ScriptScalarFieldRead, + enabled: bool, + ) { + self.emit_global(symbol); + self.emit_label(symbol); + self.emit(format!("# cellscript abi: CKB SourceView read-only ScriptRef scalar field ({})", detail)); + self.emit("# cellscript abi: args a0=SourceView; returns a0=value, a1=status"); + if !enabled { + self.emit("li a0, 0"); + self.emit(format!("li a1, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + + const SCRIPT_SIZE_OFFSET: usize = 8; + const SCRIPT_BUFFER_OFFSET: usize = 16; + const RA_OFFSET: usize = 152; + const FRAME_SIZE: usize = 160; + let requested_size = match read { + ScriptScalarFieldRead::HashType => 53u64, + ScriptScalarFieldRead::ArgsEmpty => 128u64, + }; + let invalid = self.fresh_label("script_ref_scalar_source_invalid"); + let failed = self.fresh_label("script_ref_scalar_load_failed"); + let loaded = self.fresh_label("script_ref_scalar_loaded"); + let malformed = self.fresh_label("script_ref_scalar_malformed"); + let nonempty = self.fresh_label("script_ref_scalar_nonempty"); + let done = self.fresh_label("script_ref_scalar_done"); + let abi = self.runtime_abi(); + + self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); + self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); + self.emit_decode_source_view_to_t1_t2(&invalid); + self.emit(format!("li t0, {}", requested_size)); + self.emit(format!("sd t0, {}(sp)", SCRIPT_SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); + self.emit(format!("addi a1, sp, {}", SCRIPT_SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a5, {}", field_id)); + self.emit(format!("li a7, {}", abi.load_cell_by_field)); + self.emit("ecall"); + self.emit(format!("beqz a0, {}", loaded)); + self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", loaded)); + self.emit(format!("j {}", failed)); + + self.emit_label(&loaded); + self.emit(format!("ld t3, {}(sp)", SCRIPT_SIZE_OFFSET)); + self.emit("li t1, 49"); + self.emit("sltu t2, t3, t1"); + self.emit(format!("bnez t2, {}", malformed)); + self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET); + if matches!(read, ScriptScalarFieldRead::HashType) { + self.emit("li t1, 53"); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + } else { + self.emit("sub t2, t0, t3"); + self.emit(format!("bnez t2, {}", malformed)); + } + for (offset, expected) in [(4usize, 16u64), (8, 48), (12, 49)] { + self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + offset); + self.emit(format!("li t1, {}", expected)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + } + match read { + ScriptScalarFieldRead::HashType => { + self.emit(format!("lbu a0, {}(sp)", SCRIPT_BUFFER_OFFSET + 48)); + self.emit("li a1, 0"); + self.emit(format!("j {}", done)); + } + ScriptScalarFieldRead::ArgsEmpty => { + self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + 49); + self.emit(format!("bnez t0, {}", nonempty)); + self.emit("li t1, 53"); + self.emit("sub t2, t3, t1"); + self.emit(format!("bnez t2, {}", malformed)); + self.emit("li a0, 1"); + self.emit("li a1, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&nonempty); + self.emit("li a0, 0"); + self.emit("li a1, 0"); + self.emit(format!("j {}", done)); + } + } + + self.emit_label(&invalid); + self.emit("li a0, 0"); + self.emit(format!("li a1, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit("li a0, 0"); + self.emit(format!("li a1, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&malformed); + self.emit("li a0, 0"); + self.emit(format!("li a1, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); + self.emit_label(&done); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + } + + fn emit_runtime_cell_script_args_empty_requirement_helper(&mut self, symbol: &str, detail: &str, field_id: u64, enabled: bool) { + self.emit_global(symbol); + self.emit_label(symbol); + self.emit(format!("# cellscript abi: CKB SourceView Script empty-args requirement ({})", detail)); + self.emit("# cellscript abi: args a0=SourceView; expects Molecule Script args Bytes length == 0"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + + const SCRIPT_SIZE_OFFSET: usize = 8; + const SCRIPT_BUFFER_OFFSET: usize = 16; + const EMPTY_SCRIPT_SIZE: u64 = 53; + + let invalid = self.fresh_label("script_args_source_invalid"); + let failed = self.fresh_label("script_args_load_failed"); + let nonempty = self.fresh_label("script_args_nonempty"); + let malformed = self.fresh_label("script_args_malformed"); + let done = self.fresh_label("script_args_done"); + let abi = self.runtime_abi(); + + self.emit("addi sp, sp, -160"); + self.emit("sd ra, 152(sp)"); + self.emit_decode_source_view_to_t1_t2(&invalid); + self.emit("li t0, 128"); + self.emit(format!("sd t0, {}(sp)", SCRIPT_SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); + self.emit(format!("addi a1, sp, {}", SCRIPT_SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a5, {}", field_id)); + self.emit(format!("li a7, {}", abi.load_cell_by_field)); + self.emit("ecall"); + self.emit(format!("bnez a0, {}", failed)); + + self.emit(format!("ld t0, {}(sp)", SCRIPT_SIZE_OFFSET)); + self.emit(format!("li t1, {}", EMPTY_SCRIPT_SIZE)); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", nonempty)); + + for (offset, expected) in [(0usize, EMPTY_SCRIPT_SIZE), (4, 16), (8, 48), (12, 49), (49, 0)] { + self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + offset); + self.emit(format!("li t1, {}", expected)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + } + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&nonempty); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptArgsMismatch.code())); + self.emit(format!("j {}", done)); + self.emit_label(&malformed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); + self.emit_label(&done); + self.emit("ld ra, 152(sp)"); + self.emit("addi sp, sp, 160"); + self.emit("ret"); + } + + fn emit_runtime_cell_script_args_exact_requirement_helper(&mut self, symbol: &str, detail: &str, field_id: u64, enabled: bool) { + self.emit_global(symbol); + self.emit_label(symbol); + self.emit(format!("# cellscript abi: CKB SourceView Script arbitrary exact args requirement ({})", detail)); + self.emit("# cellscript abi: args a0=SourceView, a1=expected_args_ptr, a2=expected_args_len"); + self.emit("# cellscript abi: validates Molecule packed::Script args Bytes exactly, not only 32-byte hash args"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + + const SCRIPT_SIZE_OFFSET: usize = 8; + const SCRIPT_BUFFER_OFFSET: usize = 16; + const EXPECTED_PTR_OFFSET: usize = 72; + const EXPECTED_LEN_OFFSET: usize = 80; + const ARGS_OFFSET_OFFSET: usize = 88; + const CHUNK_LEN_OFFSET: usize = 96; + const SOURCE_INDEX_OFFSET: usize = 104; + const SOURCE_KIND_OFFSET: usize = 112; + const RA_OFFSET: usize = 120; + const FRAME_SIZE: usize = 128; + const SCRIPT_PREFIX_SIZE: u64 = 53; + const CHUNK_SIZE: u64 = 32; + + let invalid = self.fresh_label("script_args_exact_source_invalid"); + let bad_expected = self.fresh_label("script_args_exact_expected_invalid"); + let prefix_loaded = self.fresh_label("script_args_exact_prefix_loaded"); + let load_failed = self.fresh_label("script_args_exact_load_failed"); + let malformed = self.fresh_label("script_args_exact_malformed"); + let mismatch = self.fresh_label("script_args_exact_mismatch"); + let chunk_loop = self.fresh_label("script_args_exact_chunk_loop"); + let chunk_tail = self.fresh_label("script_args_exact_chunk_tail"); + let chunk_loaded = self.fresh_label("script_args_exact_chunk_loaded"); + let success = self.fresh_label("script_args_exact_success"); + let done = self.fresh_label("script_args_exact_done"); + let abi = self.runtime_abi(); + + self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); + self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); + self.emit(format!("sd a1, {}(sp)", EXPECTED_PTR_OFFSET)); + self.emit(format!("sd a2, {}(sp)", EXPECTED_LEN_OFFSET)); + self.emit(format!("beqz a2, {}", bad_expected)); + self.emit(format!("beqz a1, {}", bad_expected)); + + self.emit_decode_source_view_to_t1_t2(&invalid); + self.emit(format!("sd t1, {}(sp)", SOURCE_INDEX_OFFSET)); + self.emit(format!("sd t2, {}(sp)", SOURCE_KIND_OFFSET)); + + self.emit(format!("li t0, {}", SCRIPT_PREFIX_SIZE)); + self.emit(format!("sd t0, {}(sp)", SCRIPT_SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); + self.emit(format!("addi a1, sp, {}", SCRIPT_SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit(format!("ld a3, {}(sp)", SOURCE_INDEX_OFFSET)); + self.emit(format!("ld a4, {}(sp)", SOURCE_KIND_OFFSET)); + self.emit(format!("li a5, {}", field_id)); + self.emit(format!("li a7, {}", abi.load_cell_by_field)); + self.emit("ecall"); + self.emit(format!("beqz a0, {}", prefix_loaded)); + self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", prefix_loaded)); + self.emit(format!("j {}", load_failed)); + + self.emit_label(&prefix_loaded); + self.emit(format!("ld t3, {}(sp)", SCRIPT_SIZE_OFFSET)); + self.emit(format!("li t1, {}", SCRIPT_PREFIX_SIZE)); + self.emit("sltu t2, t3, t1"); + self.emit(format!("bnez t2, {}", malformed)); + self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET); + self.emit(format!("ld t1, {}(sp)", EXPECTED_LEN_OFFSET)); + self.emit(format!("li t2, {}", SCRIPT_PREFIX_SIZE)); + self.emit("add t1, t1, t2"); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + for (offset, expected) in [(4usize, 16u64), (8, 48), (12, 49)] { + self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + offset); + self.emit(format!("li t1, {}", expected)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + } + self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + 49); + self.emit(format!("ld t1, {}(sp)", EXPECTED_LEN_OFFSET)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", mismatch)); + self.emit(format!("sd zero, {}(sp)", ARGS_OFFSET_OFFSET)); + + self.emit_label(&chunk_loop); + self.emit(format!("ld t0, {}(sp)", ARGS_OFFSET_OFFSET)); + self.emit(format!("ld t1, {}(sp)", EXPECTED_LEN_OFFSET)); + self.emit("sub t2, t1, t0"); + self.emit(format!("beqz t2, {}", success)); + self.emit(format!("li t3, {}", CHUNK_SIZE)); + self.emit("sltu t4, t2, t3"); + self.emit(format!("bnez t4, {}", chunk_tail)); + self.emit(format!("li t2, {}", CHUNK_SIZE)); + self.emit_label(&chunk_tail); + self.emit(format!("sd t2, {}(sp)", SCRIPT_SIZE_OFFSET)); + self.emit(format!("sd t2, {}(sp)", CHUNK_LEN_OFFSET)); + self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); + self.emit(format!("addi a1, sp, {}", SCRIPT_SIZE_OFFSET)); + self.emit(format!("li a2, {}", SCRIPT_PREFIX_SIZE)); + self.emit("add a2, a2, t0"); + self.emit(format!("ld a3, {}(sp)", SOURCE_INDEX_OFFSET)); + self.emit(format!("ld a4, {}(sp)", SOURCE_KIND_OFFSET)); + self.emit(format!("li a5, {}", field_id)); + self.emit(format!("li a7, {}", abi.load_cell_by_field)); + self.emit("ecall"); + self.emit(format!("beqz a0, {}", chunk_loaded)); + self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", chunk_loaded)); + self.emit(format!("j {}", load_failed)); + self.emit_label(&chunk_loaded); + self.emit(format!("ld t2, {}(sp)", CHUNK_LEN_OFFSET)); + self.emit(format!("ld t0, {}(sp)", ARGS_OFFSET_OFFSET)); + self.emit(format!("ld t1, {}(sp)", EXPECTED_PTR_OFFSET)); + self.emit("add a1, t1, t0"); + self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); + self.emit("addi a2, t2, 0"); + self.emit("call __cellscript_memcmp_fixed"); + self.emit(format!("bnez a0, {}", mismatch)); + self.emit(format!("ld t0, {}(sp)", ARGS_OFFSET_OFFSET)); + self.emit(format!("ld t2, {}(sp)", CHUNK_LEN_OFFSET)); + self.emit("add t0, t0, t2"); + self.emit(format!("sd t0, {}(sp)", ARGS_OFFSET_OFFSET)); + self.emit(format!("j {}", chunk_loop)); + + self.emit_label(&success); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit(format!("j {}", done)); + self.emit_label(&bad_expected); + self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); + self.emit(format!("j {}", done)); + self.emit_label(&load_failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&mismatch); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptArgsMismatch.code())); + self.emit(format!("j {}", done)); + self.emit_label(&malformed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); + self.emit_label(&done); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + } + + fn emit_runtime_current_script_args_empty_requirement_helper(&mut self, enabled: bool) { + self.emit_global("__ckb_require_current_script_args_empty"); + self.emit_label("__ckb_require_current_script_args_empty"); + self.emit("# cellscript abi: current-script empty-args requirement via LOAD_SCRIPT plus output lock scan"); + self.emit("# cellscript abi: expects current Script args empty and same-code/hash-type Output locks args empty"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + + const CURRENT_SIZE_OFFSET: usize = 8; + const CURRENT_BUFFER_OFFSET: usize = 16; + const OUTPUT_INDEX_OFFSET: usize = 144; + const OUTPUT_SIZE_OFFSET: usize = 152; + const OUTPUT_BUFFER_OFFSET: usize = 160; + const OUTPUT_TRUNCATED_OFFSET: usize = 288; + const EMPTY_SCRIPT_SIZE: u64 = 53; + const FRAME_SIZE: usize = 320; + const RA_OFFSET: usize = 312; + + let failed = self.fresh_label("current_script_args_load_failed"); + let current_loaded = self.fresh_label("current_script_args_loaded"); + let nonempty = self.fresh_label("current_script_args_nonempty"); + let malformed = self.fresh_label("current_script_args_malformed"); + let output_loop = self.fresh_label("current_script_args_output_loop"); + let output_loaded = self.fresh_label("current_script_args_output_loaded"); + let output_prefix_loaded = self.fresh_label("current_script_args_output_prefix_loaded"); + let output_advance = self.fresh_label("current_script_args_output_advance"); + let output_done = self.fresh_label("current_script_args_output_done"); + let output_same_hash = self.fresh_label("current_script_args_output_same_hash"); + let output_same_script = self.fresh_label("current_script_args_output_same_script"); + let output_failed = self.fresh_label("current_script_args_output_failed"); + let done = self.fresh_label("current_script_args_done"); + let abi = self.runtime_abi(); + + self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); + self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); + self.emit("li t0, 128"); + self.emit(format!("sd t0, {}(sp)", CURRENT_SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", CURRENT_BUFFER_OFFSET)); + self.emit(format!("addi a1, sp, {}", CURRENT_SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit(format!("li a7, {}", abi.load_script)); + self.emit("ecall"); + self.emit(format!("beqz a0, {}", current_loaded)); + self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", nonempty)); + self.emit(format!("j {}", failed)); + + self.emit_label(¤t_loaded); + self.emit(format!("ld t0, {}(sp)", CURRENT_SIZE_OFFSET)); + self.emit(format!("li t1, {}", EMPTY_SCRIPT_SIZE)); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", nonempty)); + + for (offset, expected) in [(0usize, EMPTY_SCRIPT_SIZE), (4, 16), (8, 48), (12, 49), (49, 0)] { + self.emit_stack_u32_le_to("t0", CURRENT_BUFFER_OFFSET + offset); + self.emit(format!("li t1, {}", expected)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + } + + self.emit("# cellscript abi: require matching output lock scripts to keep empty args"); + self.emit(format!("sd zero, {}(sp)", OUTPUT_INDEX_OFFSET)); + self.emit_label(&output_loop); + self.emit("li t0, 128"); + self.emit(format!("sd t0, {}(sp)", OUTPUT_SIZE_OFFSET)); + self.emit(format!("sd zero, {}(sp)", OUTPUT_TRUNCATED_OFFSET)); + self.emit(format!("addi a0, sp, {}", OUTPUT_BUFFER_OFFSET)); + self.emit(format!("addi a1, sp, {}", OUTPUT_SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit(format!("ld a3, {}(sp)", OUTPUT_INDEX_OFFSET)); + self.emit(format!("li a4, {}", CKB_SOURCE_OUTPUT)); + self.emit(format!("li a5, {}", CKB_CELL_FIELD_LOCK)); + self.emit(format!("li a7, {}", abi.load_cell_by_field)); + self.emit("ecall"); + self.emit(format!("beqz a0, {}", output_loaded)); + self.emit(format!("li t0, {}", CKB_INDEX_OUT_OF_BOUND)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", output_done)); + self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", output_prefix_loaded)); + self.emit(format!("j {}", output_failed)); + + self.emit_label(&output_prefix_loaded); + self.emit("li t0, 1"); + self.emit(format!("sd t0, {}(sp)", OUTPUT_TRUNCATED_OFFSET)); + self.emit_label(&output_loaded); + self.emit(format!("ld t0, {}(sp)", OUTPUT_SIZE_OFFSET)); + self.emit("li t1, 49"); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + self.emit(format!("addi a0, sp, {}", CURRENT_BUFFER_OFFSET + 16)); + self.emit(format!("addi a1, sp, {}", OUTPUT_BUFFER_OFFSET + 16)); + self.emit("li a2, 32"); + self.emit("call __cellscript_memcmp_fixed"); + self.emit(format!("beqz a0, {}", output_same_hash)); + self.emit(format!("j {}", output_advance)); + + self.emit_label(&output_same_hash); + self.emit(format!("lbu t0, {}(sp)", CURRENT_BUFFER_OFFSET + 48)); + self.emit(format!("lbu t1, {}(sp)", OUTPUT_BUFFER_OFFSET + 48)); + self.emit("sub t2, t0, t1"); + self.emit(format!("beqz t2, {}", output_same_script)); + self.emit(format!("j {}", output_advance)); + + self.emit_label(&output_same_script); + self.emit(format!("ld t0, {}(sp)", OUTPUT_TRUNCATED_OFFSET)); + self.emit(format!("bnez t0, {}", nonempty)); + self.emit(format!("ld t0, {}(sp)", OUTPUT_SIZE_OFFSET)); + self.emit(format!("li t1, {}", EMPTY_SCRIPT_SIZE)); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", nonempty)); + for (offset, expected) in [(0usize, EMPTY_SCRIPT_SIZE), (4, 16), (8, 48), (12, 49), (49, 0)] { + self.emit_stack_u32_le_to("t0", OUTPUT_BUFFER_OFFSET + offset); + self.emit(format!("li t1, {}", expected)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + } + + self.emit_label(&output_advance); + self.emit(format!("ld t0, {}(sp)", OUTPUT_INDEX_OFFSET)); + self.emit("addi t0, t0, 1"); + self.emit(format!("sd t0, {}(sp)", OUTPUT_INDEX_OFFSET)); + self.emit(format!("j {}", output_loop)); + + self.emit_label(&output_done); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&output_failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&nonempty); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptArgsMismatch.code())); + self.emit(format!("j {}", done)); + self.emit_label(&malformed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); + self.emit_label(&done); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + } + + fn emit_runtime_cell_script_args_hash_requirement_helper( + &mut self, + symbol: &str, + detail: &str, + field_id: u64, + mode: ScriptArgsHashRequirementMode, + enabled: bool, + ) { + self.emit_global(symbol); + self.emit_label(symbol); + self.emit(format!("# cellscript abi: CKB SourceView Script 32-byte args requirement ({})", detail)); + self.emit("# cellscript abi: args a0=SourceView, a1=expected_args_hash_ptr, a2=expected_args_hash_len"); + match mode { + ScriptArgsHashRequirementMode::Exact32 => { + self.emit("# cellscript abi: expects Molecule Script args Bytes length == 32 and payload == expected hash"); + } + ScriptArgsHashRequirementMode::Prefix32 => { + self.emit("# cellscript abi: expects Molecule Script args Bytes length >= 32 and first 32 bytes == expected hash"); + } + ScriptArgsHashRequirementMode::Suffix32 => { + self.emit("# cellscript abi: expects Molecule Script args Bytes length >= 32 and last 32 bytes == expected hash"); + } + } + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + + const SCRIPT_SIZE_OFFSET: usize = 8; + const SCRIPT_BUFFER_OFFSET: usize = 16; + const ARGS_PAYLOAD_OFFSET: usize = SCRIPT_BUFFER_OFFSET + 53; + const SOURCE_INDEX_OFFSET: usize = 152; + const SOURCE_KIND_OFFSET: usize = 160; + const EXPECTED_HASH_LEN_OFFSET: usize = 168; + const EXPECTED_HASH_PTR_OFFSET: usize = 176; + const RA_OFFSET: usize = 184; + const FRAME_SIZE: usize = 192; + const SCRIPT_PREFIX_SIZE: u64 = 53; + const HASH_ARGS_SCRIPT_SIZE: u64 = 85; + + let invalid = self.fresh_label("script_args_hash_source_invalid"); + let bad_expected = self.fresh_label("script_args_hash_expected_invalid"); + let loaded = self.fresh_label("script_args_hash_loaded"); + let suffix_loaded = self.fresh_label("script_args_hash_suffix_loaded"); + let failed = self.fresh_label("script_args_hash_load_failed"); + let mismatch = self.fresh_label("script_args_hash_mismatch"); + let malformed = self.fresh_label("script_args_hash_malformed"); + let done = self.fresh_label("script_args_hash_done"); + let abi = self.runtime_abi(); + + self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); + self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); + self.emit(format!("sd a1, {}(sp)", EXPECTED_HASH_PTR_OFFSET)); + self.emit(format!("sd a2, {}(sp)", EXPECTED_HASH_LEN_OFFSET)); + + self.emit(format!("beqz a1, {}", bad_expected)); + self.emit("li t0, 32"); + self.emit("sub t1, a2, t0"); + self.emit(format!("bnez t1, {}", bad_expected)); + + self.emit_decode_source_view_to_t1_t2(&invalid); + self.emit(format!("sd t1, {}(sp)", SOURCE_INDEX_OFFSET)); + self.emit(format!("sd t2, {}(sp)", SOURCE_KIND_OFFSET)); + let requested_size = match mode { + ScriptArgsHashRequirementMode::Exact32 | ScriptArgsHashRequirementMode::Prefix32 => 128u64, + ScriptArgsHashRequirementMode::Suffix32 => SCRIPT_PREFIX_SIZE, + }; + self.emit(format!("li t0, {}", requested_size)); + self.emit(format!("sd t0, {}(sp)", SCRIPT_SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); + self.emit(format!("addi a1, sp, {}", SCRIPT_SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a5, {}", field_id)); + self.emit(format!("li a7, {}", abi.load_cell_by_field)); + self.emit("ecall"); + self.emit(format!("beqz a0, {}", loaded)); + self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", loaded)); + self.emit(format!("j {}", failed)); + + self.emit_label(&loaded); + self.emit(format!("ld t3, {}(sp)", SCRIPT_SIZE_OFFSET)); + self.emit("li t1, 53"); + self.emit("sltu t2, t3, t1"); + self.emit(format!("bnez t2, {}", malformed)); + + self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET); + self.emit("sub t2, t0, t3"); + self.emit(format!("bnez t2, {}", malformed)); + for (offset, expected) in [(4usize, 16u64), (8, 48), (12, 49)] { + self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + offset); + self.emit(format!("li t1, {}", expected)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + } + + self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + 49); + match mode { + ScriptArgsHashRequirementMode::Exact32 => { + self.emit("li t1, 32"); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", mismatch)); + self.emit(format!("li t1, {}", HASH_ARGS_SCRIPT_SIZE)); + self.emit("sub t2, t3, t1"); + self.emit(format!("bnez t2, {}", malformed)); + self.emit(format!("addi a0, sp, {}", ARGS_PAYLOAD_OFFSET)); + } + ScriptArgsHashRequirementMode::Prefix32 => { + self.emit("li t1, 32"); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", mismatch)); + self.emit(format!("li t1, {}", SCRIPT_PREFIX_SIZE)); + self.emit("add t1, t1, t0"); + self.emit("sub t2, t3, t1"); + self.emit(format!("bnez t2, {}", malformed)); + self.emit(format!("addi a0, sp, {}", ARGS_PAYLOAD_OFFSET)); + } + ScriptArgsHashRequirementMode::Suffix32 => { + self.emit("li t1, 32"); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", mismatch)); + self.emit(format!("li t1, {}", SCRIPT_PREFIX_SIZE)); + self.emit("add t1, t1, t0"); + self.emit("sub t2, t3, t1"); + self.emit(format!("bnez t2, {}", malformed)); + self.emit("addi t1, t1, -32"); + self.emit("li t0, 32"); + self.emit(format!("sd t0, {}(sp)", SCRIPT_SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); + self.emit(format!("addi a1, sp, {}", SCRIPT_SIZE_OFFSET)); + self.emit("addi a2, t1, 0"); + self.emit(format!("ld a3, {}(sp)", SOURCE_INDEX_OFFSET)); + self.emit(format!("ld a4, {}(sp)", SOURCE_KIND_OFFSET)); + self.emit(format!("li a5, {}", field_id)); + self.emit(format!("li a7, {}", abi.load_cell_by_field)); + self.emit("ecall"); + self.emit(format!("beqz a0, {}", suffix_loaded)); + self.emit(format!("j {}", failed)); + self.emit_label(&suffix_loaded); + self.emit(format!("ld t0, {}(sp)", SCRIPT_SIZE_OFFSET)); + self.emit("li t1, 32"); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); + } + } + self.emit(format!("ld a1, {}(sp)", EXPECTED_HASH_PTR_OFFSET)); + self.emit("li a2, 32"); + self.emit("call __cellscript_memcmp_fixed"); + self.emit(format!("bnez a0, {}", mismatch)); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit(format!("j {}", done)); + self.emit_label(&bad_expected); + self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&mismatch); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptArgsMismatch.code())); + self.emit(format!("j {}", done)); + self.emit_label(&malformed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); + self.emit_label(&done); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + } + + fn emit_runtime_cell_script_hash_type_requirement_helper(&mut self, symbol: &str, detail: &str, field_id: u64, enabled: bool) { + self.emit_global(symbol); + self.emit_label(symbol); + self.emit(format!("# cellscript abi: CKB SourceView Script code_hash/hash_type requirement ({})", detail)); + self.emit("# cellscript abi: args a0=SourceView, a1=expected_code_hash_ptr, a2=expected_code_hash_len, a3=expected_hash_type"); + self.emit("# cellscript abi: validates Molecule Script table prefix without constraining args length"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + + const SCRIPT_SIZE_OFFSET: usize = 8; + const SCRIPT_BUFFER_OFFSET: usize = 16; + const EXPECTED_CODE_HASH_PTR_OFFSET: usize = 80; + const EXPECTED_CODE_HASH_LEN_OFFSET: usize = 88; + const EXPECTED_HASH_TYPE_OFFSET: usize = 96; + const RA_OFFSET: usize = 120; + const FRAME_SIZE: usize = 128; + const SCRIPT_PREFIX_SIZE: u64 = 53; + + let invalid = self.fresh_label("script_identity_source_invalid"); + let bad_expected = self.fresh_label("script_identity_expected_invalid"); + let bad_hash_type = self.fresh_label("script_identity_hash_type_invalid"); + let loaded = self.fresh_label("script_identity_loaded"); + let prefix_loaded = self.fresh_label("script_identity_prefix_loaded"); + let failed = self.fresh_label("script_identity_load_failed"); + let malformed = self.fresh_label("script_identity_malformed"); + let mismatch = self.fresh_label("script_identity_mismatch"); + let done = self.fresh_label("script_identity_done"); + let abi = self.runtime_abi(); + + self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); + self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); + self.emit(format!("sd a1, {}(sp)", EXPECTED_CODE_HASH_PTR_OFFSET)); + self.emit(format!("sd a2, {}(sp)", EXPECTED_CODE_HASH_LEN_OFFSET)); + self.emit(format!("sd a3, {}(sp)", EXPECTED_HASH_TYPE_OFFSET)); + + self.emit(format!("beqz a1, {}", bad_expected)); + self.emit("li t0, 32"); + self.emit("sub t1, a2, t0"); + self.emit(format!("bnez t1, {}", bad_expected)); + self.emit("li t0, 256"); + self.emit("sltu t1, a3, t0"); + self.emit(format!("beqz t1, {}", bad_hash_type)); + + self.emit_decode_source_view_to_t1_t2(&invalid); + self.emit(format!("li t0, {}", SCRIPT_PREFIX_SIZE)); + self.emit(format!("sd t0, {}(sp)", SCRIPT_SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); + self.emit(format!("addi a1, sp, {}", SCRIPT_SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a5, {}", field_id)); + self.emit(format!("li a7, {}", abi.load_cell_by_field)); + self.emit("ecall"); + self.emit(format!("beqz a0, {}", loaded)); + self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", prefix_loaded)); + self.emit(format!("j {}", failed)); + + self.emit_label(&loaded); + self.emit(format!("j {}", prefix_loaded)); + self.emit_label(&prefix_loaded); + self.emit(format!("ld t3, {}(sp)", SCRIPT_SIZE_OFFSET)); + self.emit("li t1, 49"); + self.emit("sltu t2, t3, t1"); + self.emit(format!("bnez t2, {}", malformed)); + + self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET); + self.emit(format!("li t1, {}", SCRIPT_PREFIX_SIZE)); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + for (offset, expected) in [(4usize, 16u64), (8, 48), (12, 49)] { + self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + offset); + self.emit(format!("li t1, {}", expected)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + } + + self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET + 16)); + self.emit(format!("ld a1, {}(sp)", EXPECTED_CODE_HASH_PTR_OFFSET)); + self.emit("li a2, 32"); + self.emit("call __cellscript_memcmp_fixed"); + self.emit(format!("bnez a0, {}", mismatch)); + + self.emit(format!("lbu t0, {}(sp)", SCRIPT_BUFFER_OFFSET + 48)); + self.emit(format!("ld t1, {}(sp)", EXPECTED_HASH_TYPE_OFFSET)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", mismatch)); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit(format!("j {}", done)); + self.emit_label(&bad_expected); + self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); + self.emit(format!("j {}", done)); + self.emit_label(&bad_hash_type); + self.emit(format!("li a0, {}", CellScriptRuntimeError::NumericOrDiscriminantInvalid.code())); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&malformed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&mismatch); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptIdentityMismatch.code())); + self.emit_label(&done); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + } + + fn emit_runtime_load_u64_le_helper(&mut self) { + self.emit_global("__cellscript_load_u64_le"); + self.emit_label("__cellscript_load_u64_le"); + self.emit("# cellscript abi: load unaligned little-endian u64 from pointer a0"); + self.emit("li a1, 0"); + for byte_index in 0..8 { + self.emit(format!("lbu t0, {}(a0)", byte_index)); + if byte_index != 0 { + self.emit(format!("slli t0, t0, {}", byte_index * 8)); + } + self.emit("or a1, a1, t0"); + } + self.emit("addi a0, a1, 0"); + self.emit("ret"); + } + + fn emit_runtime_mul_u128_to_u256_helper(&mut self) { + self.emit_global("__cellscript_mul_u128_to_u256"); + self.emit_label("__cellscript_mul_u128_to_u256"); + self.emit("# cellscript abi: u128*u128 -> u256 limbs; args a0=left_ptr a1=right_ptr a2=out32_ptr"); + self.emit("addi sp, sp, -96"); + self.emit("sd ra, 88(sp)"); + self.emit("sd a0, 0(sp)"); + self.emit("sd a1, 8(sp)"); + self.emit("sd a2, 16(sp)"); + + self.emit("ld a0, 0(sp)"); + self.emit("call __cellscript_load_u64_le"); + self.emit("sd a0, 24(sp)"); + self.emit("ld a0, 0(sp)"); + self.emit("addi a0, a0, 8"); + self.emit("call __cellscript_load_u64_le"); + self.emit("sd a0, 32(sp)"); + self.emit("ld a0, 8(sp)"); + self.emit("call __cellscript_load_u64_le"); + self.emit("sd a0, 40(sp)"); + self.emit("ld a0, 8(sp)"); + self.emit("addi a0, a0, 8"); + self.emit("call __cellscript_load_u64_le"); + self.emit("sd a0, 48(sp)"); + + self.emit("ld t0, 24(sp)"); + self.emit("ld t1, 40(sp)"); + self.emit("mul t2, t0, t1"); + self.emit("mulhu t3, t0, t1"); + self.emit("sd t2, 56(sp)"); + + self.emit("ld t0, 24(sp)"); + self.emit("ld t1, 48(sp)"); + self.emit("mul t4, t0, t1"); + self.emit("mulhu t5, t0, t1"); + + self.emit("ld t0, 32(sp)"); + self.emit("ld t1, 40(sp)"); + self.emit("mul t6, t0, t1"); + self.emit("mulhu a3, t0, t1"); + + self.emit("add t0, t3, t4"); + self.emit("sltu a4, t0, t3"); + self.emit("add t1, t0, t6"); + self.emit("sltu a5, t1, t0"); + self.emit("add a4, a4, a5"); + self.emit("sd t1, 64(sp)"); + + self.emit("ld t0, 32(sp)"); + self.emit("ld t1, 48(sp)"); + self.emit("mul a5, t0, t1"); + self.emit("mulhu a6, t0, t1"); + + self.emit("add t2, t5, a3"); + self.emit("sltu a7, t2, t5"); + self.emit("add t3, t2, a5"); + self.emit("sltu t4, t3, t2"); + self.emit("add t5, t3, a4"); + self.emit("sltu t6, t5, t3"); + self.emit("sd t5, 72(sp)"); + self.emit("add t0, a6, a7"); + self.emit("add t0, t0, t4"); + self.emit("add t0, t0, t6"); + self.emit("sd t0, 80(sp)"); + + self.emit("ld t0, 16(sp)"); + self.emit("ld t1, 56(sp)"); + self.emit("sd t1, 0(t0)"); + self.emit("ld t1, 64(sp)"); + self.emit("sd t1, 8(t0)"); + self.emit("ld t1, 72(sp)"); + self.emit("sd t1, 16(t0)"); + self.emit("ld t1, 80(sp)"); + self.emit("sd t1, 24(t0)"); + self.emit("ld ra, 88(sp)"); + self.emit("addi sp, sp, 96"); + self.emit("ret"); + } + + fn emit_runtime_add_u256_helper(&mut self) { + self.emit_global("__cellscript_add_u256"); + self.emit_label("__cellscript_add_u256"); + self.emit("# cellscript abi: checked u256 addition; args a0=left32_ptr a1=right32_ptr a2=out32_ptr, returns carry in a0"); + self.emit("li a3, 0"); + for limb_offset in [0, 8, 16, 24] { + self.emit(format!("ld t0, {}(a0)", limb_offset)); + self.emit(format!("ld t1, {}(a1)", limb_offset)); + self.emit("add t2, t0, t1"); + self.emit("sltu t3, t2, t0"); + self.emit("add t2, t2, a3"); + self.emit("sltu t4, t2, a3"); + self.emit(format!("sd t2, {}(a2)", limb_offset)); + self.emit("add a3, t3, t4"); + } + self.emit("addi a0, a3, 0"); + self.emit("ret"); + } + + fn emit_runtime_c256_product_requirement_helper(&mut self, symbol: &str, detail: &str, equality: bool) { + self.emit_global(symbol); + self.emit_label(symbol); + self.emit(format!("# cellscript abi: {} with overflow-safe C256 product comparison", detail)); + self.emit("# cellscript abi: args a0..a3 are u128 little-endian pointers"); + let bad_expected = self.fresh_label("c256_operand_invalid"); + let mismatch = self.fresh_label("c256_product_mismatch"); + let success = self.fresh_label("c256_product_ok"); + let done = self.fresh_label("c256_product_done"); + + self.emit("addi sp, sp, -128"); + self.emit("sd ra, 120(sp)"); + self.emit("sd a0, 0(sp)"); + self.emit("sd a1, 8(sp)"); + self.emit("sd a2, 16(sp)"); + self.emit("sd a3, 24(sp)"); + self.emit(format!("beqz a0, {}", bad_expected)); + self.emit(format!("beqz a1, {}", bad_expected)); + self.emit(format!("beqz a2, {}", bad_expected)); + self.emit(format!("beqz a3, {}", bad_expected)); + + self.emit("ld a0, 0(sp)"); + self.emit("ld a1, 8(sp)"); + self.emit("addi a2, sp, 32"); + self.emit("call __cellscript_mul_u128_to_u256"); + self.emit("ld a0, 16(sp)"); + self.emit("ld a1, 24(sp)"); + self.emit("addi a2, sp, 64"); + self.emit("call __cellscript_mul_u128_to_u256"); + + for limb_offset in [24, 16, 8, 0] { + self.emit(format!("ld t0, {}(sp)", 32 + limb_offset)); + self.emit(format!("ld t1, {}(sp)", 64 + limb_offset)); + if equality { + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", mismatch)); + } else { + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", success)); + self.emit("sltu t2, t1, t0"); + self.emit(format!("bnez t2, {}", mismatch)); + } + } + + self.emit_label(&success); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&bad_expected); + self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); + self.emit(format!("j {}", done)); + self.emit_label(&mismatch); + self.emit(format!("li a0, {}", CellScriptRuntimeError::AggregateAmountMismatch.code())); + self.emit_label(&done); + self.emit("ld ra, 120(sp)"); + self.emit("addi sp, sp, 128"); + self.emit("ret"); + } + + fn emit_runtime_c256_sum2_product_requirement_helper(&mut self, symbol: &str, detail: &str, equality: bool) { + self.emit_global(symbol); + self.emit_label(symbol); + self.emit(format!("# cellscript abi: {} with checked u256 product sums", detail)); + self.emit("# cellscript abi: args a0..a7 are u128 little-endian pointers; compares a0*a1+a2*a3 with a4*a5+a6*a7"); + let bad_expected = self.fresh_label("c256_sum_operand_invalid"); + let mismatch = self.fresh_label("c256_sum_mismatch"); + let success = self.fresh_label("c256_sum_ok"); + let done = self.fresh_label("c256_sum_done"); + + self.emit("addi sp, sp, -320"); + self.emit("sd ra, 312(sp)"); + for (index, register) in ["a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7"].into_iter().enumerate() { + self.emit(format!("sd {}, {}(sp)", register, index * 8)); + self.emit(format!("beqz {}, {}", register, bad_expected)); + } + + self.emit("ld a0, 0(sp)"); + self.emit("ld a1, 8(sp)"); + self.emit("addi a2, sp, 64"); + self.emit("call __cellscript_mul_u128_to_u256"); + self.emit("ld a0, 16(sp)"); + self.emit("ld a1, 24(sp)"); + self.emit("addi a2, sp, 96"); + self.emit("call __cellscript_mul_u128_to_u256"); + self.emit("addi a0, sp, 64"); + self.emit("addi a1, sp, 96"); + self.emit("addi a2, sp, 128"); + self.emit("call __cellscript_add_u256"); + self.emit(format!("bnez a0, {}", mismatch)); + + self.emit("ld a0, 32(sp)"); + self.emit("ld a1, 40(sp)"); + self.emit("addi a2, sp, 160"); + self.emit("call __cellscript_mul_u128_to_u256"); + self.emit("ld a0, 48(sp)"); + self.emit("ld a1, 56(sp)"); + self.emit("addi a2, sp, 192"); + self.emit("call __cellscript_mul_u128_to_u256"); + self.emit("addi a0, sp, 160"); + self.emit("addi a1, sp, 192"); + self.emit("addi a2, sp, 224"); + self.emit("call __cellscript_add_u256"); + self.emit(format!("bnez a0, {}", mismatch)); + + for limb_offset in [24, 16, 8, 0] { + self.emit(format!("ld t0, {}(sp)", 128 + limb_offset)); + self.emit(format!("ld t1, {}(sp)", 224 + limb_offset)); + if equality { + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", mismatch)); + } else { + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", success)); + self.emit("sltu t2, t1, t0"); + self.emit(format!("bnez t2, {}", mismatch)); + } + } + + self.emit_label(&success); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&bad_expected); + self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); + self.emit(format!("j {}", done)); + self.emit_label(&mismatch); + self.emit(format!("li a0, {}", CellScriptRuntimeError::AggregateAmountMismatch.code())); + self.emit_label(&done); + self.emit("ld ra, 312(sp)"); + self.emit("addi sp, sp, 320"); + self.emit("ret"); + } + + fn emit_runtime_current_role_helper(&mut self, enabled: bool) { + self.emit_global("__ckb_current_role"); + self.emit_label("__ckb_current_role"); + self.emit("# cellscript abi: current role helper; normal lowering folds role to a compile-time lock/type constant"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("addi a1, a0, 0"); + } else { + self.emit(format!("li a0, {}", CKB_ROLE_UNKNOWN)); + self.emit("li a1, 0"); + } + self.emit("ret"); + } + + fn emit_runtime_cell_occupied_capacity_helper(&mut self, enabled: bool) { + self.emit_global("__ckb_cell_occupied_capacity"); + self.emit_label("__ckb_cell_occupied_capacity"); + self.emit("# cellscript abi: CKB occupied capacity via LOAD_CELL_BY_FIELD CellField::OccupiedCapacity"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("addi a1, a0, 0"); + self.emit("ret"); + return; + } + + let invalid = self.fresh_label("occupied_capacity_source_invalid"); + let failed = self.fresh_label("occupied_capacity_load_failed"); + let malformed = self.fresh_label("occupied_capacity_field_malformed"); + let done = self.fresh_label("occupied_capacity_done"); + let abi = self.runtime_abi(); + + self.emit("addi sp, sp, -48"); + self.emit("sd ra, 40(sp)"); + self.emit_decode_source_view_to_t1_t2(&invalid); + self.emit("sd t1, 0(sp)"); + self.emit("sd t2, 8(sp)"); + self.emit("li t0, 8"); + self.emit("sd t0, 16(sp)"); + self.emit("addi a0, sp, 24"); + self.emit("addi a1, sp, 16"); + self.emit("li a2, 0"); + self.emit("ld a3, 0(sp)"); + self.emit("ld a4, 8(sp)"); + self.emit(format!("li a5, {}", CKB_CELL_FIELD_OCCUPIED_CAPACITY)); + self.emit(format!("li a7, {}", abi.load_cell_by_field)); + self.emit("ecall"); + self.emit("ld t0, 16(sp)"); + self.emit(format!("bnez a0, {}", failed)); + self.emit("li t1, 8"); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + self.emit("ld a0, 24(sp)"); + self.emit("li a1, 0"); + self.emit(format!("j {}", done)); + + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit("addi a1, a0, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit("addi a1, a0, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&malformed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); + self.emit("addi a1, a0, 0"); + self.emit_label(&done); + self.emit("ld ra, 40(sp)"); + self.emit("addi sp, sp, 48"); + self.emit("ret"); + } + + fn emit_runtime_cell_unoccupied_capacity_helper(&mut self, enabled: bool) { + self.emit_global("__ckb_cell_unoccupied_capacity"); + self.emit_label("__ckb_cell_unoccupied_capacity"); + self.emit("# cellscript abi: SourceView unoccupied capacity = capacity - occupied_capacity"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("addi a1, a0, 0"); + self.emit("ret"); + return; + } + + let failed = self.fresh_label("unoccupied_capacity_failed"); + let failed_status_ok = self.fresh_label("unoccupied_capacity_failed_status_ok"); + let underflow = self.fresh_label("unoccupied_capacity_underflow"); + let done = self.fresh_label("unoccupied_capacity_done"); + + self.emit("addi sp, sp, -48"); + self.emit("sd ra, 40(sp)"); + self.emit("sd a0, 32(sp)"); + self.emit("call __ckb_cell_capacity"); + self.emit(format!("bnez a1, {}", failed)); + self.emit("sd a0, 24(sp)"); + self.emit("ld a0, 32(sp)"); + self.emit("call __ckb_cell_occupied_capacity"); + self.emit(format!("bnez a1, {}", failed)); + self.emit("ld t0, 24(sp)"); + self.emit("sltu t1, t0, a0"); + self.emit(format!("bnez t1, {}", underflow)); + self.emit("sub a0, t0, a0"); + self.emit("li a1, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit("addi a0, a1, 0"); + self.emit(format!("bnez a0, {}", failed_status_ok)); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit_label(&failed_status_ok); + self.emit("addi a1, a0, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&underflow); + self.emit(format!("li a0, {}", CellScriptRuntimeError::NumericOrDiscriminantInvalid.code())); + self.emit("addi a1, a0, 0"); + self.emit_label(&done); + self.emit("ld ra, 40(sp)"); + self.emit("addi sp, sp, 48"); + self.emit("ret"); + } + + fn emit_runtime_cell_output_index_helper(&mut self, enabled: bool) { + self.emit_global("__ckb_cell_output_index"); + self.emit_label("__ckb_cell_output_index"); + self.emit("# cellscript abi: SourceView output index extractor"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("addi a1, a0, 0"); + self.emit("ret"); + return; + } + let invalid = self.fresh_label("source_view_invalid"); + let output = self.fresh_label("source_view_output"); + let done = self.fresh_label("source_view_output_index_done"); + self.emit(format!("li t6, {}", CKB_SOURCE_VIEW_SHIFT)); + self.emit("div t0, a0, t6"); + self.emit("rem t1, a0, t6"); + self.emit(format!("li t5, {}", CKB_SOURCE_VIEW_OUTPUT)); + self.emit("sub t4, t0, t5"); + self.emit(format!("beqz t4, {}", output)); + self.emit(format!("li t5, {}", CKB_SOURCE_VIEW_GROUP_OUTPUT)); + self.emit("sub t4, t0, t5"); + self.emit(format!("beqz t4, {}", output)); + self.emit(format!("j {}", invalid)); + self.emit_label(&output); + self.emit("addi a0, t1, 0"); + self.emit("li a1, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit("addi a1, a0, 0"); + self.emit_label(&done); + self.emit("ret"); + } + + fn emit_runtime_cell_data_size_helper(&mut self, enabled: bool) { + self.emit_global("__ckb_cell_data_size"); + self.emit_label("__ckb_cell_data_size"); + self.emit("# cellscript abi: CKB SourceView LOAD_CELL_DATA size probe"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("addi a1, a0, 0"); + self.emit("ret"); + return; + } + let invalid = self.fresh_label("source_view_invalid"); + let done = self.fresh_label("cell_data_size_done"); + let failed = self.fresh_label("cell_data_size_failed"); + let status_ok = self.fresh_label("cell_data_size_status_ok"); + let abi = self.runtime_abi(); + self.emit("addi sp, sp, -48"); + self.emit("sd ra, 40(sp)"); + self.emit_decode_source_view_to_t1_t2(&invalid); + self.emit("li t0, 0"); + self.emit("sd t0, 8(sp)"); + self.emit("addi a0, sp, 16"); + self.emit("addi a1, sp, 8"); + self.emit("li a2, 0"); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a7, {}", abi.load_cell_data)); + self.emit("ecall"); + self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", status_ok)); + self.emit(format!("beqz a0, {}", status_ok)); + self.emit(format!("j {}", failed)); + self.emit_label(&status_ok); + self.emit("ld a0, 8(sp)"); + self.emit("li a1, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit("addi a1, a0, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit("addi a1, a0, 0"); + self.emit_label(&done); + self.emit("ld ra, 40(sp)"); + self.emit("addi sp, sp, 48"); + self.emit("ret"); + } + + fn emit_runtime_bounded_cell_dep_data_hash_requirement_helper(&mut self, enabled: bool) { + self.emit_global("__ckb_require_bounded_cell_dep_data_hash"); + self.emit_label("__ckb_require_bounded_cell_dep_data_hash"); + self.emit("# cellscript abi: a0=max_deps(1..=64), a1=expected_data_hash[32]; scan resolved CellDeps with LOAD_CELL_BY_FIELD"); + if !enabled { + self.emit_fail(CellScriptRuntimeError::SyscallFailed); + return; + } + + const EXPECTED_PTR_OFFSET: usize = 8; + const LIMIT_OFFSET: usize = 16; + const INDEX_OFFSET: usize = 24; + const SIZE_OFFSET: usize = 32; + const BUFFER_OFFSET: usize = 40; + const RA_OFFSET: usize = 72; + const FRAME_SIZE: usize = 80; + + let invalid = self.fresh_label("bounded_cell_dep_invalid"); + let scan = self.fresh_label("bounded_cell_dep_scan"); + let not_found = self.fresh_label("bounded_cell_dep_not_found"); + let loaded = self.fresh_label("bounded_cell_dep_loaded"); + let mismatch = self.fresh_label("bounded_cell_dep_mismatch"); + let failed = self.fresh_label("bounded_cell_dep_load_failed"); + let done = self.fresh_label("bounded_cell_dep_done"); + let abi = self.runtime_abi(); + + self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); + self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); + self.emit(format!("sd a1, {}(sp)", EXPECTED_PTR_OFFSET)); + self.emit(format!("sd a0, {}(sp)", LIMIT_OFFSET)); + self.emit(format!("beqz a1, {}", invalid)); + self.emit(format!("beqz a0, {}", invalid)); + self.emit("li t0, 64"); + self.emit(format!("bltu t0, a0, {}", invalid)); + self.emit(format!("sd zero, {}(sp)", INDEX_OFFSET)); + + self.emit_label(&scan); + self.emit(format!("ld t0, {}(sp)", INDEX_OFFSET)); + self.emit(format!("ld t1, {}(sp)", LIMIT_OFFSET)); + self.emit(format!("bgeu t0, t1, {}", not_found)); + self.emit("li t1, 32"); + self.emit(format!("sd t1, {}(sp)", SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", BUFFER_OFFSET)); + self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit(format!("ld a3, {}(sp)", INDEX_OFFSET)); + self.emit(format!("li a4, {}", CKB_SOURCE_CELL_DEP)); + self.emit(format!("li a5, {}", CKB_CELL_FIELD_DATA_HASH)); + self.emit(format!("li a7, {}", abi.load_cell_by_field)); + self.emit("ecall"); + self.emit(format!("beqz a0, {}", loaded)); + self.emit(format!("li t0, {}", CKB_INDEX_OUT_OF_BOUND)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", not_found)); + self.emit(format!("j {}", failed)); + + self.emit_label(&loaded); + self.emit(format!("ld t0, {}(sp)", SIZE_OFFSET)); + self.emit("li t1, 32"); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", failed)); + self.emit(format!("addi a0, sp, {}", BUFFER_OFFSET)); + self.emit(format!("ld a1, {}(sp)", EXPECTED_PTR_OFFSET)); + self.emit("li a2, 32"); + self.emit("call __cellscript_memcmp_fixed"); + self.emit(format!("bnez a0, {}", mismatch)); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + + self.emit_label(&mismatch); + self.emit(format!("ld t0, {}(sp)", INDEX_OFFSET)); + self.emit("addi t0, t0, 1"); + self.emit(format!("sd t0, {}(sp)", INDEX_OFFSET)); + self.emit(format!("j {}", scan)); + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::BoundsCheckFailed.code())); + self.emit(format!("j {}", done)); + self.emit_label(¬_found); + self.emit("# cellscript runtime error 63 bounded-cell-dep-not-found"); + self.emit(format!("li a0, {}", CellScriptRuntimeError::BoundedCellDepNotFound.code())); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CellLoadFailed.code())); + self.emit_label(&done); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + } + + fn emit_runtime_cell_data_hash_helper(&mut self, symbol: &str, detail: &str, enabled: bool) { + self.emit_global(symbol); + self.emit_label(symbol); + self.emit(format!("# cellscript abi: CKB SourceView LOAD_CELL_DATA and Blake2b ({})", detail)); + self.emit("# cellscript abi: args a0=SourceView, a1=out32_ptr, a2=size_ptr; returns a0=status"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + + const SIZE_OFFSET: usize = 8; + const BUFFER_OFFSET: usize = 16; + const OUT_PTR_OFFSET: usize = BUFFER_OFFSET + RUNTIME_CELL_BUFFER_SIZE; + const SIZE_PTR_OFFSET: usize = OUT_PTR_OFFSET + 8; + const RA_OFFSET: usize = SIZE_PTR_OFFSET + 8; + const FRAME_SIZE: usize = RA_OFFSET + 8; + + let invalid = self.fresh_label("cell_data_hash_source_invalid"); + let bad_output = self.fresh_label("cell_data_hash_output_invalid"); + let failed = self.fresh_label("cell_data_hash_load_failed"); + let done = self.fresh_label("cell_data_hash_done"); + let abi = self.runtime_abi(); + + self.emit(format!("addi sp, sp, -{}", FRAME_SIZE)); + self.emit(format!("sd ra, {}(sp)", RA_OFFSET)); + self.emit(format!("sd a1, {}(sp)", OUT_PTR_OFFSET)); + self.emit(format!("sd a2, {}(sp)", SIZE_PTR_OFFSET)); + self.emit(format!("beqz a1, {}", bad_output)); + self.emit(format!("beqz a2, {}", bad_output)); + + self.emit_decode_source_view_to_t1_t2(&invalid); + self.emit(format!("li t0, {}", RUNTIME_CELL_BUFFER_SIZE)); + self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", BUFFER_OFFSET)); + self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a7, {}", abi.load_cell_data)); + self.emit("ecall"); + self.emit(format!("bnez a0, {}", failed)); + self.emit(format!("addi a0, sp, {}", BUFFER_OFFSET)); + self.emit(format!("ld a1, {}(sp)", SIZE_OFFSET)); + self.emit(format!("ld a2, {}(sp)", OUT_PTR_OFFSET)); + self.emit("call __ckb_hash_blake2b_var"); + self.emit(format!("bnez a0, {}", failed)); + self.emit(format!("ld t6, {}(sp)", SIZE_PTR_OFFSET)); + self.emit("li t0, 32"); + self.emit("sd t0, 0(t6)"); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit(format!("j {}", done)); + self.emit_label(&bad_output); + self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CellLoadFailed.code())); + self.emit_label(&done); + self.emit(format!("ld ra, {}(sp)", RA_OFFSET)); + self.emit(format!("addi sp, sp, {}", FRAME_SIZE)); + self.emit("ret"); + } + + fn emit_runtime_cell_data_hash_at_helper(&mut self, symbol: &str, detail: &str, enabled: bool) { + self.emit_global(symbol); + self.emit_label(symbol); + self.emit(format!("# cellscript abi: {}; a0=source_view, a1=offset, a2=out[32], a3=size_ptr", detail)); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + let invalid = self.fresh_label("cell_data_hash_at_source_invalid"); + let failed = self.fresh_label("cell_data_hash_at_failed"); + let loaded = self.fresh_label("cell_data_hash_at_loaded"); + let done = self.fresh_label("cell_data_hash_at_done"); + let abi = self.runtime_abi(); + + self.emit("addi sp, sp, -80"); + self.emit("sd ra, 72(sp)"); + self.emit("sd a1, 8(sp)"); + self.emit("sd a2, 16(sp)"); + self.emit("sd a3, 24(sp)"); + self.emit_decode_source_view_to_t1_t2(&invalid); + self.emit("ld a0, 16(sp)"); + self.emit("ld a1, 24(sp)"); + self.emit("ld a2, 8(sp)"); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a7, {}", abi.load_cell_data)); + self.emit("ecall"); + self.emit(format!("beqz a0, {}", loaded)); + self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); + self.emit("sub t1, a0, t0"); + self.emit(format!("bnez t1, {}", failed)); + self.emit_label(&loaded); + self.emit("# cellscript abi: normalize fixed 32-byte slice length after LOAD_CELL_DATA"); + self.emit("ld t0, 24(sp)"); + self.emit("li t1, 32"); + self.emit("sd t1, 0(t0)"); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit_label(&done); + self.emit("ld ra, 72(sp)"); + self.emit("addi sp, sp, 80"); + self.emit("ret"); + } + + fn emit_runtime_cell_data_word_le_helper(&mut self, symbol: &str, detail: &str, width: usize, enabled: bool) { + self.emit_global(symbol); + self.emit_label(symbol); + self.emit(format!("# cellscript abi: {} via LOAD_CELL_DATA offset argument", detail)); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("addi a1, a0, 0"); + self.emit("ret"); + return; + } + let invalid = self.fresh_label("source_view_invalid"); + let done = self.fresh_label("cell_data_u64_done"); + let failed = self.fresh_label("cell_data_u64_failed"); + let loaded = self.fresh_label("cell_data_u64_loaded"); + let ready = self.fresh_label("cell_data_u64_ready"); + let abi = self.runtime_abi(); + self.emit("addi sp, sp, -64"); + self.emit("sd ra, 56(sp)"); + self.emit("# cellscript abi: save requested data offset"); + self.emit("sd a1, 8(sp)"); + self.emit_decode_source_view_to_t1_t2(&invalid); + self.emit(format!("li t0, {}", width)); + self.emit("sd t0, 16(sp)"); + self.emit("addi a0, sp, 24"); + self.emit("addi a1, sp, 16"); + self.emit("ld a2, 8(sp)"); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a7, {}", abi.load_cell_data)); + self.emit("ecall"); + self.emit(format!("beqz a0, {}", loaded)); + self.emit(format!("j {}", failed)); + self.emit_label(&loaded); + self.emit_label(&ready); + if width == 4 { + self.emit("li a0, 0"); + for byte_index in 0..4 { + self.emit(format!("lbu t0, {}(sp)", 24 + byte_index)); + if byte_index != 0 { + self.emit(format!("slli t0, t0, {}", byte_index * 8)); + } + self.emit("or a0, a0, t0"); + } + } else { + self.emit("li a0, 0"); + for byte_index in 0..8 { + self.emit(format!("lbu t0, {}(sp)", 24 + byte_index)); + if byte_index != 0 { + self.emit(format!("slli t0, t0, {}", byte_index * 8)); + } + self.emit("or a0, a0, t0"); + } + } + self.emit("li a1, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit("addi a1, a0, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CellLoadFailed.code())); + self.emit("addi a1, a0, 0"); + self.emit_label(&done); + self.emit("ld ra, 56(sp)"); + self.emit("addi sp, sp, 64"); + self.emit("ret"); + } + + fn emit_runtime_dao_accumulated_rate_helper(&mut self, enabled: bool) { + self.emit_global("__dao_accumulated_rate"); + self.emit_label("__dao_accumulated_rate"); + self.emit( + "# cellscript abi: DAO accumulated-rate HeaderDep SourceView helper via LOAD_HEADER at absolute header offset 160+8", + ); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("addi a1, a0, 0"); + self.emit("ret"); + return; + } + let invalid = self.fresh_label("dao_header_source_invalid"); + let done = self.fresh_label("dao_accumulated_rate_done"); + let failed = self.fresh_label("dao_accumulated_rate_failed"); + let loaded = self.fresh_label("dao_accumulated_rate_loaded"); + let abi = self.runtime_abi(); + self.emit("addi sp, sp, -48"); + self.emit("sd ra, 40(sp)"); + self.emit(format!("li t6, {}", CKB_SOURCE_VIEW_SHIFT)); + self.emit("div t0, a0, t6"); + self.emit("rem t1, a0, t6"); + self.emit(format!("li t5, {}", CKB_SOURCE_VIEW_HEADER_DEP)); + self.emit("sub t4, t0, t5"); + self.emit(format!("bnez t4, {}", invalid)); + self.emit("li t0, 8"); + self.emit("sd t0, 8(sp)"); + self.emit("addi a0, sp, 16"); + self.emit("addi a1, sp, 8"); + self.emit(format!("li a2, {}", CKB_DAO_HEADER_ACCUMULATED_RATE_ABSOLUTE_OFFSET)); + self.emit("addi a3, t1, 0"); + self.emit(format!("li a4, {}", abi.source_header_dep)); + self.emit(format!("li a7, {}", abi.load_header)); + self.emit("ecall"); + self.emit(format!("beqz a0, {}", loaded)); + self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); + self.emit("sub t1, a0, t0"); + self.emit(format!("bnez t1, {}", failed)); + self.emit_label(&loaded); + self.emit("ld t0, 8(sp)"); + self.emit("li t1, 8"); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", failed)); + self.emit("ld a0, 16(sp)"); + self.emit("li a1, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::HeaderDepMissing.code())); + self.emit("addi a1, a0, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::DaoFieldMalformed.code())); + self.emit("addi a1, a0, 0"); + self.emit_label(&done); + self.emit("ld ra, 40(sp)"); + self.emit("addi sp, sp, 48"); + self.emit("ret"); + } + + fn emit_runtime_dao_input_accumulated_rate_helper(&mut self, enabled: bool) { + self.emit_global("__dao_input_accumulated_rate"); + self.emit_label("__dao_input_accumulated_rate"); + self.emit( + "# cellscript abi: DAO accumulated-rate from Input/GroupInput committed header via LOAD_HEADER at absolute header offset 160+8", + ); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("addi a1, a0, 0"); + self.emit("ret"); + return; + } + + let invalid = self.fresh_label("dao_input_header_source_invalid"); + let done = self.fresh_label("dao_input_accumulated_rate_done"); + let failed = self.fresh_label("dao_input_accumulated_rate_failed"); + let loaded = self.fresh_label("dao_input_accumulated_rate_loaded"); + let abi = self.runtime_abi(); + + self.emit("addi sp, sp, -48"); + self.emit("sd ra, 40(sp)"); + self.emit_decode_input_source_view_to_t1_t2(&invalid); + self.emit("li t0, 8"); + self.emit("sd t0, 8(sp)"); + self.emit("addi a0, sp, 16"); + self.emit("addi a1, sp, 8"); + self.emit(format!("li a2, {}", CKB_DAO_HEADER_ACCUMULATED_RATE_ABSOLUTE_OFFSET)); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a7, {}", abi.load_header)); + self.emit("ecall"); + self.emit(format!("beqz a0, {}", loaded)); + self.emit(format!("li t0, {}", CKB_LENGTH_NOT_ENOUGH)); + self.emit("sub t1, a0, t0"); + self.emit(format!("bnez t1, {}", failed)); + self.emit_label(&loaded); + self.emit("ld t0, 8(sp)"); + self.emit("li t1, 8"); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", failed)); + self.emit("ld a0, 16(sp)"); + self.emit("li a1, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::HeaderDepMissing.code())); + self.emit("addi a1, a0, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::DaoFieldMalformed.code())); + self.emit("addi a1, a0, 0"); + self.emit_label(&done); + self.emit("ld ra, 40(sp)"); + self.emit("addi sp, sp, 48"); + self.emit("ret"); + } + + fn emit_runtime_dao_type_classifier_helper(&mut self, enabled: bool) { + self.emit_global("__dao_has_dao_type"); + self.emit_label("__dao_has_dao_type"); + self.emit("# cellscript abi: NervosDAO type-hash classifier"); + if !enabled { + self.emit("li a0, 0"); + self.emit(format!("li a1, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + + let invalid = self.fresh_label("dao_type_source_invalid"); + let false_label = self.fresh_label("dao_type_false"); + let done = self.fresh_label("dao_type_done"); + let abi = self.runtime_abi(); + + self.emit("addi sp, sp, -64"); + self.emit("sd ra, 56(sp)"); + self.emit_decode_source_view_to_t1_t2(&invalid); + self.emit("li t0, 32"); + self.emit("sd t0, 8(sp)"); + self.emit("addi a0, sp, 16"); + self.emit("addi a1, sp, 8"); + self.emit("li a2, 0"); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a5, {}", CKB_CELL_FIELD_TYPE_HASH)); + self.emit(format!("li a7, {}", abi.load_cell_by_field)); + self.emit("ecall"); + self.emit(format!("bnez a0, {}", false_label)); + self.emit("ld t0, 8(sp)"); + self.emit("li t1, 32"); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", false_label)); + for (word_index, expected) in CKB_DAO_TYPE_HASH_WORDS_LE.iter().enumerate() { + self.emit(format!("ld t0, {}(sp)", 16 + word_index * 8)); + self.emit(format!("li t1, {}", expected)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", false_label)); + } + self.emit("li a0, 1"); + self.emit("li a1, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&false_label); + self.emit("li a0, 0"); + self.emit("li a1, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&invalid); + self.emit("li a0, 0"); + self.emit(format!("li a1, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit_label(&done); + self.emit("ld ra, 56(sp)"); + self.emit("addi sp, sp, 64"); + self.emit("ret"); + } + + fn emit_runtime_dao_cell_data_classifier_helper(&mut self, symbol: &str, detail: &str, deposit: bool, enabled: bool) { + self.emit_global(symbol); + self.emit_label(symbol); + self.emit(format!("# cellscript abi: {} via LOAD_CELL_DATA exact 8-byte DAO data", detail)); + self.emit("# cellscript abi: matches NervosDAO deposit/withdrawal-request 8-byte data convention"); + if !enabled { + self.emit("li a0, 0"); + self.emit(format!("li a1, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + + let invalid = self.fresh_label("dao_data_source_invalid"); + let false_label = self.fresh_label("dao_data_false"); + let true_label = self.fresh_label("dao_data_true"); + let done = self.fresh_label("dao_data_done"); + let abi = self.runtime_abi(); + + self.emit("addi sp, sp, -48"); + self.emit("sd ra, 40(sp)"); + self.emit_decode_source_view_to_t1_t2(&invalid); + self.emit("li t0, 8"); + self.emit("sd t0, 8(sp)"); + self.emit("addi a0, sp, 16"); + self.emit("addi a1, sp, 8"); + self.emit("li a2, 0"); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a7, {}", abi.load_cell_data)); + self.emit("ecall"); + self.emit(format!("bnez a0, {}", false_label)); + self.emit("ld t0, 8(sp)"); + self.emit("li t1, 8"); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", false_label)); + self.emit("ld t0, 16(sp)"); + if deposit { + self.emit(format!("beqz t0, {}", true_label)); + self.emit(format!("j {}", false_label)); + } else { + self.emit(format!("bnez t0, {}", true_label)); + self.emit(format!("j {}", false_label)); + } + + self.emit_label(&true_label); + self.emit("li a0, 1"); + self.emit("li a1, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&false_label); + self.emit("li a0, 0"); + self.emit("li a1, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&invalid); + self.emit("li a0, 0"); + self.emit(format!("li a1, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit_label(&done); + self.emit("ld ra, 40(sp)"); + self.emit("addi sp, sp, 48"); + self.emit("ret"); + } + + fn emit_runtime_dao_require_header_dep_for_input_helper(&mut self, enabled: bool) { + self.emit_global("__dao_require_header_dep_for_input"); + self.emit_label("__dao_require_header_dep_for_input"); + self.emit("# cellscript abi: DAO input header to HeaderDep lineage requirement"); + self.emit("# cellscript abi: args a0=input SourceView, a1=HeaderDep SourceView; compares full 32-byte DAO fields"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + + const SIZE_OFFSET: usize = 8; + const INPUT_INDEX_OFFSET: usize = 16; + const INPUT_SOURCE_OFFSET: usize = 24; + const HEADER_INDEX_OFFSET: usize = 32; + const INPUT_DAO_OFFSET: usize = 40; + const HEADER_DAO_OFFSET: usize = 72; + const HEADER_VIEW_OFFSET: usize = 104; + + let invalid_input = self.fresh_label("dao_lineage_input_source_invalid"); + let invalid_header = self.fresh_label("dao_lineage_header_source_invalid"); + let input_failed = self.fresh_label("dao_lineage_input_header_missing"); + let header_failed = self.fresh_label("dao_lineage_header_dep_missing"); + let malformed = self.fresh_label("dao_lineage_dao_field_malformed"); + let mismatch = self.fresh_label("dao_lineage_mismatch"); + let done = self.fresh_label("dao_lineage_done"); + let abi = self.runtime_abi(); + + self.emit("addi sp, sp, -128"); + self.emit("sd ra, 120(sp)"); + self.emit(format!("sd a1, {}(sp)", HEADER_VIEW_OFFSET)); + + self.emit_decode_input_source_view_to_t1_t2(&invalid_input); + self.emit(format!("sd t1, {}(sp)", INPUT_INDEX_OFFSET)); + self.emit(format!("sd t2, {}(sp)", INPUT_SOURCE_OFFSET)); + + self.emit(format!("ld a0, {}(sp)", HEADER_VIEW_OFFSET)); + self.emit(format!("li t6, {}", CKB_SOURCE_VIEW_SHIFT)); + self.emit("div t0, a0, t6"); + self.emit("rem t1, a0, t6"); + self.emit(format!("li t5, {}", CKB_SOURCE_VIEW_HEADER_DEP)); + self.emit("sub t4, t0, t5"); + self.emit(format!("bnez t4, {}", invalid_header)); + self.emit(format!("sd t1, {}(sp)", HEADER_INDEX_OFFSET)); + + self.emit("li t0, 32"); + self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", INPUT_DAO_OFFSET)); + self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); + self.emit(format!("li a2, {}", CKB_DAO_HEADER_FIELD_ABSOLUTE_OFFSET)); + self.emit(format!("ld a3, {}(sp)", INPUT_INDEX_OFFSET)); + self.emit(format!("ld a4, {}(sp)", INPUT_SOURCE_OFFSET)); + self.emit(format!("li a7, {}", abi.load_header)); + self.emit("ecall"); + self.emit(format!("bnez a0, {}", input_failed)); + self.emit(format!("ld t0, {}(sp)", SIZE_OFFSET)); + self.emit("li t1, 32"); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + + self.emit("li t0, 32"); + self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", HEADER_DAO_OFFSET)); + self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); + self.emit(format!("li a2, {}", CKB_DAO_HEADER_FIELD_ABSOLUTE_OFFSET)); + self.emit(format!("ld a3, {}(sp)", HEADER_INDEX_OFFSET)); + self.emit(format!("li a4, {}", CKB_SOURCE_HEADER_DEP)); + self.emit(format!("li a7, {}", abi.load_header)); + self.emit("ecall"); + self.emit(format!("bnez a0, {}", header_failed)); + self.emit(format!("ld t0, {}(sp)", SIZE_OFFSET)); + self.emit("li t1, 32"); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + + self.emit(format!("addi a0, sp, {}", INPUT_DAO_OFFSET)); + self.emit(format!("addi a1, sp, {}", HEADER_DAO_OFFSET)); + self.emit("li a2, 32"); + self.emit("call __cellscript_memcmp_fixed"); + self.emit(format!("bnez a0, {}", mismatch)); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + + self.emit_label(&invalid_input); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit(format!("j {}", done)); + self.emit_label(&invalid_header); + self.emit(format!("li a0, {}", CellScriptRuntimeError::HeaderDepMissing.code())); + self.emit(format!("j {}", done)); + self.emit_label(&input_failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::HeaderDepMissing.code())); + self.emit(format!("j {}", done)); + self.emit_label(&header_failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::HeaderDepMissing.code())); + self.emit(format!("j {}", done)); + self.emit_label(&malformed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::DaoFieldMalformed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&mismatch); + self.emit(format!("li a0, {}", CellScriptRuntimeError::DaoHeaderLineageMismatch.code())); + self.emit_label(&done); + self.emit("ld ra, 120(sp)"); + self.emit("addi sp, sp, 128"); + self.emit("ret"); + } + + fn emit_runtime_dao_require_input_since_at_least_helper(&mut self, enabled: bool) { + self.emit_global("__dao_require_input_since_at_least"); + self.emit_label("__dao_require_input_since_at_least"); + self.emit("# cellscript abi: DAO input since lower-bound requirement"); + self.emit("# cellscript abi: args a0=input SourceView, a1=required_since; enforces loaded_since >= required_since"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + + const SIZE_OFFSET: usize = 8; + const REQUIRED_SINCE_OFFSET: usize = 16; + const SINCE_OFFSET: usize = 24; + + let invalid = self.fresh_label("dao_since_input_source_invalid"); + let failed = self.fresh_label("dao_since_load_failed"); + let malformed = self.fresh_label("dao_since_field_malformed"); + let immature = self.fresh_label("dao_since_immature"); + let done = self.fresh_label("dao_since_done"); + let abi = self.runtime_abi(); + + self.emit("addi sp, sp, -48"); + self.emit("sd ra, 40(sp)"); + self.emit(format!("sd a1, {}(sp)", REQUIRED_SINCE_OFFSET)); + + self.emit_decode_input_source_view_to_t1_t2(&invalid); + self.emit("li t0, 8"); + self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", SINCE_OFFSET)); + self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a5, {}", CKB_INPUT_FIELD_SINCE)); + self.emit(format!("li a7, {}", abi.load_input_by_field)); + self.emit("ecall"); + self.emit(format!("bnez a0, {}", failed)); + self.emit(format!("ld t0, {}(sp)", SIZE_OFFSET)); + self.emit("li t1, 8"); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + + self.emit(format!("ld t0, {}(sp)", SINCE_OFFSET)); + self.emit(format!("ld t1, {}(sp)", REQUIRED_SINCE_OFFSET)); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bnez t2, {}", immature)); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CellLoadFailed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&malformed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::DaoFieldMalformed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&immature); + self.emit(format!("li a0, {}", CellScriptRuntimeError::DaoMaturityViolation.code())); + self.emit_label(&done); + self.emit("ld ra, 40(sp)"); + self.emit("addi sp, sp, 48"); + self.emit("ret"); + } + + fn emit_runtime_dao_require_input_relative_epoch_since_at_least_helper(&mut self, enabled: bool) { + self.emit_global("__dao_require_input_relative_epoch_since_at_least"); + self.emit_label("__dao_require_input_relative_epoch_since_at_least"); + self.emit("# cellscript abi: DAO relative epoch since maturity requirement"); + self.emit("# cellscript abi: args a0=input SourceView, a1=epoch_number, a2=epoch_index, a3=epoch_length"); + self.emit("# cellscript abi: loads input since, requires RFC0017 relative epoch flags, and compares epoch fractions"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + + const SIZE_OFFSET: usize = 8; + const REQUIRED_NUMBER_OFFSET: usize = 16; + const REQUIRED_INDEX_OFFSET: usize = 24; + const REQUIRED_LENGTH_OFFSET: usize = 32; + const SINCE_OFFSET: usize = 40; + const LOADED_NUMBER_OFFSET: usize = 48; + const LOADED_INDEX_OFFSET: usize = 56; + const LOADED_LENGTH_OFFSET: usize = 64; + + let invalid = self.fresh_label("dao_epoch_since_input_source_invalid"); + let failed = self.fresh_label("dao_epoch_since_load_failed"); + let malformed = self.fresh_label("dao_epoch_since_malformed"); + let immature = self.fresh_label("dao_epoch_since_immature"); + let success = self.fresh_label("dao_epoch_since_success"); + let done = self.fresh_label("dao_epoch_since_done"); + let abi = self.runtime_abi(); + + self.emit("addi sp, sp, -80"); + self.emit("sd ra, 72(sp)"); + self.emit(format!("sd a1, {}(sp)", REQUIRED_NUMBER_OFFSET)); + self.emit(format!("sd a2, {}(sp)", REQUIRED_INDEX_OFFSET)); + self.emit(format!("sd a3, {}(sp)", REQUIRED_LENGTH_OFFSET)); + + self.emit(format!("li t0, {}", CKB_EPOCH_NUMBER_BOUND)); + self.emit("sltu t1, a1, t0"); + self.emit(format!("beqz t1, {}", malformed)); + self.emit(format!("li t0, {}", CKB_EPOCH_FRACTION_BOUND)); + self.emit("sltu t1, a2, t0"); + self.emit(format!("beqz t1, {}", malformed)); + self.emit("sltu t1, a3, t0"); + self.emit(format!("beqz t1, {}", malformed)); + self.emit(format!("beqz a3, {}", malformed)); + self.emit("sltu t1, a2, a3"); + self.emit(format!("beqz t1, {}", malformed)); + + self.emit_decode_input_source_view_to_t1_t2(&invalid); + self.emit("li t0, 8"); + self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", SINCE_OFFSET)); + self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a5, {}", CKB_INPUT_FIELD_SINCE)); + self.emit(format!("li a7, {}", abi.load_input_by_field)); + self.emit("ecall"); + self.emit(format!("bnez a0, {}", failed)); + self.emit(format!("ld t0, {}(sp)", SIZE_OFFSET)); + self.emit("li t1, 8"); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + + self.emit(format!("ld t0, {}(sp)", SINCE_OFFSET)); + self.emit("li t1, 1"); + self.emit("slli t1, t1, 63"); + self.emit("and t2, t0, t1"); + self.emit(format!("beqz t2, {}", malformed)); + self.emit(format!("li t1, {}", CKB_SINCE_REMAIN_FLAGS_BITS)); + self.emit("and t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + self.emit(format!("li t1, {}", CKB_SINCE_METRIC_TYPE_FLAG_MASK)); + self.emit("and t2, t0, t1"); + self.emit(format!("li t3, {}", CKB_SINCE_EPOCH_NUMBER_WITH_FRACTION_FLAG)); + self.emit("sub t4, t2, t3"); + self.emit(format!("bnez t4, {}", malformed)); + + self.emit(format!("li t1, {}", CKB_SINCE_VALUE_MASK)); + self.emit("and t0, t0, t1"); + self.emit(format!("li t1, {}", CKB_EPOCH_NUMBER_MASK)); + self.emit("and t2, t0, t1"); + self.emit("srai t3, t0, 24"); + self.emit(format!("li t1, {}", CKB_EPOCH_FRACTION_MASK)); + self.emit("and t3, t3, t1"); + self.emit("srai t4, t0, 40"); + self.emit("and t4, t4, t1"); + self.emit(format!("beqz t4, {}", malformed)); + self.emit("sltu t5, t3, t4"); + self.emit(format!("beqz t5, {}", malformed)); + self.emit(format!("sd t2, {}(sp)", LOADED_NUMBER_OFFSET)); + self.emit(format!("sd t3, {}(sp)", LOADED_INDEX_OFFSET)); + self.emit(format!("sd t4, {}(sp)", LOADED_LENGTH_OFFSET)); + + self.emit(format!("ld t0, {}(sp)", REQUIRED_NUMBER_OFFSET)); + self.emit("sltu t1, t0, t2"); + self.emit(format!("bnez t1, {}", success)); + self.emit("sltu t1, t2, t0"); + self.emit(format!("bnez t1, {}", immature)); + self.emit(format!("ld t0, {}(sp)", LOADED_INDEX_OFFSET)); + self.emit(format!("ld t1, {}(sp)", REQUIRED_LENGTH_OFFSET)); + self.emit("mul t2, t0, t1"); + self.emit(format!("ld t0, {}(sp)", REQUIRED_INDEX_OFFSET)); + self.emit(format!("ld t1, {}(sp)", LOADED_LENGTH_OFFSET)); + self.emit("mul t3, t0, t1"); + self.emit("sltu t4, t2, t3"); + self.emit(format!("bnez t4, {}", immature)); + + self.emit_label(&success); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CellLoadFailed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&malformed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSinceMalformed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&immature); + self.emit(format!("li a0, {}", CellScriptRuntimeError::DaoMaturityViolation.code())); + self.emit_label(&done); + self.emit("ld ra, 72(sp)"); + self.emit("addi sp, sp, 80"); + self.emit("ret"); + } + + fn emit_runtime_xudt_amount_word_helper(&mut self, symbol: &str, detail: &str, offset: u64, enabled: bool) { + self.emit_global(symbol); + self.emit_label(symbol); + self.emit(format!("# cellscript abi: {} via LOAD_CELL_DATA offset={}", detail, offset)); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("addi a1, a0, 0"); + self.emit("ret"); + return; + } + let invalid = self.fresh_label("source_view_invalid"); + let done = self.fresh_label("xudt_amount_done"); + let failed = self.fresh_label("xudt_amount_failed"); + let abi = self.runtime_abi(); + self.emit("addi sp, sp, -48"); + self.emit("sd ra, 40(sp)"); + self.emit_decode_source_view_to_t1_t2(&invalid); + self.emit("li t0, 8"); + self.emit("sd t0, 8(sp)"); + self.emit("addi a0, sp, 16"); + self.emit("addi a1, sp, 8"); + self.emit(format!("li a2, {}", offset)); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a7, {}", abi.load_cell_data)); + self.emit("ecall"); + self.emit(format!("bnez a0, {}", failed)); + self.emit("ld a0, 16(sp)"); + self.emit("li a1, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit("addi a1, a0, 0"); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::XudtBindingMismatch.code())); + self.emit("addi a1, a0, 0"); + self.emit_label(&done); + self.emit("ld ra, 40(sp)"); + self.emit("addi sp, sp, 48"); + self.emit("ret"); + } + + fn emit_runtime_xudt_require_owner_mode_input_type_helper(&mut self, enabled: bool) { + self.emit_runtime_cell_hash_requirement_helper( + "__xudt_require_owner_mode_input_type", + "xUDT owner-mode input-type full 32-byte binding check", + CKB_CELL_FIELD_TYPE_HASH, + CellScriptRuntimeError::XudtBindingMismatch, + enabled, + ); + } + + fn emit_stack_u32_le_to(&mut self, dest: &str, stack_offset: usize) { + self.emit(format!("lbu {}, {}(sp)", dest, stack_offset)); + self.emit(format!("lbu t4, {}(sp)", stack_offset + 1)); + self.emit("slli t4, t4, 8"); + self.emit(format!("or {}, {}, t4", dest, dest)); + self.emit(format!("lbu t4, {}(sp)", stack_offset + 2)); + self.emit("slli t4, t4, 16"); + self.emit(format!("or {}, {}, t4", dest, dest)); + self.emit(format!("lbu t4, {}(sp)", stack_offset + 3)); + self.emit("slli t4, t4, 24"); + self.emit(format!("or {}, {}, t4", dest, dest)); + } + + pub(super) fn emit_u32_le_from_base_to(&mut self, dest: &str, base: &str, offset: usize, scratch: &str) { + self.emit(format!("lbu {}, {}({})", dest, offset, base)); + self.emit(format!("lbu {}, {}({})", scratch, offset + 1, base)); + self.emit(format!("slli {}, {}, 8", scratch, scratch)); + self.emit(format!("or {}, {}, {}", dest, dest, scratch)); + self.emit(format!("lbu {}, {}({})", scratch, offset + 2, base)); + self.emit(format!("slli {}, {}, 16", scratch, scratch)); + self.emit(format!("or {}, {}, {}", dest, dest, scratch)); + self.emit(format!("lbu {}, {}({})", scratch, offset + 3, base)); + self.emit(format!("slli {}, {}, 24", scratch, scratch)); + self.emit(format!("or {}, {}, {}", dest, dest, scratch)); + } + + fn emit_runtime_xudt_require_owner_mode_type_args_helper(&mut self, enabled: bool) { + self.emit_global("__xudt_require_owner_mode_type_args"); + self.emit_label("__xudt_require_owner_mode_type_args"); + self.emit("# cellscript abi: xUDT owner-mode Type Script args requirement"); + self.emit("# cellscript abi: args a0=SourceView, a1=owner_hash_ptr, a2=owner_hash_len, a3=flags_u32"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + + const SCRIPT_BUFFER_OFFSET: usize = 16; + const SCRIPT_SIZE_OFFSET: usize = 8; + const OWNER_ARGS_OFFSET: usize = SCRIPT_BUFFER_OFFSET + 53; + const FLAGS_ARGS_OFFSET: usize = OWNER_ARGS_OFFSET + 32; + + let invalid = self.fresh_label("xudt_args_source_invalid"); + let bad_expected = self.fresh_label("xudt_args_expected_invalid"); + let malformed = self.fresh_label("xudt_script_malformed"); + let failed = self.fresh_label("xudt_script_load_failed"); + let mismatch = self.fresh_label("xudt_args_mismatch"); + let done = self.fresh_label("xudt_args_done"); + let abi = self.runtime_abi(); + + self.emit("addi sp, sp, -192"); + self.emit("sd ra, 184(sp)"); + self.emit("sd a1, 176(sp)"); + self.emit("sd a2, 168(sp)"); + self.emit("sd a3, 160(sp)"); + + self.emit(format!("beqz a1, {}", bad_expected)); + self.emit("li t0, 32"); + self.emit("sub t1, a2, t0"); + self.emit(format!("bnez t1, {}", bad_expected)); + self.emit("li t0, 4294967296"); + self.emit("sltu t1, a3, t0"); + self.emit(format!("beqz t1, {}", mismatch)); + + self.emit_decode_source_view_to_t1_t2(&invalid); + self.emit("li t0, 128"); + self.emit(format!("sd t0, {}(sp)", SCRIPT_SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", SCRIPT_BUFFER_OFFSET)); + self.emit(format!("addi a1, sp, {}", SCRIPT_SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit("addi a3, t1, 0"); + self.emit("addi a4, t2, 0"); + self.emit(format!("li a5, {}", CKB_CELL_FIELD_TYPE)); + self.emit(format!("li a7, {}", abi.load_cell_by_field)); + self.emit("ecall"); + self.emit(format!("bnez a0, {}", failed)); + + self.emit(format!("ld t0, {}(sp)", SCRIPT_SIZE_OFFSET)); + self.emit("li t1, 89"); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + + for (offset, expected) in [(0usize, 89u64), (4, 16), (8, 48), (12, 49), (49, 36)] { + self.emit_stack_u32_le_to("t0", SCRIPT_BUFFER_OFFSET + offset); + self.emit(format!("li t1, {}", expected)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", malformed)); + } + + self.emit(format!("addi a0, sp, {}", OWNER_ARGS_OFFSET)); + self.emit("ld a1, 176(sp)"); + self.emit("li a2, 32"); + self.emit("call __cellscript_memcmp_fixed"); + self.emit(format!("bnez a0, {}", mismatch)); + + self.emit_stack_u32_le_to("t0", FLAGS_ARGS_OFFSET); + self.emit("ld t1, 160(sp)"); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", mismatch)); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + + self.emit_label(&invalid); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit(format!("j {}", done)); + self.emit_label(&bad_expected); + self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); + self.emit(format!("j {}", done)); + self.emit_label(&malformed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&mismatch); + self.emit(format!("li a0, {}", CellScriptRuntimeError::XudtBindingMismatch.code())); + self.emit_label(&done); + self.emit("ld ra, 184(sp)"); + self.emit("addi sp, sp, 192"); + self.emit("ret"); + } + + fn emit_runtime_xudt_require_owner_mode_type_args_current_script_helper(&mut self, enabled: bool) { + self.emit_global("__xudt_require_owner_mode_type_args_current_script"); + self.emit_label("__xudt_require_owner_mode_type_args_current_script"); + self.emit("# cellscript abi: xUDT owner-mode Type Script args requirement bound to current script hash"); + self.emit("# cellscript abi: args a0=SourceView, a1=flags_u32; owner hash is LOAD_SCRIPT_HASH(current script)"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + + const SIZE_OFFSET: usize = 8; + const SOURCE_VIEW_OFFSET: usize = 16; + const FLAGS_OFFSET: usize = 24; + const SCRIPT_HASH_OFFSET: usize = 32; + + let hash_failed = self.fresh_label("xudt_current_script_hash_load_failed"); + let hash_malformed = self.fresh_label("xudt_current_script_hash_malformed"); + let done = self.fresh_label("xudt_current_script_args_done"); + let abi = self.runtime_abi(); + + self.emit("addi sp, sp, -80"); + self.emit("sd ra, 72(sp)"); + self.emit(format!("sd a0, {}(sp)", SOURCE_VIEW_OFFSET)); + self.emit(format!("sd a1, {}(sp)", FLAGS_OFFSET)); + + self.emit("li t0, 32"); + self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", SCRIPT_HASH_OFFSET)); + self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit(format!("li a7, {}", abi.load_script_hash)); + self.emit("ecall"); + self.emit(format!("bnez a0, {}", hash_failed)); + self.emit(format!("ld t0, {}(sp)", SIZE_OFFSET)); + self.emit("li t1, 32"); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", hash_malformed)); + + self.emit(format!("ld a0, {}(sp)", SOURCE_VIEW_OFFSET)); + self.emit(format!("addi a1, sp, {}", SCRIPT_HASH_OFFSET)); + self.emit("li a2, 32"); + self.emit(format!("ld a3, {}(sp)", FLAGS_OFFSET)); + self.emit("call __xudt_require_owner_mode_type_args"); + self.emit(format!("j {}", done)); + + self.emit_label(&hash_failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&hash_malformed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); + self.emit_label(&done); + self.emit("ld ra, 72(sp)"); + self.emit("addi sp, sp, 80"); + self.emit("ret"); + } + + fn emit_runtime_fungible_type_group_conservation_helper(&mut self, symbol: &str, detail: &str, enabled: bool) { + self.emit_global(symbol); + self.emit_label(symbol); + let owner_mode = symbol == FUNGIBLE_TYPE_GROUP_V1_CODEGEN_HELPER; + if owner_mode { + self.emit(format!( + "# cellscript abi: {detail}; owner-authorized issuance or non-empty input/output checked-u128 conservation" + )); + self.emit("# cellscript abi: supply authorization: 32-byte input lock hash or 0x01-tagged 32-byte input Type Script hash"); + } else { + self.emit(format!("# cellscript abi: {detail}; requires non-empty input/output groups and conserves checked u128 sums")); + } + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + + const SIZE_OFFSET: usize = 8; + const BUFFER_OFFSET: usize = 16; + const INPUT_LOW_OFFSET: usize = 32; + const INPUT_HIGH_OFFSET: usize = 40; + const OUTPUT_LOW_OFFSET: usize = 48; + const OUTPUT_HIGH_OFFSET: usize = 56; + const INDEX_OFFSET: usize = 64; + const SOURCE_OFFSET: usize = 72; + const SUM_LOW_OFFSET: usize = 80; + const SUM_HIGH_OFFSET: usize = 88; + const CURRENT_SCRIPT_BUFFER_OFFSET: usize = 96; + const CURRENT_SCRIPT_SIZE_OFFSET: usize = 240; + const OWNER_LOCK_BUFFER_OFFSET: usize = 192; + const OWNER_LOCK_SIZE_OFFSET: usize = 224; + const OWNER_INPUT_INDEX_OFFSET: usize = 232; + const OWNER_AUTHORIZED_OFFSET: usize = 248; + const OWNER_AUTHORITY_FIELD_OFFSET: usize = 256; + const LEGACY_OWNER_SCRIPT_SIZE: u64 = 85; + const TAGGED_TYPE_OWNER_SCRIPT_SIZE: u64 = 86; + const TAGGED_TYPE_AUTHORITY: u64 = 1; + + let frame_size = if owner_mode { 272usize } else { 112usize }; + let ra_offset = frame_size - 8; + + let conservation_start = self.fresh_label("fungible_group_conservation_start"); + let owner_script_loaded = self.fresh_label("fungible_group_owner_script_loaded"); + let owner_legacy_lock_mode = self.fresh_label("fungible_group_owner_legacy_lock_mode"); + let owner_tagged_type_mode = self.fresh_label("fungible_group_owner_tagged_type_mode"); + let owner_authority_mode_ready = self.fresh_label("fungible_group_owner_authority_mode_ready"); + let owner_scan_loop = self.fresh_label("fungible_group_owner_scan_loop"); + let owner_lock_loaded = self.fresh_label("fungible_group_owner_lock_loaded"); + let owner_not_matched = self.fresh_label("fungible_group_owner_not_matched"); + let owner_matched = self.fresh_label("fungible_group_owner_matched"); + let owner_expected_type_hash = self.fresh_label("fungible_group_owner_expected_type_hash"); + let owner_expected_hash_ready = self.fresh_label("fungible_group_owner_expected_hash_ready"); + let owner_authorized = self.fresh_label("fungible_group_owner_authorized"); + let owner_script_failed = self.fresh_label("fungible_group_owner_script_failed"); + let owner_script_malformed = self.fresh_label("fungible_group_owner_script_malformed"); + let owner_scan_failed = self.fresh_label("fungible_group_owner_scan_failed"); + let scan_source = self.fresh_label("xudt_group_scan_source"); + let scan_loop = self.fresh_label("xudt_group_scan_loop"); + let scan_done = self.fresh_label("xudt_group_scan_done"); + let scan_failed = self.fresh_label("xudt_group_scan_failed"); + let scan_malformed = self.fresh_label("xudt_group_scan_malformed"); + let overflow = self.fresh_label("xudt_group_sum_overflow"); + let output_phase = self.fresh_label("xudt_group_output_phase"); + let compare = self.fresh_label("xudt_group_compare"); + let mismatch = self.fresh_label("xudt_group_mismatch"); + let done = self.fresh_label("xudt_group_done"); + let abi = self.runtime_abi(); + + self.emit(format!("addi sp, sp, -{}", frame_size)); + self.emit(format!("sd ra, {}(sp)", ra_offset)); + for offset in [INPUT_LOW_OFFSET, INPUT_HIGH_OFFSET, OUTPUT_LOW_OFFSET, OUTPUT_HIGH_OFFSET] { + self.emit(format!("sd zero, {}(sp)", offset)); + } + + if owner_mode { + self.emit("# cellscript abi: authority args are legacy 32-byte lock hash or 0x01 plus 32-byte policy Type Script hash"); + self.emit("li t0, 96"); + self.emit(format!("sd t0, {}(sp)", CURRENT_SCRIPT_SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", CURRENT_SCRIPT_BUFFER_OFFSET)); + self.emit(format!("addi a1, sp, {}", CURRENT_SCRIPT_SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit(format!("li a7, {}", abi.load_script)); + self.emit("ecall"); + self.emit(format!("beqz a0, {}", owner_script_loaded)); + self.emit(format!("j {}", owner_script_failed)); + + self.emit_label(&owner_script_loaded); + self.emit(format!("ld t0, {}(sp)", CURRENT_SCRIPT_SIZE_OFFSET)); + self.emit(format!("li t1, {}", LEGACY_OWNER_SCRIPT_SIZE)); + self.emit("sub t2, t0, t1"); + self.emit(format!("beqz t2, {}", owner_legacy_lock_mode)); + self.emit(format!("li t1, {}", TAGGED_TYPE_OWNER_SCRIPT_SIZE)); + self.emit("sub t2, t0, t1"); + self.emit(format!("beqz t2, {}", owner_tagged_type_mode)); + self.emit(format!("j {}", owner_script_malformed)); + + self.emit_label(&owner_legacy_lock_mode); + for (offset, expected) in [(0usize, LEGACY_OWNER_SCRIPT_SIZE), (4, 16), (8, 48), (12, 49), (49, 32)] { + self.emit_stack_u32_le_to("t0", CURRENT_SCRIPT_BUFFER_OFFSET + offset); + self.emit(format!("li t1, {}", expected)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", owner_script_malformed)); + } + self.emit(format!("li t0, {}", CKB_CELL_FIELD_LOCK_HASH)); + self.emit(format!("sd t0, {}(sp)", OWNER_AUTHORITY_FIELD_OFFSET)); + self.emit(format!("j {}", owner_authority_mode_ready)); + + self.emit_label(&owner_tagged_type_mode); + for (offset, expected) in [(0usize, TAGGED_TYPE_OWNER_SCRIPT_SIZE), (4, 16), (8, 48), (12, 49), (49, 33)] { + self.emit_stack_u32_le_to("t0", CURRENT_SCRIPT_BUFFER_OFFSET + offset); + self.emit(format!("li t1, {}", expected)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", owner_script_malformed)); + } + self.emit(format!("lbu t0, {}(sp)", CURRENT_SCRIPT_BUFFER_OFFSET + 53)); + self.emit(format!("li t1, {}", TAGGED_TYPE_AUTHORITY)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", owner_script_malformed)); + self.emit(format!("li t0, {}", CKB_CELL_FIELD_TYPE_HASH)); + self.emit(format!("sd t0, {}(sp)", OWNER_AUTHORITY_FIELD_OFFSET)); + + self.emit_label(&owner_authority_mode_ready); + + self.emit("# cellscript abi: supply authority succeeds only when an absolute Input lock/type hash equals Script args"); + self.emit(format!("sd zero, {}(sp)", OWNER_INPUT_INDEX_OFFSET)); + self.emit(format!("sd zero, {}(sp)", OWNER_AUTHORIZED_OFFSET)); + self.emit_label(&owner_scan_loop); + self.emit("li t0, 32"); + self.emit(format!("sd t0, {}(sp)", OWNER_LOCK_SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", OWNER_LOCK_BUFFER_OFFSET)); + self.emit(format!("addi a1, sp, {}", OWNER_LOCK_SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit(format!("ld a3, {}(sp)", OWNER_INPUT_INDEX_OFFSET)); + self.emit(format!("li a4, {}", CKB_SOURCE_INPUT)); + self.emit(format!("ld a5, {}(sp)", OWNER_AUTHORITY_FIELD_OFFSET)); + self.emit(format!("li a7, {}", abi.load_cell_by_field)); + self.emit("ecall"); + self.emit(format!("beqz a0, {}", owner_lock_loaded)); + self.emit(format!("li t0, {}", CKB_INDEX_OUT_OF_BOUND)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", conservation_start)); + self.emit(format!("li t0, {}", CKB_ITEM_MISSING)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", owner_not_matched)); + self.emit(format!("j {}", owner_scan_failed)); + + self.emit_label(&owner_lock_loaded); + self.emit(format!("ld t0, {}(sp)", OWNER_LOCK_SIZE_OFFSET)); + self.emit("li t1, 32"); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", owner_scan_failed)); + self.emit(format!("addi a0, sp, {}", OWNER_LOCK_BUFFER_OFFSET)); + self.emit(format!("ld t0, {}(sp)", OWNER_AUTHORITY_FIELD_OFFSET)); + self.emit(format!("li t1, {}", CKB_CELL_FIELD_TYPE_HASH)); + self.emit("sub t2, t0, t1"); + self.emit(format!("beqz t2, {}", owner_expected_type_hash)); + self.emit(format!("addi a1, sp, {}", CURRENT_SCRIPT_BUFFER_OFFSET + 53)); + self.emit(format!("j {}", owner_expected_hash_ready)); + self.emit_label(&owner_expected_type_hash); + self.emit(format!("addi a1, sp, {}", CURRENT_SCRIPT_BUFFER_OFFSET + 54)); + self.emit_label(&owner_expected_hash_ready); + self.emit("li a2, 32"); + self.emit("call __cellscript_memcmp_fixed"); + self.emit(format!("beqz a0, {}", owner_matched)); + self.emit(format!("j {}", owner_not_matched)); + + self.emit_label(&owner_not_matched); + self.emit(format!("ld t0, {}(sp)", OWNER_INPUT_INDEX_OFFSET)); + self.emit("addi t0, t0, 1"); + self.emit(format!("sd t0, {}(sp)", OWNER_INPUT_INDEX_OFFSET)); + self.emit(format!("j {}", owner_scan_loop)); + + self.emit_label(&owner_matched); + self.emit("li t0, 1"); + self.emit(format!("sd t0, {}(sp)", OWNER_AUTHORIZED_OFFSET)); + self.emit(format!("j {}", conservation_start)); + + self.emit_label(&owner_script_failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&owner_script_malformed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ScriptFieldMalformed.code())); + self.emit(format!("j {}", done)); + self.emit_label(&owner_scan_failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::CkbSourceViewInvalid.code())); + self.emit(format!("j {}", done)); + + self.emit_label(&conservation_start); + } + + self.emit(format!("li t0, {}", CKB_SOURCE_GROUP_FLAG | CKB_SOURCE_INPUT)); + self.emit(format!("sd t0, {}(sp)", SOURCE_OFFSET)); + self.emit(format!("addi t0, sp, {}", INPUT_LOW_OFFSET)); + self.emit(format!("addi t1, sp, {}", INPUT_HIGH_OFFSET)); + self.emit(format!("j {}", scan_source)); + + self.emit_label(&output_phase); + self.emit(format!("li t0, {}", CKB_SOURCE_GROUP_FLAG | CKB_SOURCE_OUTPUT)); + self.emit(format!("sd t0, {}(sp)", SOURCE_OFFSET)); + self.emit(format!("addi t0, sp, {}", OUTPUT_LOW_OFFSET)); + self.emit(format!("addi t1, sp, {}", OUTPUT_HIGH_OFFSET)); + + self.emit_label(&scan_source); + self.emit(format!("sd t0, {}(sp)", SUM_LOW_OFFSET)); + self.emit(format!("sd t1, {}(sp)", SUM_HIGH_OFFSET)); + self.emit(format!("sd zero, {}(sp)", INDEX_OFFSET)); + + self.emit_label(&scan_loop); + self.emit("li t0, 16"); + self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", BUFFER_OFFSET)); + self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit(format!("ld a3, {}(sp)", INDEX_OFFSET)); + self.emit(format!("ld a4, {}(sp)", SOURCE_OFFSET)); + self.emit(format!("li a7, {}", abi.load_cell_data)); + self.emit("ecall"); + self.emit(format!("beqz a0, {}", scan_done)); + self.emit(format!("li t0, {}", CKB_INDEX_OUT_OF_BOUND)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", compare)); + self.emit(format!("j {}", scan_failed)); + + self.emit_label(&scan_done); + self.emit(format!("ld t0, {}(sp)", SIZE_OFFSET)); + self.emit("li t1, 16"); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", scan_malformed)); + self.emit(format!("ld t0, {}(sp)", SUM_LOW_OFFSET)); + self.emit(format!("ld t1, {}(sp)", SUM_HIGH_OFFSET)); + self.emit("ld t2, 16(sp)"); + self.emit("ld t3, 24(sp)"); + self.emit("ld t4, 0(t0)"); + self.emit("ld t5, 0(t1)"); + self.emit("add t6, t4, t2"); + self.emit("sltu t4, t6, t4"); + self.emit("add t5, t5, t3"); + self.emit("sltu t3, t5, t3"); + self.emit(format!("bnez t3, {}", overflow)); + self.emit("add t5, t5, t4"); + self.emit("sltu t4, t5, t4"); + self.emit(format!("bnez t4, {}", overflow)); + self.emit("sd t6, 0(t0)"); + self.emit("sd t5, 0(t1)"); + self.emit(format!("ld t0, {}(sp)", INDEX_OFFSET)); + self.emit("addi t0, t0, 1"); + self.emit(format!("sd t0, {}(sp)", INDEX_OFFSET)); + self.emit(format!("j {}", scan_loop)); + + self.emit_label(&compare); + let non_empty = self.fresh_label("fungible_group_non_empty"); + if owner_mode { + self.emit(format!("ld t4, {}(sp)", OWNER_AUTHORIZED_OFFSET)); + self.emit(format!("bnez t4, {}", non_empty)); + } + self.emit(format!("ld t3, {}(sp)", INDEX_OFFSET)); + self.emit(format!("beqz t3, {}", mismatch)); + self.emit_label(&non_empty); + self.emit(format!("ld t0, {}(sp)", SOURCE_OFFSET)); + self.emit(format!("li t1, {}", CKB_SOURCE_GROUP_FLAG | CKB_SOURCE_INPUT)); + self.emit("sub t2, t0, t1"); + self.emit(format!("beqz t2, {}", output_phase)); + if owner_mode { + self.emit(format!("ld t0, {}(sp)", OWNER_AUTHORIZED_OFFSET)); + self.emit(format!("bnez t0, {}", owner_authorized)); + } + self.emit(format!("ld t0, {}(sp)", INPUT_LOW_OFFSET)); + self.emit(format!("ld t1, {}(sp)", OUTPUT_LOW_OFFSET)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", mismatch)); + self.emit(format!("ld t0, {}(sp)", INPUT_HIGH_OFFSET)); + self.emit(format!("ld t1, {}(sp)", OUTPUT_HIGH_OFFSET)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", mismatch)); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + if owner_mode { + self.emit_label(&owner_authorized); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + } + + self.emit_label(&scan_failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::XudtBindingMismatch.code())); + self.emit(format!("j {}", done)); + self.emit_label(&scan_malformed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::XudtBindingMismatch.code())); + self.emit(format!("j {}", done)); + self.emit_label(&overflow); + self.emit(format!("li a0, {}", CellScriptRuntimeError::AggregateAmountMismatch.code())); + self.emit(format!("j {}", done)); + self.emit_label(&mismatch); + self.emit(format!("li a0, {}", CellScriptRuntimeError::AggregateAmountMismatch.code())); + self.emit_label(&done); + self.emit(format!("ld ra, {}(sp)", ra_offset)); + self.emit(format!("addi sp, sp, {}", frame_size)); + self.emit("ret"); + } + + fn emit_runtime_xudt_require_group_amount_delta_helper(&mut self, symbol: &str, minted: bool, enabled: bool) { + self.emit_global(symbol); + self.emit_label(symbol); + if minted { + self.emit( + "# cellscript abi: scans current xUDT type group and requires sum(outputs.amount) == sum(inputs.amount) + delta", + ); + } else { + self.emit( + "# cellscript abi: scans current xUDT type group and requires sum(inputs.amount) == sum(outputs.amount) + delta", + ); + } + self.emit("# cellscript abi: args a0=delta_u128_le_ptr"); + if !enabled { + self.emit(format!("li a0, {}", CellScriptRuntimeError::SyscallFailed.code())); + self.emit("ret"); + return; + } + + const SIZE_OFFSET: usize = 8; + const BUFFER_OFFSET: usize = 16; + const INPUT_LOW_OFFSET: usize = 32; + const INPUT_HIGH_OFFSET: usize = 40; + const OUTPUT_LOW_OFFSET: usize = 48; + const OUTPUT_HIGH_OFFSET: usize = 56; + const INDEX_OFFSET: usize = 64; + const SOURCE_OFFSET: usize = 72; + const SUM_LOW_OFFSET: usize = 80; + const SUM_HIGH_OFFSET: usize = 88; + const DELTA_PTR_OFFSET: usize = 96; + + let bad_delta = self.fresh_label("xudt_group_delta_bad"); + let scan_source = self.fresh_label("xudt_group_delta_scan_source"); + let scan_loop = self.fresh_label("xudt_group_delta_scan_loop"); + let scan_done = self.fresh_label("xudt_group_delta_scan_done"); + let scan_failed = self.fresh_label("xudt_group_delta_scan_failed"); + let scan_malformed = self.fresh_label("xudt_group_delta_scan_malformed"); + let overflow = self.fresh_label("xudt_group_delta_overflow"); + let output_phase = self.fresh_label("xudt_group_delta_output_phase"); + let compare = self.fresh_label("xudt_group_delta_compare"); + let mismatch = self.fresh_label("xudt_group_delta_mismatch"); + let done = self.fresh_label("xudt_group_delta_done"); + let abi = self.runtime_abi(); + + self.emit("addi sp, sp, -128"); + self.emit("sd ra, 120(sp)"); + self.emit(format!("beqz a0, {}", bad_delta)); + self.emit(format!("sd a0, {}(sp)", DELTA_PTR_OFFSET)); + for offset in [INPUT_LOW_OFFSET, INPUT_HIGH_OFFSET, OUTPUT_LOW_OFFSET, OUTPUT_HIGH_OFFSET] { + self.emit(format!("sd zero, {}(sp)", offset)); + } + + self.emit(format!("li t0, {}", CKB_SOURCE_GROUP_FLAG | CKB_SOURCE_INPUT)); + self.emit(format!("sd t0, {}(sp)", SOURCE_OFFSET)); + self.emit(format!("addi t0, sp, {}", INPUT_LOW_OFFSET)); + self.emit(format!("addi t1, sp, {}", INPUT_HIGH_OFFSET)); + self.emit(format!("j {}", scan_source)); + + self.emit_label(&output_phase); + self.emit(format!("li t0, {}", CKB_SOURCE_GROUP_FLAG | CKB_SOURCE_OUTPUT)); + self.emit(format!("sd t0, {}(sp)", SOURCE_OFFSET)); + self.emit(format!("addi t0, sp, {}", OUTPUT_LOW_OFFSET)); + self.emit(format!("addi t1, sp, {}", OUTPUT_HIGH_OFFSET)); + + self.emit_label(&scan_source); + self.emit(format!("sd t0, {}(sp)", SUM_LOW_OFFSET)); + self.emit(format!("sd t1, {}(sp)", SUM_HIGH_OFFSET)); + self.emit(format!("sd zero, {}(sp)", INDEX_OFFSET)); + + self.emit_label(&scan_loop); + self.emit("li t0, 16"); + self.emit(format!("sd t0, {}(sp)", SIZE_OFFSET)); + self.emit(format!("addi a0, sp, {}", BUFFER_OFFSET)); + self.emit(format!("addi a1, sp, {}", SIZE_OFFSET)); + self.emit("li a2, 0"); + self.emit(format!("ld a3, {}(sp)", INDEX_OFFSET)); + self.emit(format!("ld a4, {}(sp)", SOURCE_OFFSET)); + self.emit(format!("li a7, {}", abi.load_cell_data)); + self.emit("ecall"); + self.emit(format!("beqz a0, {}", scan_done)); + self.emit(format!("li t0, {}", CKB_INDEX_OUT_OF_BOUND)); + self.emit("sub t1, a0, t0"); + self.emit(format!("beqz t1, {}", compare)); + self.emit(format!("j {}", scan_failed)); + + self.emit_label(&scan_done); + self.emit(format!("ld t0, {}(sp)", SIZE_OFFSET)); + self.emit("li t1, 16"); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", scan_malformed)); + self.emit(format!("ld t0, {}(sp)", SUM_LOW_OFFSET)); + self.emit(format!("ld t1, {}(sp)", SUM_HIGH_OFFSET)); + self.emit("ld t2, 16(sp)"); + self.emit("ld t3, 24(sp)"); + self.emit("ld t4, 0(t0)"); + self.emit("ld t5, 0(t1)"); + self.emit("add t6, t4, t2"); + self.emit("sltu t4, t6, t4"); + self.emit("add t5, t5, t3"); + self.emit("sltu t3, t5, t3"); + self.emit(format!("bnez t3, {}", overflow)); + self.emit("add t5, t5, t4"); + self.emit("sltu t4, t5, t4"); + self.emit(format!("bnez t4, {}", overflow)); + self.emit("sd t6, 0(t0)"); + self.emit("sd t5, 0(t1)"); + self.emit(format!("ld t0, {}(sp)", INDEX_OFFSET)); + self.emit("addi t0, t0, 1"); + self.emit(format!("sd t0, {}(sp)", INDEX_OFFSET)); + self.emit(format!("j {}", scan_loop)); + + self.emit_label(&compare); + self.emit(format!("ld t0, {}(sp)", SOURCE_OFFSET)); + self.emit(format!("li t1, {}", CKB_SOURCE_GROUP_FLAG | CKB_SOURCE_INPUT)); + self.emit("sub t2, t0, t1"); + self.emit(format!("beqz t2, {}", output_phase)); + + self.emit(format!("ld a0, {}(sp)", DELTA_PTR_OFFSET)); + self.emit("ld t2, 0(a0)"); + self.emit("ld t3, 8(a0)"); + if minted { + self.emit(format!("ld t0, {}(sp)", INPUT_LOW_OFFSET)); + self.emit(format!("ld t1, {}(sp)", INPUT_HIGH_OFFSET)); + self.emit(format!("ld t4, {}(sp)", OUTPUT_LOW_OFFSET)); + self.emit(format!("ld t5, {}(sp)", OUTPUT_HIGH_OFFSET)); + } else { + self.emit(format!("ld t0, {}(sp)", OUTPUT_LOW_OFFSET)); + self.emit(format!("ld t1, {}(sp)", OUTPUT_HIGH_OFFSET)); + self.emit(format!("ld t4, {}(sp)", INPUT_LOW_OFFSET)); + self.emit(format!("ld t5, {}(sp)", INPUT_HIGH_OFFSET)); + } + self.emit("add t6, t0, t2"); + self.emit("sltu t0, t6, t0"); + self.emit("add t1, t1, t3"); + self.emit("sltu t3, t1, t3"); + self.emit(format!("bnez t3, {}", overflow)); + self.emit("add t1, t1, t0"); + self.emit("sltu t0, t1, t0"); + self.emit(format!("bnez t0, {}", overflow)); + self.emit("sub t0, t6, t4"); + self.emit(format!("bnez t0, {}", mismatch)); + self.emit("sub t0, t1, t5"); + self.emit(format!("bnez t0, {}", mismatch)); + self.emit("li a0, 0"); + self.emit(format!("j {}", done)); + + self.emit_label(&bad_delta); + self.emit(format!("li a0, {}", CellScriptRuntimeError::FixedByteComparisonUnresolved.code())); + self.emit(format!("j {}", done)); + self.emit_label(&scan_failed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::XudtBindingMismatch.code())); + self.emit(format!("j {}", done)); + self.emit_label(&scan_malformed); + self.emit(format!("li a0, {}", CellScriptRuntimeError::XudtBindingMismatch.code())); + self.emit(format!("j {}", done)); + self.emit_label(&overflow); + self.emit(format!("li a0, {}", CellScriptRuntimeError::AggregateAmountMismatch.code())); + self.emit(format!("j {}", done)); + self.emit_label(&mismatch); + self.emit(format!("li a0, {}", CellScriptRuntimeError::AggregateAmountMismatch.code())); + self.emit_label(&done); + self.emit("ld ra, 120(sp)"); + self.emit("addi sp, sp, 128"); + self.emit("ret"); + } + + fn emit_runtime_memcmp_fixed(&mut self) { + self.emit_global("__cellscript_memcmp_fixed"); + self.emit_label("__cellscript_memcmp_fixed"); + self.emit("# cellscript abi: fixed-byte helper compares a0/a1 for a2 bytes; returns a0=0 when equal"); + let loop_label = ".L__cellscript_memcmp_fixed_loop"; + let mismatch_label = ".L__cellscript_memcmp_fixed_mismatch"; + let equal_label = ".L__cellscript_memcmp_fixed_equal"; + self.emit(format!("beqz a2, {}", equal_label)); + self.emit_label(loop_label); + self.emit("lbu t0, 0(a0)"); + self.emit("lbu t1, 0(a1)"); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", mismatch_label)); + self.emit("addi a0, a0, 1"); + self.emit("addi a1, a1, 1"); + self.emit("addi a2, a2, -1"); + self.emit(format!("bnez a2, {}", loop_label)); + self.emit_label(equal_label); + self.emit("li a0, 0"); + self.emit("ret"); + self.emit_label(mismatch_label); + self.emit("li a0, 1"); + self.emit("ret"); + } + + fn emit_runtime_memzero_fixed(&mut self) { + self.emit_global("__cellscript_memzero_fixed"); + self.emit_label("__cellscript_memzero_fixed"); + self.emit("# cellscript abi: fixed-byte helper checks a0 for a1 zero bytes; returns a0=0 when all zero"); + let loop_label = ".L__cellscript_memzero_fixed_loop"; + let mismatch_label = ".L__cellscript_memzero_fixed_mismatch"; + let equal_label = ".L__cellscript_memzero_fixed_equal"; + self.emit(format!("beqz a1, {}", equal_label)); + self.emit_label(loop_label); + self.emit("lbu t0, 0(a0)"); + self.emit(format!("bnez t0, {}", mismatch_label)); + self.emit("addi a0, a0, 1"); + self.emit("addi a1, a1, -1"); + self.emit(format!("bnez a1, {}", loop_label)); + self.emit_label(equal_label); + self.emit("li a0, 0"); + self.emit("ret"); + self.emit_label(mismatch_label); + self.emit("li a0, 1"); + self.emit("ret"); + } + + fn emit_runtime_memcpy_fixed(&mut self) { + self.emit_global("__cellscript_memcpy_fixed"); + self.emit_label("__cellscript_memcpy_fixed"); + self.emit("# cellscript abi: fixed-byte helper copies a0 to a1 for a2 bytes; returns a0=0"); + let loop_label = ".L__cellscript_memcpy_fixed_loop"; + let done_label = ".L__cellscript_memcpy_fixed_done"; + self.emit(format!("beqz a2, {}", done_label)); + self.emit_label(loop_label); + self.emit("lbu t0, 0(a0)"); + self.emit("sb t0, 0(a1)"); + self.emit("addi a0, a0, 1"); + self.emit("addi a1, a1, 1"); + self.emit("addi a2, a2, -1"); + self.emit(format!("bnez a2, {}", loop_label)); + self.emit_label(done_label); + self.emit("li a0, 0"); + self.emit("ret"); + } + + fn emit_runtime_size_guards(&mut self) { + self.emit_global("__cellscript_require_min_size"); + self.emit_label("__cellscript_require_min_size"); + self.emit("# cellscript abi: returns a0=0 when actual size a0 is at least required size a1"); + self.emit("slt a0, a0, a1"); + self.emit("ret"); + + self.emit_global("__cellscript_require_exact_size"); + self.emit_label("__cellscript_require_exact_size"); + self.emit("# cellscript abi: returns a0=0 when actual size a0 equals expected size a1"); + self.emit("sub a0, a0, a1"); + self.emit("ret"); + } + + fn emit_runtime_header_field_u64(&mut self, symbol: &str, field_name: &str, field_id: u64, enabled: bool, disabled_reason: &str) { + self.emit_global(symbol); + self.emit_label(symbol); + if !enabled { + self.emit(format!("# cellscript abi: {}", disabled_reason)); + self.emit_runtime_error_comment(CellScriptRuntimeError::ConsumeInvalidOperand); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ConsumeInvalidOperand.code())); + self.emit("ret"); + return; + } + + let abi = self.runtime_abi(); + self.emit_large_addi("sp", "sp", -32); + self.emit_stack_store("ra", 24); + self.emit(format!("# cellscript abi: LOAD_HEADER_BY_FIELD field={} source=HeaderDep index=0", field_name)); + self.emit("li t0, 8"); + self.emit_stack_store("t0", 8); + self.emit_sp_addi("a0", 16); + self.emit_sp_addi("a1", 8); + self.emit("li a2, 0"); + self.emit("li a3, 0"); + self.emit(format!("li a4, {}", abi.source_header_dep)); + self.emit(format!("li a5, {}", field_id)); + self.emit(format!("li a7, {}", abi.load_header_by_field)); + self.emit("ecall"); + self.emit_stack_load("a0", 16); + self.emit_stack_load("ra", 24); + self.emit_large_addi("sp", "sp", 32); + self.emit("ret"); + } + + fn emit_runtime_input_field_u64(&mut self, symbol: &str, field_name: &str, field_id: u64, enabled: bool, disabled_reason: &str) { + self.emit_global(symbol); + self.emit_label(symbol); + if !enabled { + self.emit(format!("# cellscript abi: {}", disabled_reason)); + self.emit_runtime_error_comment(CellScriptRuntimeError::ConsumeInvalidOperand); + self.emit(format!("li a0, {}", CellScriptRuntimeError::ConsumeInvalidOperand.code())); + self.emit("ret"); + return; + } + + let abi = self.runtime_abi(); + self.emit_large_addi("sp", "sp", -32); + self.emit_stack_store("ra", 24); + self.emit(format!("# cellscript abi: LOAD_INPUT_BY_FIELD field={} source=GroupInput index=0", field_name)); + self.emit("li t0, 8"); + self.emit_stack_store("t0", 8); + self.emit_sp_addi("a0", 16); + self.emit_sp_addi("a1", 8); + self.emit("li a2, 0"); + self.emit("li a3, 0"); + self.emit(format!("li a4, {}", abi.source_group_input)); + self.emit(format!("li a5, {}", field_id)); + self.emit(format!("li a7, {}", abi.load_input_by_field)); + self.emit("ecall"); + self.emit_stack_load("a0", 16); + self.emit_stack_load("ra", 24); + self.emit_large_addi("sp", "sp", 32); + self.emit("ret"); + } +} + +pub fn generate(ir: &IrModule, options: &CodegenOptions, format: ArtifactFormat) -> Result> { + let generator = CodeGenerator::new(options.clone()); + generator.generate(ir, format) +} + +pub fn generate_with_evidence(ir: &IrModule, options: &CodegenOptions, format: ArtifactFormat) -> Result { + let generator = CodeGenerator::new(options.clone()); + generator.generate_with_evidence(ir, format) +} + +#[derive(Debug, Clone)] +pub struct GeneratedArtifact { + pub bytes: Vec, + pub machine_layout: Option, +} + +#[derive(Debug, Clone)] +pub struct MachineLayoutEvidence { + pub text_start: u64, + pub text_end: u64, + pub entry_label: String, + pub blocks: Vec, + pub edges: Vec, + pub symbols: BTreeMap, + pub globals: BTreeSet, + pub entry_frame_sizes: BTreeMap, +} + +#[derive(Debug, Clone)] +pub struct MachineBlockEvidence { + pub index: usize, + pub label: Option, + pub start: u64, + pub end: u64, + pub terminator: MachineTerminatorEvidence, + pub runtime_error_codes: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MachineTerminatorEvidence { + Fallthrough, + Jump, + ConditionalBranch, + Return, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MachineEdgeEvidence { + pub from: usize, + pub to: usize, + pub kind: MachineEdgeKindEvidence, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MachineEdgeKindEvidence { + Fallthrough, + Jump, + ConditionalTaken, + ConditionalFallthrough, + Call, +} diff --git a/src/codegen/schema.rs b/src/codegen/schema.rs new file mode 100644 index 00000000..64d8a117 --- /dev/null +++ b/src/codegen/schema.rs @@ -0,0 +1,970 @@ +use super::*; + +impl CodeGenerator { + pub(super) fn emit_loaded_schema_bounds_check(&mut self, size_offset: usize, required_size: usize, context: &str) { + self.emit(format!("# cellscript abi: bounds check {} required={}", context, required_size)); + let ok_label = self.fresh_label("schema_bounds_ok"); + self.emit_stack_load("a0", size_offset); + self.emit(format!("li a1, {}", required_size)); + self.emit("call __cellscript_require_min_size"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); + self.emit_label(&ok_label); + } + + pub(super) fn emit_loaded_schema_exact_size_check(&mut self, size_offset: usize, expected_size: usize, context: &str) { + self.emit(format!("# cellscript abi: exact size check {} expected={}", context, expected_size)); + let ok_label = self.fresh_label("schema_size_ok"); + self.emit_stack_load("a0", size_offset); + self.emit(format!("li a1, {}", expected_size)); + self.emit("call __cellscript_require_exact_size"); + self.emit(format!("beqz a0, {}", ok_label)); + self.emit_fail(CellScriptRuntimeError::ExactSizeMismatch); + self.emit_label(&ok_label); + } + + pub(super) fn emit_molecule_table_field_bounds_to_t5( + &mut self, + base_reg: &str, + size_offset: usize, + field_index: usize, + field_width: usize, + context: &str, + ) { + self.emit(format!("# cellscript abi: molecule table field {} index={} min_width={}", context, field_index, field_width)); + let field_count = field_index + 1; + let header_size = 4 + 4 * field_count; + self.emit_loaded_schema_bounds_check(size_offset, header_size, context); + + self.emit_stack_load("a0", size_offset); + let total_ok = self.fresh_label("molecule_table_total_ok"); + self.emit_unaligned_scalar_load(base_reg, "t0", "t2", 0, 4); + self.emit("sub t2, t0, a0"); + self.emit(format!("beqz t2, {}", total_ok)); + self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); + self.emit_label(&total_ok); + + self.emit_unaligned_scalar_load(base_reg, "t5", "t2", 4 + 4 * field_index, 4); + self.emit(format!("li t1, {}", header_size)); + self.emit("sltu t2, t5, t1"); + let start_ok = self.fresh_label("molecule_table_start_ok"); + self.emit(format!("beqz t2, {}", start_ok)); + self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); + self.emit_label(&start_ok); + + if field_width > 0 { + self.emit(format!("li t1, {}", field_width)); + self.emit("add t3, t5, t1"); + self.emit("sltu t2, t3, t5"); + let overflow_ok = self.fresh_label("molecule_table_field_overflow_ok"); + self.emit(format!("beqz t2, {}", overflow_ok)); + self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); + self.emit_label(&overflow_ok); + self.emit("sltu t2, a0, t3"); + let end_ok = self.fresh_label("molecule_table_end_ok"); + self.emit(format!("beqz t2, {}", end_ok)); + self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); + self.emit_label(&end_ok); + } + } + + pub(super) fn emit_molecule_table_field_span_to_t5_t6( + &mut self, + base_reg: &str, + size_offset: usize, + field_index: usize, + field_count: usize, + context: &str, + ) { + self.emit(format!( + "# cellscript abi: molecule table dynamic field {} index={} field_count={}", + context, field_index, field_count + )); + let header_size = 4 + 4 * field_count; + self.emit_loaded_schema_bounds_check(size_offset, header_size, context); + + self.emit_stack_load("a0", size_offset); + let total_ok = self.fresh_label("molecule_table_total_ok"); + self.emit_unaligned_scalar_load(base_reg, "t0", "t2", 0, 4); + self.emit("sub t2, t0, a0"); + self.emit(format!("beqz t2, {}", total_ok)); + self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); + self.emit_label(&total_ok); + + self.emit_unaligned_scalar_load(base_reg, "t5", "t2", 4 + 4 * field_index, 4); + if field_index + 1 < field_count { + self.emit_unaligned_scalar_load(base_reg, "t6", "t2", 4 + 4 * (field_index + 1), 4); + } else { + self.emit("add t6, a0, zero"); + } + + self.emit(format!("li t1, {}", header_size)); + self.emit("sltu t2, t5, t1"); + let start_ok = self.fresh_label("molecule_table_start_ok"); + self.emit(format!("beqz t2, {}", start_ok)); + self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); + self.emit_label(&start_ok); + + self.emit("sltu t2, t6, t5"); + let order_ok = self.fresh_label("molecule_table_order_ok"); + self.emit(format!("beqz t2, {}", order_ok)); + self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); + self.emit_label(&order_ok); + + self.emit("sltu t2, a0, t6"); + let end_ok = self.fresh_label("molecule_table_end_ok"); + self.emit(format!("beqz t2, {}", end_ok)); + self.emit_fail(CellScriptRuntimeError::BoundsCheckFailed); + self.emit_label(&end_ok); + } + + pub(super) fn emit_loaded_field_equals_expected( + &mut self, + size_offset: usize, + buffer_offset: usize, + layout: &SchemaFieldLayout, + expected: &IrOperand, + context: &str, + ) { + let Some(width) = layout_fixed_scalar_width(layout) else { + return; + }; + self.emit_loaded_schema_bounds_check(size_offset, layout.offset + width, context); + self.emit(format!("# cellscript abi: verify output field {} offset={} size={}", context, layout.offset, width)); + self.emit_sp_addi("t4", buffer_offset); + self.emit_unaligned_scalar_load("t4", "t0", "t2", layout.offset, width); + let actual_value_offset = self.runtime_expr_temp_offset(RUNTIME_EXPR_TEMP_SLOTS - 1); + self.emit("# cellscript abi: preserve output scalar before expected expression"); + self.emit_stack_store("t0", actual_value_offset); + self.emit_expected_operand_to_t1(expected); + self.emit_stack_load("t0", actual_value_offset); + self.emit("sub t2, t0, t1"); + let ok_label = self.fresh_label("output_field_ok"); + self.emit(format!("beqz t2, {}", ok_label)); + self.emit_fail(CellScriptRuntimeError::CellLoadFailed); + self.emit_label(&ok_label); + } + + pub(super) fn emit_loaded_fixed_bytes_against_source( + &mut self, + output_buffer_offset: usize, + output_field_offset: usize, + source: &ExpectedFixedByteSource, + width: usize, + fail_code: CellScriptRuntimeError, + ) { + let mismatch_label = self.fresh_label("fixed_byte_mismatch"); + self.emit_sp_addi("t4", output_buffer_offset); + match source { + ExpectedFixedByteSource::SchemaField(source) => { + if self.emit_schema_field_source_pointer_to("a1", source, width) { + self.emit_sp_addi("a0", output_buffer_offset + output_field_offset); + self.emit(format!("li a2, {}", width)); + self.emit("call __cellscript_memcmp_fixed"); + self.emit(format!("bnez a0, {}", mismatch_label)); + } else { + self.emit("# cellscript abi: fail closed because schema field byte source is not addressable"); + self.emit_fail(CellScriptRuntimeError::DynamicFieldBoundsInvalid); + } + } + ExpectedFixedByteSource::Const(bytes) => { + if width >= 8 && bytes.iter().take(width).all(|byte| *byte == 0) { + self.emit_sp_addi("a0", output_buffer_offset + output_field_offset); + self.emit(format!("li a1, {}", width)); + self.emit("call __cellscript_memzero_fixed"); + self.emit(format!("bnez a0, {}", mismatch_label)); + } else { + for (byte_index, byte) in bytes.iter().take(width).enumerate() { + self.emit(format!("lbu t0, {}(t4)", output_field_offset + byte_index)); + self.emit(format!("li t1, {}", byte)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", mismatch_label)); + } + } + } + ExpectedFixedByteSource::StackSlot { var_id, .. } => { + self.emit_loaded_fixed_bytes_helper_call( + output_buffer_offset, + output_field_offset, + SourcePointer::StackAddress { offset: var_id * 8 }, + width, + &mismatch_label, + ); + } + ExpectedFixedByteSource::PointerBytes { var_id, .. } + | ExpectedFixedByteSource::ParamBytes { var_id, .. } + | ExpectedFixedByteSource::LoadedBytes { var_id, .. } => { + self.emit_loaded_fixed_bytes_helper_call( + output_buffer_offset, + output_field_offset, + SourcePointer::LoadedStackPointer { var_id: *var_id, offset: 0 }, + width, + &mismatch_label, + ); + } + } + self.emit_fixed_byte_mismatch_fail(&mismatch_label, fail_code); + } + + pub(super) fn emit_loaded_fixed_bytes_helper_call( + &mut self, + output_buffer_offset: usize, + output_field_offset: usize, + source: SourcePointer, + width: usize, + mismatch_label: &str, + ) { + self.emit_sp_addi("a0", output_buffer_offset + output_field_offset); + match source { + SourcePointer::LoadedStackPointer { var_id, offset } => { + self.emit_stack_load("a1", var_id * 8); + if offset != 0 { + self.emit_large_addi("a1", "a1", offset as i64); + } + } + SourcePointer::StackAddress { offset } => { + self.emit_sp_addi("a1", offset); + } + } + self.emit(format!("li a2, {}", width)); + self.emit("call __cellscript_memcmp_fixed"); + self.emit(format!("bnez a0, {}", mismatch_label)); + } + + pub(super) fn emit_loaded_field_bytes_equals_expected( + &mut self, + size_offset: usize, + buffer_offset: usize, + layout: &SchemaFieldLayout, + expected: &IrOperand, + context: &str, + ) -> bool { + if layout_fixed_scalar_width(layout).is_some() { + self.emit_loaded_field_equals_expected(size_offset, buffer_offset, layout, expected, context); + return true; + } + let Some(width) = layout_fixed_byte_width(layout).or_else(|| self.fixed_named_type_width(&layout.ty)) else { + return false; + }; + let Some(source) = self.expected_fixed_byte_source(expected, width) else { + return false; + }; + self.emit_loaded_schema_bounds_check(size_offset, layout.offset + width, context); + match source { + ExpectedFixedByteSource::SchemaField(source) => { + if let Some(source_size_offset) = self.schema_pointer_size_offsets.get(&source.obj_var_id).copied() { + if let Some(expected_size) = self.type_fixed_sizes.get(&source.type_name).copied() { + self.emit_loaded_schema_exact_size_check(source_size_offset, expected_size, &source.type_name); + } + self.emit_loaded_schema_bounds_check( + source_size_offset, + source.layout.offset + width, + &format!("{}.{}", source.type_name, source.field), + ); + } + self.emit(format!("# cellscript abi: verify output bytes field {} offset={} size={}", context, layout.offset, width)); + self.emit(format!( + "# cellscript abi: expected bytes field {}.{} offset={} size={}", + source.type_name, source.field, source.layout.offset, width + )); + self.emit_loaded_fixed_bytes_against_source( + buffer_offset, + layout.offset, + &ExpectedFixedByteSource::SchemaField(source), + width, + CellScriptRuntimeError::CellLoadFailed, + ); + } + ExpectedFixedByteSource::Const(bytes) => { + self.emit(format!( + "# cellscript abi: verify output bytes field {} offset={} size={} against const", + context, layout.offset, width + )); + self.emit_loaded_fixed_bytes_against_source( + buffer_offset, + layout.offset, + &ExpectedFixedByteSource::Const(bytes), + width, + CellScriptRuntimeError::CellLoadFailed, + ); + } + ExpectedFixedByteSource::StackSlot { var_id, width } => { + self.emit(format!( + "# cellscript abi: verify output bytes field {} offset={} size={} against stack slot var{}", + context, layout.offset, width, var_id + )); + self.emit_loaded_fixed_bytes_against_source( + buffer_offset, + layout.offset, + &ExpectedFixedByteSource::StackSlot { var_id, width }, + width, + CellScriptRuntimeError::CellLoadFailed, + ); + } + ExpectedFixedByteSource::PointerBytes { var_id, width } => { + self.emit(format!( + "# cellscript abi: verify output bytes field {} offset={} size={} against pointer var{}", + context, layout.offset, width, var_id + )); + self.emit_loaded_fixed_bytes_against_source( + buffer_offset, + layout.offset, + &ExpectedFixedByteSource::PointerBytes { var_id, width }, + width, + CellScriptRuntimeError::CellLoadFailed, + ); + } + ExpectedFixedByteSource::ParamBytes { var_id, size_offset, width } => { + self.emit_loaded_schema_exact_size_check(size_offset, width, &format!("param var{}", var_id)); + self.emit(format!( + "# cellscript abi: verify output bytes field {} offset={} size={} against fixed-byte param var{}", + context, layout.offset, width, var_id + )); + self.emit_loaded_fixed_bytes_against_source( + buffer_offset, + layout.offset, + &ExpectedFixedByteSource::ParamBytes { var_id, size_offset, width }, + width, + CellScriptRuntimeError::CellLoadFailed, + ); + } + ExpectedFixedByteSource::LoadedBytes { var_id, size_offset, width } => { + self.emit_loaded_schema_exact_size_check(size_offset, width, &format!("loaded bytes var{}", var_id)); + self.emit(format!( + "# cellscript abi: verify output bytes field {} offset={} size={} against loaded bytes var{}", + context, layout.offset, width, var_id + )); + self.emit_loaded_fixed_bytes_against_source( + buffer_offset, + layout.offset, + &ExpectedFixedByteSource::LoadedBytes { var_id, size_offset, width }, + width, + CellScriptRuntimeError::CellLoadFailed, + ); + } + } + true + } + + pub(super) fn emit_prepare_fixed_byte_source(&mut self, source: &ExpectedFixedByteSource, width: usize, context: &str) { + match source { + ExpectedFixedByteSource::SchemaField(source) => { + self.emit_prepare_schema_field_source(source, width); + } + ExpectedFixedByteSource::ParamBytes { var_id, size_offset, width } => { + self.emit_loaded_schema_exact_size_check(*size_offset, *width, &format!("{} param var{}", context, var_id)); + } + ExpectedFixedByteSource::LoadedBytes { var_id, size_offset, width } => { + self.emit_loaded_schema_exact_size_check(*size_offset, *width, &format!("{} loaded bytes var{}", context, var_id)); + } + ExpectedFixedByteSource::Const(_) + | ExpectedFixedByteSource::StackSlot { .. } + | ExpectedFixedByteSource::PointerBytes { .. } => {} + } + } + + pub(super) fn emit_fixed_byte_source_byte_to( + &mut self, + dest_reg: &str, + base_reg: &str, + source: &ExpectedFixedByteSource, + byte_index: usize, + ) { + match source { + ExpectedFixedByteSource::SchemaField(source) => { + if self.emit_schema_field_source_pointer_to(base_reg, source, byte_index + 1) { + self.emit(format!("lbu {}, {}({})", dest_reg, byte_index, base_reg)); + } else { + self.emit("# cellscript abi: fail closed because schema field byte source is not addressable"); + self.emit_fail(CellScriptRuntimeError::DynamicFieldBoundsInvalid); + } + } + ExpectedFixedByteSource::Const(bytes) => { + self.emit(format!("li {}, {}", dest_reg, bytes[byte_index])); + } + ExpectedFixedByteSource::StackSlot { var_id, .. } => { + self.emit_sp_addi(base_reg, var_id * 8); + self.emit(format!("lbu {}, {}({})", dest_reg, byte_index, base_reg)); + } + ExpectedFixedByteSource::PointerBytes { var_id, .. } + | ExpectedFixedByteSource::ParamBytes { var_id, .. } + | ExpectedFixedByteSource::LoadedBytes { var_id, .. } => { + self.emit_stack_load(base_reg, var_id * 8); + self.emit(format!("lbu {}, {}({})", dest_reg, byte_index, base_reg)); + } + } + } + + pub(super) fn emit_fixed_byte_source_pointer_to(&mut self, dest_reg: &str, source: &ExpectedFixedByteSource) -> bool { + match source { + ExpectedFixedByteSource::SchemaField(source) => { + let Some(width) = layout_fixed_byte_width(&source.layout).or_else(|| self.fixed_named_type_width(&source.layout.ty)) + else { + return false; + }; + self.emit_schema_field_source_pointer_to(dest_reg, source, width) + } + ExpectedFixedByteSource::StackSlot { var_id, .. } => { + self.emit_sp_addi(dest_reg, var_id * 8); + true + } + ExpectedFixedByteSource::PointerBytes { var_id, .. } + | ExpectedFixedByteSource::ParamBytes { var_id, .. } + | ExpectedFixedByteSource::LoadedBytes { var_id, .. } => { + self.emit_stack_load(dest_reg, var_id * 8); + true + } + ExpectedFixedByteSource::Const(_) => false, + } + } + + pub(super) fn emit_fixed_byte_source_pointer_or_const_to(&mut self, dest_reg: &str, source: &ExpectedFixedByteSource) -> bool { + if let ExpectedFixedByteSource::Const(bytes) = source { + let label = self.const_data_label_for_bytes(bytes.clone()); + self.emit(format!("la {}, {}", dest_reg, label)); + true + } else { + self.emit_fixed_byte_source_pointer_to(dest_reg, source) + } + } + + pub(super) fn emit_fixed_byte_mismatch_fail(&mut self, mismatch_label: &str, fail_code: CellScriptRuntimeError) { + let done_label = self.fresh_label("fixed_byte_verify_done"); + self.emit(format!("j {}", done_label)); + self.emit_label(mismatch_label); + self.emit_fail(fail_code); + self.emit_label(&done_label); + } + + pub(super) fn emit_fixed_byte_comparison(&mut self, dest: &IrVar, op: BinaryOp, left: &IrOperand, right: &IrOperand) -> bool { + let Some(width) = operand_fixed_byte_width(left) else { + return false; + }; + if operand_fixed_byte_width(right) != Some(width) { + return false; + } + let Some(left_source) = self.expected_fixed_byte_source(left, width) else { + return false; + }; + let Some(right_source) = self.expected_fixed_byte_source(right, width) else { + return false; + }; + self.emit(format!("# cellscript abi: fixed-byte {:?} comparison size={}", op, width)); + self.emit_prepare_fixed_byte_source(&left_source, width, "left fixed-byte comparison"); + self.emit_prepare_fixed_byte_source(&right_source, width, "right fixed-byte comparison"); + if width >= 8 && self.emit_fixed_byte_comparison_helper(dest, op, &left_source, &right_source, width) { + return true; + } + let mismatch_label = self.fresh_label("fixed_byte_mismatch"); + let done_label = self.fresh_label("fixed_byte_done"); + for byte_index in 0..width { + self.emit_fixed_byte_source_byte_to("t0", "t4", &left_source, byte_index); + self.emit_fixed_byte_source_byte_to("t1", "t5", &right_source, byte_index); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", mismatch_label)); + } + let equal_value = if matches!(op, BinaryOp::Eq) { 1 } else { 0 }; + let mismatch_value = if matches!(op, BinaryOp::Eq) { 0 } else { 1 }; + self.emit(format!("li t3, {}", equal_value)); + self.emit(format!("j {}", done_label)); + self.emit_label(&mismatch_label); + self.emit(format!("li t3, {}", mismatch_value)); + self.emit_label(&done_label); + self.emit_stack_store("t3", dest.id * 8); + true + } + + pub(super) fn emit_fixed_byte_comparison_helper( + &mut self, + dest: &IrVar, + op: BinaryOp, + left_source: &ExpectedFixedByteSource, + right_source: &ExpectedFixedByteSource, + width: usize, + ) -> bool { + match (left_source, right_source) { + (ExpectedFixedByteSource::Const(bytes), source) if bytes.iter().take(width).all(|byte| *byte == 0) => { + if !self.emit_fixed_byte_source_pointer_to("a0", source) { + return false; + } + self.emit(format!("li a1, {}", width)); + self.emit("call __cellscript_memzero_fixed"); + } + (source, ExpectedFixedByteSource::Const(bytes)) if bytes.iter().take(width).all(|byte| *byte == 0) => { + if !self.emit_fixed_byte_source_pointer_to("a0", source) { + return false; + } + self.emit(format!("li a1, {}", width)); + self.emit("call __cellscript_memzero_fixed"); + } + (ExpectedFixedByteSource::Const(_), _) | (_, ExpectedFixedByteSource::Const(_)) => return false, + _ => { + if !self.emit_fixed_byte_source_pointer_to("a0", left_source) { + return false; + } + let Some(left_pointer_offset) = self.checked_runtime_expr_temp_offset(0) else { + return false; + }; + self.emit_stack_store("a0", left_pointer_offset); + if !self.emit_fixed_byte_source_pointer_to("a1", right_source) { + return false; + } + self.emit_stack_load("a0", left_pointer_offset); + self.emit(format!("li a2, {}", width)); + self.emit("call __cellscript_memcmp_fixed"); + } + } + if matches!(op, BinaryOp::Eq) { + self.emit("seqz t3, a0"); + } else { + self.emit("snez t3, a0"); + } + self.emit_stack_store("t3", dest.id * 8); + true + } + + pub(super) fn expected_fixed_byte_source(&self, operand: &IrOperand, expected_width: usize) -> Option { + match operand { + IrOperand::Const(value) => { + let bytes = fixed_byte_const_bytes(value).or_else(|| { + fixed_scalar_const_value(value) + .and_then(|value| (expected_width <= 8).then(|| value.to_le_bytes()[..expected_width].to_vec())) + })?; + (bytes.len() == expected_width).then_some(ExpectedFixedByteSource::Const(bytes)) + } + IrOperand::Var(var) + if self.fixed_byte_like_width(&var.ty).or_else(|| fixed_aggregate_pointer_param_width(&var.ty)).is_some() => + { + let var_width = self.fixed_byte_like_width(&var.ty).or_else(|| fixed_aggregate_pointer_param_width(&var.ty))?; + if let Some(source) = self.schema_field_value_sources.get(&var.id).cloned() { + let source_width = + layout_fixed_byte_width(&source.layout).or_else(|| self.fixed_named_type_width(&source.layout.ty))?; + if source_width == expected_width { + return Some(ExpectedFixedByteSource::SchemaField(source)); + } + } + if let Some(bytes) = self.prelude_fixed_byte_constants.get(&var.id).cloned() + && bytes.len() == expected_width + { + return Some(ExpectedFixedByteSource::Const(bytes)); + } + if self.schema_pointer_vars.contains(&var.id) && var_width == expected_width { + if let Some(size_offset) = self.schema_pointer_size_offsets.get(&var.id).copied() { + return Some(ExpectedFixedByteSource::LoadedBytes { var_id: var.id, size_offset, width: expected_width }); + } + return Some(ExpectedFixedByteSource::PointerBytes { var_id: var.id, width: expected_width }); + } + if let Some(size_offset) = self.cell_buffer_size_offsets.get(&var.id).copied() + && var_width == expected_width + { + return Some(ExpectedFixedByteSource::LoadedBytes { var_id: var.id, size_offset, width: expected_width }); + } + if self.fixed_byte_local_offsets.contains_key(&var.id) && var_width == expected_width { + return Some(ExpectedFixedByteSource::PointerBytes { var_id: var.id, width: expected_width }); + } + if expected_width <= 8 + && (fixed_scalar_width(&var.ty, type_static_length(&var.ty)).is_some() + || (var_width == expected_width && fixed_byte_width(&var.ty, type_static_length(&var.ty)).is_some())) + && expected_width <= var_width + { + return Some(ExpectedFixedByteSource::StackSlot { var_id: var.id, width: expected_width }); + } + if self.u128_value_offsets.contains_key(&var.id) + && !self.fixed_byte_param_size_offsets.contains_key(&var.id) + && var_width == expected_width + { + return Some(ExpectedFixedByteSource::PointerBytes { var_id: var.id, width: expected_width }); + } + if self.aggregate_pointer_sources.contains_key(&var.id) && var_width == expected_width { + return Some(ExpectedFixedByteSource::PointerBytes { var_id: var.id, width: expected_width }); + } + if self.param_vars.contains(&var.id) + && var_width == expected_width + && let Some(size_offset) = self.fixed_byte_param_size_offsets.get(&var.id).copied() + { + return Some(ExpectedFixedByteSource::ParamBytes { var_id: var.id, size_offset, width: expected_width }); + } + if let Some(param_id) = self.param_type_hash_sources.get(&var.id).copied() + && var_width == expected_width + && let Some(size_offset) = self.param_type_hash_size_offsets.get(¶m_id).copied() + { + return Some(ExpectedFixedByteSource::LoadedBytes { var_id: var.id, size_offset, width: expected_width }); + } + None + } + _ => None, + } + } + + /// Generic fixed-byte comparison: when `emit_fixed_byte_comparison` can't determine + /// the source of bytes, this method loads pointers from stack slots and performs + /// a byte-by-byte comparison. Works for Var operands whose stack slots contain + /// pointers to the fixed-byte data. + pub(super) fn emit_generic_fixed_byte_comparison( + &mut self, + dest: &IrVar, + op: BinaryOp, + left: &IrOperand, + right: &IrOperand, + ) -> bool { + let left_width = operand_fixed_byte_width(left); + let right_width = operand_fixed_byte_width(right); + + // Need at least one Var operand with known width for this to work + let width = match (left_width, right_width) { + (Some(w), Some(r)) if w == r => w, + (Some(w), None) | (None, Some(w)) => w, + _ => return false, + }; + + if width == 0 { + return false; + } + + // We need at least one Var operand + let left_var = match left { + IrOperand::Var(v) => Some(v), + _ => None, + }; + let right_var = match right { + IrOperand::Var(v) => Some(v), + _ => None, + }; + if left_var.is_none() && right_var.is_none() { + return false; + } + + self.emit(format!("# cellscript abi: generic fixed-byte {:?} comparison size={}", op, width)); + + // Load left pointer to t4 + if let Some(v) = left_var { + self.emit_stack_load("t4", v.id * 8); + } else { + // Left is a constant – store it to scratch buffer and point t4 there + let size_offset = self.runtime_scratch_size_offset(); + let buffer_offset = self.runtime_scratch_buffer_offset(); + self.emit_store_fixed_byte_const_to_scratch(left, size_offset, buffer_offset, width); + self.emit_sp_addi("t4", buffer_offset); + } + + // Load right pointer to t5 + if let Some(v) = right_var { + self.emit_stack_load("t5", v.id * 8); + } else { + let size_offset = self.runtime_scratch2_size_offset(); + let buffer_offset = self.runtime_scratch2_buffer_offset(); + self.emit_store_fixed_byte_const_to_scratch(right, size_offset, buffer_offset, width); + self.emit_sp_addi("t5", buffer_offset); + } + + let mismatch_label = self.fresh_label("gen_fb_mismatch"); + let done_label = self.fresh_label("gen_fb_done"); + for byte_index in 0..width { + self.emit(format!("lbu t0, {}(t4)", byte_index)); + self.emit(format!("lbu t1, {}(t5)", byte_index)); + self.emit("sub t2, t0, t1"); + self.emit(format!("bnez t2, {}", mismatch_label)); + } + let equal_value = if matches!(op, BinaryOp::Eq) { 1 } else { 0 }; + let mismatch_value = if matches!(op, BinaryOp::Eq) { 0 } else { 1 }; + self.emit(format!("li t3, {}", equal_value)); + self.emit(format!("j {}", done_label)); + self.emit_label(&mismatch_label); + self.emit(format!("li t3, {}", mismatch_value)); + self.emit_label(&done_label); + self.emit_stack_store("t3", dest.id * 8); + true + } + + /// Store fixed-byte constant value to scratch buffer area. + pub(super) fn emit_store_fixed_byte_const_to_scratch( + &mut self, + operand: &IrOperand, + size_offset: usize, + buffer_offset: usize, + width: usize, + ) { + match operand { + IrOperand::Const(IrConst::Address(bytes)) | IrOperand::Const(IrConst::Hash(bytes)) => { + self.emit(format!("# cellscript abi: store fixed-byte const size={}", width)); + self.emit(format!("li t0, {}", width)); + self.emit_stack_store("t0", size_offset); + for (i, byte) in bytes.iter().enumerate() { + self.emit(format!("li t0, {}", byte)); + if buffer_offset + i <= 2047 { + self.emit_stack_store_byte("t0", buffer_offset + i); + } else { + self.emit(format!("li t6, {}", buffer_offset + i)); + self.emit("add t6, sp, t6"); + self.emit("sb t0, 0(t6)"); + } + } + } + IrOperand::Const(IrConst::U128(value)) => { + self.emit(format!("# cellscript abi: store u128 const size={}", width)); + self.emit(format!("li t0, {}", width)); + self.emit_stack_store("t0", size_offset); + for (i, byte) in value.to_le_bytes().iter().enumerate() { + self.emit(format!("li t0, {}", byte)); + self.emit_stack_store_byte("t0", buffer_offset + i); + } + } + IrOperand::Const(IrConst::Array(values)) => { + self.emit(format!("# cellscript abi: store fixed-byte array const size={}", width)); + self.emit(format!("li t0, {}", width)); + self.emit_stack_store("t0", size_offset); + for (i, value) in values.iter().enumerate() { + if let IrConst::U8(byte) = value { + self.emit(format!("li t0, {}", byte)); + if buffer_offset + i <= 2047 { + self.emit_stack_store_byte("t0", buffer_offset + i); + } else { + self.emit(format!("li t6, {}", buffer_offset + i)); + self.emit("add t6, sp, t6"); + self.emit("sb t0, 0(t6)"); + } + } + } + } + _ => { + self.emit("# cellscript abi: cannot store unknown const type to scratch".to_string()); + } + } + } + + pub(super) fn operand_is_u128(&self, operand: &IrOperand) -> bool { + match operand { + IrOperand::Const(IrConst::U128(_)) => true, + IrOperand::Var(var) => var.ty == IrType::U128, + _ => false, + } + } + + pub(super) fn emit_u128_add_sub_with_u64(&mut self, dest: &IrVar, op: BinaryOp, left: &IrOperand, right: &IrOperand) -> bool { + if dest.ty != IrType::U128 || !matches!(op, BinaryOp::Add | BinaryOp::Sub) { + return false; + } + let Some(dest_offset) = self.fixed_byte_local_offsets.get(&dest.id).copied() else { + return false; + }; + + let (wide_operand, delta_operand) = match op { + BinaryOp::Add if self.operand_is_u128(left) => (left, right), + BinaryOp::Add if self.operand_is_u128(right) => (right, left), + BinaryOp::Sub if self.operand_is_u128(left) => (left, right), + _ => return false, + }; + if self.expected_fixed_byte_source(wide_operand, 16).is_none() { + return false; + } + let Some(delta) = self.prelude_u64_operand_source(delta_operand) else { + return false; + }; + + match op { + BinaryOp::Add => self.emit("# cellscript abi: u128 add with carry"), + BinaryOp::Sub => self.emit("# cellscript abi: u128 sub with borrow"), + _ => unreachable!("guarded u128 binary op"), + } + if !self.emit_u128_operand_limbs("t0", "t3", "t2", "t4", wide_operand, "u128 arithmetic wide operand") { + return true; + } + let wide_low_offset = self.runtime_expr_temp_offset(0); + let wide_high_offset = self.runtime_expr_temp_offset(1); + self.emit_stack_store("t0", wide_low_offset); + self.emit_stack_store("t3", wide_high_offset); + self.emit_prelude_u64_operand_source_to_t1(&delta); + self.emit_stack_load("t0", wide_low_offset); + self.emit_stack_load("t3", wide_high_offset); + let overflow_label = self.fresh_label("u128_arithmetic_overflow"); + let done_label = self.fresh_label("u128_arithmetic_done"); + match op { + BinaryOp::Add => { + self.emit("add t5, t0, t1"); + self.emit("sltu t2, t5, t0"); + self.emit("add t6, t3, t2"); + self.emit("sltu a6, t6, t3"); + self.emit(format!("bnez a6, {}", overflow_label)); + } + BinaryOp::Sub => { + self.emit("sub t5, t0, t1"); + self.emit("sltu t2, t0, t1"); + self.emit(format!("bltu t3, t2, {}", overflow_label)); + self.emit("sub t6, t3, t2"); + } + _ => unreachable!("guarded u128 binary op"), + } + self.emit_stack_store("t5", dest_offset); + self.emit_stack_store("t6", dest_offset + 8); + self.emit_sp_addi("t0", dest_offset); + self.emit_stack_store("t0", dest.id * 8); + self.emit(format!("j {}", done_label)); + self.emit_label(&overflow_label); + self.emit_runtime_error_comment(CellScriptRuntimeError::AggregateAmountMismatch); + self.emit(format!("li a0, {}", CellScriptRuntimeError::AggregateAmountMismatch.code())); + self.emit_epilogue(); + self.emit_label(&done_label); + true + } + + pub(super) fn emit_expected_operand_to_t1(&mut self, operand: &IrOperand) { + match operand { + IrOperand::Const(IrConst::Bool(b)) => self.emit(format!("li t1, {}", if *b { 1 } else { 0 })), + IrOperand::Const(IrConst::U8(n)) => self.emit(format!("li t1, {}", n)), + IrOperand::Const(IrConst::U16(n)) => self.emit(format!("li t1, {}", n)), + IrOperand::Const(IrConst::U32(n)) => self.emit(format!("li t1, {}", n)), + IrOperand::Const(IrConst::U64(n)) => self.emit(format!("li t1, {}", n)), + IrOperand::Var(var) => { + if let Some(source) = self.schema_field_value_sources.get(&var.id).cloned() { + self.emit_schema_field_source_to_t1(&source); + } else if let Some(source) = self.prelude_u64_value_sources.get(&var.id).cloned() { + self.emit_prelude_u64_value_source_to_t1(&source); + } else if matches!(var.ty, IrType::Bool | IrType::U8 | IrType::U16 | IrType::U32 | IrType::I32 | IrType::U64) { + self.emit_stack_load("t1", var.id * 8); + } else if let Some(value) = self.prelude_scalar_immediates.get(&var.id).copied() { + self.emit(format!("li t1, {}", value)); + } else { + self.emit_stack_load("t1", var.id * 8); + } + } + _ => self.emit("li t1, 0"), + } + } + + pub(super) fn emit_prelude_u64_value_source_to_t1(&mut self, source: &PreludeU64ValueSource) { + self.emit_prelude_u64_value_source_to_t1_at_depth(source, 0); + } + + pub(super) fn emit_prelude_u64_value_source_to_t1_at_depth(&mut self, source: &PreludeU64ValueSource, _depth: usize) { + match source { + PreludeU64ValueSource::Const(n) => self.emit(format!("li t1, {}", n)), + PreludeU64ValueSource::ParamVar(var_id) => self.emit_stack_load("t1", var_id * 8), + PreludeU64ValueSource::StackVar(var_id) => self.emit_stack_load("t1", var_id * 8), + PreludeU64ValueSource::Field(source) => self.emit_schema_field_source_to_t1(source), + PreludeU64ValueSource::Binary { op, left, right } => { + self.emit(format!("# cellscript abi: expected expression u64 {:?}", op)); + let Some(temp_offset) = self.checked_runtime_expr_temp_offset(_depth) else { + self.emit("# cellscript abi: fail closed because expression verifier temp stack is exhausted"); + self.emit_fail(CellScriptRuntimeError::DataPreservationMismatch); + return; + }; + self.emit_prelude_u64_value_source_to_t1_at_depth(left, _depth + 1); + self.emit_stack_store("t1", temp_offset); + self.emit_prelude_u64_operand_source_to_t1_at_depth(right, _depth + 1); + self.emit_stack_load("t3", temp_offset); + match op { + BinaryOp::Add => self.emit("add t1, t3, t1"), + BinaryOp::Sub => self.emit("sub t1, t3, t1"), + BinaryOp::Mul => self.emit("mul t1, t3, t1"), + BinaryOp::Div => self.emit("divu t1, t3, t1"), + _ => unreachable!("prelude u64 binary source only supports add/sub/mul/div"), + } + } + PreludeU64ValueSource::Min { left, right } => { + self.emit("# cellscript abi: expected expression u64 min"); + let Some(temp_offset) = self.checked_runtime_expr_temp_offset(_depth) else { + self.emit("# cellscript abi: fail closed because expression verifier temp stack is exhausted"); + self.emit_fail(CellScriptRuntimeError::DataPreservationMismatch); + return; + }; + self.emit_prelude_u64_value_source_to_t1_at_depth(left, _depth + 1); + self.emit_stack_store("t1", temp_offset); + self.emit_prelude_u64_operand_source_to_t1_at_depth(right, _depth + 1); + self.emit_stack_load("t3", temp_offset); + self.emit("slt t2, t3, t1"); + let right_ok_label = self.fresh_label("prelude_min_right_ok"); + self.emit(format!("beqz t2, {}", right_ok_label)); + self.emit("add t1, t3, zero"); + self.emit_label(&right_ok_label); + } + } + } + + pub(super) fn emit_prelude_u64_operand_source_to_t1(&mut self, source: &PreludeU64OperandSource) { + self.emit_prelude_u64_operand_source_to_t1_at_depth(source, 0); + } + + pub(super) fn emit_prelude_u64_operand_source_to_t1_at_depth(&mut self, source: &PreludeU64OperandSource, _depth: usize) { + match source { + PreludeU64OperandSource::Const(n) => self.emit(format!("li t1, {}", n)), + PreludeU64OperandSource::ParamVar(var_id) => self.emit_stack_load("t1", var_id * 8), + PreludeU64OperandSource::StackVar(var_id) => self.emit_stack_load("t1", var_id * 8), + PreludeU64OperandSource::Field(source) => self.emit_schema_field_source_to_t1(source), + PreludeU64OperandSource::Expr(source) => self.emit_prelude_u64_value_source_to_t1_at_depth(source, _depth), + } + } + + pub(super) fn emit_schema_field_source_to_t1(&mut self, source: &SchemaFieldValueSource) { + let context = format!("{}.{}", source.type_name, source.field); + let Some(width) = layout_fixed_scalar_width(&source.layout) else { + self.emit("li t1, 0"); + return; + }; + if !self.type_fixed_sizes.contains_key(&source.type_name) { + if self.emit_schema_field_source_pointer_to("t4", source, width) { + self.emit(format!("# cellscript abi: expected table field {} index={} size={}", context, source.layout.index, width)); + self.emit_unaligned_scalar_load("t4", "t1", "t2", 0, width); + } else { + self.emit("li t1, 0"); + } + return; + } + if let Some(size_offset) = self.schema_pointer_size_offsets.get(&source.obj_var_id).copied() { + if let Some(expected_size) = self.type_fixed_sizes.get(&source.type_name).copied() { + self.emit_loaded_schema_exact_size_check(size_offset, expected_size, &source.type_name); + } + self.emit_loaded_schema_bounds_check(size_offset, source.layout.offset + width, &context); + } + self.emit(format!("# cellscript abi: expected field {} offset={} size={}", context, source.layout.offset, width)); + self.emit_stack_load("t4", source.obj_var_id * 8); + self.emit_unaligned_scalar_load("t4", "t1", "t2", source.layout.offset, width); + } + + pub(super) fn emit_prepare_schema_field_source(&mut self, source: &SchemaFieldValueSource, width: usize) { + let context = format!("{}.{}", source.type_name, source.field); + let Some(size_offset) = self.schema_pointer_size_offsets.get(&source.obj_var_id).copied() else { + return; + }; + if let Some(expected_size) = self.type_fixed_sizes.get(&source.type_name).copied() { + self.emit_loaded_schema_exact_size_check(size_offset, expected_size, &source.type_name); + self.emit_loaded_schema_bounds_check(size_offset, source.layout.offset + width, &context); + } else { + self.emit_stack_load("t4", source.obj_var_id * 8); + self.emit_molecule_table_field_bounds_to_t5("t4", size_offset, source.layout.index, width, &context); + } + } + + pub(super) fn emit_schema_field_source_pointer_to( + &mut self, + dest_reg: &str, + source: &SchemaFieldValueSource, + width: usize, + ) -> bool { + let context = format!("{}.{}", source.type_name, source.field); + if let Some(size_offset) = self.schema_pointer_size_offsets.get(&source.obj_var_id).copied() { + if let Some(expected_size) = self.type_fixed_sizes.get(&source.type_name).copied() { + self.emit_loaded_schema_exact_size_check(size_offset, expected_size, &source.type_name); + self.emit_loaded_schema_bounds_check(size_offset, source.layout.offset + width, &context); + self.emit_stack_load(dest_reg, source.obj_var_id * 8); + if source.layout.offset != 0 { + self.emit_large_addi(dest_reg, dest_reg, source.layout.offset as i64); + } + } else { + self.emit_stack_load("t4", source.obj_var_id * 8); + self.emit_molecule_table_field_bounds_to_t5("t4", size_offset, source.layout.index, width, &context); + self.emit(format!("add {}, t4, t5", dest_reg)); + } + true + } else if self.aggregate_pointer_sources.contains_key(&source.obj_var_id) + || self.type_fixed_sizes.contains_key(&source.type_name) + { + self.emit_stack_load(dest_reg, source.obj_var_id * 8); + if source.layout.offset != 0 { + self.emit_large_addi(dest_reg, dest_reg, source.layout.offset as i64); + } + true + } else { + false + } + } +} diff --git a/src/edition.rs b/src/edition.rs new file mode 100644 index 00000000..628e41b2 --- /dev/null +++ b/src/edition.rs @@ -0,0 +1,173 @@ +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::str::FromStr; + +use crate::error::CompileError; + +pub const COMPATIBILITY_PROFILE_SCHEMA: &str = "cellscript-resolved-compatibility-profile-v1"; + +/// CellScript source-language edition. +/// +/// Editions are a closed set. A package must opt into the current edition +/// explicitly in `Cell.toml`; missing or unknown editions are rejected. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum CellScriptEdition { + #[serde(rename = "2026")] + Edition2026, +} + +pub const CURRENT_EDITION: CellScriptEdition = CellScriptEdition::Edition2026; + +impl Default for CellScriptEdition { + fn default() -> Self { + CURRENT_EDITION + } +} + +impl CellScriptEdition { + pub const fn as_str(self) -> &'static str { + match self { + Self::Edition2026 => "2026", + } + } + + /// Stable source-language semantics selected by this edition. + /// + /// Target, wire ABI, assurance, metadata, and compiler release versions + /// are deliberately not edition properties. They are independent axes + /// assembled by [`resolve_compatibility_profile`]. + pub const fn source_semantics(self) -> &'static str { + match self { + Self::Edition2026 => "cellscript-source-semantics-2026", + } + } +} + +/// Resolve the complete compile-time compatibility contract from independent +/// version axes. +/// +/// The edition contributes source semantics only. Target behavior, primitive +/// assurance, metadata schemas, and entry/witness wire ABIs retain their own +/// version identities so that any of them can advance without inventing a new +/// source edition. +pub fn resolve_compatibility_profile( + edition: CellScriptEdition, + target_profile: &str, + primitive_assurance: Option<&str>, +) -> ResolvedCompatibilityProfile { + let primitive_assurance = primitive_assurance.unwrap_or("default").to_string(); + let source_semantics = edition.source_semantics().to_string(); + ResolvedCompatibilityProfile { + schema: COMPATIBILITY_PROFILE_SCHEMA.to_string(), + id: format!( + "{}-{}-target-{}-primitive-{}-entry-{}-placement-{}-metadata-{}-{}-{}-{}", + COMPATIBILITY_PROFILE_SCHEMA, + source_semantics, + target_profile, + primitive_assurance, + crate::ENTRY_WITNESS_ABI, + crate::ENTRY_WITNESS_PLACEMENT_ABI, + crate::METADATA_SCHEMA_VERSION, + crate::SOURCE_METADATA_SCHEMA_VERSION, + crate::ARTIFACT_METADATA_SCHEMA_VERSION, + crate::CONSTRAINTS_METADATA_SCHEMA_VERSION, + ), + edition, + source_semantics, + target_profile: target_profile.to_string(), + primitive_assurance, + metadata_schema_version: crate::METADATA_SCHEMA_VERSION, + source_metadata_schema_version: crate::SOURCE_METADATA_SCHEMA_VERSION, + artifact_metadata_schema_version: crate::ARTIFACT_METADATA_SCHEMA_VERSION, + constraints_metadata_schema_version: crate::CONSTRAINTS_METADATA_SCHEMA_VERSION, + entry_witness_payload_abi: crate::ENTRY_WITNESS_ABI.to_string(), + entry_witness_placement_abi: crate::ENTRY_WITNESS_PLACEMENT_ABI.to_string(), + entry_witness_placement_field: crate::ENTRY_WITNESS_PLACEMENT_FIELD.to_string(), + entry_witness_placement_source: crate::ENTRY_WITNESS_PLACEMENT_SOURCE.to_string(), + raw_entry_witness_payload_compatible: false, + } +} + +impl fmt::Display for CellScriptEdition { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.as_str()) + } +} + +impl FromStr for CellScriptEdition { + type Err = CompileError; + + fn from_str(value: &str) -> Result { + match value { + "2026" => Ok(Self::Edition2026), + other => Err(CompileError::without_span(format!("unsupported CellScript edition '{}'; expected 2026", other))), + } + } +} + +/// Fully resolved compile-time compatibility contract. +/// +/// The edition contributes source semantics. Target, assurance, metadata, and +/// wire contracts remain independently named because they evolve on separate +/// schedules and CKB-VM cannot read `Cell.toml`. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ResolvedCompatibilityProfile { + pub schema: String, + pub id: String, + pub edition: CellScriptEdition, + pub source_semantics: String, + pub target_profile: String, + pub primitive_assurance: String, + pub metadata_schema_version: u32, + pub source_metadata_schema_version: u32, + pub artifact_metadata_schema_version: u32, + pub constraints_metadata_schema_version: u32, + pub entry_witness_payload_abi: String, + pub entry_witness_placement_abi: String, + pub entry_witness_placement_field: String, + pub entry_witness_placement_source: String, + pub raw_entry_witness_payload_compatible: bool, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn only_edition_2026_is_accepted() { + assert_eq!("2026".parse::().unwrap(), CellScriptEdition::Edition2026); + assert!("unsupported".parse::().unwrap_err().message.contains("expected 2026")); + } + + #[test] + fn serde_uses_the_manifest_year() { + assert_eq!(serde_json::to_string(&CURRENT_EDITION).unwrap(), "\"2026\""); + assert_eq!(serde_json::from_str::("\"2026\"").unwrap(), CURRENT_EDITION); + assert!(serde_json::from_str::("\"unsupported\"").is_err()); + } + + #[test] + fn edition_owns_source_semantics_only() { + assert_eq!(CURRENT_EDITION.source_semantics(), "cellscript-source-semantics-2026"); + } + + #[test] + fn compatibility_profile_composes_independent_version_axes() { + let profile = resolve_compatibility_profile(CURRENT_EDITION, "ckb", Some("0.16")); + + assert_eq!(profile.schema, COMPATIBILITY_PROFILE_SCHEMA); + assert_eq!(profile.edition, CURRENT_EDITION); + assert_eq!(profile.source_semantics, CURRENT_EDITION.source_semantics()); + assert_eq!(profile.target_profile, "ckb"); + assert_eq!(profile.primitive_assurance, "0.16"); + assert_eq!(profile.metadata_schema_version, crate::METADATA_SCHEMA_VERSION); + assert_eq!(profile.source_metadata_schema_version, crate::SOURCE_METADATA_SCHEMA_VERSION); + assert_eq!(profile.artifact_metadata_schema_version, crate::ARTIFACT_METADATA_SCHEMA_VERSION); + assert_eq!(profile.constraints_metadata_schema_version, crate::CONSTRAINTS_METADATA_SCHEMA_VERSION); + assert_eq!(profile.entry_witness_payload_abi, crate::ENTRY_WITNESS_ABI); + assert_eq!(profile.entry_witness_placement_abi, crate::ENTRY_WITNESS_PLACEMENT_ABI); + + let other_assurance = resolve_compatibility_profile(CURRENT_EDITION, "ckb", Some("0.17")); + assert_ne!(profile.id, other_assurance.id); + } +} diff --git a/src/error/mod.rs b/src/error/mod.rs index 1a8474a7..bf1d3ba7 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -139,9 +139,9 @@ pub const COMPILER_ERROR_INFOS: &[CompilerErrorInfo] = &[ }, CompilerErrorInfo { code: "E2400", - name: "external-toolchain", - description: "An explicitly configured external RISC-V toolchain failed validation or execution.", - hint: "Check CELLSCRIPT_RISCV_CC or the CELLSCRIPT_RISCV_AS/CELLSCRIPT_RISCV_LD pair and their stderr output.", + name: "verified-artifact-boundary", + description: "The compiler could not construct or persist the verified lowering/source-map boundary for an ELF artifact.", + hint: "Inspect the verified lowering record, source artifact map, and canonical sidecar diagnostic.", }, CompilerErrorInfo { code: "E2900", diff --git a/src/flow/mod.rs b/src/flow/mod.rs index 4622e763..f597d94b 100644 --- a/src/flow/mod.rs +++ b/src/flow/mod.rs @@ -130,10 +130,10 @@ fn action_state_context(specs: &HashMap, action: &ActionDef) - let mut context = ActionStateContext::default(); for param in &action.params { - if let Type::Named(ty) = ¶m.ty { - if specs.contains_key(ty) { - context.variable_flow_types.insert(param.name.clone(), ty.clone()); - } + if let Type::Named(ty) = ¶m.ty + && specs.contains_key(ty) + { + context.variable_flow_types.insert(param.name.clone(), ty.clone()); } } @@ -181,12 +181,11 @@ fn validate_action_state_edges(specs: &HashMap, action: &Actio fn action_param_flow_type<'a>(specs: &HashMap, action: &'a ActionDef, binding: &str) -> Option<&'a str> { action.params.iter().find_map(|param| { - if param.name == binding { - if let Type::Named(ty) = ¶m.ty { - if specs.contains_key(ty) { - return Some(ty.as_str()); - } - } + if param.name == binding + && let Type::Named(ty) = ¶m.ty + && specs.contains_key(ty) + { + return Some(ty.as_str()); } None }) @@ -194,12 +193,11 @@ fn action_param_flow_type<'a>(specs: &HashMap, action: &'a Act fn action_output_flow_type<'a>(specs: &HashMap, action: &'a ActionDef, binding: &str) -> Option<&'a str> { action.outputs.iter().find_map(|output| { - if output.name == binding { - if let Type::Named(ty) = &output.ty { - if specs.contains_key(ty) { - return Some(ty.as_str()); - } - } + if output.name == binding + && let Type::Named(ty) = &output.ty + && specs.contains_key(ty) + { + return Some(ty.as_str()); } None }) @@ -215,10 +213,10 @@ fn collect_state_context_from_stmts(specs: &HashMap, context: } if let Some(ty) = flow_expr_type(specs, context, &let_stmt.value) { context.variable_flow_types.insert(name.clone(), ty); - } else if let Some(Type::Named(ty)) = &let_stmt.ty { - if specs.contains_key(ty) { - context.variable_flow_types.insert(name.clone(), ty.clone()); - } + } else if let Some(Type::Named(ty)) = &let_stmt.ty + && specs.contains_key(ty) + { + context.variable_flow_types.insert(name.clone(), ty.clone()); } } collect_state_context_from_expr(specs, context, &let_stmt.value); @@ -250,10 +248,10 @@ fn collect_state_context_from_stmts(specs: &HashMap, context: fn collect_state_context_from_expr(specs: &HashMap, context: &mut ActionStateContext, expr: &Expr) { match expr { Expr::Consume(consume) => { - if let Expr::Identifier(name) = consume.expr.as_ref() { - if let Some(ty) = context.variable_flow_types.get(name) { - context.consumed_flow_types.insert(ty.clone()); - } + if let Expr::Identifier(name) = consume.expr.as_ref() + && let Some(ty) = context.variable_flow_types.get(name) + { + context.consumed_flow_types.insert(ty.clone()); } collect_state_context_from_expr(specs, context, &consume.expr); } diff --git a/src/fmt/mod.rs b/src/fmt/mod.rs index b5546444..9488099e 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -218,11 +218,11 @@ impl Formatter { validity: Option<&ValidityBlock>, ) -> Result<()> { let mut header = format!("{} {}", keyword, name); - if let Some(capabilities) = capabilities { - if !capabilities.is_empty() { - let rendered = capabilities.iter().map(format_capability).collect::>().join(", "); - header.push_str(&format!(" has {}", rendered)); - } + if let Some(capabilities) = capabilities + && !capabilities.is_empty() + { + let rendered = capabilities.iter().map(format_capability).collect::>().join(", "); + header.push_str(&format!(" has {}", rendered)); } if has_type_policy(identity, default_hash_type, capacity_floor) { self.push_line(&header); @@ -292,10 +292,10 @@ impl Formatter { if let Some(capacity_floor) = capacity_floor { self.push_line(&format!("with_capacity_floor({})", capacity_floor.shannons)); } - if let Some(identity) = identity { - if !matches!(identity, IdentityPolicy::None) { - self.push_line(&format!("identity({})", format_identity_policy(identity))); - } + if let Some(identity) = identity + && !matches!(identity, IdentityPolicy::None) + { + self.push_line(&format!("identity({})", format_identity_policy(identity))); } } @@ -668,8 +668,11 @@ impl Formatter { if call.preserve_fields.is_empty() { base } else { - let fields = call.preserve_fields.join("\n"); - format!("{} {{\n{}\n}}", base, fields) + let field_indent = " ".repeat((self.indent_level + 1) * self.config.indent_width); + let closing_indent = " ".repeat(self.indent_level * self.config.indent_width); + let fields = + call.preserve_fields.iter().map(|field| format!("{}{}", field_indent, field)).collect::>().join("\n"); + format!("{} {{\n{}\n{}}}", base, fields, closing_indent) } } } @@ -1204,6 +1207,12 @@ action transfer_coin(coin: Coin, to: Address) -> next_coin: Coin { "stdlib field blocks use newline-separated field names, not comma-separated lists:\n{}", formatted ); + assert!( + formatted + .contains(" std::lifecycle::transfer(coin, next_coin, to) {\n amount\n nonce\n }"), + "stdlib field blocks should retain statement-relative indentation:\n{}", + formatted + ); } #[test] diff --git a/src/incremental/mod.rs b/src/incremental/mod.rs index 18442c6e..e93b539e 100644 --- a/src/incremental/mod.rs +++ b/src/incremental/mod.rs @@ -256,13 +256,13 @@ impl ChangeDetector { return true; } - if let Ok(mtime) = metadata.modified() { - if mtime != snapshot.mtime { - let Ok(hash) = compute_file_hash(path) else { - return true; - }; - return hash != snapshot.hash; - } + if let Ok(mtime) = metadata.modified() + && mtime != snapshot.mtime + { + let Ok(hash) = compute_file_hash(path) else { + return true; + }; + return hash != snapshot.hash; } false diff --git a/src/ir/mod.rs b/src/ir/mod.rs index 1c95a9c1..84d2b144 100644 --- a/src/ir/mod.rs +++ b/src/ir/mod.rs @@ -376,6 +376,7 @@ pub struct IrBlock { pub id: BlockId, pub instructions: Vec, pub terminator: IrTerminator, + pub runtime_error: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -695,6 +696,19 @@ fn protocol_role_name(name: &str) -> Option { normalized.split('_').find(|part| ROLE_NAMES.binary_search(part).is_ok()).map(str::to_string) } +struct IrImportContext { + type_fields: HashMap>, + type_kinds: HashMap, + receipt_claim_outputs: HashMap>, + flow_states: HashMap>, + type_validity: HashMap>, + enum_definitions: HashMap, + external_function_effects: HashMap, + external_function_param_types: HashMap>, + external_function_return_types: HashMap>, + call_target_labels: HashMap, +} + impl IrGenerator { pub fn new(module_name: String) -> Self { Self { @@ -748,19 +762,19 @@ impl IrGenerator { generator } - pub fn with_import_context( - module_name: String, - type_fields: HashMap>, - type_kinds: HashMap, - receipt_claim_outputs: HashMap>, - flow_states: HashMap>, - type_validity: HashMap>, - enum_definitions: HashMap, - external_function_effects: HashMap, - external_function_param_types: HashMap>, - external_function_return_types: HashMap>, - call_target_labels: HashMap, - ) -> Self { + fn with_import_context(module_name: String, context: IrImportContext) -> Self { + let IrImportContext { + type_fields, + type_kinds, + receipt_claim_outputs, + flow_states, + type_validity, + enum_definitions, + external_function_effects, + external_function_param_types, + external_function_return_types, + call_target_labels, + } = context; let mut generator = Self::with_type_fields(module_name, type_fields); generator.type_kinds.extend(type_kinds); generator.receipt_claim_outputs.extend(receipt_claim_outputs); @@ -1006,14 +1020,13 @@ impl IrGenerator { } fn flow_states_for_decl(&self, machine: &FlowDef) -> Vec { - if let Some(fields) = self.type_fields.get(&machine.target.base) { - if let Some(IrType::Named(enum_name)) = fields.get(&machine.target.field) { - if let Some(variants) = self.enum_variants.get(enum_name) { - let mut ordered = variants.iter().map(|(name, ordinal)| (*ordinal, name.clone())).collect::>(); - ordered.sort_by_key(|(ordinal, _)| *ordinal); - return ordered.into_iter().map(|(_, name)| name).collect(); - } - } + if let Some(fields) = self.type_fields.get(&machine.target.base) + && let Some(IrType::Named(enum_name)) = fields.get(&machine.target.field) + && let Some(variants) = self.enum_variants.get(enum_name) + { + let mut ordered = variants.iter().map(|(name, ordinal)| (*ordinal, name.clone())).collect::>(); + ordered.sort_by_key(|(ordinal, _)| *ordinal); + return ordered.into_iter().map(|(_, name)| name).collect(); } let mut states = Vec::new(); @@ -2012,10 +2025,10 @@ impl IrGenerator { if let Some(pattern) = self.cell_pattern_from_operand(receipt, "claim") { patterns.push(pattern); } - } else if let IrInstruction::Settle { operand, .. } = instruction { - if let Some(pattern) = self.cell_pattern_from_operand(operand, "settle") { - patterns.push(pattern); - } + } else if let IrInstruction::Settle { operand, .. } = instruction + && let Some(pattern) = self.cell_pattern_from_operand(operand, "settle") + { + patterns.push(pattern); } } } @@ -2086,10 +2099,10 @@ impl IrGenerator { if let Some(pattern) = self.create_pattern_from_var(dest, "claim") { patterns.push(pattern); } - } else if let IrInstruction::Settle { dest, .. } = instruction { - if let Some(pattern) = self.create_pattern_from_var(dest, "settle") { - patterns.push(pattern); - } + } else if let IrInstruction::Settle { dest, .. } = instruction + && let Some(pattern) = self.create_pattern_from_var(dest, "settle") + { + patterns.push(pattern); } } } @@ -2227,7 +2240,7 @@ impl IrGenerator { source: &IrOperand, output_ty: &IrType, active: BlockId, - blocks: &mut Vec, + blocks: &mut [IrBlock], ) -> HashMap { let (IrOperand::Var(source_var), Some(output_type_name)) = (source, Self::named_type_name_from_ir_type(output_ty)) else { return HashMap::new(); @@ -2277,10 +2290,10 @@ impl IrGenerator { } } for pattern in body.read_refs.iter().chain(body.consume_set.iter()) { - if let Some(type_hash) = pattern.type_hash { - if shared_hashes.contains(&type_hash) { - hashes.push(type_hash); - } + if let Some(type_hash) = pattern.type_hash + && shared_hashes.contains(&type_hash) + { + hashes.push(type_hash); } } for pattern in &body.create_set { @@ -2650,10 +2663,10 @@ impl IrGenerator { LoweredExpr { operand: IrOperand::Var(dest), current: Some(active) } } Expr::Call(call) => { - if let Expr::Identifier(path) = call.func.as_ref() { - if let Some(lowered) = self.try_lower_enum_constructor_path(path, &call.args, current, blocks, vars, call.span) { - return lowered; - } + if let Expr::Identifier(path) = call.func.as_ref() + && let Some(lowered) = self.try_lower_enum_constructor_path(path, &call.args, current, blocks, vars, call.span) + { + return lowered; } if let Some(lowered) = self.try_lower_builtin_call(call, current, blocks, vars) { return lowered; @@ -2897,7 +2910,7 @@ impl IrGenerator { fn push_block(&mut self, blocks: &mut Vec) -> BlockId { let id = self.new_block(); - blocks.push(IrBlock { id, instructions: Vec::new(), terminator: IrTerminator::Return(None) }); + blocks.push(IrBlock { id, instructions: Vec::new(), terminator: IrTerminator::Return(None), runtime_error: None }); id } @@ -3308,6 +3321,7 @@ impl IrGenerator { let fail_block = self.push_block(blocks); self.block_mut(blocks, active).terminator = IrTerminator::Branch { cond, then_block: ok_block, else_block: fail_block }; self.block_mut(blocks, fail_block).terminator = IrTerminator::Return(Some(self.fail_closed_return_operand())); + self.block_mut(blocks, fail_block).runtime_error = Some(CellScriptRuntimeError::AssertionFailed); LoweredExpr { operand: IrOperand::Const(IrConst::Unit), current: Some(ok_block) } } @@ -3329,6 +3343,7 @@ impl IrGenerator { let fail_block = self.push_block(blocks); self.block_mut(blocks, active).terminator = IrTerminator::Branch { cond, then_block: ok_block, else_block: fail_block }; self.block_mut(blocks, fail_block).terminator = IrTerminator::Return(Some(self.fail_closed_return_operand())); + self.block_mut(blocks, fail_block).runtime_error = Some(CellScriptRuntimeError::AssertionFailed); LoweredExpr { operand: IrOperand::Const(IrConst::Bool(true)), current: Some(ok_block) } } @@ -4181,7 +4196,7 @@ impl IrGenerator { LoweredExpr { operand: IrOperand::Var(dest), current: Some(active) } } - fn lower_read_ref_expr(&mut self, read_ref: &ReadRefExpr, current: BlockId, blocks: &mut Vec) -> LoweredExpr { + fn lower_read_ref_expr(&mut self, read_ref: &ReadRefExpr, current: BlockId, blocks: &mut [IrBlock]) -> LoweredExpr { let dest = self.new_var(format!("read_ref_{}", read_ref.ty), IrType::Ref(Box::new(IrType::Named(read_ref.ty.clone())))); self.block_mut(blocks, current).instructions.push(IrInstruction::ReadRef { dest: dest.clone(), ty: read_ref.ty.clone() }); LoweredExpr { operand: IrOperand::Var(dest), current: Some(current) } @@ -4263,21 +4278,21 @@ impl IrGenerator { return lowered_idx; }; - if let IrOperand::Var(arr_var) = &lowered_arr.operand { - if let Some(elements) = self.aggregate_elements.get(&arr_var.id) { - let Some(index_value) = const_usize_operand(&lowered_idx.operand) else { - self.record_error("local fixed-array indexing requires a compile-time constant index", index.span); - return LoweredExpr { operand: IrOperand::Const(IrConst::U64(0)), current: Some(active) }; - }; - let Some(element_var) = elements.get(index_value).cloned() else { - self.record_error( - format!("array index {} is out of bounds for local fixed array of length {}", index_value, elements.len()), - index.span, - ); - return LoweredExpr { operand: IrOperand::Const(IrConst::U64(0)), current: Some(active) }; - }; - return LoweredExpr { operand: IrOperand::Var(element_var), current: Some(active) }; - } + if let IrOperand::Var(arr_var) = &lowered_arr.operand + && let Some(elements) = self.aggregate_elements.get(&arr_var.id) + { + let Some(index_value) = const_usize_operand(&lowered_idx.operand) else { + self.record_error("local fixed-array indexing requires a compile-time constant index", index.span); + return LoweredExpr { operand: IrOperand::Const(IrConst::U64(0)), current: Some(active) }; + }; + let Some(element_var) = elements.get(index_value).cloned() else { + self.record_error( + format!("array index {} is out of bounds for local fixed array of length {}", index_value, elements.len()), + index.span, + ); + return LoweredExpr { operand: IrOperand::Const(IrConst::U64(0)), current: Some(active) }; + }; + return LoweredExpr { operand: IrOperand::Var(element_var), current: Some(active) }; } let Some(result_ty) = self.index_result_type(&lowered_arr.operand) else { @@ -4497,7 +4512,7 @@ impl IrGenerator { LoweredExpr { operand: IrOperand::Var(dest), current: Some(active) } } - fn lower_empty_array_expr_with_ir_type(&mut self, ir_ty: IrType, current: BlockId, blocks: &mut Vec) -> LoweredExpr { + fn lower_empty_array_expr_with_ir_type(&mut self, ir_ty: IrType, current: BlockId, blocks: &mut [IrBlock]) -> LoweredExpr { if !matches!(ir_ty, IrType::Array(_, 0)) { self.record_error("empty array literal requires a zero-length declared array type", Span::default()); return LoweredExpr { operand: IrOperand::Const(IrConst::U64(0)), current: Some(current) }; @@ -4649,10 +4664,10 @@ impl IrGenerator { return LoweredExpr { operand: IrOperand::Var(dest), current: Some(active) }; } - if let Some(fields) = self.aggregate_fields.get(&base_var.id) { - if let Some(field_var) = fields.get(&field.field) { - return LoweredExpr { operand: IrOperand::Var(field_var.clone()), current: Some(active) }; - } + if let Some(fields) = self.aggregate_fields.get(&base_var.id) + && let Some(field_var) = fields.get(&field.field) + { + return LoweredExpr { operand: IrOperand::Var(field_var.clone()), current: Some(active) }; } if let Some(field_var) = self.materialize_schema_field(base_var, &field.field, active, blocks) { @@ -6067,13 +6082,13 @@ impl IrGenerator { "len" if call.args.is_empty() => { let lowered = self.lower_expr(&field.expr, current, blocks, vars); let active = lowered.current?; - if let IrOperand::Var(var) = &lowered.operand { - if let Some(elements) = self.aggregate_elements.get(&var.id) { - return Some(LoweredExpr { - operand: IrOperand::Const(IrConst::U64(elements.len() as u64)), - current: Some(active), - }); - } + if let IrOperand::Var(var) = &lowered.operand + && let Some(elements) = self.aggregate_elements.get(&var.id) + { + return Some(LoweredExpr { + operand: IrOperand::Const(IrConst::U64(elements.len() as u64)), + current: Some(active), + }); } let dest = self.new_var("len_tmp", IrType::U64); self.block_mut(blocks, active) @@ -6084,13 +6099,13 @@ impl IrGenerator { "is_empty" if call.args.is_empty() => { let lowered = self.lower_expr(&field.expr, current, blocks, vars); let active = lowered.current?; - if let IrOperand::Var(var) = &lowered.operand { - if let Some(elements) = self.aggregate_elements.get(&var.id) { - return Some(LoweredExpr { - operand: IrOperand::Const(IrConst::Bool(elements.is_empty())), - current: Some(active), - }); - } + if let IrOperand::Var(var) = &lowered.operand + && let Some(elements) = self.aggregate_elements.get(&var.id) + { + return Some(LoweredExpr { + operand: IrOperand::Const(IrConst::Bool(elements.is_empty())), + current: Some(active), + }); } let len_dest = self.new_var("is_empty_len_tmp", IrType::U64); let dest = self.new_var("is_empty_tmp", IrType::Bool); @@ -6166,34 +6181,30 @@ impl IrGenerator { let collection_operand = lowered_collection.operand; if let (Expr::Identifier(receiver_name), IrOperand::Var(collection_var)) = (field.expr.as_ref(), &collection_operand) + && matches!(&collection_var.ty, IrType::Named(name) if name == "Vec") + && let (Some(item_ty), Some(receiver_var)) = + (inline_ir_type_repr(&self.operand_type(&lowered_value.operand)), vars.get_mut(receiver_name)) + && receiver_var.id == collection_var.id { - if matches!(&collection_var.ty, IrType::Named(name) if name == "Vec") { - if let (Some(item_ty), Some(receiver_var)) = - (inline_ir_type_repr(&self.operand_type(&lowered_value.operand)), vars.get_mut(receiver_name)) - { - if receiver_var.id == collection_var.id { - receiver_var.ty = IrType::Named(format!("Vec<{}>", item_ty)); - } - } - } + receiver_var.ty = IrType::Named(format!("Vec<{}>", item_ty)); } let block = self.block_mut(blocks, active); block.instructions.push(IrInstruction::CollectionPush { collection: collection_operand.clone(), value: lowered_value.operand.clone(), }); - if let IrOperand::Var(collection_var) = &collection_operand { - if let Some((root_id, field_name)) = self.schema_field_roots.get(&collection_var.id).cloned() { - self.mutated_fields.entry(root_id).or_default().insert(field_name.clone()); - self.mutated_field_transitions.entry(root_id).or_default().insert( - field_name.clone(), - MutateFieldTransition { - field: field_name, - op: MutateTransitionOp::Append, - operand: lowered_value.operand, - }, - ); - } + if let IrOperand::Var(collection_var) = &collection_operand + && let Some((root_id, field_name)) = self.schema_field_roots.get(&collection_var.id).cloned() + { + self.mutated_fields.entry(root_id).or_default().insert(field_name.clone()); + self.mutated_field_transitions.entry(root_id).or_default().insert( + field_name.clone(), + MutateFieldTransition { + field: field_name, + op: MutateTransitionOp::Append, + operand: lowered_value.operand, + }, + ); } Some(LoweredExpr { operand: IrOperand::Const(IrConst::Bool(true)), current: Some(active) }) } @@ -6245,16 +6256,12 @@ impl IrGenerator { let collection_operand = lowered_collection.operand; if let (Expr::Identifier(receiver_name), IrOperand::Var(collection_var)) = (field.expr.as_ref(), &collection_operand) + && matches!(&collection_var.ty, IrType::Named(name) if name == "Vec") + && let (Some(item_ty), Some(receiver_var)) = + (inline_ir_type_repr(&self.operand_type(&lowered_value.operand)), vars.get_mut(receiver_name)) + && receiver_var.id == collection_var.id { - if matches!(&collection_var.ty, IrType::Named(name) if name == "Vec") { - if let (Some(item_ty), Some(receiver_var)) = - (inline_ir_type_repr(&self.operand_type(&lowered_value.operand)), vars.get_mut(receiver_name)) - { - if receiver_var.id == collection_var.id { - receiver_var.ty = IrType::Named(format!("Vec<{}>", item_ty)); - } - } - } + receiver_var.ty = IrType::Named(format!("Vec<{}>", item_ty)); } let dest = self.new_var("contains_tmp", IrType::Bool); self.block_mut(blocks, active).instructions.push(IrInstruction::CollectionContains { @@ -6298,16 +6305,12 @@ impl IrGenerator { let collection_operand = lowered_collection.operand; if let (Expr::Identifier(receiver_name), IrOperand::Var(collection_var)) = (field.expr.as_ref(), &collection_operand) + && matches!(&collection_var.ty, IrType::Named(name) if name == "Vec") + && let (Some(item_ty), Some(receiver_var)) = + (inline_ir_type_repr(&self.operand_type(&lowered_value.operand)), vars.get_mut(receiver_name)) + && receiver_var.id == collection_var.id { - if matches!(&collection_var.ty, IrType::Named(name) if name == "Vec") { - if let (Some(item_ty), Some(receiver_var)) = - (inline_ir_type_repr(&self.operand_type(&lowered_value.operand)), vars.get_mut(receiver_name)) - { - if receiver_var.id == collection_var.id { - receiver_var.ty = IrType::Named(format!("Vec<{}>", item_ty)); - } - } - } + receiver_var.ty = IrType::Named(format!("Vec<{}>", item_ty)); } self.block_mut(blocks, active).instructions.push(IrInstruction::CollectionInsert { collection: collection_operand, @@ -6326,16 +6329,12 @@ impl IrGenerator { let collection_operand = lowered_collection.operand; if let (Expr::Identifier(receiver_name), IrOperand::Var(collection_var)) = (field.expr.as_ref(), &collection_operand) + && matches!(&collection_var.ty, IrType::Named(name) if name == "Vec") + && let (Some(item_ty), Some(receiver_var)) = + (inline_ir_type_repr(&self.operand_type(&lowered_value.operand)), vars.get_mut(receiver_name)) + && receiver_var.id == collection_var.id { - if matches!(&collection_var.ty, IrType::Named(name) if name == "Vec") { - if let (Some(item_ty), Some(receiver_var)) = - (inline_ir_type_repr(&self.operand_type(&lowered_value.operand)), vars.get_mut(receiver_name)) - { - if receiver_var.id == collection_var.id { - receiver_var.ty = IrType::Named(format!("Vec<{}>", item_ty)); - } - } - } + receiver_var.ty = IrType::Named(format!("Vec<{}>", item_ty)); } self.block_mut(blocks, active).instructions.push(IrInstruction::CollectionSet { collection: collection_operand, @@ -6352,18 +6351,13 @@ impl IrGenerator { let collection_operand = lowered_collection.operand; if let (Expr::Identifier(receiver_name), IrOperand::Var(collection_var)) = (field.expr.as_ref(), &collection_operand) + && matches!(&collection_var.ty, IrType::Named(name) if name == "Vec") + && let (IrType::Array(inner, _), Some(receiver_var)) = + (self.operand_type(&lowered_slice.operand), vars.get_mut(receiver_name)) + && receiver_var.id == collection_var.id + && let Some(item_ty) = inline_ir_type_repr(inner.as_ref()) { - if matches!(&collection_var.ty, IrType::Named(name) if name == "Vec") { - if let (IrType::Array(inner, _), Some(receiver_var)) = - (self.operand_type(&lowered_slice.operand), vars.get_mut(receiver_name)) - { - if receiver_var.id == collection_var.id { - if let Some(item_ty) = inline_ir_type_repr(inner.as_ref()) { - receiver_var.ty = IrType::Named(format!("Vec<{}>", item_ty)); - } - } - } - } + receiver_var.ty = IrType::Named(format!("Vec<{}>", item_ty)); } let block = self.block_mut(blocks, active); block @@ -6377,7 +6371,7 @@ impl IrGenerator { } } - fn lower_script_args_empty(&mut self, current: BlockId, blocks: &mut Vec) -> LoweredExpr { + fn lower_script_args_empty(&mut self, current: BlockId, blocks: &mut [IrBlock]) -> LoweredExpr { let aggregate = self.new_var("script_args", IrType::Named(CKB_SCRIPT_ARGS_TYPE.to_string())); let bytes = self.new_var("script_args_bytes", IrType::Array(Box::new(IrType::U8), 0)); let len = self.new_var("script_args_len", IrType::U64); @@ -6876,10 +6870,10 @@ impl IrGenerator { if let Some(label) = self.call_target_labels.get(name) { return label.clone(); } - if let Some((module, symbol)) = name.rsplit_once("::") { - if module == self.module.name { - return symbol.to_string(); - } + if let Some((module, symbol)) = name.rsplit_once("::") + && module == self.module.name + { + return symbol.to_string(); } name.to_string() } @@ -6902,13 +6896,7 @@ impl IrGenerator { .cloned() } - fn materialize_schema_field( - &mut self, - base_var: &IrVar, - field: &str, - current: BlockId, - blocks: &mut Vec, - ) -> Option { + fn materialize_schema_field(&mut self, base_var: &IrVar, field: &str, current: BlockId, blocks: &mut [IrBlock]) -> Option { let field_ty = self.lookup_field_ir_type(&base_var.ty, field)?; let field_var = self.new_var(format!("{}_{}", base_var.name, field), field_ty); self.block_mut(blocks, current).instructions.push(IrInstruction::FieldAccess { @@ -7370,10 +7358,10 @@ fn qualify_validity_dependencies( ) { match expr { Expr::Call(call) => { - if let Expr::Identifier(name) = call.func.as_mut() { - if let Some((callee_module, function)) = resolver.resolve_function_with_module(owner_module, name) { - *name = format!("{}::{}", callee_module, function_def_name(&function)); - } + if let Expr::Identifier(name) = call.func.as_mut() + && let Some((callee_module, function)) = resolver.resolve_function_with_module(owner_module, name) + { + *name = format!("{}::{}", callee_module, function_def_name(&function)); } qualify_validity_dependencies(&mut call.func, owner_module, resolver, fields, constant_dependencies); for arg in &mut call.args { @@ -7475,6 +7463,16 @@ fn generate_with_resolver_inner( generate_with_resolver_diagnostics_inner(ast, resolver, module_name, include_external_callables).map_err(collapse_ir_errors) } +#[derive(Default)] +struct ExternalCallableContext { + function_effects: HashMap, + function_param_types: HashMap>, + function_return_types: HashMap>, + target_labels: HashMap, + abis: Vec, + names: HashSet, +} + fn generate_with_resolver_diagnostics_inner( ast: &Module, resolver: &ModuleResolver, @@ -7487,13 +7485,8 @@ fn generate_with_resolver_diagnostics_inner( let mut flow_states = HashMap::new(); let mut external_type_defs = Vec::new(); let mut external_type_names = HashSet::new(); - let mut external_callable_abis = Vec::new(); - let mut external_callable_names = HashSet::new(); let mut external_enum_definitions = HashMap::new(); - let mut external_function_effects = HashMap::new(); - let mut external_function_param_types = HashMap::new(); - let mut external_function_return_types = HashMap::new(); - let mut call_target_labels = HashMap::new(); + let mut external_callables = ExternalCallableContext::default(); let mut resolved_external_types: Vec<(String, String, TypeDef)> = Vec::new(); @@ -7529,12 +7522,7 @@ fn generate_with_resolver_diagnostics_inner( &function, resolver, Some(&local_name), - &mut external_function_effects, - &mut external_function_param_types, - &mut external_function_return_types, - &mut call_target_labels, - &mut external_callable_abis, - &mut external_callable_names, + &mut external_callables, ); } } @@ -7570,28 +7558,33 @@ fn generate_with_resolver_diagnostics_inner( &function, resolver, None, - &mut external_function_effects, - &mut external_function_param_types, - &mut external_function_return_types, - &mut call_target_labels, - &mut external_callable_abis, - &mut external_callable_names, + &mut external_callables, ); } } + let ExternalCallableContext { + function_effects: external_function_effects, + function_param_types: external_function_param_types, + function_return_types: external_function_return_types, + target_labels: call_target_labels, + abis: external_callable_abis, + names: _, + } = external_callables; let generator = IrGenerator::with_import_context( ast.name.clone(), - type_fields, - type_kinds, - receipt_claim_outputs, - flow_states, - imported_type_validity, - external_enum_definitions, - external_function_effects, - external_function_param_types, - external_function_return_types, - call_target_labels, + IrImportContext { + type_fields, + type_kinds, + receipt_claim_outputs, + flow_states, + type_validity: imported_type_validity, + enum_definitions: external_enum_definitions, + external_function_effects, + external_function_param_types, + external_function_return_types, + call_target_labels, + }, ); let mut ir = generator.generate_diagnostics(ast)?; ir.external_type_defs = external_type_defs; @@ -7704,18 +7697,14 @@ fn register_external_callable_context( function: &FunctionDef, resolver: &ModuleResolver, preferred_label: Option<&str>, - external_function_effects: &mut HashMap, - external_function_param_types: &mut HashMap>, - external_function_return_types: &mut HashMap>, - call_target_labels: &mut HashMap, - external_callable_abis: &mut Vec, - external_callable_names: &mut HashSet, + context: &mut ExternalCallableContext, ) { let symbol = function_def_name(function); let full_path = format!("{}::{}", owner_module, symbol); - let label = call_target_labels + let label = context + .target_labels .get(&full_path) - .or_else(|| call_target_labels.get(source_name)) + .or_else(|| context.target_labels.get(source_name)) .cloned() .or_else(|| preferred_label.map(str::to_string)) .unwrap_or_else(|| callable_label_for(consumer_module, owner_module, symbol)); @@ -7724,14 +7713,14 @@ fn register_external_callable_context( let return_type = function_def_return_type(function); for key in [source_name.to_string(), full_path.clone(), label.clone()] { - external_function_effects.insert(key.clone(), effect); - external_function_param_types.insert(key.clone(), params.clone()); - external_function_return_types.insert(key, return_type.clone()); + context.function_effects.insert(key.clone(), effect); + context.function_param_types.insert(key.clone(), params.clone()); + context.function_return_types.insert(key, return_type.clone()); } - call_target_labels.insert(source_name.to_string(), label.clone()); - call_target_labels.entry(full_path).or_insert_with(|| label.clone()); + context.target_labels.insert(source_name.to_string(), label.clone()); + context.target_labels.entry(full_path).or_insert_with(|| label.clone()); if owner_module != consumer_module { - push_external_callable_abi(external_callable_abis, external_callable_names, label, function); + push_external_callable_abi(&mut context.abis, &mut context.names, label, function); } } diff --git a/src/lib.rs b/src/lib.rs index 6692991a..b6cfe7f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,6 @@ //! CellScript - Domain-specific language compiler for CKB blockchain //! Currently the backend can output RISC-V assembly or ELF artifacts. -// Rust 2024 makes let-chains available, so Clippy 1.97 newly proposes folding -// a large legacy control-flow surface. Keep that mechanical rewrite separate -// from the edition migration so each semantic branch remains reviewable. -#![allow(clippy::collapsible_if, clippy::collapsible_match, clippy::ptr_arg, clippy::too_many_arguments)] - pub(crate) mod aggregate_lowering; pub mod assumptions; pub mod ast; @@ -21,6 +16,7 @@ pub mod codegen; #[cfg(not(feature = "wasm"))] pub mod debug; pub mod docgen; +pub mod edition; pub mod error; pub mod flow; pub mod fmt; @@ -40,9 +36,17 @@ pub mod runtime_errors; pub mod simulate; pub mod stdlib; pub mod types; +mod verified_artifact; pub mod wasm; pub use assumptions::{BuilderAssumptionMetadata, TxValidationReport, TxValidationViolation}; +pub use cellscript_artifact_checker::{ + CheckerBudgets, CheckerReport, SourceArtifactMap, VerifiedArtifactMetadata, VerifiedArtifactState, VerifiedLoweringRecord, + CHECKER_POLICY_SCHEMA, CHECKER_VERSION, LOWERING_RECORD_SCHEMA, SOURCE_MAP_SCHEMA, +}; +pub use edition::{ + resolve_compatibility_profile, CellScriptEdition, ResolvedCompatibilityProfile, COMPATIBILITY_PROFILE_SCHEMA, CURRENT_EDITION, +}; pub use proof_plan::soundness::{ProofPlanSoundnessIssue, ProofPlanSoundnessReport}; pub use proof_plan::{EvidenceTier, ProofPlanDiagnosticMetadata, ProofPlanMetadata, ProofPlanSourceSpanMetadata}; @@ -56,6 +60,9 @@ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; /// Compile options #[derive(Debug, Clone, Default)] pub struct CompileOptions { + /// Source-language edition for in-memory or standalone-file compilation. + /// Package compilation uses the mandatory edition in `Cell.toml`. + pub edition: CellScriptEdition, /// Optimization level (0-3) pub opt_level: u8, /// Output file path @@ -203,11 +210,11 @@ fn strict_capability_name(capability: ast::Capability) -> &'static str { const DEFAULT_TARGET: &str = "riscv64-asm"; const DEFAULT_TARGET_PROFILE: &str = "ckb"; -const ARTIFACT_CACHE_VERSION: &str = "project-source-set-v8"; -pub const METADATA_SCHEMA_VERSION: u32 = 55; -pub const SOURCE_METADATA_SCHEMA_VERSION: u32 = 1; +const ARTIFACT_CACHE_VERSION: &str = "project-source-set-v10-verified-artifact"; +pub const METADATA_SCHEMA_VERSION: u32 = 58; +pub const SOURCE_METADATA_SCHEMA_VERSION: u32 = 2; pub const ARTIFACT_METADATA_SCHEMA_VERSION: u32 = 1; -pub const CONSTRAINTS_METADATA_SCHEMA_VERSION: u32 = 1; +pub const CONSTRAINTS_METADATA_SCHEMA_VERSION: u32 = 2; /// Maximum UTF-8 source bytes accepted by a single compiler input. /// /// This is a process-safety boundary shared by native, LSP, and WASM callers. @@ -215,6 +222,12 @@ pub const MAX_SOURCE_BYTES: usize = 1024 * 1024; const STACK_COLLECTION_BACKING_BYTES: usize = 256; pub const ENTRY_WITNESS_ABI: &str = "cellscript-entry-witness-v1"; pub(crate) const ENTRY_WITNESS_ABI_MAGIC: &[u8; 8] = b"CSARGv1\0"; +/// Versioned CKB placement contract for parameterized entry payloads. +pub const ENTRY_WITNESS_PLACEMENT_ABI: &str = "cellscript-witnessargs-input-type-v2"; +/// Canonical `WitnessArgs` field owned by the CellScript entry placement ABI. +pub const ENTRY_WITNESS_PLACEMENT_FIELD: &str = "input_type"; +/// Script-group-relative witness lookup order used by generated CKB entries. +pub const ENTRY_WITNESS_PLACEMENT_SOURCE: &str = "group-input-0-then-group-output-0"; pub const CKB_DEFAULT_HASH_PERSONALIZATION: &[u8; 16] = b"ckb-default-hash"; pub const CKB_BLANK_HASH: [u8; 32] = [ 68, 244, 198, 151, 68, 213, 248, 197, 93, 100, 32, 98, 148, 157, 202, 228, 155, 196, 231, 239, 67, 211, 136, 197, 161, 47, 66, @@ -284,7 +297,7 @@ impl TargetProfile { }, header_abi: "ckb-header".to_string(), scheduler_abi: "none".to_string(), - witness_abi: "ckb-molecule-witness-args+cellscript-entry-witness-v1".to_string(), + witness_abi: "ckb-molecule-witness-args-input-type-v2+cellscript-entry-witness-v1".to_string(), lock_args_abi: "ckb-script-args-typed-fixed-bytes".to_string(), source_encoding: "ckb-source-group-high-bit".to_string(), spawn_ipc_abi: "ckb-vm-v2-spawn-ipc-syscalls-2601-2608".to_string(), @@ -357,6 +370,11 @@ pub struct CompileResult { pub metadata: CompileMetadata, /// Parsed AST (for simulation, etc.) pub ast: crate::ast::Module, + /// Canonical verified-lowering sidecar for ELF artifacts. + pub verified_lowering_record: Option, + /// Canonical source-to-artifact sidecar for ELF artifacts. + pub source_artifact_map: Option, + pub(crate) verified_artifact_draft: Option, /// Whether this result was served from the incremental compilation cache pub cache_hit: bool, } @@ -384,6 +402,8 @@ pub struct CompileMetadata { #[serde(default = "missing_metadata_component_schema_version")] pub constraints_metadata_schema_version: u32, pub compiler_version: String, + pub edition: CellScriptEdition, + pub compatibility_profile: ResolvedCompatibilityProfile, pub module: String, pub artifact_format: String, pub target_profile: TargetProfileMetadata, @@ -397,6 +417,8 @@ pub struct CompileMetadata { pub source_content_hash: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub source_units: Vec, + #[serde(default)] + pub verified_artifact: VerifiedArtifactMetadata, pub lowering: LoweringMetadata, #[serde(default)] pub capability_registry: CapabilityRegistryMetadata, @@ -597,6 +619,8 @@ pub struct TemplateLayoutLeafSchemaMetadata { #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ConstraintsMetadata { + pub edition: CellScriptEdition, + pub compatibility_profile: String, pub target_profile: String, pub status: String, pub entry_abi: Vec, @@ -1101,6 +1125,24 @@ pub fn validate_compile_metadata(metadata: &CompileMetadata, artifact_format: Ar metadata.compiler_version, VERSION ))); } + let primitive_assurance = (metadata.compatibility_profile.primitive_assurance != "default") + .then_some(metadata.compatibility_profile.primitive_assurance.as_str()); + let expected_compatibility_profile = + resolve_compatibility_profile(metadata.edition, &metadata.target_profile.name, primitive_assurance); + if metadata.compatibility_profile != expected_compatibility_profile { + return Err(CompileError::without_span(format!( + "metadata compatibility_profile '{}' does not match the resolved compatibility axes for edition {} and target profile '{}'", + metadata.compatibility_profile.id, metadata.edition, metadata.target_profile.name + ))); + } + if !metadata.constraints.status.is_empty() + && (metadata.constraints.edition != metadata.edition + || metadata.constraints.compatibility_profile != metadata.compatibility_profile.id) + { + return Err(CompileError::without_span( + "metadata constraints edition/compatibility_profile does not match the top-level compile identity", + )); + } if metadata.artifact_format != artifact_format.display_name() { return Err(CompileError::without_span(format!( @@ -2038,6 +2080,8 @@ fn constraints_metadata( .to_string(); ConstraintsMetadata { + edition: metadata.edition, + compatibility_profile: metadata.compatibility_profile.id.clone(), target_profile: metadata.target_profile.name.clone(), status, entry_abi, @@ -2570,6 +2614,11 @@ fn bind_source_metadata(metadata: &mut CompileMetadata, mut source_units: Vec) -> Result<()> { + bind_source_metadata(&mut result.metadata, source_units); + result.refresh_verified_artifact_boundary() +} + fn append_source_hash_material(out: &mut Vec, unit: &SourceUnitMetadata, include_path: bool) { out.extend_from_slice(unit.role.as_bytes()); out.push(0); @@ -3991,13 +4040,13 @@ fn validate_template_layout_metadata(metadata: &CompileMetadata) -> Result<()> { layout.type_name ))); } - if let Some(root_hash) = layout.root_hash.as_deref() { - if !is_canonical_hash_hex(root_hash) { - return Err(CompileError::without_span(format!( - "metadata template_layout '{}' root_hash '{}' is not a canonical 32-byte lowercase hex hash", - layout.type_name, root_hash - ))); - } + if let Some(root_hash) = layout.root_hash.as_deref() + && !is_canonical_hash_hex(root_hash) + { + return Err(CompileError::without_span(format!( + "metadata template_layout '{}' root_hash '{}' is not a canonical 32-byte lowercase hex hash", + layout.type_name, root_hash + ))); } if !is_canonical_hash_hex(&layout.template_layout_hash) { return Err(CompileError::without_span(format!( @@ -4099,6 +4148,14 @@ pub fn validate_compile_result(result: &CompileResult) -> Result<()> { if vm_abi_trailer_version(&result.artifact_bytes)?.is_some() { return Err(CompileError::without_span("RISC-V assembly artifacts must not embed a VM ABI trailer")); } + if result.metadata.verified_artifact.state != VerifiedArtifactState::NotEmittedNonElf + || result.verified_lowering_record.is_some() + || result.source_artifact_map.is_some() + { + return Err(CompileError::without_span( + "RISC-V assembly result must not claim or carry the ELF verified-artifact boundary", + )); + } } ArtifactFormat::RiscvElf => { if !result.artifact_bytes.starts_with(b"\x7fELF") { @@ -4126,6 +4183,18 @@ pub fn validate_compile_result(result: &CompileResult) -> Result<()> { } None => {} } + if result.metadata.verified_artifact.state != VerifiedArtifactState::Emitted { + return Err(CompileError::without_span("RISC-V ELF metadata does not claim emitted verified-artifact sidecars")); + } + let record = result + .verified_lowering_record + .as_ref() + .ok_or_else(|| CompileError::without_span("RISC-V ELF result is missing its verified lowering record"))?; + let source_map = result + .source_artifact_map + .as_ref() + .ok_or_else(|| CompileError::without_span("RISC-V ELF result is missing its source artifact map"))?; + verified_artifact::validate_boundary_values(&result.artifact_bytes, &result.metadata, record, source_map)?; } } @@ -4408,12 +4477,10 @@ impl ActionMetadata { pub fn scheduler_witness_bytes(&self) -> Result> { let scheduler_witness_hex = non_empty_metadata_field(&self.scheduler_witness_hex); let scheduler_witness_molecule_hex = non_empty_metadata_field(&self.scheduler_witness_molecule_hex); - if let (Some(primary), Some(alias)) = (scheduler_witness_hex, scheduler_witness_molecule_hex) { - if primary != alias { - return Err(CompileError::without_span( - "conflicting scheduler_witness_hex and scheduler_witness_molecule_hex metadata", - )); - } + if let (Some(primary), Some(alias)) = (scheduler_witness_hex, scheduler_witness_molecule_hex) + && primary != alias + { + return Err(CompileError::without_span("conflicting scheduler_witness_hex and scheduler_witness_molecule_hex metadata")); } if let Some(scheduler_witness_hex) = scheduler_witness_hex { if self.scheduler_witness_abi != SCHEDULER_WITNESS_ABI_MOLECULE { @@ -5052,6 +5119,7 @@ pub struct LoadedModule { pub path: Utf8PathBuf, pub source: String, pub ast: ast::Module, + pub edition: CellScriptEdition, } #[derive(Debug)] @@ -5073,6 +5141,24 @@ impl CompileResult { validate_compile_result(self) } + pub(crate) fn refresh_verified_artifact_boundary(&mut self) -> Result<()> { + if self.artifact_format != ArtifactFormat::RiscvElf { + self.metadata.verified_artifact = VerifiedArtifactMetadata::default(); + self.verified_lowering_record = None; + self.source_artifact_map = None; + return Ok(()); + } + let draft = self.verified_artifact_draft.as_ref().ok_or_else(|| { + CompileError::without_span("RISC-V ELF result is missing the verified-artifact draft").with_code("E2400") + })?; + let (record, source_map, boundary) = + verified_artifact::build_verified_artifact_boundary(&self.artifact_bytes, &self.metadata, draft)?; + self.metadata.verified_artifact = boundary; + self.verified_lowering_record = Some(record); + self.source_artifact_map = Some(source_map); + Ok(()) + } + /// Default output path pub fn default_output_path(&self, input_path: &Utf8Path) -> Utf8PathBuf { input_path.with_extension(self.artifact_format.file_extension()) @@ -5102,6 +5188,58 @@ impl CompileResult { metadata_output_path_from_artifact(artifact_path) } + pub fn default_lowering_record_path(&self, artifact_path: &Utf8Path) -> Utf8PathBuf { + lowering_record_output_path_from_artifact(artifact_path) + } + + pub fn default_source_map_path(&self, artifact_path: &Utf8Path) -> Utf8PathBuf { + source_map_output_path_from_artifact(artifact_path) + } + + pub fn write_verified_artifact_sidecars(&self, artifact_path: &Utf8Path) -> Result> { + if self.artifact_format != ArtifactFormat::RiscvElf { + return Ok(None); + } + self.validate()?; + let record = self + .verified_lowering_record + .as_ref() + .ok_or_else(|| CompileError::without_span("ELF result is missing verified lowering record").with_code("E2400"))?; + let source_map = self + .source_artifact_map + .as_ref() + .ok_or_else(|| CompileError::without_span("ELF result is missing source artifact map").with_code("E2400"))?; + let record_path = self.default_lowering_record_path(artifact_path); + let source_map_path = self.default_source_map_path(artifact_path); + if let Some(parent) = record_path.parent() { + std::fs::create_dir_all(parent).map_err(|error| { + CompileError::new( + format!("failed to create verified artifact sidecar directory '{}': {}", parent, error), + error::Span::default(), + ) + .with_category(error::CompileErrorCategory::Io) + .with_source(error) + })?; + } + let record_bytes = cellscript_artifact_checker::canonical_bytes(record) + .map_err(|error| CompileError::without_span(error.to_string()).with_code("E2400"))?; + let source_map_bytes = cellscript_artifact_checker::canonical_bytes(source_map) + .map_err(|error| CompileError::without_span(error.to_string()).with_code("E2400"))?; + std::fs::write(&record_path, record_bytes).map_err(|error| { + CompileError::new(format!("failed to write lowering record '{}': {}", record_path, error), error::Span::default()) + .with_category(error::CompileErrorCategory::Io) + .with_file(record_path.clone()) + .with_source(error) + })?; + std::fs::write(&source_map_path, source_map_bytes).map_err(|error| { + CompileError::new(format!("failed to write source map '{}': {}", source_map_path, error), error::Span::default()) + .with_category(error::CompileErrorCategory::Io) + .with_file(source_map_path.clone()) + .with_source(error) + })?; + Ok(Some((record_path, source_map_path))) + } + pub fn write_metadata_to_path(&self, output_path: &Utf8Path) -> Result<()> { self.validate()?; if let Some(parent) = output_path.parent() { @@ -5213,6 +5351,7 @@ fn load_project_for_entry_diagnostics( fn load_virtual_project_for_entry_diagnostics( sources: &[InMemorySource], entry_path: &str, + edition: CellScriptEdition, ) -> std::result::Result> { if sources.is_empty() { return Err(vec![CompileError::without_span("multi-file compile requires at least one source")]); @@ -5237,7 +5376,7 @@ fn load_virtual_project_for_entry_diagnostics( diagnostics.push(CompileError::without_span(format!("duplicate multi-file compile source path '{}'", source.path))); continue; } - match parse_loaded_module_diagnostics(Utf8PathBuf::from(source.path.clone()), source.source.clone()) { + match parse_loaded_module_diagnostics(Utf8PathBuf::from(source.path.clone()), source.source.clone(), edition) { Ok(module) => modules.push(module), Err(errors) => diagnostics.extend(errors), } @@ -5258,6 +5397,7 @@ fn load_project_modules_for_entry(entry_path: &Utf8Path, entry_source_override: source_paths .into_iter() .map(|path| { + let edition = source_edition(&path)?; let source = if path == entry_path { if let Some(source) = entry_source_override { source.to_string() @@ -5267,7 +5407,7 @@ fn load_project_modules_for_entry(entry_path: &Utf8Path, entry_source_override: } else { read_module_source(&path)? }; - parse_loaded_module(path, source) + parse_loaded_module(path, source, edition) }) .collect() } @@ -5281,6 +5421,13 @@ fn load_project_modules_for_entry_diagnostics( let mut modules = Vec::with_capacity(source_paths.len()); let mut diagnostics = Vec::new(); for path in source_paths { + let edition = match source_edition(&path) { + Ok(edition) => edition, + Err(error) => { + diagnostics.push(error); + continue; + } + }; let source = if path == entry_path { if let Some(source) = entry_source_override { source.to_string() @@ -5302,7 +5449,7 @@ fn load_project_modules_for_entry_diagnostics( } } }; - match parse_loaded_module_diagnostics(path, source) { + match parse_loaded_module_diagnostics(path, source, edition) { Ok(module) => modules.push(module), Err(errors) => diagnostics.extend(errors), } @@ -5319,17 +5466,28 @@ fn read_module_source(path: &Utf8Path) -> Result { .map_err(|e| CompileError::new(format!("failed to read module '{}': {}", path, e), error::Span::default())) } -fn parse_loaded_module(path: Utf8PathBuf, source: String) -> Result { +fn parse_loaded_module(path: Utf8PathBuf, source: String, edition: CellScriptEdition) -> Result { let tokens = lexer::lex(&source).map_err(|e| e.with_file(path.clone()))?; let ast = parser::parse(&tokens).map_err(|e| e.with_file(path.clone()))?; - Ok(LoadedModule { path, source, ast }) + Ok(LoadedModule { path, source, ast, edition }) } -fn parse_loaded_module_diagnostics(path: Utf8PathBuf, source: String) -> std::result::Result> { +fn parse_loaded_module_diagnostics( + path: Utf8PathBuf, + source: String, + edition: CellScriptEdition, +) -> std::result::Result> { let tokens = lexer::lex(&source).map_err(|e| vec![e.with_file(path.clone())])?; let ast = parser::parse_diagnostics(&tokens) .map_err(|errors| errors.into_iter().map(|error| error.with_file(path.clone())).collect::>())?; - Ok(LoadedModule { path, source, ast }) + Ok(LoadedModule { path, source, ast, edition }) +} + +fn source_edition(path: &Utf8Path) -> Result { + find_package_root(path)? + .map(|root| load_manifest(&root).map(|manifest| manifest.package.edition)) + .transpose() + .map(|edition| edition.unwrap_or(CURRENT_EDITION)) } fn build_module_resolver_from_loaded_modules(modules: &[LoadedModule]) -> Result { @@ -5373,7 +5531,7 @@ pub fn compile(source: &str, options: CompileOptions) -> Result { let ast = parser::parse(&tokens)?; let mut result = compile_ast(&ast, &options, None)?; - bind_source_metadata(&mut result.metadata, vec![source_unit_from_bytes("", "memory", source.as_bytes())]); + bind_compile_result_source_metadata(&mut result, vec![source_unit_from_bytes("", "memory", source.as_bytes())])?; result.validate()?; Ok(result) } @@ -5384,7 +5542,7 @@ pub fn compile_fungible_type_group_entry(source: &str, options: CompileOptions) let tokens = lexer::lex(source)?; let ast = parser::parse(&tokens)?; let mut result = compile_ast_with_build(&ast, &options, None, None, Some(&CompileEntryScope::FungibleTypeGroupV1))?; - bind_source_metadata(&mut result.metadata, vec![source_unit_from_bytes("", "memory", source.as_bytes())]); + bind_compile_result_source_metadata(&mut result, vec![source_unit_from_bytes("", "memory", source.as_bytes())])?; result.validate()?; Ok(result) } @@ -5400,13 +5558,13 @@ pub fn compile_fungible_type_group_entry_for( let ast = parser::parse(&tokens)?; let scope = CompileEntryScope::FungibleTypeGroupV1For(type_name.into()); let mut result = compile_ast_with_build(&ast, &options, None, None, Some(&scope))?; - bind_source_metadata(&mut result.metadata, vec![source_unit_from_bytes("", "memory", source.as_bytes())]); + bind_compile_result_source_metadata(&mut result, vec![source_unit_from_bytes("", "memory", source.as_bytes())])?; result.validate()?; Ok(result) } /// Only generate compile metadata, without asm/elf artifact. -pub fn compile_metadata(source: &str, target: Option) -> Result { +pub fn compile_metadata(source: &str, edition: CellScriptEdition, target: Option) -> Result { let tokens = lexer::lex(source)?; let ast = parser::parse(&tokens)?; let artifact_format = ArtifactFormat::from_target(target.as_deref().unwrap_or(DEFAULT_TARGET))?; @@ -5414,7 +5572,7 @@ pub fn compile_metadata(source: &str, target: Option) -> Result", "memory", source.as_bytes())]); validate_compile_metadata(&metadata, artifact_format)?; Ok(metadata) @@ -5444,7 +5602,11 @@ pub struct InMemorySource { /// Lexer failures remain fatal single diagnostics. Parser recovery collects /// independent item and statement errors, then semantic phases collect type, /// flow, and IR diagnostics when parsing succeeds. -pub fn compile_metadata_with_diagnostics(source: &str, target: Option) -> CompileMetadataDiagnosticReport { +pub fn compile_metadata_with_diagnostics( + source: &str, + edition: CellScriptEdition, + target: Option, +) -> CompileMetadataDiagnosticReport { let tokens = match lexer::lex(source) { Ok(tokens) => tokens, Err(error) => return CompileMetadataDiagnosticReport { metadata: None, diagnostics: vec![error] }, @@ -5476,7 +5638,7 @@ pub fn compile_metadata_with_diagnostics(source: &str, target: Option) - return CompileMetadataDiagnosticReport { metadata: None, diagnostics }; } }; - let mut metadata = compile_metadata_from_ir(&ir, artifact_format, target_profile); + let mut metadata = compile_metadata_from_ir(&ir, artifact_format, target_profile, edition, None); bind_source_metadata(&mut metadata, vec![source_unit_from_bytes("", "memory", source.as_bytes())]); if let Err(error) = validate_compile_metadata(&metadata, artifact_format) { diagnostics.push(error); @@ -5489,13 +5651,14 @@ pub fn compile_metadata_with_diagnostics(source: &str, target: Option) - pub fn compile_sources_metadata_with_diagnostics( sources: &[InMemorySource], entry_path: &str, + edition: CellScriptEdition, target: Option, ) -> CompileMetadataDiagnosticReport { - let project = match load_virtual_project_for_entry_diagnostics(sources, entry_path) { + let project = match load_virtual_project_for_entry_diagnostics(sources, entry_path, edition) { Ok(project) => project, Err(diagnostics) => return CompileMetadataDiagnosticReport { metadata: None, diagnostics }, }; - let options = CompileOptions { target, ..CompileOptions::default() }; + let options = CompileOptions { edition, target, ..CompileOptions::default() }; let artifact_format = match ArtifactFormat::from_target(resolve_target(&options, None)) { Ok(format) => format, Err(error) => return CompileMetadataDiagnosticReport { metadata: None, diagnostics: vec![error] }, @@ -5515,7 +5678,7 @@ pub fn compile_sources_metadata_with_diagnostics( return CompileMetadataDiagnosticReport { metadata: None, diagnostics }; } }; - let mut metadata = compile_metadata_from_ir(&ir, artifact_format, target_profile); + let mut metadata = compile_metadata_from_ir(&ir, artifact_format, target_profile, edition, None); let source_units = sources .iter() .map(|source| { @@ -5567,17 +5730,20 @@ pub fn compile_path_metadata_with_diagnostics_for_source>( fn compile_file_metadata_with_diagnostics( path: &Utf8Path, - options: CompileOptions, + mut options: CompileOptions, entry_source_override: Option<&str>, ) -> CompileMetadataDiagnosticReport { - let project = match load_project_for_entry_diagnostics(path, entry_source_override) { - Ok(project) => project, - Err(diagnostics) => return CompileMetadataDiagnosticReport { metadata: None, diagnostics }, - }; let manifest = match find_package_root(path).and_then(|root| root.map(|root| load_manifest(&root)).transpose()) { Ok(manifest) => manifest, Err(error) => return CompileMetadataDiagnosticReport { metadata: None, diagnostics: vec![error] }, }; + if let Some(manifest) = manifest.as_ref() { + options.edition = manifest.package.edition; + } + let project = match load_project_for_entry_diagnostics(path, entry_source_override) { + Ok(project) => project, + Err(diagnostics) => return CompileMetadataDiagnosticReport { metadata: None, diagnostics }, + }; let build = manifest.as_ref().map(|manifest| &manifest.build); if let Err(error) = validate_compile_options(&options) { @@ -5610,7 +5776,8 @@ fn compile_file_metadata_with_diagnostics( return CompileMetadataDiagnosticReport { metadata: None, diagnostics }; } }; - let mut metadata = compile_metadata_from_ir(&ir, artifact_format, target_profile); + let mut metadata = + compile_metadata_from_ir(&ir, artifact_format, target_profile, options.edition, options.primitive_compat.as_deref()); match collect_source_units_for_compile_file(path).and_then(|source_units| { bind_source_metadata(&mut metadata, source_units); if let Some(manifest) = manifest.as_ref() { @@ -5646,10 +5813,10 @@ fn project_frontend_diagnostics(project: &LoadedProject, options: &CompileOption } let module_error_start = diagnostics.len(); - if options.is_primitive_strict_015() { - if let Err(error) = check_primitive_strict_015(&module.ast) { - diagnostics.push(attach_file_if_missing(error, &module.path)); - } + if options.is_primitive_strict_015() + && let Err(error) = check_primitive_strict_015(&module.ast) + { + diagnostics.push(attach_file_if_missing(error, &module.path)); } diagnostics.extend(attach_default_file( @@ -5660,10 +5827,10 @@ fn project_frontend_diagnostics(project: &LoadedProject, options: &CompileOption let module_has_errors = diagnostics[module_error_start..].iter().any(|diagnostic| diagnostic.severity == DiagnosticSeverity::Error); - if !module_has_errors { - if let Err(errors) = ir::generate_with_resolver_diagnostics(&module.ast, &project.resolver, &module.ast.name) { - diagnostics.extend(attach_default_file(errors, &module.path)); - } + if !module_has_errors + && let Err(errors) = ir::generate_with_resolver_diagnostics(&module.ast, &project.resolver, &module.ast.name) + { + diagnostics.extend(attach_default_file(errors, &module.path)); } } diagnostics @@ -5696,7 +5863,7 @@ action bad_two() -> bool { return 1 } "#; - let report = compile_metadata_with_diagnostics(source, None); + let report = compile_metadata_with_diagnostics(source, CURRENT_EDITION, None); assert!(report.metadata.is_none()); assert_eq!(report.diagnostics.len(), 2); assert!(report.diagnostics.iter().any(|error| error.message.contains("expected U64, found Bool"))); @@ -5715,7 +5882,7 @@ action bad() -> bool { return true } "#; - let report = compile_metadata_with_diagnostics(source, None); + let report = compile_metadata_with_diagnostics(source, CURRENT_EDITION, None); assert!(report.metadata.is_none()); assert_eq!(report.diagnostics.len(), 2); assert!(report.diagnostics.iter().any(|error| error.message.contains("expected '=', found 'true'"))); @@ -5754,7 +5921,7 @@ action also_bad() -> bool { .to_string(), }, ]; - let report = compile_sources_metadata_with_diagnostics(&sources, "src/main.cell", None); + let report = compile_sources_metadata_with_diagnostics(&sources, "src/main.cell", CURRENT_EDITION, None); assert!(report.metadata.is_none()); assert_eq!(report.diagnostics.len(), 2); assert!(report.diagnostics.iter().any(|error| error.file.as_ref().is_some_and(|file| file.as_str() == "src/main.cell"))); @@ -5773,7 +5940,7 @@ action bad() -> bool { return true } "#; - let report = compile_metadata_with_diagnostics(source, None); + let report = compile_metadata_with_diagnostics(source, CURRENT_EDITION, None); assert!(report.metadata.is_none()); assert_eq!(report.diagnostics.len(), 2); assert!(report.diagnostics.iter().any(|error| error.message.contains("expected U64, found Bool"))); @@ -5790,7 +5957,7 @@ action bad() -> bool { return 1 } "#; - let report = compile_metadata_with_diagnostics(source, None); + let report = compile_metadata_with_diagnostics(source, CURRENT_EDITION, None); assert!(report.metadata.is_none()); assert_eq!(report.diagnostics.len(), 1); let diagnostic = &report.diagnostics[0]; @@ -5827,7 +5994,7 @@ action issue_two(amount: u64) -> Token { return out } "#; - let report = compile_metadata_with_diagnostics(source, None); + let report = compile_metadata_with_diagnostics(source, CURRENT_EDITION, None); assert!(report.metadata.is_none()); assert_eq!(report.diagnostics.len(), 2); assert!(report.diagnostics.iter().any(|error| error.message.contains("action 'issue_one'"))); @@ -5900,7 +6067,8 @@ fn compile_ast_with_build( }; let ir = scoped_ir.as_ref().unwrap_or(&ir); - let mut metadata = compile_metadata_from_ir(ir, artifact_format, target_profile); + let mut metadata = + compile_metadata_from_ir(ir, artifact_format, target_profile, options.edition, options.primitive_compat.as_deref()); let target_policy_violations = target_profile_artifact_policy_violations(&metadata, target_profile); if !target_policy_violations.is_empty() { return Err(CompileError::without_span(format!( @@ -5912,13 +6080,14 @@ fn compile_ast_with_build( // 5. Code generation let codegen_options = codegen::CodegenOptions { opt_level: options.opt_level, debug: options.debug, target_profile }; - let mut artifact_bytes = codegen::generate(ir, &codegen_options, artifact_format).map_err(|error| { + let generated = codegen::generate_with_evidence(ir, &codegen_options, artifact_format).map_err(|error| { if error.code.is_some() { error } else { error.with_code("E2000") } })?; + let mut artifact_bytes = generated.bytes; if artifact_bytes.is_empty() { return Err(CompileError::new("backend produced an empty artifact", error::Span::default()).with_code("E2001")); } @@ -5962,9 +6131,19 @@ fn compile_ast_with_build( validate_primitive_strict_017_metadata(&metadata)?; } - let result = CompileResult { artifact_bytes, artifact_format, artifact_hash, metadata, ast: ast.clone(), cache_hit: false }; - result.validate()?; - Ok(result) + let verified_artifact_draft = + generated.machine_layout.map(|layout| verified_artifact::VerifiedArtifactDraft::new(layout, lowering_ast)); + Ok(CompileResult { + artifact_bytes, + artifact_format, + artifact_hash, + metadata, + ast: ast.clone(), + verified_lowering_record: None, + source_artifact_map: None, + verified_artifact_draft, + cache_hit: false, + }) } /// Compile from file, package directory, or Cell.toml @@ -6048,24 +6227,27 @@ pub fn compile_path_with_fungible_type_group_entry_for>( fn compile_file_with_entry_scope>( path: P, - options: CompileOptions, + mut options: CompileOptions, entry_scope: Option, ) -> Result { let path = path.as_ref(); let path = canonical_utf8_path(path)?; + let manifest = find_package_root(&path)?.map(|root| load_manifest(&root)).transpose()?; + if let Some(manifest) = manifest.as_ref() { + options.edition = manifest.package.edition; + } let source_units = collect_source_units_for_compile_file(&path)?; let cache_units = collect_cache_units_for_compile_file(&path, &source_units)?; // Incremental compilation: skip recompilation if cache hit and source unchanged. // Cache is only used for default entry scope (no --entry-action / --entry-lock). - if entry_scope.is_none() { - if let Some(cached) = incremental_cache_hit(&path, &cache_units, &options) { - return Ok(cached); - } + if entry_scope.is_none() + && let Some(cached) = incremental_cache_hit(&path, &cache_units, &options) + { + return Ok(cached); } let project = load_project_for_entry(&path, None)?; - let manifest = find_package_root(&path)?.map(|root| load_manifest(&root)).transpose()?; let diagnostics = project_frontend_diagnostics(&project, &options, manifest.is_some()); if diagnostics.iter().any(|diagnostic| diagnostic.severity == DiagnosticSeverity::Error) { return Err(diagnostics_to_compile_error(diagnostics)); @@ -6080,7 +6262,7 @@ fn compile_file_with_entry_scope>( manifest.as_ref().map(|manifest| &manifest.build), entry_scope.as_ref(), )?; - bind_source_metadata(&mut result.metadata, source_units); + bind_compile_result_source_metadata(&mut result, source_units)?; if let Some(manifest) = manifest.as_ref() { apply_manifest_deploy_metadata(&mut result.metadata, manifest)?; } @@ -6118,6 +6300,8 @@ fn incremental_cache_hit(path: &Utf8Path, cache_units: &[SourceUnitMetadata], op let artifact_path = entry_dir.join("artifact"); let metadata_path = entry_dir.join("metadata.json"); + let lowering_record_path = entry_dir.join("lowering.json"); + let source_map_path = entry_dir.join("sourcemap.json"); if !artifact_path.exists() || !metadata_path.exists() { return None; @@ -6135,6 +6319,16 @@ fn incremental_cache_hit(path: &Utf8Path, cache_units: &[SourceUnitMetadata], op let artifact_bytes = std::fs::read(&artifact_path).ok()?; let metadata_json = std::fs::read_to_string(&metadata_path).ok()?; let metadata: CompileMetadata = serde_json::from_str(&metadata_json).ok()?; + let (verified_lowering_record, source_artifact_map) = if metadata.artifact_format == "RISC-V ELF" { + let lowering_bytes = std::fs::read(&lowering_record_path).ok()?; + let source_map_bytes = std::fs::read(&source_map_path).ok()?; + ( + Some(cellscript_artifact_checker::parse_lowering_record(&lowering_bytes, &CheckerBudgets::default()).ok()?), + Some(cellscript_artifact_checker::parse_source_map(&source_map_bytes, &CheckerBudgets::default()).ok()?), + ) + } else { + (None, None) + }; let artifact_hash: [u8; 32] = { let hash_hex = metadata.artifact_hash.as_deref().unwrap_or(""); @@ -6150,6 +6344,9 @@ fn incremental_cache_hit(path: &Utf8Path, cache_units: &[SourceUnitMetadata], op artifact_hash, metadata, ast: ast::Module { name: String::new(), items: Vec::new(), span: crate::error::Span { start: 0, end: 0, line: 0, column: 0 } }, + verified_lowering_record, + source_artifact_map, + verified_artifact_draft: None, cache_hit: true, }; result.validate().ok()?; @@ -6167,6 +6364,16 @@ fn incremental_cache_store(path: &Utf8Path, cache_units: &[SourceUnitMetadata], let _ = std::fs::write(entry_dir.join("artifact"), &result.artifact_bytes); let metadata_json = serde_json::to_string_pretty(&result.metadata).unwrap_or_default(); let _ = std::fs::write(entry_dir.join("metadata.json"), metadata_json); + if let Some(record) = &result.verified_lowering_record + && let Ok(bytes) = cellscript_artifact_checker::canonical_bytes(record) + { + let _ = std::fs::write(entry_dir.join("lowering.json"), bytes); + } + if let Some(source_map) = &result.source_artifact_map + && let Ok(bytes) = cellscript_artifact_checker::canonical_bytes(source_map) + { + let _ = std::fs::write(entry_dir.join("sourcemap.json"), bytes); + } let source_hash = source_set_hash(cache_units); let _ = std::fs::write(entry_dir.join("source_hash"), &source_hash); if let Ok(cache_units_json) = serde_json::to_string_pretty(cache_units) { @@ -6200,6 +6407,10 @@ fn incremental_cache_key(cache_units: &[SourceUnitMetadata], options: &CompileOp key_input.push_str(&format!("-O{}", options.opt_level)); key_input.push_str(&format!("-{}", options.target.as_deref().unwrap_or("default"))); key_input.push_str(&format!("-{}", options.target_profile.as_deref().unwrap_or("default"))); + key_input.push_str(&format!("-edition-{}", options.edition)); + let target_profile = options.target_profile.as_deref().unwrap_or(DEFAULT_TARGET_PROFILE); + let compatibility_profile = resolve_compatibility_profile(options.edition, target_profile, options.primitive_compat.as_deref()); + key_input.push_str(&format!("-compatibility-profile-{}", compatibility_profile.id)); key_input.push_str(&format!("-debug{}", options.debug)); key_input.push_str(&format!("-primitive-{}", options.primitive_compat.as_deref().unwrap_or("default"))); hex_encode(&ckb_blake2b256(key_input.as_bytes())) @@ -6248,7 +6459,12 @@ fn collect_source_paths_for_compile_file(entry_path: &Utf8Path) -> Result, package_roots: &mut BTreeSet, ) -> Result<()> { @@ -6314,14 +6536,18 @@ fn collect_package_roots_recursive( return Ok(()); } package_roots.insert(package_root.clone()); - for dep_root in local_dependency_roots(&package_root)? { - collect_package_roots_recursive(&dep_root, visited_roots, package_roots)?; + for dep_root in local_dependency_roots(&package_root, scope)? { + let dep_root = canonical_utf8_path(&dep_root)?; + if visited_roots.insert(dep_root.clone()) { + package_roots.insert(dep_root); + } } Ok(()) } fn collect_package_source_paths_recursive( package_root: &Utf8Path, + scope: crate::package::DependencyScope, visited_roots: &mut HashSet, source_paths: &mut BTreeSet, ) -> Result<()> { @@ -6333,8 +6559,13 @@ fn collect_package_source_paths_recursive( for source_path in collect_package_cell_files(&package_root)? { source_paths.insert(source_path); } - for dep_root in local_dependency_roots(&package_root)? { - collect_package_source_paths_recursive(&dep_root, visited_roots, source_paths)?; + for dep_root in local_dependency_roots(&package_root, scope)? { + let dep_root = canonical_utf8_path(&dep_root)?; + if visited_roots.insert(dep_root.clone()) { + for source_path in collect_package_cell_files(&dep_root)? { + source_paths.insert(source_path); + } + } } Ok(()) @@ -6403,9 +6634,9 @@ pub fn resolve_workspace_members(workspace_root: &Utf8Path) -> Result Result> { +fn local_dependency_roots(package_root: &Utf8Path, scope: crate::package::DependencyScope) -> Result> { let mut manager = crate::package::PackageManager::new(package_root.as_std_path()); - manager.resolve_dependencies()?; + manager.resolve_locked_dependencies(&crate::package::active_resolution_options(scope))?; let mut roots = Vec::new(); for package in manager.get_resolved().values() { let dep_root = Utf8PathBuf::from_path_buf(package.path.clone()).map_err(|path| { @@ -6495,7 +6726,23 @@ fn metadata_output_path_from_artifact(artifact_path: &Utf8Path) -> Utf8PathBuf { artifact_path.with_file_name(metadata_name) } -fn compile_metadata_from_ir(ir: &ir::IrModule, artifact_format: ArtifactFormat, target_profile: TargetProfile) -> CompileMetadata { +pub fn lowering_record_output_path_from_artifact(artifact_path: &Utf8Path) -> Utf8PathBuf { + let file_name = artifact_path.file_name().unwrap_or("artifact"); + artifact_path.with_file_name(format!("{}.lowering.json", file_name)) +} + +pub fn source_map_output_path_from_artifact(artifact_path: &Utf8Path) -> Utf8PathBuf { + let file_name = artifact_path.file_name().unwrap_or("artifact"); + artifact_path.with_file_name(format!("{}.sourcemap.json", file_name)) +} + +fn compile_metadata_from_ir( + ir: &ir::IrModule, + artifact_format: ArtifactFormat, + target_profile: TargetProfile, + edition: CellScriptEdition, + primitive_assurance: Option<&str>, +) -> CompileMetadata { let type_layouts = metadata_type_layouts(ir); let type_defs = metadata_type_defs_by_name(ir); let flow_states = metadata_flow_states(ir); @@ -6554,12 +6801,15 @@ fn compile_metadata_from_ir(ir: &ir::IrModule, artifact_format: ArtifactFormat, let transaction_view_handles = transaction_view_handle_metadata(ir); let borrow_regions = borrow_region_metadata(ir); let capability_proofs = capability_proof_metadata(ir); + let compatibility_profile = resolve_compatibility_profile(edition, target_profile.name(), primitive_assurance); let mut metadata = CompileMetadata { metadata_schema_version: METADATA_SCHEMA_VERSION, source_metadata_schema_version: SOURCE_METADATA_SCHEMA_VERSION, artifact_metadata_schema_version: ARTIFACT_METADATA_SCHEMA_VERSION, constraints_metadata_schema_version: CONSTRAINTS_METADATA_SCHEMA_VERSION, compiler_version: VERSION.to_string(), + edition, + compatibility_profile, module: ir.name.clone(), artifact_format: artifact_format.display_name().to_string(), target_profile: target_profile.metadata(artifact_format), @@ -6568,6 +6818,7 @@ fn compile_metadata_from_ir(ir: &ir::IrModule, artifact_format: ArtifactFormat, source_hash: None, source_content_hash: None, source_units: Vec::new(), + verified_artifact: VerifiedArtifactMetadata::default(), lowering: LoweringMetadata { protocol_semantics: "CellScript IR records consume/read_ref/create summaries before RISC-V codegen".to_string(), assembly_path: "riscv64-asm emits executable CKB-style syscall paths plus metadata for verifier obligations".to_string(), @@ -7111,6 +7362,7 @@ fn scope_ir_to_fungible_type_group_v1(ir: &ir::IrModule, selected_type: Option<& args: Vec::new(), }], terminator: ir::IrTerminator::Return(None), + runtime_error: None, }], }, effect_class: ir::EffectClass::ReadOnly, @@ -9187,10 +9439,10 @@ fn body_transaction_resource_obligations( } } for pattern in &body.create_set { - if matches!(pattern.operation.as_str(), "transfer" | "claim" | "settle") { - if let Some(check) = create_output_verification_obligation(pattern, type_layouts, &availability) { - checks.push(check); - } + if matches!(pattern.operation.as_str(), "transfer" | "claim" | "settle") + && let Some(check) = create_output_verification_obligation(pattern, type_layouts, &availability) + { + checks.push(check); } } checks.extend(read_ref_cell_dep_data_obligations(body)); @@ -12064,10 +12316,8 @@ fn launch_distribution_sum_coupling_is_checked( u64_sources.insert(dest.id, sources); } } - ir::IrInstruction::LoadVar { dest, name } if dest.ty == ir::IrType::Bool => { - if named_bool_sources.contains(name) { - checked_bool_vars.insert(dest.id); - } + ir::IrInstruction::LoadVar { dest, name } if dest.ty == ir::IrType::Bool && named_bool_sources.contains(name) => { + checked_bool_vars.insert(dest.id); } _ => {} } @@ -13391,10 +13641,10 @@ fn body_consumed_named_types(body: &ir::IrBody) -> BTreeSet { ir::IrInstruction::Claim { receipt, .. } => Some(receipt), _ => None, }; - if let Some(ir::IrOperand::Var(var)) = operand { - if let Some(type_name) = named_type_name(&var.ty) { - types.insert(type_name.to_string()); - } + if let Some(ir::IrOperand::Var(var)) = operand + && let Some(type_name) = named_type_name(&var.ty) + { + types.insert(type_name.to_string()); } } } @@ -13739,10 +13989,10 @@ fn body_fail_closed_runtime_features( features.insert("claim-expression".to_string()); } } - ir::IrInstruction::Settle { dest, .. } => { - if !metadata_output_operation_is_verifier_covered(body, "settle", dest, type_layouts, &prelude_availability) { - features.insert("settle-expression".to_string()); - } + ir::IrInstruction::Settle { dest, .. } + if !metadata_output_operation_is_verifier_covered(body, "settle", dest, type_layouts, &prelude_availability) => + { + features.insert("settle-expression".to_string()); } _ => {} } @@ -13921,14 +14171,14 @@ fn metadata_prelude_availability( loaded_constructed_vector_names.insert(dest.id, name.clone()); } } - if let Some(source_id) = named_constructed_vectors.get(name).copied() { - if let Some(byte_count) = availability.constructed_byte_vector_vars.get(&source_id).copied() { - availability.constructed_byte_vector_vars.insert(dest.id, byte_count); - if let Some(root_id) = availability.constructed_byte_vector_roots.get(&source_id).copied() { - availability.constructed_byte_vector_roots.insert(dest.id, root_id); - } - loaded_constructed_vector_names.insert(dest.id, name.clone()); + if let Some(source_id) = named_constructed_vectors.get(name).copied() + && let Some(byte_count) = availability.constructed_byte_vector_vars.get(&source_id).copied() + { + availability.constructed_byte_vector_vars.insert(dest.id, byte_count); + if let Some(root_id) = availability.constructed_byte_vector_roots.get(&source_id).copied() { + availability.constructed_byte_vector_roots.insert(dest.id, root_id); } + loaded_constructed_vector_names.insert(dest.id, name.clone()); } if named_fixed_vars.contains_key(name) { availability.fixed_value_vars.insert(dest.id); @@ -14261,16 +14511,15 @@ fn metadata_prelude_availability( availability.scalar_vars.insert(dest.id); availability.fixed_value_vars.insert(dest.id); } - if let Some(width) = metadata_ir_type_fixed_width(&dest.ty, type_layouts) { - if metadata_fixed_value_available_with_layout_width(src, &availability, width, type_layouts) { - availability.fixed_value_vars.insert(dest.id); - if width > 8 - || named_type_name(&dest.ty).is_some_and(|name| type_layouts.enum_fixed_sizes.contains_key(name)) - { - availability - .aggregate_pointer_vars - .insert(dest.id, MetadataAggregatePointerSource { ty: dest.ty.clone() }); - } + if let Some(width) = metadata_ir_type_fixed_width(&dest.ty, type_layouts) + && metadata_fixed_value_available_with_layout_width(src, &availability, width, type_layouts) + { + availability.fixed_value_vars.insert(dest.id); + if width > 8 || named_type_name(&dest.ty).is_some_and(|name| type_layouts.enum_fixed_sizes.contains_key(name)) + { + availability + .aggregate_pointer_vars + .insert(dest.id, MetadataAggregatePointerSource { ty: dest.ty.clone() }); } } if dest.ty == ir::IrType::U64 && metadata_u64_value_available(src, &availability) { @@ -14571,10 +14820,10 @@ fn metadata_fixed_value_available_with_width( availability: &MetadataPreludeAvailability, expected_width: usize, ) -> bool { - if let ir::IrOperand::Const(value) = operand { - if metadata_fixed_scalar_const_value(value).is_some() { - return metadata_scalar_const_fits_width(value, expected_width); - } + if let ir::IrOperand::Const(value) = operand + && metadata_fixed_scalar_const_value(value).is_some() + { + return metadata_scalar_const_fits_width(value, expected_width); } if expected_width <= 8 && matches!(operand, ir::IrOperand::Var(_)) && metadata_scalar_available(operand, availability) { return true; @@ -14589,10 +14838,10 @@ fn metadata_fixed_value_available_with_layout_width( expected_width: usize, type_layouts: &MetadataTypeLayouts, ) -> bool { - if let ir::IrOperand::Const(value) = operand { - if metadata_fixed_scalar_const_value(value).is_some() { - return metadata_scalar_const_fits_width(value, expected_width); - } + if let ir::IrOperand::Const(value) = operand + && metadata_fixed_scalar_const_value(value).is_some() + { + return metadata_scalar_const_fits_width(value, expected_width); } if expected_width <= 8 && matches!(operand, ir::IrOperand::Var(_)) && metadata_scalar_available(operand, availability) { return true; @@ -17485,10 +17734,10 @@ fn param_type_hash_param_ids(body: &ir::IrBody) -> BTreeSet { let mut ids = BTreeSet::new(); for block in &body.blocks { for instruction in &block.instructions { - if let ir::IrInstruction::TypeHash { operand: ir::IrOperand::Var(var), .. } = instruction { - if named_type_name(&var.ty).is_some() { - ids.insert(var.id); - } + if let ir::IrInstruction::TypeHash { operand: ir::IrOperand::Var(var), .. } = instruction + && named_type_name(&var.ty).is_some() + { + ids.insert(var.id); } } } @@ -17917,10 +18166,10 @@ fn collect_package_cell_files(package_root: &Utf8Path) -> Result crate::error::Result<()> { + let mut manager = crate::package::PackageManager::new(root.as_std_path()); + let manifest = manager.read_manifest()?; + let options = crate::package::active_resolution_options(crate::package::DependencyScope::Runtime); + manager.resolve_dependencies_with_options(&options)?; + + let mut lockfile = crate::package::Lockfile::new(); + lockfile.package = crate::package::LockfilePackageInfo { + edition: manifest.package.edition, + name: manifest.package.name.clone(), + version: manifest.package.version.clone(), + namespace: manifest.package.namespace.clone(), + source_hash: None, + compiler_source_hash: None, + }; + lockfile.replace_with_resolution(&manager, &manifest, &options)?; + lockfile.write_to_root(root.as_std_path()) + } + fn rebind_artifact_integrity_for_test(result: &mut crate::CompileResult) { result.artifact_hash = crate::ckb_blake2b256(&result.artifact_bytes); result.metadata.artifact_hash = Some(crate::hex_encode(&result.artifact_hash)); @@ -18068,7 +18336,8 @@ mod tests { crate::types::check(&ast).unwrap(); crate::flow::check(&ast).unwrap(); let ir = ir::generate(&ast).unwrap(); - let metadata = crate::compile_metadata_from_ir(&ir, ArtifactFormat::RiscvAssembly, target_profile); + let metadata = + crate::compile_metadata_from_ir(&ir, ArtifactFormat::RiscvAssembly, target_profile, crate::CURRENT_EDITION, None); crate::validate_compile_metadata(&metadata, ArtifactFormat::RiscvAssembly).unwrap(); metadata } @@ -26138,9 +26407,44 @@ action inspect() -> u64 { let result = compile(SIMPLE_PROGRAM, CompileOptions { target: Some("riscv64-elf".to_string()), ..CompileOptions::default() }).unwrap(); + assert_eq!(result.metadata.edition, crate::CURRENT_EDITION); + assert_eq!(result.metadata.compatibility_profile.schema, crate::COMPATIBILITY_PROFILE_SCHEMA); + assert_eq!(result.metadata.compatibility_profile.source_semantics, crate::CURRENT_EDITION.source_semantics()); + assert_eq!(result.metadata.compatibility_profile.metadata_schema_version, crate::METADATA_SCHEMA_VERSION); + assert_eq!(result.metadata.compatibility_profile.source_metadata_schema_version, crate::SOURCE_METADATA_SCHEMA_VERSION); + assert_eq!(result.metadata.compatibility_profile.artifact_metadata_schema_version, crate::ARTIFACT_METADATA_SCHEMA_VERSION); + assert_eq!( + result.metadata.compatibility_profile.constraints_metadata_schema_version, + crate::CONSTRAINTS_METADATA_SCHEMA_VERSION + ); + assert_eq!(result.metadata.compatibility_profile.entry_witness_payload_abi, crate::ENTRY_WITNESS_ABI); + assert_eq!(result.metadata.compatibility_profile.entry_witness_placement_abi, crate::ENTRY_WITNESS_PLACEMENT_ABI); + assert_eq!(result.metadata.compatibility_profile.entry_witness_placement_field, crate::ENTRY_WITNESS_PLACEMENT_FIELD); + assert_eq!(result.metadata.constraints.edition, crate::CURRENT_EDITION); + assert_eq!(result.metadata.constraints.compatibility_profile, result.metadata.compatibility_profile.id); result.validate().unwrap(); } + #[test] + fn compile_result_validation_rejects_tampered_compatibility_profile() { + let mut result = compile(SIMPLE_PROGRAM, CompileOptions::default()).unwrap(); + result.metadata.compatibility_profile.entry_witness_placement_source = "global-input-0".to_string(); + + let err = result.validate().unwrap_err(); + + assert!(err.message.contains("compatibility_profile"), "unexpected error: {}", err.message); + } + + #[test] + fn compile_result_validation_rejects_tampered_profile_schema_axis() { + let mut result = compile(SIMPLE_PROGRAM, CompileOptions::default()).unwrap(); + result.metadata.compatibility_profile.metadata_schema_version -= 1; + + let err = result.validate().unwrap_err(); + + assert!(err.message.contains("compatibility_profile"), "unexpected error: {}", err.message); + } + #[test] fn compile_rejects_unsupported_optimization_level() { let err = compile(SIMPLE_PROGRAM, CompileOptions { opt_level: 4, ..CompileOptions::default() }).unwrap_err(); @@ -26342,6 +26646,7 @@ action mint(amount: u64) -> Receipt { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "deploy_manifest" version = "0.1.0" entry = "src/main.cell" @@ -26435,6 +26740,7 @@ action mint(amount: u64) -> Token { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "conflicting_cell_dep_location" version = "0.1.0" entry = "src/main.cell" @@ -26481,6 +26787,7 @@ action add(a: u64, b: u64) -> u64 { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "incomplete_cell_dep_location" version = "0.1.0" entry = "src/main.cell" @@ -26525,6 +26832,7 @@ action add(a: u64, b: u64) -> u64 { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "bad_deploy_manifest" version = "0.1.0" entry = "src/main.cell" @@ -26563,6 +26871,7 @@ action add(a: u64, b: u64) -> u64 { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "bad_hash_type_manifest" version = "0.1.0" entry = "src/main.cell" @@ -28400,6 +28709,7 @@ flow Offer.state { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" source_roots = ["src", "shared"] @@ -31168,14 +31478,24 @@ action spend(amount: u64) -> u64 { assert!(asm.contains(".global _cellscript_entry"), "parameterized entrypoints need a generated ELF entry wrapper:\n{}", asm); assert!( - asm.contains("# cellscript entry abi: _cellscript_entry loads Input#0 witness args for spend and falls back to GroupInput#0/GroupOutput#0"), + asm.contains( + "# cellscript entry abi: _cellscript_entry loads GroupInput#0 witness args for spend and falls back to GroupOutput#0" + ), "entry wrapper did not document its target ABI:\n{}", asm ); assert!( - asm.contains("# cellscript abi: LOAD_WITNESS reason=entry_args source=Input index=0") - && asm.contains("# cellscript abi: LOAD_WITNESS reason=entry_args_fallback_group_input source=GroupInput index=0"), - "entry wrapper did not load positional arguments from Input witness with GroupInput fallback:\n{}", + asm.contains("# cellscript abi: LOAD_WITNESS reason=entry_args source=GroupInput index=0") + && asm.contains("# cellscript abi: LOAD_WITNESS reason=entry_args_fallback_group_output source=GroupOutput index=0") + && !asm.contains("LOAD_WITNESS reason=entry_args source=Input index=0"), + "entry wrapper did not use script-group-relative witness sourcing:\n{}", + asm + ); + assert!( + asm.contains("# cellscript entry placement profile: validate the exact three-field WitnessArgs table") + && asm.contains("# cellscript entry placement v2: copy input_type payload over the table envelope") + && !asm.contains("detect raw-v1"), + "entry wrapper did not expose the versioned WitnessArgs.input_type placement ABI:\n{}", asm ); assert!( @@ -31409,6 +31729,7 @@ action raw(data: Vec) -> u64 { dep_root.join("Cell.toml"), r#" [package] +edition = "2026" name = "dep_pkg" version = "0.1.0" "#, @@ -31430,6 +31751,7 @@ resource Token has store, replace, relock, consume, burn { app_root.join("Cell.toml"), r#" [package] +edition = "2026" name = "app_pkg" version = "0.1.0" @@ -31454,6 +31776,7 @@ action pass_through(token: Token) -> Token { ) .unwrap(); + lock_package_for_test(&app_root).unwrap(); let result = compile_file(&app_entry, CompileOptions::default()).unwrap(); assert_eq!(result.artifact_format, ArtifactFormat::RiscvAssembly); assert!(!result.artifact_bytes.is_empty()); @@ -31473,6 +31796,7 @@ action pass_through(token: Token) -> Token { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "nested_layout" version = "0.1.0" "#, @@ -31528,6 +31852,7 @@ action inspect(witness signed: Signed) -> u64 { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -31562,6 +31887,7 @@ action ping() -> u64 { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -31595,6 +31921,7 @@ action ping() -> u64 { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -31632,6 +31959,7 @@ action ping() -> u64 { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -31669,6 +31997,7 @@ action ping() -> u64 { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -31706,6 +32035,7 @@ action ping() -> u64 { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -31744,6 +32074,7 @@ action ping() -> u64 { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -31782,6 +32113,7 @@ action ping() -> u64 { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -31803,7 +32135,7 @@ action ping() -> u64 { ) .unwrap(); - let err = compile_path(root, CompileOptions::default()).unwrap_err(); + let err = lock_package_for_test(root).unwrap_err(); assert!(err.message.contains("requires a namespace")); assert!(err.message.contains("token_std")); } @@ -31818,6 +32150,7 @@ action ping() -> u64 { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -31839,7 +32172,7 @@ action ping() -> u64 { ) .unwrap(); - let err = compile_path(root, CompileOptions::default()).unwrap_err(); + let err = lock_package_for_test(root).unwrap_err(); assert!(err.message.contains("not found at path")); assert!(err.message.contains("token_std")); } @@ -31855,6 +32188,7 @@ action ping() -> u64 { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -31889,6 +32223,7 @@ action ping() -> u64 { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" entry = "contracts/main.cell" @@ -31940,6 +32275,7 @@ action pass(token: Token) -> Token { dep_root.join("Cell.toml"), r#" [package] +edition = "2026" name = "dep_pkg" version = "0.1.0" @@ -31965,6 +32301,7 @@ action dep_ping() -> u64 { app_root.join("Cell.toml"), r#" [package] +edition = "2026" name = "app_pkg" version = "0.1.0" @@ -31986,7 +32323,7 @@ action app_ping() -> u64 { ) .unwrap(); - let err = compile_path(app_root, CompileOptions::default()).unwrap_err(); + let err = lock_package_for_test(&app_root).unwrap_err(); assert!(err.message.contains("Circular dependency detected")); } @@ -32001,6 +32338,7 @@ action app_ping() -> u64 { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" entry = "contracts/main.cell" @@ -32049,6 +32387,7 @@ action pass(token: Token) -> Token { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "bad-import" version = "0.1.0" entry = "src/main.cell" @@ -32096,6 +32435,7 @@ action pass(token: Token) -> Token { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "unreferenced-bad" version = "0.1.0" entry = "src/main.cell" @@ -32143,6 +32483,7 @@ action broken() -> u64 { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "cache-drift" version = "0.1.0" entry = "src/main.cell" @@ -32206,6 +32547,7 @@ resource Pair { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" entry = "contracts/main.cell" @@ -32242,6 +32584,7 @@ action ping() -> u64 { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" entry = "contracts/main.cell" @@ -32795,7 +33138,7 @@ resource Token has store, create { .to_string(), }, ]; - let report = crate::compile_sources_metadata_with_diagnostics(&sources, "src/main.cell", None); + let report = crate::compile_sources_metadata_with_diagnostics(&sources, "src/main.cell", crate::CURRENT_EDITION, None); assert!(report.diagnostics.is_empty(), "unexpected diagnostics: {:?}", report.diagnostics); let metadata = report.metadata.expect("imported validity metadata"); let token = metadata.types.iter().find(|ty| ty.name == "Token").expect("imported Token metadata"); diff --git a/src/lsp/mod.rs b/src/lsp/mod.rs index 02e472f4..9b583c0c 100644 --- a/src/lsp/mod.rs +++ b/src/lsp/mod.rs @@ -237,7 +237,7 @@ impl LspServer { let report = uri_path .as_ref() .map(|path| crate::compile_path_metadata_with_diagnostics_for_source(path, content, crate::CompileOptions::default())) - .unwrap_or_else(|| crate::compile_metadata_with_diagnostics(content, None)); + .unwrap_or_else(|| crate::compile_metadata_with_diagnostics(content, crate::CURRENT_EDITION, None)); let mut diagnostics = report .diagnostics .iter() @@ -476,10 +476,10 @@ impl LspServer { _ => None, }?; fields.iter().find_map(|field| { - if field.name == field_name { - if let Type::Named(name) = &field.ty { - return Some(name.clone()); - } + if field.name == field_name + && let Type::Named(name) = &field.ty + { + return Some(name.clone()); } None }) @@ -957,20 +957,20 @@ impl LspServer { break; } } - if let Stmt::Let(let_stmt) = stmt { - if let BindingPattern::Name(name) = &let_stmt.pattern { - items.push(CompletionItem { - label: name.clone(), - kind: CompletionItemKind::Variable, - detail: Some(format!( - "let{}: {}", - if let_stmt.is_mut { " mut" } else { "" }, - let_stmt.ty.as_ref().map(type_to_string).unwrap_or_else(|| "_".to_string()) - )), - documentation: None, - insert_text: Some(name.clone()), - }); - } + if let Stmt::Let(let_stmt) = stmt + && let BindingPattern::Name(name) = &let_stmt.pattern + { + items.push(CompletionItem { + label: name.clone(), + kind: CompletionItemKind::Variable, + detail: Some(format!( + "let{}: {}", + if let_stmt.is_mut { " mut" } else { "" }, + let_stmt.ty.as_ref().map(type_to_string).unwrap_or_else(|| "_".to_string()) + )), + documentation: None, + insert_text: Some(name.clone()), + }); } } } @@ -1247,12 +1247,11 @@ impl LspServer { // Check local let bindings. for stmt in body { - if let Stmt::Let(let_stmt) = stmt { - if let BindingPattern::Name(name) = &let_stmt.pattern { - if name == symbol { - return Some(Location { uri: uri.to_string(), range: span_to_range(content, let_stmt.span) }); - } - } + if let Stmt::Let(let_stmt) = stmt + && let BindingPattern::Name(name) = &let_stmt.pattern + && name == symbol + { + return Some(Location { uri: uri.to_string(), range: span_to_range(content, let_stmt.span) }); } } } @@ -1298,7 +1297,7 @@ impl LspServer { // 1. Try top-level item hover (existing logic). if let (Some(ast), Some(source)) = (self.ast_cache.get(uri), self.documents.get(uri)) { - let metadata = crate::compile_metadata(source, None).ok(); + let metadata = crate::compile_metadata(source, crate::CURRENT_EDITION, None).ok(); if let Some(hover) = ast.items.iter().find_map(|item| { if item_name(item) == Some(symbol.as_str()) { self.item_hover(source, item, metadata.as_ref()) @@ -1322,7 +1321,7 @@ impl LspServer { // 4. Try workspace modules. for module in self.workspace_modules(uri) { - let metadata = crate::compile_metadata(&module.source, None).ok(); + let metadata = crate::compile_metadata(&module.source, module.edition, None).ok(); if let Some(hover) = module.ast.items.iter().find_map(|item| { if item_name(item) == Some(symbol.as_str()) { self.item_hover(&module.source, item, metadata.as_ref()) @@ -1427,21 +1426,20 @@ impl LspServer { // Check local let bindings. for stmt in body { - if let Stmt::Let(let_stmt) = stmt { - if let BindingPattern::Name(name) = &let_stmt.pattern { - if name == symbol { - let ty_str = let_stmt.ty.as_ref().map(type_to_string).unwrap_or_else(|| "_".to_string()); - return Some(Hover { - contents: format!( - "```cellscript\n{}{}: {}\n```\n\nLocal variable", - if let_stmt.is_mut { "mut " } else { "" }, - name, - ty_str - ), - range: Some(span_to_range(content, let_stmt.span)), - }); - } - } + if let Stmt::Let(let_stmt) = stmt + && let BindingPattern::Name(name) = &let_stmt.pattern + && name == symbol + { + let ty_str = let_stmt.ty.as_ref().map(type_to_string).unwrap_or_else(|| "_".to_string()); + return Some(Hover { + contents: format!( + "```cellscript\n{}{}: {}\n```\n\nLocal variable", + if let_stmt.is_mut { "mut " } else { "" }, + name, + ty_str + ), + range: Some(span_to_range(content, let_stmt.span)), + }); } } } @@ -1733,17 +1731,15 @@ impl LspServer { }); } } - Item::Shared(s) => { - if !s.fields.is_empty() { - let range = span_to_range(content, s.span); - ranges.push(FoldingRange { - start_line: range.start.line, - start_character: Some(range.start.character), - end_line: range.end.line, - end_character: Some(range.end.character), - kind: Some(FoldingRangeKind::Region), - }); - } + Item::Shared(s) if !s.fields.is_empty() => { + let range = span_to_range(content, s.span); + ranges.push(FoldingRange { + start_line: range.start.line, + start_character: Some(range.start.character), + end_line: range.end.line, + end_character: Some(range.end.character), + kind: Some(FoldingRangeKind::Region), + }); } _ => {} } @@ -1822,10 +1818,10 @@ impl LspServer { } fn find_signature(&self, uri: &str, name: &str) -> Option { - if let Some(ast) = self.ast_cache.get(uri) { - if let Some(info) = self.find_signature_in_items(&ast.items, name) { - return Some(info); - } + if let Some(ast) = self.ast_cache.get(uri) + && let Some(info) = self.find_signature_in_items(&ast.items, name) + { + return Some(info); } for module in self.workspace_modules(uri) { @@ -1939,17 +1935,17 @@ impl LspServer { } fn find_top_level_symbol(&self, uri: &str, symbol: &str) -> Option { - if let (Some(ast), Some(source)) = (self.ast_cache.get(uri), self.documents.get(uri)) { - if let Some(location) = ast.items.iter().find_map(|item| { + if let (Some(ast), Some(source)) = (self.ast_cache.get(uri), self.documents.get(uri)) + && let Some(location) = ast.items.iter().find_map(|item| { let name = item_name(item)?; if name == symbol { Some(Location { uri: uri.to_string(), range: item_name_range(source, item, name) }) } else { None } - }) { - return Some(location); - } + }) + { + return Some(location); } for module in self.workspace_modules(uri) { @@ -1980,7 +1976,7 @@ impl LspServer { module.source = content.clone(); module.ast = ast.clone(); } else { - modules.push(crate::LoadedModule { path, source: content.clone(), ast: ast.clone() }); + modules.push(crate::LoadedModule { path, source: content.clone(), ast: ast.clone(), edition: crate::CURRENT_EDITION }); } } @@ -2523,17 +2519,17 @@ fn action_metadata_hover(name: &str, metadata: Option<&crate::CompileMetadata>) } fn function_metadata_hover(name: &str, function: &FnDef, metadata: Option<&crate::CompileMetadata>) -> String { - if let Some(metadata) = metadata { - if let Some(function_metadata) = metadata.functions.iter().find(|candidate| candidate.name == name) { - let declared = function_metadata.declared_effect_class.as_deref().unwrap_or("inferred"); - return format!( - "\n\n**Effect metadata**\n\nDeclared: `{}`\n\nInferred: `{}`\n\nEffective: `{}`\n\nEvidence: `{}`", - declared, - function_metadata.inferred_effect_class, - function_metadata.effect_class, - function_metadata.effect_evidence_tier.as_str() - ); - } + if let Some(metadata) = metadata + && let Some(function_metadata) = metadata.functions.iter().find(|candidate| candidate.name == name) + { + let declared = function_metadata.declared_effect_class.as_deref().unwrap_or("inferred"); + return format!( + "\n\n**Effect metadata**\n\nDeclared: `{}`\n\nInferred: `{}`\n\nEffective: `{}`\n\nEvidence: `{}`", + declared, + function_metadata.inferred_effect_class, + function_metadata.effect_class, + function_metadata.effect_evidence_tier.as_str() + ); } if function.effect_declared { @@ -2683,10 +2679,10 @@ fn word_occurrences(source: &str, symbol: &str) -> Vec<(usize, usize)> { return matches; }; for token in tokens { - if let TokenKind::Identifier(name) = token.kind { - if name == symbol { - matches.push((token.span.start, token.span.end)); - } + if let TokenKind::Identifier(name) = token.kind + && name == symbol + { + matches.push((token.span.start, token.span.end)); } } matches @@ -3349,8 +3345,11 @@ action update(amount: u64) -> u64 { let temp = tempdir().unwrap(); let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()).unwrap(); std::fs::create_dir_all(root.join("src")).unwrap(); - std::fs::write(root.join("Cell.toml"), "[package]\nname = \"demo\"\nversion = \"0.1.0\"\nentry = \"src/main.cell\"\n") - .unwrap(); + std::fs::write( + root.join("Cell.toml"), + "[package]\nedition = \"2026\"\nname = \"demo\"\nversion = \"0.1.0\"\nentry = \"src/main.cell\"\n", + ) + .unwrap(); std::fs::write(root.join("src/types.cell"), "module demo::types\n\nresource Token {\n amount: u64,\n}\n").unwrap(); let main_source = "module demo::main\n\nuse demo::types::Token\n\naction inspect(token: Token) -> u64 {\n verification\n token.amount\n}\n"; @@ -3371,8 +3370,11 @@ action update(amount: u64) -> u64 { let temp = tempdir().unwrap(); let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()).unwrap(); std::fs::create_dir_all(root.join("src")).unwrap(); - std::fs::write(root.join("Cell.toml"), "[package]\nname = \"demo\"\nversion = \"0.1.0\"\nentry = \"src/main.cell\"\n") - .unwrap(); + std::fs::write( + root.join("Cell.toml"), + "[package]\nedition = \"2026\"\nname = \"demo\"\nversion = \"0.1.0\"\nentry = \"src/main.cell\"\n", + ) + .unwrap(); let types_source = "module demo::types\n\nresource Token {\n amount: u64,\n}\n"; let types_path = root.join("src/types.cell"); std::fs::write(&types_path, types_source).unwrap(); @@ -3395,8 +3397,11 @@ action update(amount: u64) -> u64 { let temp = tempdir().unwrap(); let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()).unwrap(); std::fs::create_dir_all(root.join("src")).unwrap(); - std::fs::write(root.join("Cell.toml"), "[package]\nname = \"demo\"\nversion = \"0.1.0\"\nentry = \"src/main.cell\"\n") - .unwrap(); + std::fs::write( + root.join("Cell.toml"), + "[package]\nedition = \"2026\"\nname = \"demo\"\nversion = \"0.1.0\"\nentry = \"src/main.cell\"\n", + ) + .unwrap(); let types_source = "module demo::types\n\nresource Token {\n amount: u64,\n}\n"; let types_path = root.join("src/types.cell"); std::fs::write(&types_path, types_source).unwrap(); diff --git a/src/main.rs b/src/main.rs index 73f27f4a..b9f8de76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,3 @@ -// Keep the Edition 2024 let-chain cleanup separate from the toolchain migration. -#![allow(clippy::collapsible_if)] - use camino::Utf8Path; use cellscript::error::{CompileError, CompileErrorCategory}; use clap::{Parser, ValueEnum}; @@ -284,6 +281,7 @@ fn main() { let output = cli.output.clone(); let options = CompileOptions { + edition: cellscript::CURRENT_EDITION, opt_level: cli.opt, output: output.clone(), debug: cli.debug, @@ -323,6 +321,9 @@ fn main() { if let Err(e) = result.write_metadata_to_path(&metadata_path) { terminate_cli_error(&e, message_format, None, None); } + let verified_sidecars = result + .write_verified_artifact_sidecars(&output_path) + .unwrap_or_else(|e| terminate_cli_error(&e, message_format, None, None)); if message_format == MessageFormat::Json { let payload = serde_json::json!({ @@ -330,6 +331,8 @@ fn main() { "mode": "direct-build", "artifact": output_path.as_str(), "metadata": metadata_path.as_str(), + "lowering_record": verified_sidecars.as_ref().map(|paths| paths.0.as_str()), + "source_map": verified_sidecars.as_ref().map(|paths| paths.1.as_str()), "artifact_format": result.artifact_format.display_name(), "target_profile": result.metadata.target_profile.name, "artifact_hash": result.metadata.artifact_hash, @@ -343,6 +346,10 @@ fn main() { println!(" Artifact hash: {:x?}", result.artifact_hash); println!(" Output: {}", output_path); println!(" Metadata: {}", metadata_path); + if let Some((lowering_record, source_map)) = verified_sidecars { + println!(" Lowering record: {}", lowering_record); + println!(" Source map: {}", source_map); + } } } Err(e) => { @@ -668,10 +675,10 @@ fn diagnostic_source( } let file = error.file.as_deref().or(fallback_file)?; - if Some(file) == fallback_file { - if let Some(source) = fallback_source { - return Some((file.to_string(), source.to_string())); - } + if Some(file) == fallback_file + && let Some(source) = fallback_source + { + return Some((file.to_string(), source.to_string())); } std::fs::read_to_string(file.as_std_path()).ok().map(|source| (file.to_string(), source)) diff --git a/src/optimize/mod.rs b/src/optimize/mod.rs index eeecb661..fcc2e658 100644 --- a/src/optimize/mod.rs +++ b/src/optimize/mod.rs @@ -103,12 +103,11 @@ impl Optimizer { ty: let_stmt.ty.clone(), value: { let value = self.optimize_expr(&let_stmt.value)?; - if !let_stmt.is_mut { - if let BindingPattern::Name(name) = &let_stmt.pattern { - if let Some(constant) = self.try_eval_const(&value) { - self.insert_const(name, constant); - } - } + if !let_stmt.is_mut + && let BindingPattern::Name(name) = &let_stmt.pattern + && let Some(constant) = self.try_eval_const(&value) + { + self.insert_const(name, constant); } value }, @@ -177,10 +176,10 @@ impl Optimizer { Expr::Binary(bin) => { let left = self.optimize_expr(&bin.left)?; let right = self.optimize_expr(&bin.right)?; - if let (Some(left_const), Some(right_const)) = (self.try_eval_const(&left), self.try_eval_const(&right)) { - if let Some(value) = fold_binary(bin.op, &left_const, &right_const) { - return Ok(const_to_expr(value)); - } + if let (Some(left_const), Some(right_const)) = (self.try_eval_const(&left), self.try_eval_const(&right)) + && let Some(value) = fold_binary(bin.op, &left_const, &right_const) + { + return Ok(const_to_expr(value)); } if let Some(simplified) = simplify_binary(bin.op, &left, &right) { return Ok(simplified); @@ -192,12 +191,11 @@ impl Optimizer { if let Some(value) = self.try_eval_const(&inner).and_then(|value| fold_unary(unary.op, &value)) { return Ok(const_to_expr(value)); } - if unary.op == UnaryOp::Not { - if let Expr::Unary(nested) = &inner { - if nested.op == UnaryOp::Not { - return Ok(*nested.expr.clone()); - } - } + if unary.op == UnaryOp::Not + && let Expr::Unary(nested) = &inner + && nested.op == UnaryOp::Not + { + return Ok(*nested.expr.clone()); } Ok(Expr::Unary(UnaryExpr { op: unary.op, expr: Box::new(inner), span: unary.span })) } @@ -207,10 +205,10 @@ impl Optimizer { args.push(self.optimize_expr(arg)?); } let func = self.optimize_expr(&call.func)?; - if let Expr::Identifier(name) = &func { - if let Some(inlined) = self.inline_call(name, &args)? { - return Ok(inlined); - } + if let Expr::Identifier(name) = &func + && let Some(inlined) = self.inline_call(name, &args)? + { + return Ok(inlined); } Ok(Expr::Call(CallExpr { func: Box::new(func), type_args: call.type_args.clone(), args, span: call.span })) } @@ -355,10 +353,10 @@ impl Optimizer { fn seed_top_level_constants(&mut self, module: &Module) { for item in &module.items { - if let Item::Const(def) = item { - if let Some(value) = self.try_eval_const(&def.value) { - self.insert_const(&def.name, value); - } + if let Item::Const(def) = item + && let Some(value) = self.try_eval_const(&def.value) + { + self.insert_const(&def.name, value); } } } diff --git a/src/package/mod.rs b/src/package/mod.rs index 1cc0ee89..399d8c66 100644 --- a/src/package/mod.rs +++ b/src/package/mod.rs @@ -1,7 +1,12 @@ +use crate::edition::{CellScriptEdition, CURRENT_EDITION}; use crate::error::{CompileError, Result}; use serde::{Deserialize, Serialize}; -use std::collections::{BTreeMap, HashMap}; +use sha2::{Digest, Sha256}; +use std::cell::RefCell; +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::io::{Read, Write}; use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; pub mod registry; @@ -15,6 +20,14 @@ pub struct PackageManifest { #[serde(default)] pub dev_dependencies: HashMap, #[serde(default)] + pub features: BTreeMap>, + #[serde(default)] + pub environments: BTreeMap, + #[serde(default)] + pub dependency_overrides: BTreeMap>, + #[serde(default)] + pub resolvers: BTreeMap, + #[serde(default)] pub build: BuildConfig, #[serde(default)] pub policy: PolicyConfig, @@ -28,6 +41,7 @@ pub struct PackageManifest { pub struct PackageInfo { pub name: String, pub version: String, + pub edition: CellScriptEdition, #[serde(default)] pub namespace: Option, #[serde(default)] @@ -126,8 +140,28 @@ fn canonical_path(path: &Path) -> Result { std::fs::canonicalize(path).map_err(|e| CompileError::without_span(format!("failed to canonicalize '{}': {}", path.display(), e))) } +fn relative_path(from: &Path, to: &Path) -> Option { + let from_components: Vec<_> = from.components().collect(); + let to_components: Vec<_> = to.components().collect(); + let common = from_components.iter().zip(&to_components).take_while(|(left, right)| left == right).count(); + if common == 0 { + return None; + } + let mut relative = PathBuf::new(); + for _ in common..from_components.len() { + relative.push(".."); + } + for component in &to_components[common..] { + relative.push(component.as_os_str()); + } + Some(if relative.as_os_str().is_empty() { PathBuf::from(".") } else { relative }) +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] +// Keep the public manifest API and untagged TOML representation source-compatible; +// boxing Detailed would force every programmatic manifest author to wrap it. +#[allow(clippy::large_enum_variant)] pub enum Dependency { Simple(String), Detailed(DetailedDependency), @@ -139,6 +173,14 @@ pub struct DetailedDependency { pub version: String, #[serde(default)] pub namespace: Option, + /// Declared package name when the dependency's local alias differs. + #[serde(default)] + pub package: Option, + /// Name of a bounded external resolver declared in `[resolvers.]`. + /// It is invoked only by explicit lock/update operations and is normalized + /// to an ordinary immutable Registry or Git source before Cell.lock is written. + #[serde(default)] + pub resolver: Option, #[serde(default)] pub git: Option, #[serde(default)] @@ -155,6 +197,66 @@ pub struct DetailedDependency { pub features: Vec, #[serde(default = "default_true")] pub default_features: bool, + /// Persisted acknowledgement that this dependency may resolve from a + /// source_published or indexed_pending Registry entry. + #[serde(default, skip_serializing_if = "is_false")] + pub allow_unverified: bool, + /// Persisted incident-review acknowledgement for quarantined entries. + #[serde(default, skip_serializing_if = "is_false")] + pub allow_quarantined: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResolverConfig { + pub command: String, + pub sha256: String, + #[serde(default)] + pub args: Vec, +} + +#[derive(Debug, Serialize)] +struct ExternalResolverRequest<'a> { + schema: &'static str, + alias: &'a str, + package: &'a str, + version_requirement: &'a str, + environment: Option>, +} + +#[derive(Debug, Serialize)] +struct ExternalResolverEnvironment<'a> { + name: &'a str, + chain_id: &'a str, + genesis_hash: &'a str, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ExternalResolverResponse { + schema: String, + dependency: ExternalResolvedDependency, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ExternalResolvedDependency { + package: String, + version: String, + #[serde(default)] + namespace: Option, + #[serde(default)] + git: Option, + #[serde(default)] + rev: Option, +} + +const EXTERNAL_RESOLVER_REQUEST_SCHEMA: &str = "cellscript-dependency-resolver-request-v1"; +const EXTERNAL_RESOLVER_RESPONSE_SCHEMA: &str = "cellscript-dependency-resolver-response-v1"; +const EXTERNAL_RESOLVER_TIMEOUT: Duration = Duration::from_secs(10); +const EXTERNAL_RESOLVER_MAX_OUTPUT_BYTES: u64 = 1024 * 1024; + +fn is_false(value: &bool) -> bool { + !*value } fn default_true() -> bool { @@ -235,20 +337,84 @@ pub struct CkbCellDepConfig { pub type_id: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CkbEnvironment { + pub chain_id: String, + pub genesis_hash: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DependencyScope { + Runtime, + Test, +} + +#[derive(Debug, Clone)] +pub struct ResolutionOptions { + pub scope: DependencyScope, + pub features: BTreeSet, + pub all_features: bool, + pub no_default_features: bool, + pub environment: Option, + pub offline: bool, +} + +impl Default for ResolutionOptions { + fn default() -> Self { + Self { + scope: DependencyScope::Runtime, + features: BTreeSet::new(), + all_features: false, + no_default_features: false, + environment: None, + offline: false, + } + } +} + +thread_local! { + static RESOLUTION_OPTIONS_STACK: RefCell> = const { RefCell::new(Vec::new()) }; +} + +pub fn with_resolution_options(options: ResolutionOptions, operation: impl FnOnce() -> T) -> T { + RESOLUTION_OPTIONS_STACK.with(|stack| stack.borrow_mut().push(options)); + struct PopResolutionOptions; + impl Drop for PopResolutionOptions { + fn drop(&mut self) { + RESOLUTION_OPTIONS_STACK.with(|stack| { + stack.borrow_mut().pop(); + }); + } + } + let _guard = PopResolutionOptions; + operation() +} + +pub(crate) fn active_resolution_options(scope: DependencyScope) -> ResolutionOptions { + RESOLUTION_OPTIONS_STACK.with(|stack| { + let mut options = stack.borrow().last().cloned().unwrap_or_default(); + options.scope = scope; + options + }) +} + pub struct PackageManager { root: PathBuf, - resolved: HashMap, + resolved: BTreeMap, + root_dependencies: BTreeMap, } #[derive(Debug, Clone)] pub struct ResolvedPackage { + pub node_id: String, pub name: String, pub version: String, pub path: PathBuf, pub source: PackageSource, - pub dependencies: Vec, + pub dependencies: BTreeMap, pub namespace: Option, pub source_hash: Option, + pub manifest_digest: String, } /// Emit yank-related notices to stderr during registry resolution. @@ -270,11 +436,12 @@ fn emit_yank_notices(namespace: &str, name: &str, requested: &str, selected: &st // Prefer the publisher-declared replacement (`replaced_by`) when present; // otherwise fall back to the latest non-yanked version. let suggestion = entry.replaced_by.clone().or_else(|| { - index.versions.iter().filter(|v| !v.yanked && v.version != selected).map(|v| v.version.clone()).max_by(|a, b| { - let a_parts = parse_numeric_version(a); - let b_parts = parse_numeric_version(b); - compare_version_tuples(&a_parts, &b_parts) - }) + index + .versions + .iter() + .filter(|v| !v.yanked && v.version != selected) + .map(|v| v.version.clone()) + .max_by(|a, b| compare_semver(a, b)) }); let reason = entry.yanked_reason.as_deref().map(|r| format!(" (reason: {})", r)).unwrap_or_default(); match suggestion { @@ -318,20 +485,13 @@ fn registry_resolution_blocked_error( )) } -fn parse_numeric_version(version: &str) -> Vec { - let core = version.split_once('-').map(|(c, _)| c).unwrap_or(version); - core.split('.').filter_map(|p| p.parse().ok()).collect() -} - -fn compare_version_tuples(a: &[u32], b: &[u32]) -> std::cmp::Ordering { - let max_len = a.len().max(b.len()); - for i in 0..max_len { - match a.get(i).cmp(&b.get(i)) { - std::cmp::Ordering::Equal => continue, - other => return other, - } +fn compare_semver(left: &str, right: &str) -> std::cmp::Ordering { + match (semver::Version::parse(left), semver::Version::parse(right)) { + (Ok(left), Ok(right)) => left.cmp(&right), + (Ok(_), Err(_)) => std::cmp::Ordering::Greater, + (Err(_), Ok(_)) => std::cmp::Ordering::Less, + (Err(_), Err(_)) => left.cmp(right), } - std::cmp::Ordering::Equal } #[derive(Debug, Clone)] @@ -349,11 +509,168 @@ pub enum VersionReq { Any, } +fn manifest_digest(bytes: &[u8]) -> String { + format!("sha256:{}", hex::encode(Sha256::digest(bytes))) +} + +pub fn compute_manifest_digest(root: &Path) -> Result { + let path = root.join("Cell.toml"); + let bytes = std::fs::read(&path) + .map_err(|error| CompileError::without_span(format!("failed to read manifest '{}': {}", path.display(), error)))?; + Ok(manifest_digest(&bytes)) +} + +fn sha256_file(path: &Path) -> Result { + let mut file = std::fs::File::open(path)?; + let mut digest = Sha256::new(); + let mut buffer = [0_u8; 64 * 1024]; + loop { + let read = file.read(&mut buffer)?; + if read == 0 { + break; + } + digest.update(&buffer[..read]); + } + Ok(hex::encode(digest.finalize())) +} + +fn read_bounded_resolver_output(path: &Path) -> Result> { + let metadata = std::fs::metadata(path)?; + if metadata.len() > EXTERNAL_RESOLVER_MAX_OUTPUT_BYTES { + return Err(CompileError::without_span(format!("external resolver output '{}' exceeds 1 MiB", path.display()))); + } + Ok(std::fs::read(path)?) +} + +fn sanitize_node_component(value: &str) -> String { + value + .chars() + .map(|character| if character.is_ascii_alphanumeric() || character == '-' || character == '_' { character } else { '_' }) + .take(80) + .collect() +} + +fn package_node_id(package: &ResolvedPackage, options: &ResolutionOptions) -> String { + let source = match &package.source { + PackageSource::Local(path) => format!("path:{}", path.to_string_lossy().replace('\\', "/")), + PackageSource::Git { url, revision } => format!("git:{url}#{revision}"), + PackageSource::Registry { registry, namespace, version, revision, .. } => { + format!("registry:{registry}:{namespace}/{}@{version}#{revision}", package.name) + } + }; + let mut features: Vec<_> = options.features.iter().cloned().collect(); + if options.all_features { + features.push("*".to_string()); + } + if !options.no_default_features { + features.push("default".to_string()); + } + features.sort(); + let environment = options.environment.as_deref().unwrap_or("default"); + format!("{}@{}|{}|env={}|features={}", package.name, package.version, source, environment, features.join(",")) +} + +fn dependency_package_name(alias: &str, dependency: &Dependency) -> String { + match dependency { + Dependency::Detailed(detail) => detail.package.clone().unwrap_or_else(|| alias.to_string()), + Dependency::Simple(_) => alias.to_string(), + } +} + +fn dependency_is_optional(dependency: &Dependency) -> bool { + matches!(dependency, Dependency::Detailed(detail) if detail.optional) +} + +fn dependency_resolution_options(dependency: &Dependency, parent: &ResolutionOptions) -> ResolutionOptions { + let mut options = ResolutionOptions { + scope: DependencyScope::Runtime, + environment: parent.environment.clone(), + offline: parent.offline, + ..ResolutionOptions::default() + }; + if let Dependency::Detailed(detail) = dependency { + options.features.extend(detail.features.iter().cloned()); + options.no_default_features = !detail.default_features; + } + options +} + +fn active_optional_dependencies(manifest: &PackageManifest, options: &ResolutionOptions) -> Result> { + let mut requested = options.features.clone(); + if options.all_features { + requested.extend(manifest.features.keys().filter(|name| name.as_str() != "default").cloned()); + } + if !options.no_default_features && manifest.features.contains_key("default") { + requested.insert("default".to_string()); + } + + let mut active_dependencies = BTreeSet::new(); + let mut visited = BTreeSet::new(); + let mut visiting = Vec::new(); + for feature in requested { + expand_feature(manifest, &feature, &mut visited, &mut visiting, &mut active_dependencies)?; + } + Ok(active_dependencies) +} + +fn expand_feature( + manifest: &PackageManifest, + feature: &str, + visited: &mut BTreeSet, + visiting: &mut Vec, + active_dependencies: &mut BTreeSet, +) -> Result<()> { + if visited.contains(feature) { + return Ok(()); + } + if visiting.iter().any(|candidate| candidate == feature) { + let mut cycle = visiting.clone(); + cycle.push(feature.to_string()); + return Err(CompileError::without_span(format!("feature cycle detected: {}", cycle.join(" -> ")))); + } + let members = manifest + .features + .get(feature) + .ok_or_else(|| CompileError::without_span(format!("unknown package feature '{}'", feature)))? + .clone(); + visiting.push(feature.to_string()); + for member in members { + if let Some(alias) = member.strip_prefix("dep:") { + let dependency = manifest.dependencies.get(alias).or_else(|| manifest.dev_dependencies.get(alias)).ok_or_else(|| { + CompileError::without_span(format!("feature '{}' activates unknown dependency alias '{}'", feature, alias)) + })?; + if !dependency_is_optional(dependency) { + return Err(CompileError::without_span(format!( + "feature '{}' uses dep:{} but dependency '{}' is not optional", + feature, alias, alias + ))); + } + active_dependencies.insert(alias.to_string()); + } else { + expand_feature(manifest, &member, visited, visiting, active_dependencies)?; + } + } + visiting.pop(); + visited.insert(feature.to_string()); + Ok(()) +} + +fn validate_environment(name: &str, environment: &CkbEnvironment) -> Result<()> { + if name.trim().is_empty() || environment.chain_id.trim().is_empty() { + return Err(CompileError::without_span("package environment names and chain_id values must not be empty")); + } + let hash = environment.genesis_hash.strip_prefix("0x").unwrap_or(&environment.genesis_hash); + if hash.len() != 64 || !hash.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err(CompileError::without_span(format!("environment '{}' genesis_hash must contain exactly 32 bytes of hex", name))); + } + Ok(()) +} + impl PackageManager { pub fn new(root: impl AsRef) -> Self { let root = root.as_ref().to_path_buf(); - Self { root, resolved: HashMap::new() } + Self { root, resolved: BTreeMap::new(), root_dependencies: BTreeMap::new() } } pub fn read_manifest(&self) -> Result { @@ -403,6 +720,7 @@ impl PackageManager { package: PackageInfo { name: name.to_string(), version: "0.1.0".to_string(), + edition: CURRENT_EDITION, namespace: None, authors: vec![], description: String::new(), @@ -421,6 +739,10 @@ impl PackageManager { workspace: None, dependencies: HashMap::new(), dev_dependencies: HashMap::new(), + features: BTreeMap::new(), + environments: BTreeMap::new(), + dependency_overrides: BTreeMap::new(), + resolvers: BTreeMap::new(), build: BuildConfig::default(), policy: PolicyConfig::default(), deploy: DeployConfig::default(), @@ -459,15 +781,384 @@ dist/ } pub fn resolve_dependencies(&mut self) -> Result<()> { + self.resolve_dependencies_with_options(&ResolutionOptions::default()) + } + + pub fn resolve_dependencies_with_options(&mut self, options: &ResolutionOptions) -> Result<()> { + let manifest = self.read_manifest()?; + self.validate_manifest_package_contract(&manifest)?; + self.resolved.clear(); + self.root_dependencies.clear(); + + let dependencies = self.selected_dependencies(&manifest, options, true)?; + for (alias, dep) in dependencies { + let node_id = + self.resolve_dependency_from_root(&alias, &dep, &self.root.clone(), options, &mut Vec::new(), &mut Vec::new())?; + self.root_dependencies.insert(alias, node_id); + } + + Ok(()) + } + + pub fn resolve_locked_dependencies(&mut self, options: &ResolutionOptions) -> Result<()> { let manifest = self.read_manifest()?; + self.validate_manifest_package_contract(&manifest)?; + let selected = self.selected_dependencies(&manifest, options, true)?; + self.resolved.clear(); + self.root_dependencies.clear(); + if selected.is_empty() { + if let Some(lockfile) = Lockfile::read_from_root(&self.root)? { + let actual_manifest_digest = compute_manifest_digest(&self.root)?; + if !lockfile.root.manifest_digest.is_empty() && lockfile.root.manifest_digest != actual_manifest_digest { + return Err(CompileError::without_span(format!( + "Cell.lock manifest digest '{}' does not match Cell.toml '{}'; run 'cellc lock' or 'cellc update' explicitly", + lockfile.root.manifest_digest, actual_manifest_digest + ))); + } + } + return Ok(()); + } + + let lockfile = Lockfile::read_from_root(&self.root)?.ok_or_else(|| { + CompileError::without_span( + "Cell.toml declares dependencies but Cell.lock is missing; run 'cellc lock' or 'cellc update' explicitly", + ) + })?; + let manifest_bytes = std::fs::read(self.root.join("Cell.toml"))?; + let actual_manifest_digest = manifest_digest(&manifest_bytes); + if lockfile.root.manifest_digest != actual_manifest_digest { + return Err(CompileError::without_span(format!( + "Cell.lock manifest digest '{}' does not match Cell.toml '{}'; run 'cellc lock' or 'cellc update' explicitly", + lockfile.root.manifest_digest, actual_manifest_digest + ))); + } + + let (runtime_edges, dev_edges) = if let Some(environment_name) = options.environment.as_deref() { + let locked_environment = lockfile.environments.get(environment_name).ok_or_else(|| { + CompileError::without_span(format!( + "environment '{}' is not pinned in Cell.lock; run 'cellc lock --environment {}'", + environment_name, environment_name + )) + })?; + let manifest_environment = manifest + .environments + .get(environment_name) + .ok_or_else(|| CompileError::without_span(format!("unknown package environment '{}'", environment_name)))?; + if locked_environment.chain_id != manifest_environment.chain_id + || !locked_environment.genesis_hash.eq_ignore_ascii_case(&manifest_environment.genesis_hash) + { + return Err(CompileError::without_span(format!( + "environment '{}' chain identity differs between Cell.toml and Cell.lock; run 'cellc update --environment {}'", + environment_name, environment_name + ))); + } + (&locked_environment.dependencies, &locked_environment.dev_dependencies) + } else { + (&lockfile.root.dependencies, &lockfile.root.dev_dependencies) + }; - for (name, dep) in &manifest.dependencies { - self.resolve_dependency_from_root(name, dep, &self.root.clone(), &mut Vec::new())?; + for (alias, dependency) in selected { + let edges = if options.scope == DependencyScope::Test && manifest.dev_dependencies.contains_key(&alias) { + dev_edges + } else { + runtime_edges + }; + let node_id = edges.get(&alias).ok_or_else(|| { + CompileError::without_span(format!( + "dependency alias '{}' is not pinned for the selected mode/environment; run 'cellc lock' or 'cellc update' explicitly", + alias + )) + })?; + let locked = lockfile + .dependencies + .get(node_id) + .ok_or_else(|| CompileError::without_span(format!("Cell.lock edge '{}' targets missing node '{}'", alias, node_id)))?; + let issues = lock_dependency_consistency_issues(&alias, &dependency, locked, manifest.package.namespace.as_deref()); + if !issues.is_empty() { + return Err(CompileError::without_span(format!( + "Cell.lock dependency '{}' is inconsistent with Cell.toml: {}; run 'cellc update' explicitly", + alias, + issues.join("; ") + ))); + } + let node_options = dependency_resolution_options(&dependency, options); + self.materialize_locked_node(node_id, &lockfile, &node_options, &mut Vec::new())?; + self.root_dependencies.insert(alias, node_id.clone()); } Ok(()) } + fn materialize_locked_node( + &mut self, + node_id: &str, + lockfile: &Lockfile, + options: &ResolutionOptions, + stack: &mut Vec, + ) -> Result<()> { + if self.resolved.contains_key(node_id) { + return Ok(()); + } + if stack.iter().any(|candidate| candidate == node_id) { + let mut cycle = stack.clone(); + cycle.push(node_id.to_string()); + return Err(CompileError::without_span(format!("Cell.lock dependency cycle: {}", cycle.join(" -> ")))); + } + let locked = lockfile + .dependencies + .get(node_id) + .ok_or_else(|| CompileError::without_span(format!("Cell.lock is missing dependency node '{}'", node_id)))?; + let package_path = self.locked_source_path(locked, options.offline)?; + let manifest_path = package_path.join("Cell.toml"); + let bytes = std::fs::read(&manifest_path).map_err(|error| { + CompileError::without_span(format!("failed to read locked dependency manifest '{}': {}", manifest_path.display(), error)) + })?; + let digest = manifest_digest(&bytes); + if digest != locked.manifest_digest { + return Err(CompileError::without_span(format!( + "locked dependency '{}' manifest digest mismatch: expected '{}', got '{}'", + node_id, locked.manifest_digest, digest + ))); + } + let manifest_source = std::str::from_utf8(&bytes).map_err(|error| { + CompileError::without_span(format!("locked dependency manifest '{}' is not UTF-8: {}", manifest_path.display(), error)) + })?; + let manifest: PackageManifest = toml::from_str(manifest_source).map_err(|error| { + CompileError::without_span(format!("failed to parse locked dependency manifest '{}': {}", manifest_path.display(), error)) + })?; + if manifest.package.name != locked.name || manifest.package.version != locked.version { + return Err(CompileError::without_span(format!( + "locked dependency '{}' manifest identity is '{}@{}', expected '{}@{}'", + node_id, manifest.package.name, manifest.package.version, locked.name, locked.version + ))); + } + let source_hash = registry::compute_source_hash(&package_path)?; + if locked.source_hash.as_deref() != Some(source_hash.as_str()) { + return Err(CompileError::without_span(format!( + "locked dependency '{}' source hash mismatch: expected '{}', got '{}'", + node_id, + locked.source_hash.as_deref().unwrap_or(""), + source_hash + ))); + } + + let selected_dependencies = self.selected_dependencies(&manifest, options, false)?; + let mut selected_edges = BTreeMap::new(); + stack.push(node_id.to_string()); + for (alias, dependency) in selected_dependencies { + let target = locked.dependencies.get(&alias).ok_or_else(|| { + CompileError::without_span(format!( + "locked dependency node '{}' has no edge for selected dependency alias '{}'", + node_id, alias + )) + })?; + let target_lock = lockfile.dependencies.get(target).ok_or_else(|| { + CompileError::without_span(format!( + "locked dependency node '{}' edge '{}' targets missing node '{}'", + node_id, alias, target + )) + })?; + let issues = lock_dependency_consistency_issues(&alias, &dependency, target_lock, manifest.package.namespace.as_deref()); + if !issues.is_empty() { + return Err(CompileError::without_span(format!( + "locked dependency node '{}' edge '{}' is inconsistent with its manifest: {}", + node_id, + alias, + issues.join("; ") + ))); + } + let child_options = dependency_resolution_options(&dependency, options); + self.materialize_locked_node(target, lockfile, &child_options, stack)?; + selected_edges.insert(alias, target.clone()); + } + stack.pop(); + + self.resolved.insert( + node_id.to_string(), + ResolvedPackage { + node_id: node_id.to_string(), + name: locked.name.clone(), + version: locked.version.clone(), + path: package_path, + source: locked_source_to_package_source(&locked.source), + dependencies: selected_edges, + namespace: locked.namespace.clone(), + source_hash: Some(source_hash), + manifest_digest: digest, + }, + ); + Ok(()) + } + + fn locked_source_path(&self, locked: &LockedDependency, offline: bool) -> Result { + match &locked.source { + LockedSource::Path { path } => { + let path = self.root.join(path); + if !path.is_dir() { + return Err(CompileError::without_span(format!("locked path dependency '{}' does not exist", path.display()))); + } + Ok(path) + } + LockedSource::Git { url, revision } => { + let path = self.git_cache_dir().join(format!("{}-git-{}", locked.name, revision)); + if !path.exists() { + if offline { + return Err(CompileError::without_span(format!( + "offline mode cannot materialize missing git cache '{}' for {}", + path.display(), + locked.name + ))); + } + std::fs::create_dir_all(self.git_cache_dir())?; + Self::git_materialize_locked(url, &path, revision).map_err(CompileError::without_span)?; + } + let actual = Self::git_revision(&path).map_err(CompileError::without_span)?; + if actual != *revision { + return Err(CompileError::without_span(format!( + "locked git cache '{}' has revision '{}', expected '{}'", + path.display(), + actual, + revision + ))); + } + Ok(path) + } + LockedSource::Registry { url, revision, namespace, version, .. } => { + let suffix = revision.trim_start_matches("sha256:"); + let path = self.git_cache_dir().join(format!("{}-snapshot-{}", locked.name, suffix)); + if !path.exists() { + if offline { + return Err(CompileError::without_span(format!( + "offline mode cannot materialize missing Registry cache '{}' for {}", + path.display(), + locked.name + ))); + } + registry::materialize_locked_public_source_snapshot( + url, + revision, + &self.git_cache_dir(), + namespace, + &locked.name, + version, + locked.source_hash.as_deref().unwrap_or_default(), + )?; + } + Ok(path) + } + } + } + + fn validate_manifest_package_contract(&self, manifest: &PackageManifest) -> Result<()> { + semver::Version::parse(&manifest.package.version).map_err(|error| { + CompileError::without_span(format!( + "package '{}' has invalid semantic version '{}': {error}", + manifest.package.name, manifest.package.version + )) + })?; + for (name, environment) in &manifest.environments { + validate_environment(name, environment)?; + } + for environment in manifest.dependency_overrides.keys() { + if !manifest.environments.contains_key(environment) { + return Err(CompileError::without_span(format!( + "dependency override environment '{}' has no matching [environments.{}] declaration", + environment, environment + ))); + } + } + for (name, resolver) in &manifest.resolvers { + if name.trim().is_empty() { + return Err(CompileError::without_span("resolver names must not be empty")); + } + let command = Path::new(&resolver.command); + if !command.is_absolute() { + return Err(CompileError::without_span(format!("resolver '{}' command must be an absolute executable path", name))); + } + let digest = resolver.sha256.strip_prefix("sha256:").unwrap_or(&resolver.sha256); + if digest.len() != 64 || !digest.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err(CompileError::without_span(format!("resolver '{}' sha256 must contain exactly 32 bytes of hex", name))); + } + } + let declared_dependencies = manifest + .dependencies + .iter() + .chain(manifest.dev_dependencies.iter()) + .chain(manifest.dependency_overrides.values().flat_map(|dependencies| dependencies.iter())); + for (alias, dependency) in declared_dependencies { + if let Dependency::Detailed(detail) = dependency + && let Some(resolver) = detail.resolver.as_deref() + { + if detail.path.is_some() || detail.git.is_some() { + return Err(CompileError::without_span(format!( + "dependency '{}' cannot combine resolver with path or git", + alias + ))); + } + if !manifest.resolvers.contains_key(resolver) { + return Err(CompileError::without_span(format!( + "dependency '{}' selects undeclared resolver '{}'", + alias, resolver + ))); + } + } + } + if !manifest.build.dependencies.is_empty() { + return Err(CompileError::without_span( + "[build.dependencies] is reserved until isolated build-script execution is implemented; use [dependencies] for compile-time imports", + )); + } + Ok(()) + } + + fn selected_dependencies( + &self, + manifest: &PackageManifest, + options: &ResolutionOptions, + root: bool, + ) -> Result> { + let mut dependencies: BTreeMap = + manifest.dependencies.iter().map(|(name, dep)| (name.clone(), dep.clone())).collect(); + if options.scope == DependencyScope::Test && root { + for (name, dep) in &manifest.dev_dependencies { + if dependencies.insert(name.clone(), dep.clone()).is_some() { + return Err(CompileError::without_span(format!( + "dependency alias '{}' is declared in both [dependencies] and [dev_dependencies]", + name + ))); + } + } + } + + if let Some(environment) = options.environment.as_deref() { + if root && !manifest.environments.contains_key(environment) { + return Err(CompileError::without_span(format!( + "unknown package environment '{}'; declare [environments.{}] with chain_id and genesis_hash", + environment, environment + ))); + } + if let Some(overrides) = manifest.dependency_overrides.get(environment) { + for (alias, dependency) in overrides { + if !dependencies.contains_key(alias) { + return Err(CompileError::without_span(format!( + "environment '{}' overrides unknown dependency alias '{}'", + environment, alias + ))); + } + dependencies.insert(alias.clone(), dependency.clone()); + } + } + } else if root && !manifest.dependency_overrides.is_empty() { + return Err(CompileError::without_span( + "Cell.toml declares environment-specific dependency overrides; select one explicitly with --environment", + )); + } + + let active_optional = active_optional_dependencies(manifest, options)?; + dependencies.retain(|alias, dependency| !dependency_is_optional(dependency) || active_optional.contains(alias)); + Ok(dependencies) + } + /// Extract the version-requirement string carried by a dependency, if any. /// /// Path and git dependencies without a meaningful version return `None`, @@ -489,68 +1180,306 @@ dist/ } } - fn resolve_dependency_from_root(&mut self, name: &str, dep: &Dependency, base_root: &Path, stack: &mut Vec) -> Result<()> { - if stack.iter().any(|item| item == name) { - let mut cycle = stack.clone(); - cycle.push(name.to_string()); - return Err(CompileError::without_span(format!("Circular dependency detected: {}", cycle.join(" -> ")))); - } - - // Unified (single-version-per-package) resolution: if this package was - // already resolved elsewhere in the graph, the new version requirement - // must be satisfied by the already-selected version. If it is not, the - // dependency graph is unsatisfiable and we fail closed instead of - // silently keeping whichever version was resolved first. - if let Some(existing) = self.resolved.get(name) { - if let Some(req_str) = self.version_requirement_of(dep) { - let req = version::parse_version_req(&req_str)?; - if !version::satisfies(&existing.version, &req) { - return Err(CompileError::without_span(format!( - "version conflict for '{}': already resolved to '{}', which does not satisfy requirement '{}'", - name, existing.version, req_str - ))); - } - } - return Ok(()); - } - - stack.push(name.to_string()); - - let (resolved, child_dependencies) = match dep { + fn resolve_dependency_from_root( + &mut self, + alias: &str, + dep: &Dependency, + base_root: &Path, + parent_options: &ResolutionOptions, + stack_ids: &mut Vec, + stack_labels: &mut Vec, + ) -> Result { + let package_name = dependency_package_name(alias, dep); + let (mut resolved, manifest) = match dep { Dependency::Simple(version) => { - let (resolved, manifest) = - self.resolve_from_registry_with_manifest(name, version, None, registry::RegistryResolutionPolicy::default())?; - (resolved, manifest.dependencies) + self.resolve_from_registry_with_manifest(&package_name, version, None, registry::RegistryResolutionPolicy::default())? } Dependency::Detailed(detailed) => { - if let Some(path) = &detailed.path { - let (resolved, manifest) = self.resolve_from_path_at(name, path, base_root)?; - (resolved, manifest.dependencies) + if detailed.resolver.is_some() { + let normalized = self.resolve_external_dependency(alias, &package_name, detailed, base_root, parent_options)?; + let resolved = if let Some(git) = &normalized.git { + self.resolve_from_git_with_manifest(&package_name, git, &normalized)? + } else { + self.resolve_from_registry_with_manifest( + &package_name, + &normalized.version, + normalized.namespace.as_deref(), + registry::RegistryResolutionPolicy::default(), + )? + }; + let exact = version::parse_version_req(&normalized.version)?; + if !version::satisfies(&resolved.0.version, &exact) { + return Err(CompileError::without_span(format!( + "external resolver for '{}' declared version inconsistent with materialized package '{}'", + alias, resolved.0.version + ))); + } + resolved + } else if let Some(path) = &detailed.path { + self.resolve_from_path_at(&package_name, path, base_root)? } else if let Some(git) = &detailed.git { - let (resolved, manifest) = self.resolve_from_git_with_manifest(name, git, detailed)?; - (resolved, manifest.dependencies) + self.resolve_from_git_with_manifest(&package_name, git, detailed)? } else { let ns = detailed.namespace.as_deref(); - let (resolved, manifest) = self.resolve_from_registry_with_manifest( - name, + self.resolve_from_registry_with_manifest( + &package_name, &detailed.version, ns, - registry::RegistryResolutionPolicy::default(), - )?; - (resolved, manifest.dependencies) + registry::RegistryResolutionPolicy { + allow_unverified: detailed.allow_unverified, + allow_quarantined: detailed.allow_quarantined, + }, + )? } } }; - let package_root = resolved.path.clone(); - self.resolved.insert(name.to_string(), resolved); + self.validate_manifest_package_contract(&manifest)?; + if manifest.package.name != package_name { + return Err(CompileError::without_span(format!( + "dependency alias '{}' expects package '{}' but '{}' declares package name '{}'", + alias, + package_name, + resolved.path.display(), + manifest.package.name + ))); + } + let child_options = dependency_resolution_options(dep, parent_options); + let node_id = package_node_id(&resolved, &child_options); + if let Some(requirement) = self.version_requirement_of(dep) { + let requirement = version::parse_version_req(&requirement)?; + if !version::satisfies(&resolved.version, &requirement) { + return Err(CompileError::without_span(format!( + "dependency alias '{}' resolved package '{}' to '{}', which does not satisfy its requirement", + alias, package_name, resolved.version + ))); + } + } + if let Some(existing) = self.resolved.get(&node_id) { + if existing.manifest_digest != resolved.manifest_digest || existing.source_hash != resolved.source_hash { + return Err(CompileError::without_span(format!( + "dependency node '{}' resolved with conflicting manifest or source identity", + node_id + ))); + } + return Ok(node_id); + } + if let Some(position) = stack_ids.iter().position(|item| item == &node_id) { + let mut cycle = stack_labels[position..].to_vec(); + cycle.push(alias.to_string()); + return Err(CompileError::without_span(format!("Circular dependency detected: {}", cycle.join(" -> ")))); + } + + stack_ids.push(node_id.clone()); + stack_labels.push(alias.to_string()); + let child_dependencies = self.selected_dependencies(&manifest, &child_options, false)?; + let mut child_edges = BTreeMap::new(); + for (child_alias, child_dep) in child_dependencies { + let child_id = + self.resolve_dependency_from_root(&child_alias, &child_dep, &resolved.path, &child_options, stack_ids, stack_labels)?; + child_edges.insert(child_alias, child_id); + } + stack_ids.pop(); + stack_labels.pop(); + + resolved.node_id = node_id.clone(); + resolved.dependencies = child_edges; + self.resolved.insert(node_id.clone(), resolved); + Ok(node_id) + } + + fn resolve_external_dependency( + &self, + alias: &str, + package_name: &str, + dependency: &DetailedDependency, + owner_root: &Path, + options: &ResolutionOptions, + ) -> Result { + if options.offline { + return Err(CompileError::without_span(format!( + "offline mode cannot invoke external resolver for dependency '{}'", + alias + ))); + } + let resolver_name = dependency + .resolver + .as_deref() + .ok_or_else(|| CompileError::without_span(format!("dependency '{}' has no external resolver name", alias)))?; + let owner_manifest_path = owner_root.join("Cell.toml"); + let owner_manifest: PackageManifest = toml::from_str(&std::fs::read_to_string(&owner_manifest_path).map_err(|error| { + CompileError::without_span(format!( + "failed to read resolver owner manifest '{}': {}", + owner_manifest_path.display(), + error + )) + })?)?; + let resolver = owner_manifest.resolvers.get(resolver_name).ok_or_else(|| { + CompileError::without_span(format!("dependency '{}' selects undeclared resolver '{}'", alias, resolver_name)) + })?; + if resolver.args.len() > 64 || resolver.args.iter().any(|argument| argument.len() > 4096) { + return Err(CompileError::without_span(format!("resolver '{}' exceeds the bounded argument contract", resolver_name))); + } + let command_path = Path::new(&resolver.command); + if !command_path.is_absolute() || !command_path.is_file() { + return Err(CompileError::without_span(format!( + "resolver '{}' command must be an existing absolute executable path", + resolver_name + ))); + } + let expected_digest = resolver.sha256.strip_prefix("sha256:").unwrap_or(&resolver.sha256).to_ascii_lowercase(); + let actual_digest = sha256_file(command_path)?; + if actual_digest != expected_digest { + return Err(CompileError::without_span(format!( + "resolver '{}' executable digest mismatch: expected sha256:{}, got sha256:{}", + resolver_name, expected_digest, actual_digest + ))); + } - for (child_name, child_dep) in child_dependencies { - self.resolve_dependency_from_root(&child_name, &child_dep, &package_root, stack)?; + let environment = options.environment.as_deref().map(|name| { + let config = owner_manifest.environments.get(name).expect("selected environment was validated"); + ExternalResolverEnvironment { name, chain_id: &config.chain_id, genesis_hash: &config.genesis_hash } + }); + let request = ExternalResolverRequest { + schema: EXTERNAL_RESOLVER_REQUEST_SCHEMA, + alias, + package: package_name, + version_requirement: &dependency.version, + environment, + }; + let request = serde_json::to_vec(&request)?; + let temp_root = self.root.join(".cell/resolver-tmp"); + std::fs::create_dir_all(&temp_root)?; + let nonce = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_nanos(); + let stem = format!("{}-{}-{nonce}", std::process::id(), sanitize_node_component(alias)); + let stdout_path = temp_root.join(format!("{stem}.stdout")); + let stderr_path = temp_root.join(format!("{stem}.stderr")); + let stdout_file = std::fs::OpenOptions::new().write(true).create_new(true).open(&stdout_path)?; + let stderr_file = std::fs::OpenOptions::new().write(true).create_new(true).open(&stderr_path)?; + let mut child = std::process::Command::new(command_path) + .args(&resolver.args) + .current_dir(owner_root) + .env_clear() + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::from(stdout_file)) + .stderr(std::process::Stdio::from(stderr_file)) + .spawn() + .map_err(|error| CompileError::without_span(format!("failed to start resolver '{}': {}", resolver_name, error)))?; + if let Some(mut stdin) = child.stdin.take() { + stdin.write_all(&request)?; + stdin.write_all(b"\n")?; } - stack.pop(); - Ok(()) + let started = Instant::now(); + let status = loop { + if let Some(status) = child.try_wait()? { + break status; + } + let output_too_large = [&stdout_path, &stderr_path] + .iter() + .any(|path| std::fs::metadata(path).is_ok_and(|metadata| metadata.len() > EXTERNAL_RESOLVER_MAX_OUTPUT_BYTES)); + if output_too_large || started.elapsed() >= EXTERNAL_RESOLVER_TIMEOUT { + let _ = child.kill(); + let _ = child.wait(); + let _ = std::fs::remove_file(&stdout_path); + let _ = std::fs::remove_file(&stderr_path); + let reason = if output_too_large { "output exceeded 1 MiB" } else { "timed out after 10 seconds" }; + return Err(CompileError::without_span(format!("resolver '{}' {}", resolver_name, reason))); + } + std::thread::sleep(Duration::from_millis(10)); + }; + let stdout = read_bounded_resolver_output(&stdout_path)?; + let stderr = read_bounded_resolver_output(&stderr_path)?; + let _ = std::fs::remove_file(&stdout_path); + let _ = std::fs::remove_file(&stderr_path); + if !status.success() { + return Err(CompileError::without_span(format!( + "resolver '{}' exited with {}: {}", + resolver_name, + status, + String::from_utf8_lossy(&stderr).trim() + ))); + } + let response: ExternalResolverResponse = serde_json::from_slice(&stdout) + .map_err(|error| CompileError::without_span(format!("resolver '{}' returned invalid JSON: {}", resolver_name, error)))?; + if response.schema != EXTERNAL_RESOLVER_RESPONSE_SCHEMA { + return Err(CompileError::without_span(format!( + "resolver '{}' returned unsupported schema '{}'", + resolver_name, response.schema + ))); + } + if response.dependency.package != package_name { + return Err(CompileError::without_span(format!( + "resolver '{}' returned package '{}', expected '{}'", + resolver_name, response.dependency.package, package_name + ))); + } + semver::Version::parse(&response.dependency.version).map_err(|error| { + CompileError::without_span(format!( + "resolver '{}' version '{}' is not exact SemVer: {}", + resolver_name, response.dependency.version, error + )) + })?; + let requested = version::parse_version_req(&dependency.version)?; + if !version::satisfies(&response.dependency.version, &requested) { + return Err(CompileError::without_span(format!( + "resolver '{}' returned version '{}' outside requested range '{}'", + resolver_name, response.dependency.version, dependency.version + ))); + } + + match (&response.dependency.git, &response.dependency.namespace) { + (Some(git), None) => { + let revision = + response.dependency.rev.as_deref().ok_or_else(|| { + CompileError::without_span(format!("resolver '{}' Git response requires rev", resolver_name)) + })?; + if revision.len() != 40 || !revision.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err(CompileError::without_span(format!( + "resolver '{}' Git rev must be a full 40-hex commit", + resolver_name + ))); + } + Ok(DetailedDependency { + version: format!("={}", response.dependency.version), + namespace: None, + package: dependency.package.clone(), + resolver: None, + git: Some(git.clone()), + branch: None, + tag: None, + rev: Some(revision.to_string()), + path: None, + optional: dependency.optional, + features: dependency.features.clone(), + default_features: dependency.default_features, + allow_unverified: false, + allow_quarantined: false, + }) + } + (None, Some(namespace)) => { + if response.dependency.rev.is_some() || namespace.trim().is_empty() { + return Err(CompileError::without_span(format!("resolver '{}' Registry response is malformed", resolver_name))); + } + Ok(DetailedDependency { + version: format!("={}", response.dependency.version), + namespace: Some(namespace.clone()), + package: dependency.package.clone(), + resolver: None, + git: None, + branch: None, + tag: None, + rev: None, + path: None, + optional: dependency.optional, + features: dependency.features.clone(), + default_features: dependency.default_features, + allow_unverified: dependency.allow_unverified, + allow_quarantined: dependency.allow_quarantined, + }) + } + _ => Err(CompileError::without_span(format!("resolver '{}' must return exactly one of git or namespace", resolver_name))), + } } pub fn resolve_from_registry(&self, name: &str, version: &str) -> Result { @@ -595,36 +1524,45 @@ dist/ )) })?; - // 2. Clone/update discovery index → find source repo URL + // 2. Resolve accepted public-registry state (or an explicitly selected + // offline Git mirror) → find the source repository URL. let cache_dir = self.registry_cache_dir(); - let registry_url = registry::default_registry_url(); - let discovery = registry::DiscoveryIndex::new(®istry_url, &cache_dir); - let entry = discovery.lookup(&resolved_namespace, name).map_err(|e| { + let registry_resolution = registry::lookup_for_resolution(&resolved_namespace, name, &cache_dir).map_err(|e| { CompileError::without_span(format!( - "failed to resolve registry dependency '{}/{}@{}' via discovery index '{}': {}", - resolved_namespace, name, version, registry_url, e + "failed to resolve registry dependency '{}/{}@{}': {}", + resolved_namespace, name, version, e )) })?; + let registry::RegistryResolution { registry_url, entry, authoritative_index, mut source_snapshots } = registry_resolution; + let repository_url = entry.source; - // 3. Clone source repo - let source_url = &entry.source; + // 3. Prepare the immutable Registry snapshot cache, or clone the + // explicitly selected legacy Git mirror. let source_cache = self.git_cache_dir(); std::fs::create_dir_all(&source_cache) .map_err(|e| CompileError::without_span(format!("failed to create source cache directory: {}", e)))?; - - let cache_key = format!("{}#{}", source_url, version); - let cache_name = format!("{}-{:016x}", name, simple_hash(&cache_key)); - let clone_dir = source_cache.join(&cache_name); - - if clone_dir.exists() && clone_dir.join(".git").exists() { - registry::git_update(&clone_dir).map_err(CompileError::without_span)?; + let public_registry_authoritative = authoritative_index.is_some(); + let legacy_clone = if public_registry_authoritative { + None } else { - let _ = std::fs::remove_dir_all(&clone_dir); - registry::git_clone(source_url, &clone_dir).map_err(CompileError::without_span)?; - } + let cache_key = format!("{}#{}", repository_url, version); + let cache_name = format!("{}-{:016x}", name, simple_hash(&cache_key)); + let clone_dir = source_cache.join(&cache_name); + if clone_dir.exists() && clone_dir.join(".git").exists() { + registry::git_update(&clone_dir).map_err(CompileError::without_span)?; + } else { + let _ = std::fs::remove_dir_all(&clone_dir); + registry::git_clone(&repository_url, &clone_dir).map_err(CompileError::without_span)?; + } + Some(clone_dir) + }; - // 4. Resolve version from registry.json and check out its declared tag. - let reg_index = registry::RegistryIndex::read_from_repo(&clone_dir)?; + // 4. Resolve versions against production-accepted status. A legacy + // Git override retains the historical registry.json authority. + let reg_index = match authoritative_index { + Some(index) => index, + None => registry::RegistryIndex::read_from_repo(legacy_clone.as_ref().expect("legacy clone exists"))?, + }; if reg_index.schema_version != registry::RegistryIndex::CURRENT_SCHEMA_VERSION { return Err(CompileError::without_span(format!( "registry package '{}/{}' uses unsupported registry.json schema_version {}; expected {}", @@ -650,50 +1588,80 @@ dist/ if selected_version.source_hash.is_empty() { return Err(CompileError::without_span(format!( "registry package '{}/{}@{}' has no source_hash in registry.json", - resolved_namespace, name, selected_version.version - ))); - } - registry::git_checkout(&clone_dir, &selected_version.tag).map_err(CompileError::without_span)?; - - let revision = registry::git_revision(&clone_dir).unwrap_or_else(|_| "unknown".to_string()); - - // 5. Re-read registry.json at the checked-out tag and verify source_hash. - let tagged_index = registry::RegistryIndex::read_from_repo(&clone_dir)?; - if tagged_index.schema_version != registry::RegistryIndex::CURRENT_SCHEMA_VERSION { - return Err(CompileError::without_span(format!( - "registry package '{}/{}@{}' uses unsupported registry.json schema_version {}; expected {}", - resolved_namespace, - name, - selected_version.version, - tagged_index.schema_version, - registry::RegistryIndex::CURRENT_SCHEMA_VERSION - ))); - } - if tagged_index.name != name || tagged_index.namespace != resolved_namespace { - return Err(CompileError::without_span(format!( - "registry.json identity mismatch for checked-out '{}/{}@{}': found '{}/{}'", - resolved_namespace, name, selected_version.version, tagged_index.namespace, tagged_index.name - ))); - } - let tagged_version = tagged_index.versions.iter().find(|v| v.version == selected_version.version).ok_or_else(|| { - CompileError::without_span(format!( - "registry package '{}/{}@{}' tag '{}' does not contain a matching registry.json version entry", - resolved_namespace, name, selected_version.version, selected_version.tag - )) - })?; - if tagged_version.source_hash.is_empty() { - return Err(CompileError::without_span(format!( - "registry package '{}/{}@{}' has no source_hash in registry.json", - resolved_namespace, name, tagged_version.version + resolved_namespace, name, selected_version.version ))); } - if tagged_version - .resolver_block_reason(policy, matches!(crate::package::version::parse_version_req(version), Ok(VersionReq::Exact(_)))) - .is_some() - { - return Err(registry_resolution_blocked_error(&resolved_namespace, name, version, tagged_version, policy)); - } - let computed_source_hash = registry::compute_source_hash(&clone_dir)?; + + // 5. Public resolution materializes the content-addressed Registry + // snapshot. The explicit Git override retains tag/registry.json + // cross-checking for offline and private mirrors. + let (package_dir, revision, source_url, tagged_version) = if public_registry_authoritative { + let snapshot = source_snapshots.remove(&selected_version.version).ok_or_else(|| { + CompileError::without_span(format!( + "public registry package '{}/{}@{}' has no immutable source snapshot", + resolved_namespace, name, selected_version.version + )) + })?; + let source_url = snapshot.url.clone(); + let revision = snapshot.snapshot_hash.clone(); + let package_dir = registry::materialize_public_source_snapshot( + &snapshot, + &source_cache, + &resolved_namespace, + name, + &selected_version.version, + &selected_version.source_hash, + )?; + (package_dir, revision, source_url, selected_version.clone()) + } else { + let clone_dir = legacy_clone.expect("legacy clone exists"); + registry::git_checkout(&clone_dir, &selected_version.tag).map_err(CompileError::without_span)?; + let revision = registry::git_revision(&clone_dir).unwrap_or_else(|_| "unknown".to_string()); + let tagged_index = registry::RegistryIndex::read_from_repo(&clone_dir)?; + if tagged_index.schema_version != registry::RegistryIndex::CURRENT_SCHEMA_VERSION { + return Err(CompileError::without_span(format!( + "registry package '{}/{}@{}' uses unsupported registry.json schema_version {}; expected {}", + resolved_namespace, + name, + selected_version.version, + tagged_index.schema_version, + registry::RegistryIndex::CURRENT_SCHEMA_VERSION + ))); + } + if tagged_index.name != name || tagged_index.namespace != resolved_namespace { + return Err(CompileError::without_span(format!( + "registry.json identity mismatch for checked-out '{}/{}@{}': found '{}/{}'", + resolved_namespace, name, selected_version.version, tagged_index.namespace, tagged_index.name + ))); + } + let tagged_version = + tagged_index.versions.iter().find(|candidate| candidate.version == selected_version.version).cloned().ok_or_else( + || { + CompileError::without_span(format!( + "registry package '{}/{}@{}' tag '{}' does not contain a matching registry.json version entry", + resolved_namespace, name, selected_version.version, selected_version.tag + )) + }, + )?; + if tagged_version.source_hash != selected_version.source_hash + || tagged_version.tag != selected_version.tag + || tagged_version.edition != selected_version.edition + || tagged_version.compatibility_profile_hash != selected_version.compatibility_profile_hash + { + return Err(CompileError::without_span(format!( + "registry identity mismatch for '{}/{}@{}' between the selected index and checked-out tag", + resolved_namespace, name, tagged_version.version + ))); + } + if tagged_version + .resolver_block_reason(policy, matches!(crate::package::version::parse_version_req(version), Ok(VersionReq::Exact(_)))) + .is_some() + { + return Err(registry_resolution_blocked_error(&resolved_namespace, name, version, &tagged_version, policy)); + } + (clone_dir, revision, repository_url.clone(), tagged_version) + }; + let computed_source_hash = registry::compute_source_hash(&package_dir)?; if computed_source_hash != tagged_version.source_hash { return Err(CompileError::without_span(format!( "source_hash mismatch for '{}/{}@{}': expected '{}', got '{}'", @@ -702,7 +1670,7 @@ dist/ } // 6. Read Cell.toml and resolve transitive dependencies - let manifest_path = clone_dir.join("Cell.toml"); + let manifest_path = package_dir.join("Cell.toml"); if !manifest_path.exists() { return Err(CompileError::without_span(format!( "registry package '{}/{}' does not contain Cell.toml", @@ -720,7 +1688,7 @@ dist/ } if manifest.package.version != tagged_version.version { return Err(CompileError::without_span(format!( - "registry package '{}/{}' registry.json version '{}' does not match Cell.toml version '{}'", + "registry package '{}/{}' selected version '{}' does not match Cell.toml version '{}'", resolved_namespace, name, tagged_version.version, manifest.package.version ))); } @@ -733,19 +1701,21 @@ dist/ Ok(( ResolvedPackage { + node_id: String::new(), name: name.to_string(), version: manifest.package.version.clone(), - path: clone_dir.clone(), + path: package_dir, source: PackageSource::Registry { registry: registry_url, - url: source_url.clone(), + url: source_url, revision, namespace: resolved_namespace.clone(), version: manifest.package.version.clone(), }, - dependencies: manifest.dependencies.keys().cloned().collect(), + dependencies: BTreeMap::new(), namespace: Some(resolved_namespace), source_hash: Some(computed_source_hash), + manifest_digest: manifest_digest(content.as_bytes()), }, manifest, )) @@ -761,7 +1731,10 @@ dist/ } fn resolve_from_path_at(&self, name: &str, path: &str, base_root: &Path) -> Result<(ResolvedPackage, PackageManifest)> { - let package_path = base_root.join(path); + let requested_path = base_root.join(path); + let package_path = canonical_path(&requested_path).map_err(|_| { + CompileError::without_span(format!("Dependency '{}' not found at path '{}'", name, requested_path.display())) + })?; let manifest_path = package_path.join("Cell.toml"); if !manifest_path.exists() { @@ -770,22 +1743,22 @@ dist/ let content = std::fs::read_to_string(&manifest_path)?; let manifest: PackageManifest = toml::from_str(&content)?; + let source_hash = registry::compute_source_hash(&package_path)?; - let source_path = if base_root == self.root { - PathBuf::from(path) - } else { - package_path.strip_prefix(&self.root).unwrap_or(&package_path).to_path_buf() - }; + let canonical_root = canonical_path(&self.root)?; + let source_path = relative_path(&canonical_root, &package_path).unwrap_or_else(|| package_path.clone()); Ok(( ResolvedPackage { + node_id: String::new(), name: name.to_string(), version: manifest.package.version.clone(), path: package_path, source: PackageSource::Local(source_path), - dependencies: manifest.dependencies.keys().cloned().collect(), + dependencies: BTreeMap::new(), namespace: manifest.package.namespace.clone(), - source_hash: None, + source_hash: Some(source_hash), + manifest_digest: manifest_digest(content.as_bytes()), }, manifest, )) @@ -822,14 +1795,39 @@ dist/ git_result.map_err(|e| CompileError::without_span(format!("git dependency '{}' from '{}' failed: {}", name, url, e)))?; if let Some(ref_str) = requested_ref { - Self::git_checkout(&clone_dir, ref_str).map_err(|e| { - CompileError::without_span(format!("git dependency '{}' failed to checkout '{}': {}", name, ref_str, e)) + let checkout_ref = detailed.branch.as_ref().map(|branch| format!("origin/{branch}")).unwrap_or_else(|| ref_str.clone()); + Self::git_checkout(&clone_dir, &checkout_ref).map_err(|e| { + CompileError::without_span(format!("git dependency '{}' failed to checkout '{}': {}", name, checkout_ref, e)) })?; } - let revision = Self::git_revision(&clone_dir).unwrap_or_else(|_| "unknown".to_string()); + let revision = Self::git_revision(&clone_dir).map_err(|error| { + CompileError::without_span(format!("git dependency '{}' could not resolve an immutable revision: {}", name, error)) + })?; + if revision.len() != 40 || !revision.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err(CompileError::without_span(format!( + "git dependency '{}' resolved non-canonical revision '{}'; expected a full 40-hex commit", + name, revision + ))); + } + let immutable_dir = cache_dir.join(format!("{}-git-{}", name, revision)); + if immutable_dir.exists() { + let cached_revision = Self::git_revision(&immutable_dir).map_err(CompileError::without_span)?; + if cached_revision != revision { + return Err(CompileError::without_span(format!( + "immutable git cache '{}' has revision '{}', expected '{}'", + immutable_dir.display(), + cached_revision, + revision + ))); + } + } else { + Self::git_materialize_immutable(&clone_dir, &immutable_dir, &revision).map_err(|error| { + CompileError::without_span(format!("failed to materialize immutable git dependency '{}': {}", name, error)) + })?; + } - let manifest_path = clone_dir.join("Cell.toml"); + let manifest_path = immutable_dir.join("Cell.toml"); if !manifest_path.exists() { return Err(CompileError::without_span(format!( "git dependency '{}' from '{}' does not contain Cell.toml at repository root", @@ -839,16 +1837,19 @@ dist/ let content = std::fs::read_to_string(&manifest_path)?; let manifest: PackageManifest = toml::from_str(&content)?; + let source_hash = registry::compute_source_hash(&immutable_dir)?; Ok(( ResolvedPackage { + node_id: String::new(), name: name.to_string(), version: manifest.package.version.clone(), - path: clone_dir.clone(), + path: immutable_dir, source: PackageSource::Git { url: url.to_string(), revision }, - dependencies: manifest.dependencies.keys().cloned().collect(), + dependencies: BTreeMap::new(), namespace: manifest.package.namespace.clone(), - source_hash: None, + source_hash: Some(source_hash), + manifest_digest: manifest_digest(content.as_bytes()), }, manifest, )) @@ -922,17 +1923,61 @@ dist/ Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } - pub fn get_resolved(&self) -> &HashMap { + fn git_materialize_immutable(source: &Path, target: &Path, revision: &str) -> std::result::Result<(), String> { + let output = std::process::Command::new("git") + .args(["clone", "--no-checkout", "--no-hardlinks", &source.to_string_lossy(), &target.to_string_lossy()]) + .output() + .map_err(|error| format!("failed to clone immutable cache: {error}"))?; + if !output.status.success() { + return Err(format!("git clone failed: {}", String::from_utf8_lossy(&output.stderr).trim())); + } + let output = std::process::Command::new("git") + .args(["checkout", "--detach", revision]) + .current_dir(target) + .output() + .map_err(|error| format!("failed to checkout immutable revision: {error}"))?; + if !output.status.success() { + let _ = std::fs::remove_dir_all(target); + return Err(format!("git checkout failed: {}", String::from_utf8_lossy(&output.stderr).trim())); + } + Ok(()) + } + + fn git_materialize_locked(url: &str, target: &Path, revision: &str) -> std::result::Result<(), String> { + let output = std::process::Command::new("git") + .args(["clone", "--no-checkout", url, &target.to_string_lossy()]) + .output() + .map_err(|error| format!("failed to clone locked git source: {error}"))?; + if !output.status.success() { + return Err(format!("git clone failed: {}", String::from_utf8_lossy(&output.stderr).trim())); + } + let output = std::process::Command::new("git") + .args(["checkout", "--detach", revision]) + .current_dir(target) + .output() + .map_err(|error| format!("failed to checkout locked git revision: {error}"))?; + if !output.status.success() { + let _ = std::fs::remove_dir_all(target); + return Err(format!("git checkout failed: {}", String::from_utf8_lossy(&output.stderr).trim())); + } + Ok(()) + } + + pub fn get_resolved(&self) -> &BTreeMap { &self.resolved } + pub fn root_dependencies(&self) -> &BTreeMap { + &self.root_dependencies + } + pub fn build_dependency_graph(&self) -> DependencyGraph { let mut graph = DependencyGraph::new(); - for (name, package) in &self.resolved { - graph.add_node(name.clone()); - for dep in &package.dependencies { - graph.add_edge(name.clone(), dep.clone()); + for (node_id, package) in &self.resolved { + graph.add_node(node_id.clone()); + for dependency_id in package.dependencies.values() { + graph.add_edge(node_id.clone(), dependency_id.clone()); } } @@ -954,6 +1999,20 @@ dist/ } } +fn locked_source_to_package_source(source: &LockedSource) -> PackageSource { + match source { + LockedSource::Path { path } => PackageSource::Local(PathBuf::from(path)), + LockedSource::Git { url, revision } => PackageSource::Git { url: url.clone(), revision: revision.clone() }, + LockedSource::Registry { registry, url, revision, namespace, version } => PackageSource::Registry { + registry: registry.clone(), + url: url.clone(), + revision: revision.clone(), + namespace: namespace.clone(), + version: version.clone(), + }, + } +} + pub struct DependencyGraph { nodes: Vec, edges: HashMap>, @@ -985,10 +2044,10 @@ impl DependencyGraph { let mut rec_stack = Vec::new(); for node in &self.nodes { - if !visited.contains_key(node) { - if let Some(cycle) = self.dfs_find_cycle(node, &mut visited, &mut rec_stack) { - return Some(cycle); - } + if !visited.contains_key(node) + && let Some(cycle) = self.dfs_find_cycle(node, &mut visited, &mut rec_stack) + { + return Some(cycle); } } @@ -1031,17 +2090,41 @@ fn simple_hash(s: &str) -> u64 { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Lockfile { pub version: u32, - #[serde(default)] + pub schema: String, pub package: LockfilePackageInfo, + pub root: LockedRootGraph, pub dependencies: BTreeMap, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub environments: BTreeMap, #[serde(default, skip_serializing_if = "Option::is_none")] pub package_build: Option, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub deployment: BTreeMap, } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct LockedRootGraph { + #[serde(default, skip_serializing_if = "String::is_empty")] + pub manifest_digest: String, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub dependencies: BTreeMap, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub dev_dependencies: BTreeMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LockedEnvironment { + pub chain_id: String, + pub genesis_hash: String, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub dependencies: BTreeMap, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub dev_dependencies: BTreeMap, +} + #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct LockfilePackageInfo { + pub edition: CellScriptEdition, #[serde(default, skip_serializing_if = "String::is_empty")] pub name: String, #[serde(default, skip_serializing_if = "String::is_empty")] @@ -1069,13 +2152,17 @@ pub struct LockfileDeploymentRef { } impl Lockfile { - pub const CURRENT_VERSION: u32 = 1; + pub const CURRENT_VERSION: u32 = 3; + pub const CURRENT_SCHEMA: &'static str = "cellscript-lock-v0.24-graph-v1"; pub fn new() -> Self { Self { version: Self::CURRENT_VERSION, + schema: Self::CURRENT_SCHEMA.to_string(), package: LockfilePackageInfo::default(), + root: LockedRootGraph::default(), dependencies: BTreeMap::new(), + environments: BTreeMap::new(), package_build: None, deployment: BTreeMap::new(), } @@ -1088,21 +2175,115 @@ impl Lockfile { } let content = std::fs::read_to_string(&lock_path) .map_err(|error| CompileError::without_span(format!("failed to read lockfile '{}': {}", lock_path.display(), error)))?; - let lockfile = toml::from_str(&content) + let lockfile: Self = toml::from_str(&content) .map_err(|error| CompileError::without_span(format!("failed to parse lockfile '{}': {}", lock_path.display(), error)))?; + lockfile.validate_schema()?; Ok(Some(lockfile)) } pub fn write_to_root(&self, root: &Path) -> Result<()> { + self.validate_schema()?; let lock_path = root.join("Cell.lock"); let content = toml::to_string_pretty(self)?; std::fs::write(&lock_path, content)?; Ok(()) } - pub fn update_from_resolved(&mut self, resolved: &HashMap) { - for (name, package) in resolved { + pub fn validate_schema(&self) -> Result<()> { + if self.version != Self::CURRENT_VERSION { + return Err(CompileError::without_span(format!( + "unsupported Cell.lock version {}; expected {}", + self.version, + Self::CURRENT_VERSION + ))); + } + if self.schema != Self::CURRENT_SCHEMA { + return Err(CompileError::without_span(format!( + "unsupported Cell.lock schema '{}'; expected '{}'", + self.schema, + Self::CURRENT_SCHEMA + ))); + } + if let Some(build) = &self.package_build { + if build.edition != self.package.edition { + return Err(CompileError::without_span(format!( + "Cell.lock package/build edition mismatch: package is '{}' but build is '{}'", + self.package.edition, build.edition + ))); + } + if build.compatibility_profile_hash.is_empty() { + return Err(CompileError::without_span("Cell.lock v3 package_build requires compatibility_profile_hash")); + } + } + self.validate_graph()?; + Ok(()) + } + + fn validate_graph(&self) -> Result<()> { + for (node_id, dependency) in &self.dependencies { + if dependency.name.is_empty() + || dependency.manifest_digest.is_empty() + || dependency.source_hash.as_deref().is_none_or(str::is_empty) + { + return Err(CompileError::without_span(format!( + "Cell.lock dependency node '{}' requires name, manifest_digest, and source_hash", + node_id + ))); + } + for (alias, target) in &dependency.dependencies { + if !self.dependencies.contains_key(target) { + return Err(CompileError::without_span(format!( + "Cell.lock dependency node '{}' edge '{}' targets missing node '{}'", + node_id, alias, target + ))); + } + } + } + self.validate_root_edges("root dependencies", &self.root.dependencies)?; + self.validate_root_edges("root dev-dependencies", &self.root.dev_dependencies)?; + for (name, environment) in &self.environments { + validate_environment( + name, + &CkbEnvironment { chain_id: environment.chain_id.clone(), genesis_hash: environment.genesis_hash.clone() }, + )?; + self.validate_root_edges(&format!("environment '{}' dependencies", name), &environment.dependencies)?; + self.validate_root_edges(&format!("environment '{}' dev-dependencies", name), &environment.dev_dependencies)?; + } + let graph = self.dependency_graph(); + if let Some(cycle) = graph.find_cycle() { + return Err(CompileError::without_span(format!("Cell.lock dependency graph contains a cycle: {}", cycle.join(" -> ")))); + } + Ok(()) + } + + fn validate_root_edges(&self, label: &str, edges: &BTreeMap) -> Result<()> { + for (alias, target) in edges { + if !self.dependencies.contains_key(target) { + return Err(CompileError::without_span(format!( + "Cell.lock {} edge '{}' targets missing node '{}'", + label, alias, target + ))); + } + } + Ok(()) + } + + fn dependency_graph(&self) -> DependencyGraph { + let mut graph = DependencyGraph::new(); + for (node_id, dependency) in &self.dependencies { + graph.add_node(node_id.clone()); + for target in dependency.dependencies.values() { + graph.add_edge(node_id.clone(), target.clone()); + } + } + graph + } + + pub fn update_from_resolved(&mut self, resolved: &BTreeMap) { + for (node_id, package) in resolved { let locked = LockedDependency { + name: package.name.clone(), + namespace: package.namespace.clone(), version: package.version.clone(), source: match &package.source { PackageSource::Local(path) => LockedSource::Path { path: path.to_string_lossy().to_string() }, @@ -1116,17 +2297,72 @@ impl Lockfile { }, }, source_hash: package.source_hash.clone(), + manifest_digest: package.manifest_digest.clone(), + dependencies: package.dependencies.clone(), build: None, }; - self.dependencies.insert(name.clone(), locked); + self.dependencies.insert(node_id.clone(), locked); } } - pub fn replace_with_resolved(&mut self, resolved: &HashMap) { + pub fn replace_with_resolved(&mut self, resolved: &BTreeMap) { self.dependencies.clear(); self.update_from_resolved(resolved); } + pub fn replace_with_resolution( + &mut self, + manager: &PackageManager, + manifest: &PackageManifest, + options: &ResolutionOptions, + ) -> Result<()> { + self.dependencies.clear(); + self.root = LockedRootGraph::default(); + self.environments.clear(); + self.merge_resolution(manager, manifest, options) + } + + pub fn merge_resolution( + &mut self, + manager: &PackageManager, + manifest: &PackageManifest, + options: &ResolutionOptions, + ) -> Result<()> { + self.update_from_resolved(manager.get_resolved()); + let manifest_bytes = std::fs::read(manager.root.join("Cell.toml"))?; + self.root.manifest_digest = manifest_digest(&manifest_bytes); + + let mut runtime = BTreeMap::new(); + let mut dev = BTreeMap::new(); + for (alias, node_id) in manager.root_dependencies() { + if options.scope == DependencyScope::Test && manifest.dev_dependencies.contains_key(alias) { + dev.insert(alias.clone(), node_id.clone()); + } else { + runtime.insert(alias.clone(), node_id.clone()); + } + } + + if let Some(environment_name) = options.environment.as_deref() { + let environment = manifest + .environments + .get(environment_name) + .ok_or_else(|| CompileError::without_span(format!("unknown package environment '{}'", environment_name)))?; + self.environments.insert( + environment_name.to_string(), + LockedEnvironment { + chain_id: environment.chain_id.clone(), + genesis_hash: environment.genesis_hash.clone(), + dependencies: runtime, + dev_dependencies: dev, + }, + ); + } else { + self.root.dependencies = runtime; + self.root.dev_dependencies = dev; + } + self.validate_schema() + } + pub fn is_consistent(&self, manifest: &PackageManifest) -> bool { self.consistency_issues(manifest).is_empty() } @@ -1138,7 +2374,7 @@ impl Lockfile { pub fn consistency_issues_with_resolved( &self, manifest: &PackageManifest, - resolved: &HashMap, + resolved: &BTreeMap, ) -> Vec { self.consistency_issues_with_expected(manifest, Some(resolved)) } @@ -1146,48 +2382,156 @@ impl Lockfile { fn consistency_issues_with_expected( &self, manifest: &PackageManifest, - resolved: Option<&HashMap>, + resolved: Option<&BTreeMap>, ) -> Vec { let mut issues = Vec::new(); if self.version != Self::CURRENT_VERSION { issues.push(format!("Cell.lock version {} is not supported; expected {}", self.version, Self::CURRENT_VERSION)); } + if self.package.edition != manifest.package.edition { + issues.push(format!( + "package edition mismatch: Cell.toml has '{}' but Cell.lock records '{}'", + manifest.package.edition, self.package.edition + )); + } - for name in manifest.dependencies.keys() { - let Some(locked) = self.dependencies.get(name) else { - issues.push(format!("dependency '{}' is missing from Cell.lock", name)); + if manifest.dependency_overrides.is_empty() { + issues.extend(self.root_graph_consistency_issues( + "root", + &manifest.dependencies, + &manifest.dev_dependencies, + &self.root.dependencies, + &self.root.dev_dependencies, + manifest.package.namespace.as_deref(), + )); + } + for (environment_name, environment) in &manifest.environments { + let Some(locked_environment) = self.environments.get(environment_name) else { + issues.push(format!("environment '{}' is missing from Cell.lock", environment_name)); continue; }; - if let Some(dep) = manifest.dependencies.get(name) { - issues.extend(lock_dependency_consistency_issues(name, dep, locked, manifest.package.namespace.as_deref())); + if locked_environment.chain_id != environment.chain_id + || !locked_environment.genesis_hash.eq_ignore_ascii_case(&environment.genesis_hash) + { + issues.push(format!("environment '{}' chain identity differs between Cell.toml and Cell.lock", environment_name)); + } + let mut dependencies = manifest.dependencies.clone(); + if let Some(overrides) = manifest.dependency_overrides.get(environment_name) { + dependencies.extend(overrides.clone()); } + issues.extend(self.root_graph_consistency_issues( + &format!("environment '{}'", environment_name), + &dependencies, + &manifest.dev_dependencies, + &locked_environment.dependencies, + &locked_environment.dev_dependencies, + manifest.package.namespace.as_deref(), + )); } if let Some(resolved) = resolved { - for (name, package) in resolved { - let Some(locked) = self.dependencies.get(name) else { - issues.push(format!("resolved dependency '{}' is missing from Cell.lock", name)); + for (node_id, package) in resolved { + let Some(locked) = self.dependencies.get(node_id) else { + issues.push(format!("resolved dependency node '{}' is missing from Cell.lock", node_id)); continue; }; - issues.extend(resolved_dependency_consistency_issues(name, package, locked)); + issues.extend(resolved_dependency_consistency_issues(node_id, package, locked)); } } - for name in self.dependencies.keys() { - let expected_by_manifest = manifest.dependencies.contains_key(name); - let expected_by_resolved = resolved.is_some_and(|resolved| resolved.contains_key(name)); - if !expected_by_manifest && !expected_by_resolved { - issues.push(format!("Cell.lock contains stale dependency '{}' not present in Cell.toml", name)); + let reachable = self.reachable_nodes(); + for node_id in self.dependencies.keys() { + if !reachable.contains(node_id) { + issues.push(format!("Cell.lock contains unreachable dependency node '{}'", node_id)); } } issues } + + fn root_graph_consistency_issues( + &self, + label: &str, + dependencies: &HashMap, + dev_dependencies: &HashMap, + locked_dependencies: &BTreeMap, + locked_dev_dependencies: &BTreeMap, + namespace: Option<&str>, + ) -> Vec { + let mut issues = Vec::new(); + for (alias, dependency) in dependencies { + let Some(node_id) = locked_dependencies.get(alias) else { + if !dependency_is_optional(dependency) { + issues.push(format!("{} dependency '{}' is missing from Cell.lock", label, alias)); + } + continue; + }; + match self.dependencies.get(node_id) { + Some(locked) => issues.extend(lock_dependency_consistency_issues(alias, dependency, locked, namespace)), + None => issues.push(format!("{} dependency '{}' targets missing node '{}'", label, alias, node_id)), + } + } + for (alias, dependency) in dev_dependencies { + let Some(node_id) = locked_dev_dependencies.get(alias) else { + if !dependency_is_optional(dependency) { + issues.push(format!("{} dev-dependency '{}' is missing from Cell.lock", label, alias)); + } + continue; + }; + match self.dependencies.get(node_id) { + Some(locked) => issues.extend(lock_dependency_consistency_issues(alias, dependency, locked, namespace)), + None => issues.push(format!("{} dev-dependency '{}' targets missing node '{}'", label, alias, node_id)), + } + } + for alias in locked_dependencies.keys() { + if !dependencies.contains_key(alias) { + issues.push(format!("{} contains stale dependency alias '{}'", label, alias)); + } + } + for alias in locked_dev_dependencies.keys() { + if !dev_dependencies.contains_key(alias) { + issues.push(format!("{} contains stale dev-dependency alias '{}'", label, alias)); + } + } + issues + } + + fn reachable_nodes(&self) -> BTreeSet { + let mut pending: Vec = self + .root + .dependencies + .values() + .chain(self.root.dev_dependencies.values()) + .chain( + self.environments + .values() + .flat_map(|environment| environment.dependencies.values().chain(environment.dev_dependencies.values())), + ) + .cloned() + .collect(); + let mut reachable = BTreeSet::new(); + while let Some(node_id) = pending.pop() { + if !reachable.insert(node_id.clone()) { + continue; + } + if let Some(node) = self.dependencies.get(&node_id) { + pending.extend(node.dependencies.values().cloned()); + } + } + reachable + } } fn resolved_dependency_consistency_issues(name: &str, package: &ResolvedPackage, locked: &LockedDependency) -> Vec { let mut issues = Vec::new(); + if locked.name != package.name { + issues.push(format!( + "resolved dependency node '{}' has package name '{}' but Cell.lock records '{}'", + name, package.name, locked.name + )); + } + if locked.version != package.version { issues.push(format!( "resolved dependency '{}' has package version '{}' but Cell.lock records '{}'", @@ -1195,6 +2539,16 @@ fn resolved_dependency_consistency_issues(name: &str, package: &ResolvedPackage, )); } + if locked.manifest_digest != package.manifest_digest { + issues.push(format!( + "resolved dependency node '{}' manifest digest '{}' does not match Cell.lock '{}'", + name, package.manifest_digest, locked.manifest_digest + )); + } + if locked.dependencies != package.dependencies { + issues.push(format!("resolved dependency node '{}' edges do not match Cell.lock", name)); + } + match (&package.source, &locked.source) { (PackageSource::Local(path), LockedSource::Path { path: locked_path }) if locked_path == path.to_string_lossy().as_ref() => {} (PackageSource::Git { url, revision }, LockedSource::Git { url: locked_url, revision: locked_revision }) @@ -1230,8 +2584,8 @@ fn resolved_dependency_consistency_issues(name: &str, package: &ResolvedPackage, )), None => issues.push(format!("resolved dependency '{}' is missing source_hash in Cell.lock", name)), } - } else if matches!(package.source, PackageSource::Registry { .. }) { - issues.push(format!("resolved registry dependency '{}' did not produce a source_hash", name)); + } else { + issues.push(format!("resolved dependency '{}' did not produce a source_hash", name)); } issues @@ -1244,11 +2598,18 @@ fn lock_dependency_consistency_issues( consuming_namespace: Option<&str>, ) -> Vec { let mut issues = Vec::new(); + let expected_package = dependency_package_name(name, dep); + if locked.name != expected_package { + issues.push(format!( + "dependency alias '{}' expects package '{}' but Cell.lock node declares '{}'", + name, expected_package, locked.name + )); + } match dep { Dependency::Simple(version) => match &locked.source { LockedSource::Registry { namespace: locked_namespace, version: locked_version, .. } - if Some(locked_namespace.as_str()) == consuming_namespace && locked_version == version => {} + if Some(locked_namespace.as_str()) == consuming_namespace && locked_version == &locked.version => {} source => issues.push(format!( "dependency '{}' expects registry source {}@{} but Cell.lock records {}", name, @@ -1258,7 +2619,10 @@ fn lock_dependency_consistency_issues( )), }, Dependency::Detailed(detail) => { - if let Some(path) = &detail.path { + if detail.resolver.is_some() { + // Update-time resolvers are normalized into the immutable + // source recorded here. Locked builds never invoke them. + } else if let Some(path) = &detail.path { match &locked.source { LockedSource::Path { path: locked_path } if locked_path == path => {} source => issues.push(format!( @@ -1268,7 +2632,6 @@ fn lock_dependency_consistency_issues( locked_source_display(source) )), } - push_locked_version_issue(name, &detail.version, &locked.version, &mut issues); } else if let Some(git) = &detail.git { match &locked.source { LockedSource::Git { url, revision } if url == git => { @@ -1289,12 +2652,11 @@ fn lock_dependency_consistency_issues( locked_source_display(source) )), } - push_locked_version_issue(name, &detail.version, &locked.version, &mut issues); } else { match &locked.source { LockedSource::Registry { namespace: locked_namespace, version: locked_version, .. } if Some(locked_namespace.as_str()) == detail.namespace.as_deref().or(consuming_namespace) - && locked_version == &detail.version => {} + && locked_version == &locked.version => {} source => issues.push(format!( "dependency '{}' expects registry source {}@{} but Cell.lock records {}", name, @@ -1307,13 +2669,22 @@ fn lock_dependency_consistency_issues( } } - issues -} - -fn push_locked_version_issue(name: &str, expected: &str, actual: &str, issues: &mut Vec) { - if expected != "*" && expected != actual { - issues.push(format!("dependency '{}' expects package version '{}' but Cell.lock records '{}'", name, expected, actual)); + if let Some(requirement) = match dep { + Dependency::Simple(requirement) => Some(requirement.as_str()), + Dependency::Detailed(detail) if detail.version != "*" && !detail.version.is_empty() => Some(detail.version.as_str()), + Dependency::Detailed(_) => None, + } { + match version::parse_version_req(requirement) { + Ok(requirement) if version::satisfies(&locked.version, &requirement) => {} + Ok(_) => issues.push(format!( + "dependency '{}' requires '{}' but Cell.lock records package version '{}'", + name, requirement, locked.version + )), + Err(error) => issues.push(error.message.clone()), + } } + + issues } fn locked_source_display(source: &LockedSource) -> String { @@ -1340,6 +2711,8 @@ impl Default for Lockfile { #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct LockedBuildInfo { + pub edition: CellScriptEdition, + pub compatibility_profile_hash: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub compiler_version: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1360,10 +2733,18 @@ pub struct LockedBuildInfo { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LockedDependency { + #[serde(default)] + pub name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub namespace: Option, pub version: String, pub source: LockedSource, #[serde(default, skip_serializing_if = "Option::is_none")] pub source_hash: Option, + #[serde(default, skip_serializing_if = "String::is_empty")] + pub manifest_digest: String, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub dependencies: BTreeMap, #[serde(default, skip_serializing_if = "Option::is_none")] pub build: Option, } @@ -1379,114 +2760,40 @@ pub mod version { use super::*; pub fn parse_version_req(req: &str) -> Result { - if req == "*" { - return Ok(VersionReq::Any); - } - - if let Some(stripped) = req.strip_prefix('^') { - return Ok(VersionReq::Compatible(stripped.to_string())); - } - - if let Some(stripped) = req.strip_prefix('=') { - return Ok(VersionReq::Exact(stripped.to_string())); - } - - if req.contains(',') || req.contains('>') || req.contains('<') { - return Ok(VersionReq::Range(req.to_string())); - } - - Ok(VersionReq::Compatible(req.to_string())) + let req = req.trim(); + let parsed = if req == "*" { + VersionReq::Any + } else if let Some(stripped) = req.strip_prefix('^') { + VersionReq::Compatible(stripped.to_string()) + } else if let Some(stripped) = req.strip_prefix('=') { + VersionReq::Exact(stripped.to_string()) + } else if req.contains(',') || req.contains('>') || req.contains('<') || req.contains('~') { + VersionReq::Range(req.to_string()) + } else { + // Preserve CellScript's historical bare-version-as-compatible + // surface while using the standard SemVer compatibility rules. + VersionReq::Compatible(req.to_string()) + }; + standard_requirement(&parsed)?; + Ok(parsed) } pub fn satisfies(version: &str, req: &VersionReq) -> bool { - match req { - VersionReq::Any => true, - VersionReq::Exact(v) => version == v, - VersionReq::Compatible(v) => is_compatible(version, v), - VersionReq::Range(r) => satisfies_range(version, r), - } - } - - fn is_compatible(version: &str, base: &str) -> bool { - let Some(v_parts) = parse_numeric_version(version) else { - return false; - }; - let Some(b_parts) = parse_numeric_version(base) else { + let Ok(version) = semver::Version::parse(version) else { return false; }; - - if v_parts[0] != b_parts[0] { - return false; - } - - if v_parts[0] == 0 { - if v_parts.len() < 2 || b_parts.len() < 2 { - return false; - } - if v_parts[1] != b_parts[1] { - return false; - } - } - - true - } - - fn satisfies_range(_version: &str, _range: &str) -> bool { - for clause in _range.split(',').map(str::trim).filter(|clause| !clause.is_empty()) { - let Some((op, expected)) = parse_range_clause(clause) else { - return false; - }; - let Some(ordering) = compare_versions(_version, expected) else { - return false; - }; - let satisfied = match op { - ">" => ordering.is_gt(), - ">=" => ordering.is_gt() || ordering.is_eq(), - "<" => ordering.is_lt(), - "<=" => ordering.is_lt() || ordering.is_eq(), - "=" | "==" => ordering.is_eq(), - _ => false, - }; - if !satisfied { - return false; - } - } - true - } - - fn parse_range_clause(clause: &str) -> Option<(&str, &str)> { - for op in [">=", "<=", "==", ">", "<", "="] { - if let Some(version) = clause.strip_prefix(op) { - return Some((op, version.trim())); - } - } - None - } - - fn compare_versions(left: &str, right: &str) -> Option { - let left = parse_numeric_version(left)?; - let right = parse_numeric_version(right)?; - let max_len = left.len().max(right.len()); - for idx in 0..max_len { - let lhs = *left.get(idx).unwrap_or(&0); - let rhs = *right.get(idx).unwrap_or(&0); - match lhs.cmp(&rhs) { - std::cmp::Ordering::Equal => {} - ordering => return Some(ordering), - } - } - Some(std::cmp::Ordering::Equal) + standard_requirement(req).is_ok_and(|requirement| requirement.matches(&version)) } - fn parse_numeric_version(version: &str) -> Option> { - let core = version.split_once('-').map(|(core, _)| core).unwrap_or(version); - let parts: Option> = core.split('.').map(|part| part.parse().ok()).collect(); - let parts = parts?; - if parts.is_empty() { - None - } else { - Some(parts) - } + fn standard_requirement(req: &VersionReq) -> Result { + let source = match req { + VersionReq::Any => "*".to_string(), + VersionReq::Exact(version) => format!("={version}"), + VersionReq::Compatible(version) => format!("^{version}"), + VersionReq::Range(range) => range.clone(), + }; + semver::VersionReq::parse(&source) + .map_err(|error| CompileError::without_span(format!("invalid semantic version requirement '{source}': {error}"))) } } @@ -1494,13 +2801,13 @@ pub mod version { // Deployed.toml — Deployment Fact Record // --------------------------------------------------------------------------- -/// The schema identifier for Deployed.toml files produced by CellScript v0.19+. -pub const DEPLOYED_MANIFEST_SCHEMA: &str = "cellscript-deployed-v0.19"; +/// The only supported Deployed.toml schema for edition 2026. +pub const DEPLOYED_MANIFEST_SCHEMA: &str = "cellscript-deployed-v0.23-edition-2026"; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeployedManifest { pub version: u32, - pub schema: Option, + pub schema: String, pub package: DeployedPackageInfo, #[serde(default, skip_serializing_if = "Option::is_none")] pub build: Option, @@ -1509,24 +2816,70 @@ pub struct DeployedManifest { } impl DeployedManifest { - pub const CURRENT_VERSION: u32 = 1; + pub const CURRENT_VERSION: u32 = 2; pub fn read_from_root(root: &Path) -> Result> { let path = root.join("Deployed.toml"); if !path.exists() { return Ok(None); } - let content = std::fs::read_to_string(&path) - .map_err(|e| CompileError::without_span(format!("failed to read Deployed.toml '{}': {}", path.display(), e)))?; - let manifest: Self = toml::from_str(&content) - .map_err(|e| CompileError::without_span(format!("failed to parse Deployed.toml '{}': {}", path.display(), e)))?; - Ok(Some(manifest)) - } - - pub fn write_to_root(&self, root: &Path) -> Result<()> { - let path = root.join("Deployed.toml"); - let content = toml::to_string_pretty(self)?; - std::fs::write(&path, content)?; + let content = std::fs::read_to_string(&path) + .map_err(|e| CompileError::without_span(format!("failed to read Deployed.toml '{}': {}", path.display(), e)))?; + let manifest: Self = toml::from_str(&content) + .map_err(|e| CompileError::without_span(format!("failed to parse Deployed.toml '{}': {}", path.display(), e)))?; + manifest.validate_schema()?; + Ok(Some(manifest)) + } + + pub fn write_to_root(&self, root: &Path) -> Result<()> { + self.validate_schema()?; + let path = root.join("Deployed.toml"); + let content = toml::to_string_pretty(self)?; + std::fs::write(&path, content)?; + Ok(()) + } + + pub fn validate_schema(&self) -> Result<()> { + if self.version != Self::CURRENT_VERSION || self.schema != DEPLOYED_MANIFEST_SCHEMA { + return Err(CompileError::without_span(format!( + "unsupported Deployed.toml identity; expected version {} and schema '{}'", + Self::CURRENT_VERSION, + DEPLOYED_MANIFEST_SCHEMA + ))); + } + if let Some(build) = &self.build { + if build.edition != self.package.edition { + return Err(CompileError::without_span(format!( + "Deployed.toml package/build edition mismatch: package is '{}' but build is '{}'", + self.package.edition, build.edition + ))); + } + if build.compatibility_profile_hash.is_empty() { + return Err(CompileError::without_span("Deployed.toml v2 build requires compatibility_profile_hash")); + } + } + for deployment in &self.deployments { + if deployment.edition != self.package.edition { + return Err(CompileError::without_span(format!( + "Deployed.toml package/deployment edition mismatch for network '{}': package is '{}' but deployment is '{}'", + deployment.network, self.package.edition, deployment.edition + ))); + } + if deployment.compatibility_profile_hash.is_empty() { + return Err(CompileError::without_span(format!( + "Deployed.toml v2 deployment for network '{}' requires compatibility_profile_hash", + deployment.network + ))); + } + if let Some(build) = &self.build + && deployment.compatibility_profile_hash != build.compatibility_profile_hash + { + return Err(CompileError::without_span(format!( + "Deployed.toml build/deployment compatibility profile mismatch for network '{}'", + deployment.network + ))); + } + } Ok(()) } } @@ -1535,12 +2888,15 @@ impl DeployedManifest { pub struct DeployedPackageInfo { pub name: String, pub version: String, + pub edition: CellScriptEdition, #[serde(default, skip_serializing_if = "Option::is_none")] pub source_hash: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct DeployedBuildInfo { + pub edition: CellScriptEdition, + pub compatibility_profile_hash: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub compiler_version: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1582,6 +2938,7 @@ pub enum ScriptRole { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeploymentRecord { // Required fields (Phase 1) + pub edition: CellScriptEdition, pub network: String, pub chain_id: String, pub tx_hash: String, @@ -1607,6 +2964,7 @@ pub struct DeploymentRecord { pub constraints_hash: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub compiler_version: Option, + pub compatibility_profile_hash: String, // Optional fields (Phase 2 — governance and upgrade) #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1653,6 +3011,7 @@ mod tests { package: PackageInfo { name: "test".to_string(), version: "0.1.0".to_string(), + edition: CURRENT_EDITION, namespace: None, authors: vec!["Test Author".to_string()], description: "Test package".to_string(), @@ -1671,6 +3030,10 @@ mod tests { workspace: None, dependencies: HashMap::new(), dev_dependencies: HashMap::new(), + features: BTreeMap::new(), + environments: BTreeMap::new(), + dependency_overrides: BTreeMap::new(), + resolvers: BTreeMap::new(), build: BuildConfig::default(), policy: PolicyConfig::default(), deploy: DeployConfig::default(), @@ -1680,6 +3043,43 @@ mod tests { let toml_str = toml::to_string(&manifest).unwrap(); assert!(toml_str.contains("name = \"test\"")); assert!(toml_str.contains("version = \"0.1.0\"")); + assert!(toml_str.contains("edition = \"2026\"")); + let parsed: PackageManifest = toml::from_str(&toml_str).unwrap(); + assert_eq!(parsed.package.edition, CURRENT_EDITION); + } + + #[test] + fn package_manifest_requires_edition_2026() { + let missing = toml::from_str::( + r#" +[package] +name = "demo" +version = "0.1.0" +"#, + ) + .unwrap_err(); + assert!(missing.to_string().contains("missing field `edition`")); + + let unsupported = toml::from_str::( + r#" +[package] +edition = "unsupported" +name = "demo" +version = "0.1.0" +"#, + ) + .unwrap_err(); + assert!(unsupported.to_string().contains("2026")); + } + + #[test] + fn package_manager_init_writes_current_edition() { + let temp = tempdir().unwrap(); + PackageManager::new(temp.path()).init("demo").unwrap(); + let source = std::fs::read_to_string(temp.path().join("Cell.toml")).unwrap(); + assert!(source.contains("edition = \"2026\"")); + let manifest: PackageManifest = toml::from_str(&source).unwrap(); + assert_eq!(manifest.package.edition, CURRENT_EDITION); } #[test] @@ -1697,19 +3097,51 @@ mod tests { assert!(graph.find_cycle().is_some()); } + fn locked_path(name: &str, version: &str, path: &str, dependencies: BTreeMap) -> LockedDependency { + LockedDependency { + name: name.to_string(), + namespace: None, + version: version.to_string(), + source: LockedSource::Path { path: path.to_string() }, + source_hash: Some(format!("hash-{name}")), + manifest_digest: format!("manifest-{name}"), + dependencies, + build: None, + } + } + + fn resolved_path(name: &str, version: &str, path: &str, dependencies: BTreeMap) -> ResolvedPackage { + ResolvedPackage { + node_id: name.to_string(), + name: name.to_string(), + version: version.to_string(), + path: PathBuf::from(path), + source: PackageSource::Local(PathBuf::from(path)), + dependencies, + namespace: None, + source_hash: Some(format!("hash-{name}")), + manifest_digest: format!("manifest-{name}"), + } + } + #[test] fn test_version_compatibility() { assert!(version::satisfies("1.2.3", &VersionReq::Compatible("1.0.0".to_string()))); assert!(version::satisfies("1.5.0", &VersionReq::Compatible("1.2.3".to_string()))); + assert!(!version::satisfies("1.1.9", &VersionReq::Compatible("1.2.3".to_string()))); assert!(!version::satisfies("2.0.0", &VersionReq::Compatible("1.0.0".to_string()))); assert!(!version::satisfies("0.2.0", &VersionReq::Compatible("0.1.0".to_string()))); assert!(version::satisfies("0.1.5", &VersionReq::Compatible("0.1.0".to_string()))); + assert!(!version::satisfies("0.1.0-alpha.1", &VersionReq::Compatible("0.1.0".to_string()))); + assert!(version::satisfies("0.1.0-alpha.2", &VersionReq::Compatible("0.1.0-alpha.1".to_string()))); + assert!(version::satisfies("1.2.3+build.7", &VersionReq::Exact("1.2.3".to_string()))); assert!(version::satisfies("1.2.3", &VersionReq::Range(">=1.0.0, <2.0.0".to_string()))); assert!(!version::satisfies("2.0.0", &VersionReq::Range(">=1.0.0, <2.0.0".to_string()))); assert!(!version::satisfies("1.2.3", &VersionReq::Range(">=1.3.0".to_string()))); assert!(!version::satisfies("1.bad", &VersionReq::Compatible("1.0.0".to_string()))); assert!(!version::satisfies("1.2.3", &VersionReq::Compatible("1.bad".to_string()))); assert!(!version::satisfies("1.bad", &VersionReq::Range(">=1.0.0".to_string()))); + assert!(version::parse_version_req("^1.bad").is_err()); } #[test] @@ -1721,6 +3153,7 @@ mod tests { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "app" version = "0.1.0" @@ -1734,6 +3167,7 @@ path = "deps/math" root.join("deps/math/Cell.toml"), r#" [package] +edition = "2026" name = "math" version = "0.1.0" "#, @@ -1743,7 +3177,8 @@ version = "0.1.0" let mut manager = PackageManager::new(root); manager.resolve_dependencies().unwrap(); - let math = manager.get_resolved().get("math").expect("path dependency should resolve"); + let math_id = manager.root_dependencies().get("math").expect("root math edge"); + let math = manager.get_resolved().get(math_id).expect("path dependency should resolve"); assert_eq!(math.name, "math"); assert_eq!(math.version, "0.1.0"); assert!(matches!(math.source, PackageSource::Local(_))); @@ -1759,6 +3194,7 @@ version = "0.1.0" root.join("Cell.toml"), r#" [package] +edition = "2026" name = "app" version = "0.1.0" @@ -1771,6 +3207,7 @@ path = "deps/math" root.join("deps/math/Cell.toml"), r#" [package] +edition = "2026" name = "math" version = "0.2.0" "#, @@ -1780,7 +3217,8 @@ version = "0.2.0" let mut manager = PackageManager::new(root); manager.resolve_dependencies().unwrap(); - let math = manager.get_resolved().get("math").expect("path dependency should resolve"); + let math_id = manager.root_dependencies().get("math").expect("root math edge"); + let math = manager.get_resolved().get(math_id).expect("path dependency should resolve"); assert_eq!(math.version, "0.2.0"); } @@ -1794,6 +3232,7 @@ version = "0.2.0" root.join("Cell.toml"), r#" [package] +edition = "2026" name = "app" version = "0.1.0" @@ -1807,6 +3246,7 @@ path = "deps/math" root.join("deps/math/Cell.toml"), r#" [package] +edition = "2026" name = "math" version = "0.1.0" @@ -1820,6 +3260,7 @@ path = "../util" root.join("deps/util/Cell.toml"), r#" [package] +edition = "2026" name = "util" version = "0.1.0" "#, @@ -1829,9 +3270,10 @@ version = "0.1.0" let mut manager = PackageManager::new(root); manager.resolve_dependencies().unwrap(); - assert!(manager.get_resolved().contains_key("math")); - assert!(manager.get_resolved().contains_key("util")); - assert_eq!(manager.get_resolved()["math"].dependencies, vec!["util"]); + let math_id = manager.root_dependencies().get("math").expect("root math edge"); + let math = manager.get_resolved().get(math_id).expect("math node"); + let util_id = math.dependencies.get("util").expect("math util edge"); + assert!(manager.get_resolved().contains_key(util_id)); } #[test] @@ -1844,6 +3286,7 @@ version = "0.1.0" root.join("Cell.toml"), r#" [package] +edition = "2026" name = "app" version = "0.1.0" @@ -1856,6 +3299,7 @@ path = "deps/a" root.join("deps/a/Cell.toml"), r#" [package] +edition = "2026" name = "a" version = "0.1.0" @@ -1868,6 +3312,7 @@ path = "../b" root.join("deps/b/Cell.toml"), r#" [package] +edition = "2026" name = "b" version = "0.1.0" @@ -1884,11 +3329,361 @@ path = "../a" assert!(error.message.contains("a -> b -> a"), "{}", error.message); } + fn write_test_lock(root: &Path, options: &ResolutionOptions) { + let mut manager = PackageManager::new(root); + let manifest = manager.read_manifest().unwrap(); + manager.resolve_dependencies_with_options(options).unwrap(); + let mut lockfile = Lockfile::new(); + lockfile.package = LockfilePackageInfo { + edition: manifest.package.edition, + name: manifest.package.name.clone(), + version: manifest.package.version.clone(), + namespace: manifest.package.namespace.clone(), + source_hash: Some(registry::compute_source_hash(root).unwrap()), + compiler_source_hash: None, + }; + lockfile.replace_with_resolution(&manager, &manifest, options).unwrap(); + lockfile.write_to_root(root).unwrap(); + } + + fn write_path_package(root: &Path, relative: &str, name: &str, version: &str) { + let package = root.join(relative); + std::fs::create_dir_all(package.join("src")).unwrap(); + std::fs::write( + package.join("Cell.toml"), + format!("[package]\nedition = \"2026\"\nname = \"{name}\"\nversion = \"{version}\"\n"), + ) + .unwrap(); + std::fs::write(package.join("src/lib.cell"), format!("module {name};\n")).unwrap(); + } + + #[test] + fn locked_resolution_requires_explicit_lock_and_detects_source_drift() { + let temp = tempdir().unwrap(); + let root = temp.path(); + write_path_package(root, "deps/math", "math", "1.2.3"); + std::fs::write( + root.join("Cell.toml"), + r#" +[package] +edition = "2026" +name = "app" +version = "0.1.0" + +[dependencies.math] +path = "deps/math" +version = "^1.2.0" +"#, + ) + .unwrap(); + + let mut manager = PackageManager::new(root); + let missing = manager.resolve_locked_dependencies(&ResolutionOptions::default()).unwrap_err(); + assert!(missing.message.contains("Cell.lock is missing"), "{}", missing.message); + + write_test_lock(root, &ResolutionOptions::default()); + let mut manager = PackageManager::new(root); + manager.resolve_locked_dependencies(&ResolutionOptions::default()).unwrap(); + std::fs::write(root.join("deps/math/src/lib.cell"), "module math;\n// changed\n").unwrap(); + let mut manager = PackageManager::new(root); + let drift = manager.resolve_locked_dependencies(&ResolutionOptions::default()).unwrap_err(); + assert!(drift.message.contains("source hash mismatch"), "{}", drift.message); + } + + #[cfg(unix)] + #[test] + fn external_resolver_is_bounded_normalized_and_absent_from_locked_builds() { + use std::os::unix::fs::PermissionsExt; + + let temp = tempdir().unwrap(); + let root = temp.path(); + let dependency_repo = root.join("resolver-package"); + write_path_package(root, "resolver-package", "resolved_math", "1.2.3"); + for arguments in [ + vec!["init", "-q"], + vec!["config", "user.email", "tests@cellscript.dev"], + vec!["config", "user.name", "CellScript Tests"], + vec!["add", "."], + vec!["commit", "-q", "-m", "initial"], + ] { + let status = std::process::Command::new("git").args(arguments).current_dir(&dependency_repo).status().unwrap(); + assert!(status.success()); + } + let revision = PackageManager::git_revision(&dependency_repo).unwrap(); + let response = serde_json::json!({ + "schema": EXTERNAL_RESOLVER_RESPONSE_SCHEMA, + "dependency": { + "package": "resolved_math", + "version": "1.2.3", + "git": dependency_repo.to_string_lossy(), + "rev": revision, + } + }); + let resolver_path = root.join("resolver.sh"); + std::fs::write(&resolver_path, format!("#!/bin/sh\nprintf '%s\\n' '{}'\n", response)).unwrap(); + let mut permissions = std::fs::metadata(&resolver_path).unwrap().permissions(); + permissions.set_mode(0o700); + std::fs::set_permissions(&resolver_path, permissions).unwrap(); + let resolver_digest = sha256_file(&resolver_path).unwrap(); + std::fs::write( + root.join("Cell.toml"), + format!( + r#" +[package] +edition = "2026" +name = "app" +version = "0.1.0" + +[resolvers.local] +command = "{}" +sha256 = "sha256:{}" + +[dependencies.math] +package = "resolved_math" +version = "^1.2.0" +resolver = "local" +"#, + resolver_path.display(), + resolver_digest + ), + ) + .unwrap(); + + write_test_lock(root, &ResolutionOptions::default()); + let lockfile = Lockfile::read_from_root(root).unwrap().unwrap(); + let target = lockfile.root.dependencies.get("math").unwrap(); + assert!(matches!(lockfile.dependencies[target].source, LockedSource::Git { .. })); + + std::fs::remove_file(&resolver_path).unwrap(); + let mut locked = PackageManager::new(root); + locked.resolve_locked_dependencies(&ResolutionOptions::default()).unwrap(); + assert_eq!(locked.get_resolved()[target].version, "1.2.3"); + } + + #[test] + fn moving_git_branch_changes_only_after_explicit_repin() { + let temp = tempdir().unwrap(); + let root = temp.path(); + let dependency_repo = root.join("moving-package"); + write_path_package(root, "moving-package", "moving_math", "1.2.3"); + for arguments in [ + vec!["init", "-q", "--initial-branch=main"], + vec!["config", "user.email", "tests@cellscript.dev"], + vec!["config", "user.name", "CellScript Tests"], + vec!["add", "."], + vec!["commit", "-q", "-m", "first"], + ] { + let status = std::process::Command::new("git").args(arguments).current_dir(&dependency_repo).status().unwrap(); + assert!(status.success()); + } + std::fs::write( + root.join("Cell.toml"), + format!( + r#" +[package] +edition = "2026" +name = "app" +version = "0.1.0" + +[dependencies.math] +package = "moving_math" +version = "^1.2.0" +git = "{}" +branch = "main" +"#, + dependency_repo.display() + ), + ) + .unwrap(); + + write_test_lock(root, &ResolutionOptions::default()); + let first_lock = Lockfile::read_from_root(root).unwrap().unwrap(); + let first_target = first_lock.root.dependencies.get("math").unwrap(); + let first_revision = match &first_lock.dependencies[first_target].source { + LockedSource::Git { revision, .. } => revision.clone(), + source => panic!("expected Git source, got {source:?}"), + }; + + std::fs::write(dependency_repo.join("src/lib.cell"), "module moving_math;\n// second commit\n").unwrap(); + for arguments in [vec!["add", "."], vec!["commit", "-q", "-m", "second"]] { + let status = std::process::Command::new("git").args(arguments).current_dir(&dependency_repo).status().unwrap(); + assert!(status.success()); + } + let second_revision = PackageManager::git_revision(&dependency_repo).unwrap(); + assert_ne!(first_revision, second_revision); + + let mut locked = PackageManager::new(root); + locked.resolve_locked_dependencies(&ResolutionOptions::default()).unwrap(); + assert!(locked.get_resolved().contains_key(first_target)); + + write_test_lock(root, &ResolutionOptions::default()); + let repinned = Lockfile::read_from_root(root).unwrap().unwrap(); + let repinned_target = repinned.root.dependencies.get("math").unwrap(); + let repinned_revision = match &repinned.dependencies[repinned_target].source { + LockedSource::Git { revision, .. } => revision, + source => panic!("expected Git source, got {source:?}"), + }; + assert_eq!(repinned_revision, &second_revision); + assert_ne!(repinned_revision, &first_revision); + } + + #[test] + fn optional_features_and_dev_dependencies_select_locked_subgraphs() { + let temp = tempdir().unwrap(); + let root = temp.path(); + write_path_package(root, "deps/base", "base", "1.0.0"); + write_path_package(root, "deps/extra", "extra", "1.0.0"); + write_path_package(root, "deps/test-kit", "test-kit", "1.0.0"); + std::fs::write( + root.join("Cell.toml"), + r#" +[package] +edition = "2026" +name = "app" +version = "0.1.0" + +[dependencies.base] +path = "deps/base" + +[dependencies.extra] +path = "deps/extra" +optional = true + +[dev_dependencies.test] +package = "test-kit" +path = "deps/test-kit" + +[features] +default = [] +extended = ["dep:extra"] +"#, + ) + .unwrap(); + write_test_lock(root, &ResolutionOptions { scope: DependencyScope::Test, all_features: true, ..ResolutionOptions::default() }); + + let mut runtime = PackageManager::new(root); + runtime.resolve_locked_dependencies(&ResolutionOptions::default()).unwrap(); + assert_eq!(runtime.root_dependencies().keys().cloned().collect::>(), vec!["base"]); + + let mut extended = PackageManager::new(root); + extended + .resolve_locked_dependencies(&ResolutionOptions { + features: BTreeSet::from(["extended".to_string()]), + ..ResolutionOptions::default() + }) + .unwrap(); + assert_eq!(extended.root_dependencies().keys().cloned().collect::>(), vec!["base", "extra"]); + + let mut tests = PackageManager::new(root); + tests + .resolve_locked_dependencies(&ResolutionOptions { scope: DependencyScope::Test, ..ResolutionOptions::default() }) + .unwrap(); + assert_eq!(tests.root_dependencies().keys().cloned().collect::>(), vec!["base", "test"]); + let test_node = tests.root_dependencies().get("test").unwrap(); + assert_eq!(tests.get_resolved()[test_node].name, "test-kit"); + } + + #[test] + fn environment_overrides_bind_chain_identity_and_dependency_graph() { + let temp = tempdir().unwrap(); + let root = temp.path(); + write_path_package(root, "deps/mainnet", "contracts", "1.0.0"); + write_path_package(root, "deps/testnet", "contracts", "2.0.0"); + std::fs::write( + root.join("Cell.toml"), + format!( + r#" +[package] +edition = "2026" +name = "app" +version = "0.1.0" + +[dependencies.contracts] +path = "deps/mainnet" + +[environments.mainnet] +chain_id = "ckb-mainnet" +genesis_hash = "0x{}" + +[environments.testnet] +chain_id = "ckb-testnet" +genesis_hash = "0x{}" + +[dependency_overrides.testnet.contracts] +path = "deps/testnet" +"#, + "11".repeat(32), + "22".repeat(32) + ), + ) + .unwrap(); + + let manifest = PackageManager::new(root).read_manifest().unwrap(); + let mut lockfile = Lockfile::new(); + lockfile.package.edition = CURRENT_EDITION; + for environment in manifest.environments.keys() { + let options = ResolutionOptions { + environment: Some(environment.clone()), + scope: DependencyScope::Test, + all_features: true, + ..ResolutionOptions::default() + }; + let mut manager = PackageManager::new(root); + manager.resolve_dependencies_with_options(&options).unwrap(); + lockfile.merge_resolution(&manager, &manifest, &options).unwrap(); + } + lockfile.write_to_root(root).unwrap(); + + let mut mainnet = PackageManager::new(root); + mainnet + .resolve_locked_dependencies(&ResolutionOptions { + environment: Some("mainnet".to_string()), + ..ResolutionOptions::default() + }) + .unwrap(); + let mainnet_node = mainnet.root_dependencies().get("contracts").unwrap(); + assert_eq!(mainnet.get_resolved()[mainnet_node].version, "1.0.0"); + + let mut testnet = PackageManager::new(root); + testnet + .resolve_locked_dependencies(&ResolutionOptions { + environment: Some("testnet".to_string()), + ..ResolutionOptions::default() + }) + .unwrap(); + let testnet_node = testnet.root_dependencies().get("contracts").unwrap(); + assert_eq!(testnet.get_resolved()[testnet_node].version, "2.0.0"); + + let missing = PackageManager::new(root).resolve_locked_dependencies(&ResolutionOptions::default()).unwrap_err(); + assert!(missing.message.contains("--environment"), "{}", missing.message); + } + + #[test] + fn build_dependencies_fail_closed_until_isolated_execution_exists() { + let manifest: PackageManifest = toml::from_str( + r#" +[package] +edition = "2026" +name = "app" +version = "0.1.0" + +[build.dependencies] +codegen = "1.0.0" +"#, + ) + .unwrap(); + let temp = tempdir().unwrap(); + PackageManager::new(temp.path()).write_manifest(&manifest).unwrap(); + let error = PackageManager::new(temp.path()).resolve_dependencies().unwrap_err(); + assert!(error.message.contains("reserved"), "{}", error.message); + } + #[test] fn lockfile_consistency_reports_stale_and_mismatched_path_sources() { let manifest: PackageManifest = toml::from_str( r#" [package] +edition = "2026" name = "app" version = "0.1.0" @@ -1899,18 +3694,13 @@ path = "deps/math" ) .unwrap(); let mut lockfile = Lockfile::new(); + lockfile.root.dependencies.insert("math".to_string(), "math-node".to_string()); + lockfile.dependencies.insert("math-node".to_string(), locked_path("math", "0.2.0", "deps/old-math", BTreeMap::new())); lockfile.dependencies.insert( - "math".to_string(), - LockedDependency { - version: "0.2.0".to_string(), - source: LockedSource::Path { path: "deps/old-math".to_string() }, - source_hash: None, - build: None, - }, - ); - lockfile.dependencies.insert( - "stale".to_string(), + "stale-node".to_string(), LockedDependency { + name: "stale".to_string(), + namespace: Some("stale".to_string()), version: "1.0.0".to_string(), source: LockedSource::Registry { registry: "cellscript-registry".to_string(), @@ -1919,7 +3709,9 @@ path = "deps/math" namespace: "stale".to_string(), version: "1.0.0".to_string(), }, - source_hash: None, + source_hash: Some("hash-stale".to_string()), + manifest_digest: "manifest-stale".to_string(), + dependencies: BTreeMap::new(), build: None, }, ); @@ -1927,8 +3719,8 @@ path = "deps/math" let issues = lockfile.consistency_issues(&manifest); assert!(issues.iter().any(|issue| issue.contains("expects path source 'deps/math'")), "{issues:?}"); - assert!(issues.iter().any(|issue| issue.contains("expects package version '0.1.0'")), "{issues:?}"); - assert!(issues.iter().any(|issue| issue.contains("stale dependency 'stale'")), "{issues:?}"); + assert!(issues.iter().any(|issue| issue.contains("requires '0.1.0'")), "{issues:?}"); + assert!(issues.iter().any(|issue| issue.contains("unreachable dependency node 'stale-node'")), "{issues:?}"); assert!(!lockfile.is_consistent(&manifest)); } @@ -1937,6 +3729,7 @@ path = "deps/math" let manifest: PackageManifest = toml::from_str( r#" [package] +edition = "2026" name = "app" version = "0.1.0" @@ -1947,49 +3740,13 @@ path = "deps/math" ) .unwrap(); let mut lockfile = Lockfile::new(); - lockfile.dependencies.insert( - "math".to_string(), - LockedDependency { - version: "0.1.0".to_string(), - source: LockedSource::Path { path: "deps/math".to_string() }, - source_hash: None, - build: None, - }, - ); - lockfile.dependencies.insert( - "util".to_string(), - LockedDependency { - version: "0.1.0".to_string(), - source: LockedSource::Path { path: "deps/math/../util".to_string() }, - source_hash: None, - build: None, - }, - ); - let mut resolved = HashMap::new(); - resolved.insert( - "math".to_string(), - ResolvedPackage { - name: "math".to_string(), - version: "0.1.0".to_string(), - path: PathBuf::from("deps/math"), - source: PackageSource::Local(PathBuf::from("deps/math")), - dependencies: vec!["util".to_string()], - namespace: None, - source_hash: None, - }, - ); - resolved.insert( - "util".to_string(), - ResolvedPackage { - name: "util".to_string(), - version: "0.1.0".to_string(), - path: PathBuf::from("deps/util"), - source: PackageSource::Local(PathBuf::from("deps/math/../util")), - dependencies: Vec::new(), - namespace: None, - source_hash: None, - }, - ); + lockfile.root.dependencies.insert("math".to_string(), "math-node".to_string()); + let math_edges = BTreeMap::from([("util".to_string(), "util-node".to_string())]); + lockfile.dependencies.insert("math-node".to_string(), locked_path("math", "0.1.0", "deps/math", math_edges.clone())); + lockfile.dependencies.insert("util-node".to_string(), locked_path("util", "0.1.0", "deps/math/../util", BTreeMap::new())); + let mut resolved = BTreeMap::new(); + resolved.insert("math-node".to_string(), resolved_path("math", "0.1.0", "deps/math", math_edges)); + resolved.insert("util-node".to_string(), resolved_path("util", "0.1.0", "deps/math/../util", BTreeMap::new())); let issues = lockfile.consistency_issues_with_resolved(&manifest, &resolved); @@ -2002,6 +3759,8 @@ path = "deps/math" lockfile.dependencies.insert( "old".to_string(), LockedDependency { + name: "old".to_string(), + namespace: Some("old".to_string()), version: "1.0.0".to_string(), source: LockedSource::Registry { registry: "cellscript-registry".to_string(), @@ -2010,24 +3769,15 @@ path = "deps/math" namespace: "old".to_string(), version: "1.0.0".to_string(), }, - source_hash: None, + source_hash: Some("hash-old".to_string()), + manifest_digest: "manifest-old".to_string(), + dependencies: BTreeMap::new(), build: None, }, ); - let mut resolved = HashMap::new(); - resolved.insert( - "math".to_string(), - ResolvedPackage { - name: "math".to_string(), - version: "0.1.0".to_string(), - path: PathBuf::from("deps/math"), - source: PackageSource::Local(PathBuf::from("deps/math")), - dependencies: Vec::new(), - namespace: None, - source_hash: None, - }, - ); + let mut resolved = BTreeMap::new(); + resolved.insert("math".to_string(), resolved_path("math", "0.1.0", "deps/math", BTreeMap::new())); lockfile.replace_with_resolved(&resolved); @@ -2045,6 +3795,41 @@ path = "deps/math" assert!(error.message.contains("failed to parse lockfile"), "{}", error.message); } + #[test] + fn lockfile_requires_package_and_build_profile_identity() { + let missing_package = toml::from_str::( + r#" +version = 3 +schema = "cellscript-lock-v0.24-graph-v1" + +[root] + +[dependencies] +"#, + ) + .unwrap_err(); + assert!(missing_package.to_string().contains("missing field `package`")); + + let missing_profile = toml::from_str::( + r#" +version = 3 +schema = "cellscript-lock-v0.24-graph-v1" + +[package] +edition = "2026" + +[root] + +[package_build] +edition = "2026" + +[dependencies] +"#, + ) + .unwrap_err(); + assert!(missing_profile.to_string().contains("missing field `compatibility_profile_hash`")); + } + #[test] fn package_manager_rejects_registry_dependencies_fail_closed() { let temp = tempdir().unwrap(); @@ -2052,6 +3837,7 @@ path = "deps/math" temp.path().join("Cell.toml"), r#" [package] +edition = "2026" name = "app" version = "0.1.0" @@ -2076,6 +3862,7 @@ remote = "1.2.3" temp.path().join("Cell.toml"), r#" [package] +edition = "2026" name = "app" version = "0.1.0" @@ -2098,14 +3885,17 @@ rev = "abc123" #[test] fn deployed_manifest_round_trip() { let manifest = DeployedManifest { - version: 1, - schema: Some(DEPLOYED_MANIFEST_SCHEMA.to_string()), + version: DeployedManifest::CURRENT_VERSION, + schema: DEPLOYED_MANIFEST_SCHEMA.to_string(), package: DeployedPackageInfo { + edition: CURRENT_EDITION, name: "amm_pool".to_string(), version: "1.2.0".to_string(), source_hash: Some("blake2b:0xabcd".to_string()), }, build: Some(DeployedBuildInfo { + edition: CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), compiler_version: Some("0.19.0".to_string()), artifact_hash: Some("blake2b:0x1234".to_string()), metadata_hash: None, @@ -2115,6 +3905,8 @@ rev = "abc123" constraints_hash: None, }), deployments: vec![DeploymentRecord { + edition: CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), network: "aggron4".to_string(), chain_id: "ckb-testnet".to_string(), tx_hash: "0xaaaa".to_string(), @@ -2154,7 +3946,7 @@ rev = "abc123" assert!(toml_str.contains("code_hash = \"0xbbbb\"")); let parsed: DeployedManifest = toml::from_str(&toml_str).unwrap(); - assert_eq!(parsed.version, 1); + assert_eq!(parsed.version, DeployedManifest::CURRENT_VERSION); assert_eq!(parsed.package.name, "amm_pool"); assert_eq!(parsed.deployments.len(), 1); assert_eq!(parsed.deployments[0].network, "aggron4"); @@ -2162,16 +3954,43 @@ rev = "abc123" } #[test] - fn deployed_manifest_backward_compatible() { - // Old format without new optional fields should parse successfully + fn deployed_manifest_rejects_legacy_identity() { let toml_str = r#" version = 1 [package] +edition = "2026" +name = "token" +version = "0.3.0" + +[[deployments]] +network = "ckb-mainnet" +chain_id = "ckb-mainnet" +tx_hash = "0x1111" +output_index = 0 +code_hash = "0x2222" +hash_type = "type" +dep_type = "code" +data_hash = "0x3333" + out_point = "0x1111:0" +"#; + let error = toml::from_str::(toml_str).unwrap_err(); + assert!(error.to_string().contains("missing field")); + } + + #[test] + fn deployed_manifest_requires_profile_identity() { + let toml_str = r#" +version = 2 +schema = "cellscript-deployed-v0.23-edition-2026" + +[package] +edition = "2026" name = "token" version = "0.3.0" [[deployments]] +edition = "2026" network = "ckb-mainnet" chain_id = "ckb-mainnet" tx_hash = "0x1111" @@ -2182,11 +4001,7 @@ dep_type = "code" data_hash = "0x3333" out_point = "0x1111:0" "#; - let parsed: DeployedManifest = toml::from_str(toml_str).unwrap(); - assert_eq!(parsed.package.name, "token"); - assert_eq!(parsed.deployments.len(), 1); - assert!(parsed.deployments[0].type_id.is_none()); - assert!(parsed.deployments[0].status.is_none()); - assert!(parsed.build.is_none()); + let error = toml::from_str::(toml_str).unwrap_err(); + assert!(error.to_string().contains("missing field `compatibility_profile_hash`")); } } diff --git a/src/package/registry.rs b/src/package/registry.rs index 15bcd515..4b0d4c4b 100644 --- a/src/package/registry.rs +++ b/src/package/registry.rs @@ -1,14 +1,24 @@ -//! Two-tier Git registry client for CellScript packages. +//! Public artifact registry and local fixture support for CellScript packages. //! -//! Model: Go-style + GitHub based -//! - Discovery index: lightweight Git repo mapping `namespace/name` → source URL -//! - Per-package version index: `registry.json` inside each source repository +//! Production resolution reads accepted package state from the public registry +//! API, then downloads and verifies the registry's immutable source snapshot. +//! The repository URL and tag remain provenance/audit fields rather than an +//! availability dependency. The Git discovery index is retained only for the +//! explicit offline fixture editing commands; dependency resolution never +//! falls back to it. //! //! Resolution priority: path > git > registry use crate::error::{CompileError, Result}; +use crate::package::PackageManifest; +#[cfg(feature = "cli")] +use base64::Engine; use serde::{Deserialize, Serialize}; +#[cfg(feature = "cli")] +use sha2::{Digest, Sha256}; use std::collections::BTreeMap; +#[cfg(feature = "cli")] +use std::io::Read; use std::path::{Path, PathBuf}; // --------------------------------------------------------------------------- @@ -19,12 +29,403 @@ use std::path::{Path, PathBuf}; pub const DEFAULT_REGISTRY_URL: &str = "https://github.com/cellscript/cellscript-registry"; pub const REGISTRY_URL_ENV: &str = "CELLSCRIPT_REGISTRY_URL"; pub const DEFAULT_PUBLIC_REGISTRY_ORIGIN: &str = "https://api.registry.cellscript.dev"; +pub const REGISTRY_API_URL_ENV: &str = "CELLSCRIPT_REGISTRY_API_URL"; +pub const REGISTRY_ORIGIN_ENV: &str = "CELLSCRIPT_REGISTRY_ORIGIN"; pub const REGISTRY_AUTH_PROTOCOL: &str = "cellscript-registry-auth-v1"; pub const AUTHORIZE_CAPABILITY_ACTION: &str = "authorize_capability"; pub const REVOKE_CAPABILITY_ACTION: &str = "revoke_capability"; pub const REGISTRY_PUBLISH_PROTOCOL: &str = "cellscript-registry-publish-v1"; pub const PUBLISH_ACTION: &str = "publish"; +/// Compute the cross-process identity of a parsed package manifest. +/// +/// `PackageManifest` contains hash maps, so serializing it directly can emit a +/// different key order in another process. Registry identities must instead +/// hash recursively key-sorted JSON so the publisher and isolated verifier +/// agree for the same `Cell.toml`. +pub fn compute_package_manifest_hash(manifest: &PackageManifest) -> Result { + let value = serde_json::to_value(manifest) + .map_err(|error| CompileError::without_span(format!("failed to serialize package manifest for digest: {error}")))?; + let bytes = serde_json::to_vec(&canonical_json_value(&value)) + .map_err(|error| CompileError::without_span(format!("failed to serialize canonical package manifest: {error}")))?; + Ok(crate::hex_encode(&crate::ckb_blake2b256(&bytes))) +} + +pub fn canonical_json_value(value: &serde_json::Value) -> serde_json::Value { + match value { + serde_json::Value::Array(items) => serde_json::Value::Array(items.iter().map(canonical_json_value).collect()), + serde_json::Value::Object(object) => { + let mut keys = object.keys().collect::>(); + keys.sort_unstable(); + let mut canonical = serde_json::Map::new(); + for key in keys { + if let Some(item) = object.get(key) { + canonical.insert(key.clone(), canonical_json_value(item)); + } + } + serde_json::Value::Object(canonical) + } + other => other.clone(), + } +} + +pub const ARTIFACT_PROFILE_CONTRACT_SCHEMA: &str = "cellscript-registry-profile-contract-v1"; +pub const LS_IDL_INTERFACE_SCHEMA: &str = "cellscript-registry-ls-idl-interface-v1"; +pub const LS_IDL_CONTENT_TYPE: &str = "application/vnd.ckb.ls-idl+json"; +pub const LS_IDL_FORMAT_VERSION: &str = "0.1"; +pub const MAX_LS_IDL_BYTES: usize = 256 * 1024; + +#[derive(Debug, Clone, Copy, Default)] +pub struct ArtifactContractHashes<'a> { + pub artifact_hash: Option<&'a str>, + pub abi_hash: Option<&'a str>, + pub abi_sha256: Option<&'a str>, + pub executable_ls_idl_bound: Option, + pub build_recipe_hash: Option<&'a str>, + pub audit_report_hash: Option<&'a str>, +} + +pub fn canonical_artifact_contract_json(value: &serde_json::Value) -> std::result::Result { + serde_json::to_string(&canonical_json_value(value)) + .map_err(|error| format!("failed to serialize canonical artifact profile contract: {error}")) +} + +pub fn validate_artifact_profile_contract( + artifact_kind: &str, + profile: &str, + value: &serde_json::Value, + hashes: ArtifactContractHashes<'_>, +) -> std::result::Result<(), String> { + let contract = registry_contract_object(value, "profile contract")?; + registry_exact_keys( + contract, + &["schema", "artifact_kind", "profile", "build", "security", "ckb", "interface", "verifier", "reproduction", "copy"], + "profile contract", + )?; + registry_require_literal(contract, "schema", ARTIFACT_PROFILE_CONTRACT_SCHEMA, "profile contract")?; + registry_require_literal(contract, "artifact_kind", artifact_kind, "profile contract")?; + registry_require_literal(contract, "profile", profile, "profile contract")?; + + match (artifact_kind, profile) { + ("runtime_verifier", "ckb_executable") => { + let reproducible = validate_registry_build_contract(contract, None)?; + validate_registry_security_contract(contract, hashes.audit_report_hash)?; + validate_registry_ckb_contract(contract)?; + validate_registry_abi_contract(contract, hashes.abi_hash)?; + validate_registry_reproduction_contract(contract, reproducible, hashes)?; + let verifier = registry_required_object(contract, "verifier", "profile contract")?; + registry_exact_keys(verifier, &["verifier_id", "ipc_abi", "ipc_abi_hash"], "verifier")?; + registry_require_nonempty_string(verifier, "verifier_id", "verifier")?; + registry_require_nonempty_string(verifier, "ipc_abi", "verifier")?; + registry_require_matching_hash(verifier, "ipc_abi_hash", hashes.abi_hash, "verifier")?; + registry_forbid_keys(contract, &["interface", "copy"], "profile contract")?; + } + ("deployable_contract", "ckb_executable") => { + let reproducible = validate_registry_build_contract(contract, None)?; + validate_registry_security_contract(contract, hashes.audit_report_hash)?; + validate_registry_ckb_contract(contract)?; + validate_registry_abi_contract(contract, hashes.abi_hash)?; + validate_registry_ls_idl_interface(contract, hashes)?; + validate_registry_reproduction_contract(contract, reproducible, hashes)?; + registry_forbid_keys(contract, &["verifier", "copy"], "profile contract")?; + } + ("reproducible_binary", "reproducible_build") => { + validate_registry_build_contract(contract, Some(true))?; + validate_registry_security_contract(contract, hashes.audit_report_hash)?; + registry_forbid_keys(contract, &["ckb", "interface", "verifier", "copy"], "profile contract")?; + validate_registry_reproduction_contract(contract, true, hashes)?; + } + ("template", "copy_material") => { + registry_forbid_keys( + contract, + &["build", "security", "ckb", "interface", "verifier", "reproduction"], + "profile contract", + )?; + let copy = registry_required_object(contract, "copy", "profile contract")?; + registry_exact_keys(copy, &["format", "entrypoint"], "copy")?; + registry_require_one_of(copy, "format", &["file_map_v1"], "copy")?; + registry_require_nonempty_string(copy, "entrypoint", "copy")?; + } + _ => return Err(format!("artifact kind '{artifact_kind}' does not match profile '{profile}'")), + } + Ok(()) +} + +fn validate_registry_build_contract( + contract: &serde_json::Map, + expected_reproducible: Option, +) -> std::result::Result { + let build = registry_required_object(contract, "build", "profile contract")?; + registry_exact_keys(build, &["target", "toolchain", "profile", "source_revision", "reproducible"], "build")?; + registry_require_nonempty_string(build, "target", "build")?; + registry_require_nonempty_string(build, "toolchain", "build")?; + registry_require_nonempty_string(build, "profile", "build")?; + registry_require_nonempty_string(build, "source_revision", "build")?; + let reproducible = build + .get("reproducible") + .and_then(serde_json::Value::as_bool) + .ok_or_else(|| "build.reproducible must be a boolean".to_string())?; + if let Some(expected) = expected_reproducible + && reproducible != expected + { + return Err(format!("build.reproducible must be {expected}")); + } + Ok(reproducible) +} + +fn validate_registry_reproduction_contract( + contract: &serde_json::Map, + reproducible: bool, + hashes: ArtifactContractHashes<'_>, +) -> std::result::Result<(), String> { + if !reproducible { + registry_forbid_keys(contract, &["reproduction"], "profile contract")?; + if hashes.build_recipe_hash.is_some() { + return Err("a build_recipe object requires build.reproducible=true".to_string()); + } + return Ok(()); + } + let reproduction = registry_required_object(contract, "reproduction", "profile contract")?; + registry_exact_keys(reproduction, &["environment", "command", "recipe_hash", "expected_artifact_hash"], "reproduction")?; + registry_require_nonempty_string(reproduction, "environment", "reproduction")?; + registry_require_nonempty_string(reproduction, "command", "reproduction")?; + registry_require_matching_hash(reproduction, "recipe_hash", hashes.build_recipe_hash, "reproduction")?; + registry_require_matching_hash(reproduction, "expected_artifact_hash", hashes.artifact_hash, "reproduction") +} + +fn validate_registry_security_contract( + contract: &serde_json::Map, + audit_report_hash: Option<&str>, +) -> std::result::Result<(), String> { + let security = registry_required_object(contract, "security", "profile contract")?; + registry_exact_keys(security, &["status", "audit_report_hash"], "security")?; + let status = registry_require_one_of(security, "status", &["unaudited", "review_required", "audited", "rejected"], "security")?; + if status == "audited" || security.contains_key("audit_report_hash") { + registry_require_matching_hash(security, "audit_report_hash", audit_report_hash, "security")?; + } else if audit_report_hash.is_some() { + return Err("security.audit_report_hash must bind the supplied audit_report object".to_string()); + } + Ok(()) +} + +fn validate_registry_ckb_contract(contract: &serde_json::Map) -> std::result::Result<(), String> { + let ckb = registry_required_object(contract, "ckb", "profile contract")?; + registry_exact_keys(ckb, &["vm_version", "script_role", "hash_type", "dep_type", "abi_hash"], "ckb")?; + registry_require_one_of(ckb, "vm_version", &["0", "1", "2"], "ckb")?; + registry_require_one_of(ckb, "script_role", &["lock", "type", "dual_role", "helper"], "ckb")?; + registry_require_one_of(ckb, "hash_type", &["data", "data1", "data2", "type"], "ckb")?; + registry_require_one_of(ckb, "dep_type", &["code", "dep_group"], "ckb")?; + Ok(()) +} + +fn validate_registry_abi_contract( + contract: &serde_json::Map, + expected: Option<&str>, +) -> std::result::Result<(), String> { + let ckb = registry_required_object(contract, "ckb", "profile contract")?; + registry_require_matching_hash(ckb, "abi_hash", expected, "ckb") +} + +fn validate_registry_ls_idl_interface( + contract: &serde_json::Map, + hashes: ArtifactContractHashes<'_>, +) -> std::result::Result<(), String> { + let Some(interface_value) = contract.get("interface") else { + return Ok(()); + }; + let interface = registry_contract_object(interface_value, "interface")?; + registry_exact_keys( + interface, + &["schema", "format", "format_version", "object_role", "content_type", "encoding", "commitment"], + "interface", + )?; + registry_require_literal(interface, "schema", LS_IDL_INTERFACE_SCHEMA, "interface")?; + registry_require_literal(interface, "format", "ls-idl", "interface")?; + registry_require_literal(interface, "format_version", LS_IDL_FORMAT_VERSION, "interface")?; + registry_require_literal(interface, "object_role", "abi", "interface")?; + registry_require_literal(interface, "content_type", LS_IDL_CONTENT_TYPE, "interface")?; + registry_require_literal(interface, "encoding", "linear-le-v0", "interface")?; + + let ckb = registry_required_object(contract, "ckb", "profile contract")?; + registry_require_literal(ckb, "script_role", "lock", "ckb")?; + let commitment = registry_required_object(interface, "commitment", "interface")?; + registry_exact_keys(commitment, &["algorithm", "placement", "digest"], "interface.commitment")?; + registry_require_literal(commitment, "algorithm", "sha256", "interface.commitment")?; + registry_require_literal(commitment, "placement", "code-cell-data-suffix-32", "interface.commitment")?; + registry_require_matching_hash(commitment, "digest", hashes.abi_sha256, "interface.commitment")?; + if hashes.executable_ls_idl_bound != Some(true) { + return Err("interface commitment is not the exact 32-byte suffix of the executable object".to_string()); + } + Ok(()) +} + +/// Validate the bounded LS-IDL 0.1 document accepted by the Registry profile. +/// +/// The digest commits the exact input bytes; this function parses only for +/// schema admission and never reserializes the document as its identity. +pub fn validate_ls_idl_document(bytes: &[u8]) -> std::result::Result<(), String> { + if bytes.is_empty() || bytes.len() > MAX_LS_IDL_BYTES { + return Err(format!("LS-IDL must be a non-empty JSON document no larger than {MAX_LS_IDL_BYTES} bytes")); + } + let value: serde_json::Value = serde_json::from_slice(bytes).map_err(|error| format!("LS-IDL is not valid JSON: {error}"))?; + let document = registry_contract_object(&value, "LS-IDL")?; + registry_exact_keys(document, &["idl_version", "name", "witness", "description", "script_version", "signing"], "LS-IDL")?; + for key in ["idl_version", "name", "description", "script_version"] { + if let Some(value) = document.get(key) { + let text = value.as_str().ok_or_else(|| format!("LS-IDL.{key} must be a string"))?; + if text.len() > 1024 { + return Err(format!("LS-IDL.{key} exceeds the 1024-byte limit")); + } + } + } + let fields = + document.get("witness").and_then(serde_json::Value::as_array).ok_or_else(|| "LS-IDL.witness must be an array".to_string())?; + if fields.len() > 256 { + return Err("LS-IDL.witness may contain at most 256 fields".to_string()); + } + let mut names = std::collections::BTreeSet::new(); + for (index, field_value) in fields.iter().enumerate() { + let label = format!("LS-IDL.witness[{index}]"); + let field = registry_contract_object(field_value, &label)?; + registry_exact_keys(field, &["name", "type", "required", "description"], &label)?; + let name = registry_require_nonempty_string(field, "name", &label)?; + if name.len() > 128 || !names.insert(name) { + return Err(format!("{label}.name must be unique and no longer than 128 bytes")); + } + registry_require_one_of( + field, + "type", + &["uint8", "uint32", "uint64", "secp256k1_sig", "secp256k1_pubkey", "schnorr_sig", "bytes"], + &label, + )?; + if !matches!(field.get("required"), Some(serde_json::Value::Bool(_))) { + return Err(format!("{label}.required must be a boolean")); + } + if let Some(description) = field.get("description") { + let description = description.as_str().ok_or_else(|| format!("{label}.description must be a string"))?; + if description.len() > 1024 { + return Err(format!("{label}.description exceeds the 1024-byte limit")); + } + } + } + if let Some(signing_value) = document.get("signing") { + let signing = registry_contract_object(signing_value, "LS-IDL.signing")?; + registry_exact_keys(signing, &["algorithm", "message", "hasher"], "LS-IDL.signing")?; + for key in ["algorithm", "message", "hasher"] { + let value = registry_require_nonempty_string(signing, key, "LS-IDL.signing")?; + if value.len() > 1024 { + return Err(format!("LS-IDL.signing.{key} exceeds the 1024-byte limit")); + } + } + } + Ok(()) +} + +fn registry_contract_object<'a>( + value: &'a serde_json::Value, + label: &str, +) -> std::result::Result<&'a serde_json::Map, String> { + value.as_object().ok_or_else(|| format!("{label} must be a JSON object")) +} + +fn registry_required_object<'a>( + object: &'a serde_json::Map, + key: &str, + label: &str, +) -> std::result::Result<&'a serde_json::Map, String> { + object + .get(key) + .ok_or_else(|| format!("{label}.{key} is required")) + .and_then(|value| registry_contract_object(value, &format!("{label}.{key}"))) +} + +fn registry_exact_keys( + object: &serde_json::Map, + allowed: &[&str], + label: &str, +) -> std::result::Result<(), String> { + if let Some(key) = object.keys().find(|key| !allowed.contains(&key.as_str())) { + return Err(format!("{label}.{key} is not recognised")); + } + Ok(()) +} + +fn registry_forbid_keys( + object: &serde_json::Map, + forbidden: &[&str], + label: &str, +) -> std::result::Result<(), String> { + if let Some(key) = forbidden.iter().find(|key| object.contains_key(**key)) { + return Err(format!("{label}.{key} is not valid for this artifact kind")); + } + Ok(()) +} + +fn registry_require_literal( + object: &serde_json::Map, + key: &str, + expected: &str, + label: &str, +) -> std::result::Result<(), String> { + let value = registry_require_nonempty_string(object, key, label)?; + if value != expected { + return Err(format!("{label}.{key} must be '{expected}'")); + } + Ok(()) +} + +fn registry_require_nonempty_string<'a>( + object: &'a serde_json::Map, + key: &str, + label: &str, +) -> std::result::Result<&'a str, String> { + object + .get(key) + .and_then(serde_json::Value::as_str) + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| format!("{label}.{key} must be a non-empty string")) +} + +fn registry_require_one_of<'a>( + object: &'a serde_json::Map, + key: &str, + allowed: &[&str], + label: &str, +) -> std::result::Result<&'a str, String> { + let value = registry_require_nonempty_string(object, key, label)?; + if !allowed.contains(&value) { + return Err(format!("{label}.{key} must be one of {}", allowed.join(", "))); + } + Ok(value) +} + +fn registry_require_hash<'a>( + object: &'a serde_json::Map, + key: &str, + label: &str, +) -> std::result::Result<&'a str, String> { + let value = registry_require_nonempty_string(object, key, label)?; + let bare = value.strip_prefix("0x").unwrap_or(value); + if bare.len() != 64 || !bare.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err(format!("{label}.{key} must be a 32-byte hexadecimal hash")); + } + Ok(value) +} + +fn registry_require_matching_hash( + object: &serde_json::Map, + key: &str, + expected: Option<&str>, + label: &str, +) -> std::result::Result<(), String> { + let value = registry_require_hash(object, key, label)?; + let expected = expected.ok_or_else(|| format!("{label}.{key} has no computed bundle identity to bind"))?; + if !value.trim_start_matches("0x").eq_ignore_ascii_case(expected.trim_start_matches("0x")) { + return Err(format!("{label}.{key} does not match the corresponding immutable bundle object")); + } + Ok(()) +} + /// Effective discovery index URL. /// /// The environment override is intentionally small: it lets tests and private @@ -38,6 +439,16 @@ pub fn default_registry_url() -> String { .unwrap_or_else(|| DEFAULT_REGISTRY_URL.to_string()) } +/// Effective production artifact API used by dependency resolution. +pub fn resolver_registry_url() -> String { + std::env::var(REGISTRY_API_URL_ENV) + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .or_else(|| std::env::var(REGISTRY_ORIGIN_ENV).ok().map(|value| value.trim().to_string()).filter(|value| !value.is_empty())) + .unwrap_or_else(|| DEFAULT_PUBLIC_REGISTRY_ORIGIN.to_string()) +} + /// A single entry in the discovery index: maps `namespace/name` to a source repo URL. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DiscoveryEntry { @@ -46,6 +457,573 @@ pub struct DiscoveryEntry { pub source: String, } +pub struct RegistryResolution { + pub registry_url: String, + pub entry: DiscoveryEntry, + /// Accepted production releases from the public API. + pub authoritative_index: Option, + /// Immutable install snapshots keyed by package version. + pub source_snapshots: BTreeMap, +} + +/// Resolve a CellScript dependency through the production artifact API. +pub fn lookup_for_resolution(namespace: &str, name: &str, cache_dir: &Path) -> Result { + let _ = cache_dir; + let registry_url = resolver_registry_url(); + let (entry, authoritative_index, source_snapshots) = lookup_public_registry(®istry_url, namespace, name)?; + Ok(RegistryResolution { registry_url, entry, authoritative_index: Some(authoritative_index), source_snapshots }) +} + +#[cfg(feature = "cli")] +fn lookup_public_registry( + registry_url: &str, + namespace: &str, + name: &str, +) -> Result<(DiscoveryEntry, RegistryIndex, BTreeMap)> { + let url = format!("{}/v1/artifacts/{}/{}", registry_url.trim_end_matches('/'), namespace, name); + let response = reqwest::blocking::Client::builder() + .timeout(std::time::Duration::from_secs(15)) + .build() + .map_err(|error| CompileError::without_span(format!("failed to initialize public registry client: {error}")))? + .get(&url) + .header(reqwest::header::ACCEPT, "application/json") + .header(reqwest::header::USER_AGENT, format!("cellc/{}", env!("CARGO_PKG_VERSION"))) + .send() + .map_err(|error| CompileError::without_span(format!("public registry request '{}' failed: {error}", url)))?; + if response.status() == reqwest::StatusCode::NOT_FOUND { + return Err(CompileError::without_span(format!("package '{namespace}/{name}' is not present in the public registry"))); + } + if !response.status().is_success() { + return Err(CompileError::without_span(format!("public registry request '{}' returned HTTP {}", url, response.status()))); + } + let payload: PublicRegistryPackage = response + .json() + .map_err(|error| CompileError::without_span(format!("public registry response '{}' is invalid: {error}", url)))?; + payload.into_resolution(namespace, name) +} + +#[cfg(not(feature = "cli"))] +fn lookup_public_registry( + _registry_url: &str, + namespace: &str, + name: &str, +) -> Result<(DiscoveryEntry, RegistryIndex, BTreeMap)> { + Err(CompileError::without_span(format!("public registry resolution for '{namespace}/{name}' requires the 'cli' feature"))) +} + +#[cfg(feature = "cli")] +#[derive(Debug, Deserialize)] +struct PublicRegistryPackage { + schema: String, + namespace: String, + name: String, + repository: Option, + artifact: RegistryArtifactDescriptor, + releases: Vec, +} + +#[cfg(feature = "cli")] +#[derive(Debug, Deserialize)] +struct PublicRegistryVersion { + release: String, + verification_status: String, + availability_status: String, + registry_entry: RegistryIndex, + immutable_bundle: PublicRegistrySourceSnapshot, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct PublicRegistrySourceSnapshot { + pub schema: String, + pub url: String, + pub snapshot_hash: String, + pub source_hash: String, + pub size_bytes: u64, + pub content_type: String, +} + +#[cfg(feature = "cli")] +impl PublicRegistryPackage { + fn into_resolution( + self, + expected_namespace: &str, + expected_name: &str, + ) -> Result<(DiscoveryEntry, RegistryIndex, BTreeMap)> { + if self.schema != "cellscript-registry-artifact" { + return Err(CompileError::without_span(format!("public registry returned unsupported schema '{}'", self.schema))); + } + if self.namespace != expected_namespace || self.name != expected_name { + return Err(CompileError::without_span(format!( + "public registry identity mismatch for '{expected_namespace}/{expected_name}': found '{}/{}'", + self.namespace, self.name + ))); + } + if self.artifact.profile != "cellscript_source" || self.artifact.consumption_mode != "dependency" { + return Err(CompileError::without_span(format!( + "artifact '{expected_namespace}/{expected_name}' is not a resolver-safe CellScript dependency" + ))); + } + let source = self.repository.filter(|value| !value.trim().is_empty()).unwrap_or_default(); + let mut versions = Vec::with_capacity(self.releases.len()); + let mut source_snapshots = BTreeMap::new(); + for public_version in self.releases { + if public_version.registry_entry.schema_version != RegistryIndex::CURRENT_SCHEMA_VERSION + || public_version.registry_entry.namespace != expected_namespace + || public_version.registry_entry.name != expected_name + { + return Err(CompileError::without_span(format!( + "public registry version '{}' contains mismatched registry identity", + public_version.release + ))); + } + let mut matching = public_version + .registry_entry + .versions + .into_iter() + .find(|version| version.version == public_version.release) + .ok_or_else(|| { + CompileError::without_span(format!( + "public registry version '{}' has no matching signed version entry", + public_version.release + )) + })?; + matching.status = + public_registry_release_status(&public_version.verification_status, &public_version.availability_status)?; + matching.yanked = public_version.availability_status == "yanked"; + if public_version.immutable_bundle.schema != "cellscript-registry-immutable-bundle" + || public_version.immutable_bundle.source_hash != matching.source_hash + { + return Err(CompileError::without_span(format!( + "public registry version '{}' contains invalid source snapshot identity", + public_version.release + ))); + } + if source_snapshots.insert(public_version.release.clone(), public_version.immutable_bundle).is_some() { + return Err(CompileError::without_span(format!( + "public registry returned duplicate version '{}'", + public_version.release + ))); + } + versions.push(matching); + } + if versions.is_empty() { + return Err(CompileError::without_span(format!( + "public registry package '{expected_namespace}/{expected_name}' has no visible versions" + ))); + } + Ok(( + DiscoveryEntry { name: expected_name.to_string(), namespace: expected_namespace.to_string(), source }, + RegistryIndex { + schema_version: RegistryIndex::CURRENT_SCHEMA_VERSION, + name: expected_name.to_string(), + namespace: expected_namespace.to_string(), + versions, + }, + source_snapshots, + )) + } +} + +#[cfg(feature = "cli")] +fn public_registry_release_status(verification: &str, availability: &str) -> Result { + match availability { + "deprecated" => return Ok(RegistryEntryStatus::Deprecated), + "yanked" => return Ok(RegistryEntryStatus::Yanked), + "quarantined" => return Ok(RegistryEntryStatus::Quarantined), + "active" => {} + value => { + return Err(CompileError::without_span(format!("public registry returned unknown availability state '{value}'"))); + } + } + match verification { + "verified" => Ok(RegistryEntryStatus::VerifiedBuild), + "pending" => Ok(RegistryEntryStatus::SourcePublished), + "evidence_required" => Ok(RegistryEntryStatus::IndexedPending), + "rejected" => Ok(RegistryEntryStatus::Quarantined), + value => Err(CompileError::without_span(format!("public registry returned unknown verification state '{value}'"))), + } +} + +#[cfg(feature = "cli")] +const MAX_PUBLIC_SOURCE_SNAPSHOT_BYTES: u64 = 5 * 1024 * 1024; + +#[cfg(feature = "cli")] +#[derive(Debug, Deserialize)] +struct GeneratedSourceSnapshot { + schema: String, + package: GeneratedSourceSnapshotPackage, + files: Vec, +} + +#[cfg(feature = "cli")] +#[derive(Debug, Deserialize)] +struct GeneratedSourceSnapshotPackage { + namespace: Option, + name: String, + version: String, +} + +#[cfg(feature = "cli")] +#[derive(Debug, Deserialize)] +struct GeneratedSourceSnapshotFile { + path: String, + blake2b256: String, + content_base64: String, +} + +/// Download, authenticate, and atomically materialize a public Registry source +/// snapshot. The current source-package profile accepts only the generated JSON +/// snapshot shape; opaque archives remain publish evidence but are not executed +/// or unpacked by the dependency resolver. +#[cfg(feature = "cli")] +pub fn materialize_public_source_snapshot( + snapshot: &PublicRegistrySourceSnapshot, + cache_root: &Path, + namespace: &str, + name: &str, + version: &str, + expected_source_hash: &str, +) -> Result { + validate_public_source_snapshot_descriptor(snapshot, expected_source_hash)?; + let bytes = download_public_source_snapshot(snapshot)?; + std::fs::create_dir_all(cache_root).map_err(|error| { + CompileError::without_span(format!("failed to create source snapshot cache '{}': {error}", cache_root.display())) + })?; + let cache_suffix = snapshot.snapshot_hash.trim_start_matches("sha256:"); + let target = cache_root.join(format!("{name}-snapshot-{cache_suffix}")); + let temporary = unique_snapshot_temp_dir(cache_root, name)?; + let materialized = (|| { + materialize_generated_source_snapshot_bytes(&bytes, &temporary, namespace, name, version, expected_source_hash)?; + remove_cache_entry(&target)?; + std::fs::rename(&temporary, &target).map_err(|error| { + CompileError::without_span(format!( + "failed to commit source snapshot cache '{}' to '{}': {error}", + temporary.display(), + target.display() + )) + })?; + Ok(target.clone()) + })(); + if materialized.is_err() { + let _ = std::fs::remove_dir_all(&temporary); + } + materialized +} + +/// Materialize an already-pinned public Registry snapshot without consulting +/// mutable discovery or version-selection state. The URL is transport only; +/// the lockfile's exact SHA-256 snapshot identity and whole-tree source hash +/// remain authoritative. +#[cfg(feature = "cli")] +pub fn materialize_locked_public_source_snapshot( + url: &str, + snapshot_hash: &str, + cache_root: &Path, + namespace: &str, + name: &str, + version: &str, + expected_source_hash: &str, +) -> Result { + let digest = snapshot_hash + .strip_prefix("sha256:") + .ok_or_else(|| CompileError::without_span("locked Registry snapshot hash must use sha256:"))?; + if digest.len() != 64 || !digest.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err(CompileError::without_span("locked Registry snapshot hash must contain 32 bytes of hex")); + } + let parsed = reqwest::Url::parse(url) + .map_err(|error| CompileError::without_span(format!("locked Registry snapshot URL is invalid: {error}")))?; + if !matches!(parsed.scheme(), "http" | "https") + || !parsed.username().is_empty() + || parsed.password().is_some() + || parsed.fragment().is_some() + { + return Err(CompileError::without_span("locked Registry snapshot URL must be HTTP(S) without credentials or a fragment")); + } + std::fs::create_dir_all(cache_root)?; + let target = cache_root.join(format!("{name}-snapshot-{digest}")); + if target.exists() { + return Ok(target); + } + let client = reqwest::blocking::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(|error| CompileError::without_span(format!("failed to initialize locked snapshot client: {error}")))?; + let response = client + .get(url) + .header(reqwest::header::ACCEPT, "application/vnd.cellscript.source-snapshot+json") + .header(reqwest::header::USER_AGENT, format!("cellc/{}", env!("CARGO_PKG_VERSION"))) + .send() + .map_err(|error| CompileError::without_span(format!("locked snapshot request '{url}' failed: {error}")))?; + if !response.status().is_success() { + return Err(CompileError::without_span(format!("locked snapshot request '{url}' returned HTTP {}", response.status()))); + } + if response.content_length().is_some_and(|length| length == 0 || length > MAX_PUBLIC_SOURCE_SNAPSHOT_BYTES) { + return Err(CompileError::without_span("locked Registry snapshot Content-Length exceeds the bounded source contract")); + } + let mut bytes = Vec::new(); + response + .take(MAX_PUBLIC_SOURCE_SNAPSHOT_BYTES + 1) + .read_to_end(&mut bytes) + .map_err(|error| CompileError::without_span(format!("failed to read locked snapshot '{url}': {error}")))?; + if bytes.is_empty() || bytes.len() as u64 > MAX_PUBLIC_SOURCE_SNAPSHOT_BYTES { + return Err(CompileError::without_span("locked Registry snapshot exceeds the bounded source contract")); + } + let actual = format!("sha256:{}", hex::encode(Sha256::digest(&bytes))); + if !actual.eq_ignore_ascii_case(snapshot_hash) { + return Err(CompileError::without_span(format!( + "locked Registry snapshot hash mismatch: expected '{}', got '{}'", + snapshot_hash, actual + ))); + } + let temporary = unique_snapshot_temp_dir(cache_root, name)?; + let result = (|| { + materialize_generated_source_snapshot_bytes(&bytes, &temporary, namespace, name, version, expected_source_hash)?; + std::fs::rename(&temporary, &target).map_err(|error| { + CompileError::without_span(format!( + "failed to commit locked Registry snapshot '{}' to '{}': {error}", + temporary.display(), + target.display() + )) + })?; + Ok(target.clone()) + })(); + if result.is_err() { + let _ = std::fs::remove_dir_all(&temporary); + } + result +} + +#[cfg(not(feature = "cli"))] +pub fn materialize_locked_public_source_snapshot( + _url: &str, + _snapshot_hash: &str, + _cache_root: &Path, + namespace: &str, + name: &str, + version: &str, + _expected_source_hash: &str, +) -> Result { + Err(CompileError::without_span(format!( + "locked Registry dependency resolution for '{namespace}/{name}@{version}' requires the 'cli' feature" + ))) +} + +/// Authenticate and materialize the generated JSON source-snapshot profile +/// into a caller-owned, non-existent directory. This is shared by dependency +/// resolution and the isolated Registry build-verification worker so both +/// paths enforce identical identity, path, per-file hash, size, and whole-tree +/// source-hash checks. +#[cfg(feature = "cli")] +pub fn materialize_generated_source_snapshot_bytes( + bytes: &[u8], + destination: &Path, + namespace: &str, + name: &str, + version: &str, + expected_source_hash: &str, +) -> Result<()> { + if destination.exists() { + return Err(CompileError::without_span(format!("source snapshot destination '{}' already exists", destination.display()))); + } + unpack_generated_source_snapshot(bytes, destination, namespace, name, version)?; + let computed_source_hash = compute_source_hash(destination)?; + if computed_source_hash != expected_source_hash { + return Err(CompileError::without_span(format!( + "public registry source snapshot for '{namespace}/{name}@{version}' has source_hash '{computed_source_hash}', expected '{expected_source_hash}'" + ))); + } + Ok(()) +} + +#[cfg(not(feature = "cli"))] +pub fn materialize_public_source_snapshot( + _snapshot: &PublicRegistrySourceSnapshot, + _cache_root: &Path, + namespace: &str, + name: &str, + version: &str, + _expected_source_hash: &str, +) -> Result { + Err(CompileError::without_span(format!( + "public registry source snapshot resolution for '{namespace}/{name}@{version}' requires the 'cli' feature" + ))) +} + +#[cfg(feature = "cli")] +fn validate_public_source_snapshot_descriptor(snapshot: &PublicRegistrySourceSnapshot, expected_source_hash: &str) -> Result<()> { + if snapshot.schema != "cellscript-registry-immutable-bundle" { + return Err(CompileError::without_span(format!("unsupported public registry source snapshot schema '{}'", snapshot.schema))); + } + let digest = snapshot + .snapshot_hash + .strip_prefix("sha256:") + .ok_or_else(|| CompileError::without_span("public registry source snapshot hash must use the sha256: form"))?; + if digest.len() != 64 || !digest.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err(CompileError::without_span("public registry source snapshot hash must contain 32 bytes of hex")); + } + if snapshot.source_hash != expected_source_hash { + return Err(CompileError::without_span("public registry source snapshot does not match the selected package source_hash")); + } + if snapshot.size_bytes == 0 || snapshot.size_bytes > MAX_PUBLIC_SOURCE_SNAPSHOT_BYTES { + return Err(CompileError::without_span(format!( + "public registry source snapshot size must be between 1 and {MAX_PUBLIC_SOURCE_SNAPSHOT_BYTES} bytes" + ))); + } + if snapshot.content_type != "application/vnd.cellscript.source-snapshot+json" { + return Err(CompileError::without_span(format!( + "public registry dependency resolution does not support source snapshot content type '{}'", + snapshot.content_type + ))); + } + let url = reqwest::Url::parse(&snapshot.url) + .map_err(|error| CompileError::without_span(format!("public registry source snapshot URL is invalid: {error}")))?; + if !matches!(url.scheme(), "http" | "https") || !url.username().is_empty() || url.password().is_some() || url.fragment().is_some() + { + return Err(CompileError::without_span( + "public registry source snapshot URL must be an HTTP(S) URL without credentials or a fragment", + )); + } + Ok(()) +} + +#[cfg(feature = "cli")] +fn download_public_source_snapshot(snapshot: &PublicRegistrySourceSnapshot) -> Result> { + let client = reqwest::blocking::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(|error| CompileError::without_span(format!("failed to initialize source snapshot client: {error}")))?; + let response = client + .get(&snapshot.url) + .header(reqwest::header::ACCEPT, snapshot.content_type.as_str()) + .header(reqwest::header::USER_AGENT, format!("cellc/{}", env!("CARGO_PKG_VERSION"))) + .send() + .map_err(|error| CompileError::without_span(format!("source snapshot request '{}' failed: {error}", snapshot.url)))?; + if !response.status().is_success() { + return Err(CompileError::without_span(format!( + "source snapshot request '{}' returned HTTP {}", + snapshot.url, + response.status() + ))); + } + if response.content_length().is_some_and(|length| length != snapshot.size_bytes) { + return Err(CompileError::without_span("public registry source snapshot Content-Length does not match its descriptor")); + } + let mut bytes = Vec::with_capacity(snapshot.size_bytes as usize); + response + .take(snapshot.size_bytes + 1) + .read_to_end(&mut bytes) + .map_err(|error| CompileError::without_span(format!("failed to read source snapshot '{}': {error}", snapshot.url)))?; + if bytes.len() as u64 != snapshot.size_bytes { + return Err(CompileError::without_span("downloaded public registry source snapshot size does not match its descriptor")); + } + let actual = format!("sha256:{}", hex::encode(Sha256::digest(&bytes))); + if actual != snapshot.snapshot_hash.to_ascii_lowercase() { + return Err(CompileError::without_span(format!( + "public registry source snapshot hash mismatch: expected '{}', got '{actual}'", + snapshot.snapshot_hash + ))); + } + Ok(bytes) +} + +#[cfg(feature = "cli")] +fn unpack_generated_source_snapshot(bytes: &[u8], destination: &Path, namespace: &str, name: &str, version: &str) -> Result<()> { + let snapshot: GeneratedSourceSnapshot = serde_json::from_slice(bytes) + .map_err(|error| CompileError::without_span(format!("failed to parse public registry source snapshot: {error}")))?; + if snapshot.schema != "cellscript-source-snapshot-v1" { + return Err(CompileError::without_span(format!("unsupported generated source snapshot schema '{}'", snapshot.schema))); + } + if snapshot.package.namespace.as_deref() != Some(namespace) || snapshot.package.name != name || snapshot.package.version != version + { + return Err(CompileError::without_span(format!( + "public registry source snapshot identity does not match '{namespace}/{name}@{version}'" + ))); + } + if snapshot.files.is_empty() || snapshot.files.len() > 4096 { + return Err(CompileError::without_span("public registry source snapshot must contain between 1 and 4096 files")); + } + std::fs::create_dir(destination).map_err(|error| { + CompileError::without_span(format!("failed to create source snapshot staging directory '{}': {error}", destination.display())) + })?; + let mut paths = std::collections::BTreeSet::new(); + let mut decoded_bytes = 0_u64; + for file in snapshot.files { + let relative = validated_snapshot_path(&file.path)?; + if !paths.insert(relative.clone()) { + return Err(CompileError::without_span(format!("source snapshot contains duplicate path '{}'", file.path))); + } + let content = base64::engine::general_purpose::STANDARD.decode(&file.content_base64).map_err(|error| { + CompileError::without_span(format!("source snapshot file '{}' has invalid base64: {error}", file.path)) + })?; + decoded_bytes = decoded_bytes.saturating_add(content.len() as u64); + if decoded_bytes > MAX_PUBLIC_SOURCE_SNAPSHOT_BYTES { + return Err(CompileError::without_span("decoded source snapshot exceeds the package size limit")); + } + let actual_file_hash = crate::hex_encode(&crate::ckb_blake2b256(&content)); + if actual_file_hash != file.blake2b256.to_ascii_lowercase() { + return Err(CompileError::without_span(format!("source snapshot file '{}' failed its blake2b256 check", file.path))); + } + let output = destination.join(&relative); + if let Some(parent) = output.parent() { + std::fs::create_dir_all(parent)?; + } + let mut options = std::fs::OpenOptions::new(); + options.create_new(true).write(true); + let mut handle = options.open(&output).map_err(|error| { + CompileError::without_span(format!("failed to create source snapshot file '{}': {error}", output.display())) + })?; + std::io::Write::write_all(&mut handle, &content)?; + } + if !destination.join("Cell.toml").is_file() { + return Err(CompileError::without_span("public registry source snapshot does not contain Cell.toml")); + } + Ok(()) +} + +#[cfg(feature = "cli")] +fn validated_snapshot_path(value: &str) -> Result { + if value.is_empty() || value.len() > 1024 || value.starts_with('/') || value.ends_with('/') || value.contains('\\') { + return Err(CompileError::without_span(format!("source snapshot path '{value}' is unsafe"))); + } + let segments: Vec<_> = value.split('/').collect(); + if segments.len() > 32 || segments.iter().any(|segment| segment.is_empty() || *segment == "." || *segment == "..") { + return Err(CompileError::without_span(format!("source snapshot path '{value}' is unsafe"))); + } + let allowed = value == "Cell.toml" || value == "Cell.lock" || value.ends_with(".cell"); + if !allowed || segments.first().is_some_and(|segment| segment.starts_with('.')) { + return Err(CompileError::without_span(format!("source snapshot path '{value}' is outside the source-package profile"))); + } + Ok(segments.iter().collect()) +} + +#[cfg(feature = "cli")] +fn unique_snapshot_temp_dir(cache_root: &Path, name: &str) -> Result { + for attempt in 0..100_u32 { + let candidate = cache_root.join(format!(".{name}-snapshot-{}-{attempt}.tmp", std::process::id())); + if std::fs::symlink_metadata(&candidate).is_err() { + return Ok(candidate); + } + } + Err(CompileError::without_span(format!("failed to allocate a source snapshot staging path in '{}'", cache_root.display()))) +} + +#[cfg(feature = "cli")] +fn remove_cache_entry(path: &Path) -> Result<()> { + let metadata = match std::fs::symlink_metadata(path) { + Ok(metadata) => metadata, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(error) => return Err(error.into()), + }; + if metadata.file_type().is_dir() && !metadata.file_type().is_symlink() { + std::fs::remove_dir_all(path)?; + } else { + std::fs::remove_file(path)?; + } + Ok(()) +} + /// Schema version file in the discovery index root. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DiscoverySchema { @@ -148,16 +1126,24 @@ pub struct RegistryPublishPayload { pub name: String, pub version: String, pub source_hash: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub manifest_hash: Option, + pub manifest_hash: String, pub capability_key_id: String, pub nonce: String, pub issued_at: String, pub expires_at: String, pub cli_version: String, + pub artifact: RegistryArtifactDescriptor, pub registry_entry: serde_json::Value, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct RegistryArtifactDescriptor { + pub kind: String, + pub profile: String, + pub consumption_mode: String, + pub language: String, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RegistryCapabilitySignature { pub algorithm: String, @@ -284,7 +1270,7 @@ pub enum RegistryEntryStatus { IndexedPending, VerifiedBuild, Deployed, - OnChainAttested, + OnChainCommitted, Deprecated, Yanked, Quarantined, @@ -297,7 +1283,7 @@ impl RegistryEntryStatus { Self::IndexedPending => "indexed_pending", Self::VerifiedBuild => "verified_build", Self::Deployed => "deployed", - Self::OnChainAttested => "on_chain_attested", + Self::OnChainCommitted => "on_chain_committed", Self::Deprecated => "deprecated", Self::Yanked => "yanked", Self::Quarantined => "quarantined", @@ -305,7 +1291,7 @@ impl RegistryEntryStatus { } pub fn is_baseline_verified(&self) -> bool { - matches!(self, Self::VerifiedBuild | Self::Deployed | Self::OnChainAttested) + matches!(self, Self::VerifiedBuild | Self::Deployed | Self::OnChainCommitted) } pub fn is_unverified_direct_install(&self) -> bool { @@ -313,13 +1299,6 @@ impl RegistryEntryStatus { } } -/// Missing status in legacy registry mirrors is treated as unverified. Public -/// registry writes must emit an explicit status, and old mirrors must opt in -/// with `--allow-unverified` instead of being trusted as verified by default. -fn default_registry_entry_status() -> RegistryEntryStatus { - RegistryEntryStatus::SourcePublished -} - #[derive(Debug, Clone, Copy, Default)] pub struct RegistryResolutionPolicy { pub allow_unverified: bool, @@ -333,7 +1312,10 @@ pub struct RegistryVersion { pub tag: String, pub source_hash: String, pub cellscript_version: String, - #[serde(default)] + /// Long-lived source-language semantics epoch. + pub edition: crate::CellScriptEdition, + /// Hash of the resolved source/target/assurance/ABI/schema profile. + pub compatibility_profile_hash: String, pub dependencies: BTreeMap, #[serde(default, skip_serializing_if = "Option::is_none")] pub abi_index: Option, @@ -343,9 +1325,7 @@ pub struct RegistryVersion { pub license: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub released_at: Option, - #[serde(default = "default_registry_entry_status")] pub status: RegistryEntryStatus, - #[serde(default)] pub yanked: bool, /// When the version was yanked (ISO 8601 UTC). Present only when `yanked` is true. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -375,7 +1355,7 @@ impl RegistryVersion { } match self.status { - RegistryEntryStatus::VerifiedBuild | RegistryEntryStatus::Deployed | RegistryEntryStatus::OnChainAttested => None, + RegistryEntryStatus::VerifiedBuild | RegistryEntryStatus::Deployed | RegistryEntryStatus::OnChainCommitted => None, RegistryEntryStatus::SourcePublished | RegistryEntryStatus::IndexedPending if policy.allow_unverified => None, RegistryEntryStatus::SourcePublished | RegistryEntryStatus::IndexedPending => Some("unverified"), RegistryEntryStatus::Quarantined if policy.allow_quarantined => None, @@ -407,6 +1387,17 @@ pub struct RegistryAuditInfo { impl RegistryIndex { pub const CURRENT_SCHEMA_VERSION: u32 = 1; + fn ensure_current_schema(&self) -> Result<()> { + if self.schema_version != Self::CURRENT_SCHEMA_VERSION { + return Err(CompileError::without_span(format!( + "unsupported registry.json schema_version {}; current registry contract requires schema_version {}", + self.schema_version, + Self::CURRENT_SCHEMA_VERSION, + ))); + } + Ok(()) + } + /// Read registry.json from a repository directory. pub fn read_from_repo(repo_dir: &Path) -> Result { let path = repo_dir.join("registry.json"); @@ -417,11 +1408,13 @@ impl RegistryIndex { std::fs::read_to_string(&path).map_err(|e| CompileError::without_span(format!("failed to read registry.json: {}", e)))?; let index: Self = serde_json::from_str(&content).map_err(|e| CompileError::without_span(format!("failed to parse registry.json: {}", e)))?; + index.ensure_current_schema()?; Ok(index) } /// Write registry.json to a repository directory. pub fn write_to_repo(&self, repo_dir: &Path) -> Result<()> { + self.ensure_current_schema()?; let path = repo_dir.join("registry.json"); let content = serde_json::to_string_pretty(self) .map_err(|e| CompileError::without_span(format!("failed to serialize registry.json: {}", e)))?; @@ -497,12 +1490,7 @@ impl RegistryIndex { .iter() .filter(|v| v.resolver_block_reason(policy, allow_suppressed_exact_pin).is_none()) .filter(|v| crate::package::version::satisfies(&v.version, req)) - .max_by(|a, b| { - // Compare versions numerically - let a_parts = parse_version_parts(&a.version); - let b_parts = parse_version_parts(&b.version); - compare_version_parts(&a_parts, &b_parts) - }) + .max_by(|a, b| compare_registry_versions(&a.version, &b.version)) } } @@ -750,22 +1738,13 @@ fn simple_hash(s: &str) -> u64 { hash } -fn parse_version_parts(version: &str) -> Vec { - let core = version.split_once('-').map(|(c, _)| c).unwrap_or(version); - core.split('.').filter_map(|p| p.parse().ok()).collect() -} - -fn compare_version_parts(a: &[u32], b: &[u32]) -> std::cmp::Ordering { - let max_len = a.len().max(b.len()); - for i in 0..max_len { - let av = a.get(i).unwrap_or(&0); - let bv = b.get(i).unwrap_or(&0); - match av.cmp(bv) { - std::cmp::Ordering::Equal => continue, - other => return other, - } +fn compare_registry_versions(left: &str, right: &str) -> std::cmp::Ordering { + match (semver::Version::parse(left), semver::Version::parse(right)) { + (Ok(left), Ok(right)) => left.cmp(&right), + (Ok(_), Err(_)) => std::cmp::Ordering::Greater, + (Err(_), Ok(_)) => std::cmp::Ordering::Less, + (Err(_), Err(_)) => left.cmp(right), } - std::cmp::Ordering::Equal } /// A streaming blake2b-256 hasher (simplified, using the existing ckb_blake2b256 on final content). @@ -799,14 +1778,353 @@ mod ckb_blake2b256_stream { mod tests { use super::*; + #[test] + fn ls_idl_profile_binds_exact_bytes_to_lock_executable_suffix() { + use sha2::Digest as _; + + let idl = br#"{ + "witness": [ + {"name":"signature","type":"secp256k1_sig","required":true}, + {"name":"memo","type":"bytes","required":false} + ] +}"#; + validate_ls_idl_document(idl).unwrap(); + let abi_hash = crate::hex_encode(&crate::ckb_blake2b256(idl)); + let digest = sha2::Sha256::digest(idl); + let digest_hex = crate::hex_encode(digest.as_slice()); + let contract = serde_json::json!({ + "schema": ARTIFACT_PROFILE_CONTRACT_SCHEMA, + "artifact_kind": "deployable_contract", + "profile": "ckb_executable", + "build": { + "target": "riscv64imac-unknown-none-elf", + "toolchain": "rustc 1.97.1", + "profile": "release", + "source_revision": "0123456789abcdef", + "reproducible": false + }, + "security": { "status": "review_required" }, + "ckb": { + "vm_version": "2", + "script_role": "lock", + "hash_type": "data1", + "dep_type": "code", + "abi_hash": abi_hash + }, + "interface": { + "schema": LS_IDL_INTERFACE_SCHEMA, + "format": "ls-idl", + "format_version": LS_IDL_FORMAT_VERSION, + "object_role": "abi", + "content_type": LS_IDL_CONTENT_TYPE, + "encoding": "linear-le-v0", + "commitment": { + "algorithm": "sha256", + "placement": "code-cell-data-suffix-32", + "digest": digest_hex + } + } + }); + validate_artifact_profile_contract( + "deployable_contract", + "ckb_executable", + &contract, + ArtifactContractHashes { + abi_hash: Some(&abi_hash), + abi_sha256: Some(&digest_hex), + executable_ls_idl_bound: Some(true), + ..Default::default() + }, + ) + .unwrap(); + + let error = validate_artifact_profile_contract( + "deployable_contract", + "ckb_executable", + &contract, + ArtifactContractHashes { + abi_hash: Some(&abi_hash), + abi_sha256: Some(&digest_hex), + executable_ls_idl_bound: Some(false), + ..Default::default() + }, + ) + .unwrap_err(); + assert!(error.contains("exact 32-byte suffix")); + } + + #[test] + fn ls_idl_schema_rejects_unknown_types_and_duplicate_fields() { + let unknown = br#"{"witness":[{"name":"digest","type":"[u8;32]","required":true}]}"#; + assert!(validate_ls_idl_document(unknown).unwrap_err().contains("must be one of")); + let duplicate = br#"{"witness":[{"name":"n","type":"uint8","required":true},{"name":"n","type":"uint64","required":true}]}"#; + assert!(validate_ls_idl_document(duplicate).unwrap_err().contains("unique")); + } + + #[test] + fn audited_artifact_contract_binds_the_immutable_audit_report() { + let artifact_hash = "11".repeat(32); + let abi_hash = "22".repeat(32); + let audit_report_hash = "33".repeat(32); + let contract = serde_json::json!({ + "schema": ARTIFACT_PROFILE_CONTRACT_SCHEMA, + "artifact_kind": "deployable_contract", + "profile": "ckb_executable", + "build": { + "target": "riscv64imac-unknown-none-elf", + "toolchain": "rustc 1.97.1", + "profile": "release", + "source_revision": "0123456789abcdef", + "reproducible": false + }, + "security": { + "status": "audited", + "audit_report_hash": audit_report_hash + }, + "ckb": { + "vm_version": "2", + "script_role": "type", + "hash_type": "data1", + "dep_type": "code", + "abi_hash": abi_hash + } + }); + let hashes = ArtifactContractHashes { + artifact_hash: Some(&artifact_hash), + abi_hash: Some(&abi_hash), + abi_sha256: None, + executable_ls_idl_bound: None, + build_recipe_hash: None, + audit_report_hash: Some(&audit_report_hash), + }; + + validate_artifact_profile_contract("deployable_contract", "ckb_executable", &contract, hashes).unwrap(); + + let error = validate_artifact_profile_contract( + "deployable_contract", + "ckb_executable", + &contract, + ArtifactContractHashes { audit_report_hash: None, ..hashes }, + ) + .unwrap_err(); + assert!(error.contains("security.audit_report_hash")); + } + + #[test] + fn package_manifest_hash_is_independent_of_map_insertion_order() { + let first: PackageManifest = toml::from_str( + r#"[package] +edition = "2026" +name = "demo" +version = "1.2.3" + +[dependencies] +alpha = "1" +beta = "2" + +[metadata] +left = "a" +right = "b" +"#, + ) + .unwrap(); + let second: PackageManifest = toml::from_str( + r#"[package] +edition = "2026" +name = "demo" +version = "1.2.3" + +[dependencies] +beta = "2" +alpha = "1" + +[metadata] +right = "b" +left = "a" +"#, + ) + .unwrap(); + + assert_eq!(compute_package_manifest_hash(&first).unwrap(), compute_package_manifest_hash(&second).unwrap()); + } + + #[test] + fn public_registry_states_override_publisher_claim() { + let payload: PublicRegistryPackage = serde_json::from_value(serde_json::json!({ + "schema": "cellscript-registry-artifact", + "namespace": "cellscript", + "name": "demo", + "repository": "https://github.com/cellscript/demo", + "artifact": { + "kind": "source_library", + "profile": "cellscript_source", + "consumption_mode": "dependency", + "language": "cellscript" + }, + "releases": [{ + "release": "1.2.3", + "verification_status": "verified", + "availability_status": "active", + "registry_entry": { + "schema_version": 1, + "namespace": "cellscript", + "name": "demo", + "versions": [{ + "version": "1.2.3", + "tag": "v1.2.3", + "source_hash": "source-hash", + "cellscript_version": "0.23.0", + "edition": "2026", + "compatibility_profile_hash": "profile-hash", + "dependencies": {}, + "status": "source_published", + "yanked": false + }] + }, + "immutable_bundle": { + "schema": "cellscript-registry-immutable-bundle", + "url": "https://registry.cellscript.dev/source-snapshots/cellscript/demo/1.2.3/example.json", + "snapshot_hash": format!("sha256:{}", "1".repeat(64)), + "source_hash": "source-hash", + "size_bytes": 123, + "content_type": "application/vnd.cellscript.source-snapshot+json" + } + }] + })) + .unwrap(); + + let (entry, index, snapshots) = payload.into_resolution("cellscript", "demo").unwrap(); + assert_eq!(entry.source, "https://github.com/cellscript/demo"); + assert_eq!(index.versions.len(), 1); + assert_eq!(index.versions[0].status, RegistryEntryStatus::VerifiedBuild); + assert!(!index.versions[0].yanked); + assert_eq!(snapshots["1.2.3"].source_hash, "source-hash"); + } + + #[cfg(feature = "cli")] + #[test] + fn generated_source_snapshot_materialization_checks_paths_and_file_hashes() { + use base64::Engine as _; + + let manifest = b"[package]\nname = \"demo\"\nversion = \"1.2.3\"\nnamespace = \"cellscript\"\nedition = \"2026\"\nentry = \"src/main.cell\"\n"; + let source = b"script Demo {}\n"; + let file = |path: &str, content: &[u8]| { + serde_json::json!({ + "path": path, + "blake2b256": crate::hex_encode(&crate::ckb_blake2b256(content)), + "content_base64": base64::engine::general_purpose::STANDARD.encode(content), + }) + }; + let snapshot = serde_json::to_vec(&serde_json::json!({ + "schema": "cellscript-source-snapshot-v1", + "package": { "namespace": "cellscript", "name": "demo", "version": "1.2.3" }, + "files": [file("Cell.toml", manifest), file("src/main.cell", source)], + })) + .unwrap(); + let root = tempfile::tempdir().unwrap(); + let destination = root.path().join("valid"); + unpack_generated_source_snapshot(&snapshot, &destination, "cellscript", "demo", "1.2.3").unwrap(); + assert_eq!(std::fs::read(destination.join("src/main.cell")).unwrap(), source); + + let unsafe_snapshot = serde_json::to_vec(&serde_json::json!({ + "schema": "cellscript-source-snapshot-v1", + "package": { "namespace": "cellscript", "name": "demo", "version": "1.2.3" }, + "files": [file("../Cell.toml", manifest)], + })) + .unwrap(); + let error = unpack_generated_source_snapshot(&unsafe_snapshot, &root.path().join("unsafe"), "cellscript", "demo", "1.2.3") + .unwrap_err(); + assert!(error.to_string().contains("unsafe")); + assert!(!root.path().join("Cell.toml").exists()); + } + + #[cfg(feature = "cli")] + #[test] + fn public_snapshot_descriptor_rejects_opaque_archives() { + let snapshot = PublicRegistrySourceSnapshot { + schema: "cellscript-registry-immutable-bundle".to_string(), + url: "https://registry.cellscript.dev/source-snapshots/demo.tar".to_string(), + snapshot_hash: format!("sha256:{}", "1".repeat(64)), + source_hash: "source-hash".to_string(), + size_bytes: 42, + content_type: "application/x-tar".to_string(), + }; + let error = validate_public_source_snapshot_descriptor(&snapshot, "source-hash").unwrap_err(); + assert!(error.to_string().contains("does not support source snapshot content type")); + } + + #[cfg(feature = "cli")] + #[test] + fn public_source_snapshot_download_is_hash_bound_and_materialized_without_git() { + use base64::Engine as _; + use std::io::Read as _; + + let source_root = tempfile::tempdir().unwrap(); + std::fs::create_dir(source_root.path().join("src")).unwrap(); + let manifest = b"[package]\nedition = \"2026\"\nname = \"demo\"\nnamespace = \"cellscript\"\nversion = \"1.2.3\"\nentry = \"src/main.cell\"\n"; + let source = b"script Demo {}\n"; + std::fs::write(source_root.path().join("Cell.toml"), manifest).unwrap(); + std::fs::write(source_root.path().join("src/main.cell"), source).unwrap(); + let source_hash = compute_source_hash(source_root.path()).unwrap(); + let snapshot_file = |path: &str, content: &[u8]| { + serde_json::json!({ + "path": path, + "blake2b256": crate::hex_encode(&crate::ckb_blake2b256(content)), + "content_base64": base64::engine::general_purpose::STANDARD.encode(content), + }) + }; + let bytes = serde_json::to_vec(&serde_json::json!({ + "schema": "cellscript-source-snapshot-v1", + "package": { "namespace": "cellscript", "name": "demo", "version": "1.2.3" }, + "files": [snapshot_file("Cell.toml", manifest), snapshot_file("src/main.cell", source)], + })) + .unwrap(); + let snapshot_hash = format!("sha256:{}", hex::encode(Sha256::digest(&bytes))); + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let address = listener.local_addr().unwrap(); + let response_bytes = bytes.clone(); + let server = std::thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let mut request = [0_u8; 4096]; + let _ = stream.read(&mut request).unwrap(); + let headers = format!( + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n", + response_bytes.len() + ); + std::io::Write::write_all(&mut stream, headers.as_bytes()).unwrap(); + std::io::Write::write_all(&mut stream, &response_bytes).unwrap(); + }); + let descriptor = PublicRegistrySourceSnapshot { + schema: "cellscript-registry-immutable-bundle".to_string(), + url: format!("http://{address}/snapshot.json"), + snapshot_hash: snapshot_hash.clone(), + source_hash: source_hash.clone(), + size_bytes: bytes.len() as u64, + content_type: "application/vnd.cellscript.source-snapshot+json".to_string(), + }; + let cache = tempfile::tempdir().unwrap(); + let materialized = + materialize_public_source_snapshot(&descriptor, cache.path(), "cellscript", "demo", "1.2.3", &source_hash).unwrap(); + server.join().unwrap(); + assert_eq!(compute_source_hash(&materialized).unwrap(), source_hash); + assert_eq!(std::fs::read(materialized.join("src/main.cell")).unwrap(), source); + assert!(materialized + .file_name() + .unwrap() + .to_string_lossy() + .contains(snapshot_hash.trim_start_matches("sha256:").get(..16).unwrap())); + } + #[test] fn registry_index_find_matching_version() { let index = RegistryIndex { - schema_version: 1, + schema_version: RegistryIndex::CURRENT_SCHEMA_VERSION, name: "token".to_string(), namespace: "cellscript".to_string(), versions: vec![ RegistryVersion { + edition: crate::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.1.0".to_string(), tag: "v0.1.0".to_string(), source_hash: "hash1".to_string(), @@ -824,6 +2142,8 @@ mod tests { audit: None, }, RegistryVersion { + edition: crate::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.3.2".to_string(), tag: "v0.3.2".to_string(), source_hash: "hash2".to_string(), @@ -841,6 +2161,8 @@ mod tests { audit: None, }, RegistryVersion { + edition: crate::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.3.0".to_string(), tag: "v0.3.0".to_string(), source_hash: "hash3".to_string(), @@ -876,10 +2198,12 @@ mod tests { #[test] fn registry_index_skips_yanked_versions() { let index = RegistryIndex { - schema_version: 1, + schema_version: RegistryIndex::CURRENT_SCHEMA_VERSION, name: "pkg".to_string(), namespace: "ns".to_string(), versions: vec![RegistryVersion { + edition: crate::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "1.0.0".to_string(), tag: "v1.0.0".to_string(), source_hash: "h1".to_string(), @@ -910,6 +2234,8 @@ mod tests { // A yanked version with full Phase 2 metadata must survive JSON // serialization and also omit cleanly when the fields are absent. let yanked = RegistryVersion { + edition: crate::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "1.2.0".to_string(), tag: "v1.2.0".to_string(), source_hash: "h".to_string(), @@ -935,7 +2261,14 @@ mod tests { // The optional yank fields are omitted from JSON when absent, so older // registry.json files without them still parse (backward compatible). - let clean = RegistryVersion { yanked_at: None, yanked_reason: None, replaced_by: None, ..yanked }; + let clean = RegistryVersion { + edition: crate::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), + yanked_at: None, + yanked_reason: None, + replaced_by: None, + ..yanked + }; let clean_json = serde_json::to_string(&clean).unwrap(); assert!(!clean_json.contains("yanked_at")); assert!(!clean_json.contains("yanked_reason")); @@ -950,6 +2283,8 @@ mod tests { namespace: "cellscript".to_string(), versions: vec![ RegistryVersion { + edition: crate::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.1.0".to_string(), tag: "v0.1.0".to_string(), source_hash: "hash-v010".to_string(), @@ -967,6 +2302,8 @@ mod tests { audit: None, }, RegistryVersion { + edition: crate::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.2.0".to_string(), tag: "v0.2.0".to_string(), source_hash: "hash-v020".to_string(), @@ -984,6 +2321,8 @@ mod tests { audit: None, }, RegistryVersion { + edition: crate::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.3.0".to_string(), tag: "v0.3.0".to_string(), source_hash: "hash-v030".to_string(), @@ -1020,32 +2359,58 @@ mod tests { } #[test] - fn missing_registry_status_is_unverified_by_default() { - let json = r#"{ - "schema_version": 1, - "name": "amm", - "namespace": "cellscript", - "versions": [ - { - "version": "1.0.0", - "tag": "v1.0.0", - "source_hash": "hash-v100", - "cellscript_version": "0.20.0", - "dependencies": {}, - "yanked": false - } - ] - }"#; - let index: RegistryIndex = serde_json::from_str(json).unwrap(); - assert_eq!(index.versions[0].status, RegistryEntryStatus::SourcePublished); - assert!( - index.find_matching_version_for_resolution("*", RegistryResolutionPolicy::default()).is_none(), - "entries missing status must not be selected by the default resolver", - ); - let selected = index - .find_matching_version_for_resolution("*", RegistryResolutionPolicy { allow_unverified: true, allow_quarantined: false }) - .expect("explicit unverified install may select a legacy mirror"); - assert_eq!(selected.version, "1.0.0"); + fn registry_index_rejects_missing_required_version_fields() { + let complete = serde_json::json!({ + "schema_version": 1, + "name": "amm", + "namespace": "cellscript", + "versions": [{ + "version": "1.0.0", + "tag": "v1.0.0", + "source_hash": "hash-v100", + "cellscript_version": "0.20.0", + "edition": "2026", + "compatibility_profile_hash": "test-compatibility-profile", + "dependencies": {}, + "status": "source_published", + "yanked": false + }] + }); + + for field in ["dependencies", "status", "yanked"] { + let mut incomplete = complete.clone(); + incomplete["versions"][0].as_object_mut().unwrap().remove(field); + let error = serde_json::from_value::(incomplete).unwrap_err(); + assert!(error.to_string().contains(&format!("missing field `{field}`"))); + } + } + + #[test] + fn registry_index_rejects_unknown_schema() { + let dir = tempfile::tempdir().unwrap(); + std::fs::write( + dir.path().join("registry.json"), + r#"{ + "schema_version": 2, + "name": "amm", + "namespace": "cellscript", + "versions": [{ + "version": "1.0.0", + "tag": "v1.0.0", + "source_hash": "hash-v100", + "cellscript_version": "0.23.0", + "edition": "2026", + "compatibility_profile_hash": "test-compatibility-profile", + "dependencies": {}, + "status": "source_published", + "yanked": false + }] + }"#, + ) + .unwrap(); + + let error = RegistryIndex::read_from_repo(dir.path()).unwrap_err(); + assert!(error.to_string().contains("current registry contract requires schema_version 1")); } #[test] @@ -1066,10 +2431,12 @@ mod tests { #[test] fn registry_index_serialization_round_trip() { let index = RegistryIndex { - schema_version: 1, + schema_version: RegistryIndex::CURRENT_SCHEMA_VERSION, name: "amm_pool".to_string(), namespace: "cellscript".to_string(), versions: vec![RegistryVersion { + edition: crate::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "1.2.0".to_string(), tag: "v1.2.0".to_string(), source_hash: "blake2b:0xabcd".to_string(), diff --git a/src/proof_plan/mod.rs b/src/proof_plan/mod.rs index 45151725..502ee1ec 100644 --- a/src/proof_plan/mod.rs +++ b/src/proof_plan/mod.rs @@ -1108,11 +1108,11 @@ fn reads_for_source(source: &str) -> &'static [&'static str] { fn reads_for_obligation(obligation: &VerifierObligationMetadata, body_reads: &[String]) -> Vec { let mut reads = body_reads.to_vec(); - if obligation.category == "cell-access" { - if let Some(source) = obligation.feature.split(':').nth(1).and_then(|source| source.split('#').next()) { - for read in reads_for_source(source) { - reads.push(read.to_string()); - } + if obligation.category == "cell-access" + && let Some(source) = obligation.feature.split(':').nth(1).and_then(|source| source.split('#').next()) + { + for read in reads_for_source(source) { + reads.push(read.to_string()); } } if obligation.detail.contains("witness") || obligation.feature.contains("witness") { diff --git a/src/proof_plan/soundness.rs b/src/proof_plan/soundness.rs index 78eaa255..623aaec2 100644 --- a/src/proof_plan/soundness.rs +++ b/src/proof_plan/soundness.rs @@ -317,17 +317,17 @@ fn check_plan_record(plan: &ProofPlanMetadata, strict: bool, issues: &mut Vec start and a one-based line", - ); - } + if let Some(span) = &plan.source_span + && (span.end <= span.start || span.line == 0) + { + push_issue( + issues, + "error", + "PP0211", + &plan.origin, + &plan.feature, + "ProofPlan source span must have end > start and a one-based line", + ); } if plan.on_chain_checked @@ -511,8 +511,3 @@ fn push_issue(issues: &mut Vec, severity: &str, code: & message: message.to_string(), }); } - -#[allow(dead_code)] -fn _obligation_debug_key(obligation: &VerifierObligationMetadata) -> String { - obligation_key(&obligation.scope, &obligation.category, &obligation.feature, &obligation.status, &obligation.detail) -} diff --git a/src/resolve/mod.rs b/src/resolve/mod.rs index 5b66eb12..49a4d574 100644 --- a/src/resolve/mod.rs +++ b/src/resolve/mod.rs @@ -179,10 +179,10 @@ impl ModuleResolver { return Some(ty.clone()); } - if let Some(full_path) = table.imported.get(name) { - if let Some((target_module, symbol)) = full_path.rsplit_once("::") { - return self.symbol_tables.get(target_module).and_then(|target_table| target_table.types.get(symbol).cloned()); - } + if let Some(full_path) = table.imported.get(name) + && let Some((target_module, symbol)) = full_path.rsplit_once("::") + { + return self.symbol_tables.get(target_module).and_then(|target_table| target_table.types.get(symbol).cloned()); } } @@ -206,12 +206,11 @@ impl ModuleResolver { return Some((module.to_string(), func.clone())); } - if let Some(full_path) = table.imported.get(name) { - if let Some((target_module, symbol)) = full_path.rsplit_once("::") { - if let Some(target_table) = self.symbol_tables.get(target_module) { - return target_table.functions.get(symbol).cloned().map(|function| (target_module.to_string(), function)); - } - } + if let Some(full_path) = table.imported.get(name) + && let Some((target_module, symbol)) = full_path.rsplit_once("::") + && let Some(target_table) = self.symbol_tables.get(target_module) + { + return target_table.functions.get(symbol).cloned().map(|function| (target_module.to_string(), function)); } } @@ -235,12 +234,11 @@ impl ModuleResolver { return Some((module.to_string(), constant.clone())); } - if let Some(full_path) = table.imported.get(name) { - if let Some((target_module, symbol)) = full_path.rsplit_once("::") { - if let Some(target_table) = self.symbol_tables.get(target_module) { - return target_table.constants.get(symbol).cloned().map(|constant| (target_module.to_string(), constant)); - } - } + if let Some(full_path) = table.imported.get(name) + && let Some((target_module, symbol)) = full_path.rsplit_once("::") + && let Some(target_table) = self.symbol_tables.get(target_module) + { + return target_table.constants.get(symbol).cloned().map(|constant| (target_module.to_string(), constant)); } } diff --git a/src/simulate.rs b/src/simulate.rs index d8aedd17..07a5b8a4 100644 --- a/src/simulate.rs +++ b/src/simulate.rs @@ -123,6 +123,7 @@ pub enum SimulateError { UndefinedFunction { name: String }, TypeError { expected: String, got: String }, Unsupported { description: String }, + RuntimeError { code: u64, name: String }, } impl std::fmt::Display for SimulateError { @@ -133,6 +134,7 @@ impl std::fmt::Display for SimulateError { SimulateError::UndefinedFunction { name } => write!(f, "undefined function '{}'", name), SimulateError::TypeError { expected, got } => write!(f, "type error: expected {}, got {}", expected, got), SimulateError::Unsupported { description } => write!(f, "unsupported: {}", description), + SimulateError::RuntimeError { code, name } => write!(f, "CellScript runtime error {} ({})", code, name), } } } @@ -160,7 +162,15 @@ impl SimulateInterpreter { } pub fn simulate_action(&mut self, name: &str, args: &[SimValue]) -> Result { - let key = format!("action::{}", name); + self.simulate_entry("action", name, args) + } + + pub fn simulate_lock(&mut self, name: &str, args: &[SimValue]) -> Result { + self.simulate_entry("lock", name, args) + } + + fn simulate_entry(&mut self, kind: &str, name: &str, args: &[SimValue]) -> Result { + let key = format!("{}::{}", kind, name); let (params, body) = self.functions.get(&key).cloned().ok_or(SimulateError::UndefinedFunction { name: key })?; for (param, arg) in params.iter().zip(args.iter()) { @@ -381,7 +391,8 @@ impl SimulateInterpreter { let msg = self.eval_expr(&assert.message)?; self.trace.push(TraceEvent::Assert { condition: cond.clone(), message: msg.to_string() }); if !self.is_truthy(&cond) { - Ok(SimValue::Simulated { ty: "assert_failed".to_string(), description: msg.to_string() }) + let error = crate::runtime_errors::CellScriptRuntimeError::AssertionFailed; + Err(SimulateError::RuntimeError { code: error.code(), name: error.name().to_string() }) } else { Ok(SimValue::Unit) } @@ -390,7 +401,8 @@ impl SimulateInterpreter { let cond = self.eval_expr(&require.condition)?; self.trace.push(TraceEvent::Assert { condition: cond.clone(), message: "require failed".to_string() }); if !self.is_truthy(&cond) { - Ok(SimValue::Simulated { ty: "require_failed".to_string(), description: "require failed".to_string() }) + let error = crate::runtime_errors::CellScriptRuntimeError::AssertionFailed; + Err(SimulateError::RuntimeError { code: error.code(), name: error.name().to_string() }) } else { Ok(SimValue::Bool(true)) } diff --git a/src/types/mod.rs b/src/types/mod.rs index 4c8ea442..0b229056 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -467,10 +467,10 @@ impl<'a> TypeChecker<'a> { let mut seen_symbols = HashSet::new(); let mut seen_type_ids = HashMap::new(); for item in &module.items { - if let Some((symbol, span)) = item_symbol_name_and_span(item) { - if !seen_symbols.insert(symbol.to_string()) { - diagnostics.push(CompileError::new(format!("duplicate symbol '{}'", symbol), span)); - } + if let Some((symbol, span)) = item_symbol_name_and_span(item) + && !seen_symbols.insert(symbol.to_string()) + { + diagnostics.push(CompileError::new(format!("duplicate symbol '{}'", symbol), span)); } match item { Item::Const(const_def) => { @@ -828,35 +828,29 @@ impl<'a> TypeChecker<'a> { } fn flow_states_for_decl(&self, machine: &FlowDef, field_ty: &Type) -> Result<(Vec, Option)> { - if let Type::Named(enum_name) = field_ty { - if let Some(variants) = self.resolve_enum_variants(enum_name) { - if variants.iter().any(|variant| self.enum_variant_has_payload(enum_name, variant)) { - return Err(CompileError::new( - format!( - "flow field '{}.{}' enum '{}' must not have payload variants", - machine.target.base, machine.target.field, enum_name - ), - machine.target.span, - )); - } - for transition in &machine.transitions { - self.canonical_state_name_for_flow( - &machine.target.base, - Some(enum_name), - &variants, - &transition.from, - transition.span, - )?; - self.canonical_state_name_for_flow( - &machine.target.base, - Some(enum_name), - &variants, - &transition.to, - transition.span, - )?; - } - return Ok((variants, Some(enum_name.clone()))); + if let Type::Named(enum_name) = field_ty + && let Some(variants) = self.resolve_enum_variants(enum_name) + { + if variants.iter().any(|variant| self.enum_variant_has_payload(enum_name, variant)) { + return Err(CompileError::new( + format!( + "flow field '{}.{}' enum '{}' must not have payload variants", + machine.target.base, machine.target.field, enum_name + ), + machine.target.span, + )); } + for transition in &machine.transitions { + self.canonical_state_name_for_flow( + &machine.target.base, + Some(enum_name), + &variants, + &transition.from, + transition.span, + )?; + self.canonical_state_name_for_flow(&machine.target.base, Some(enum_name), &variants, &transition.to, transition.span)?; + } + return Ok((variants, Some(enum_name.clone()))); } if !is_state_storage_type(field_ty) { @@ -1002,10 +996,10 @@ impl<'a> TypeChecker<'a> { if !self.is_bool_type(&ty) { return Err(CompileError::new(format!("validity predicate for '{}' must be boolean", type_name), require.span)); } - if let Some(message) = &require.message { - if !matches!(message.as_ref(), Expr::String(_)) { - return Err(CompileError::new("validity require message must be a string literal", expr_span(message))); - } + if let Some(message) = &require.message + && !matches!(message.as_ref(), Expr::String(_)) + { + return Err(CompileError::new("validity require message must be a string literal", expr_span(message))); } } Ok(()) @@ -2072,27 +2066,27 @@ impl<'a> TypeChecker<'a> { for state_edge in &action.state_edges { let from_path = &state_edge.path; let to_path = &state_edge.to_path; - if let Some(previous_output) = lineage_inputs.insert(from_path.base.clone(), to_path.base.clone()) { - if previous_output != to_path.base { - return Err(CompileError::new( - format!( - "state transition binding '{}' points to both '{}' and '{}'; split/merge lineage is not supported", - from_path.base, previous_output, to_path.base - ), - state_edge.span, - )); - } + if let Some(previous_output) = lineage_inputs.insert(from_path.base.clone(), to_path.base.clone()) + && previous_output != to_path.base + { + return Err(CompileError::new( + format!( + "state transition binding '{}' points to both '{}' and '{}'; split/merge lineage is not supported", + from_path.base, previous_output, to_path.base + ), + state_edge.span, + )); } - if let Some(previous_input) = lineage_outputs.insert(to_path.base.clone(), from_path.base.clone()) { - if previous_input != from_path.base { - return Err(CompileError::new( - format!( - "state transition output '{}' is reached from both '{}' and '{}'; split/merge lineage is not supported", - to_path.base, previous_input, from_path.base - ), - state_edge.span, - )); - } + if let Some(previous_input) = lineage_outputs.insert(to_path.base.clone(), from_path.base.clone()) + && previous_input != from_path.base + { + return Err(CompileError::new( + format!( + "state transition output '{}' is reached from both '{}' and '{}'; split/merge lineage is not supported", + to_path.base, previous_input, from_path.base + ), + state_edge.span, + )); } } @@ -2384,35 +2378,32 @@ impl<'a> TypeChecker<'a> { span, )); } - if let Type::Named(enum_name) = return_type { - if let Some(variants) = self.resolve_enum_variant_fields(enum_name) { - let has_payload = variants.values().any(|fields| !fields.is_empty()); - if has_payload && callable_kind != "function" { + if let Type::Named(enum_name) = return_type + && let Some(variants) = self.resolve_enum_variant_fields(enum_name) + { + let has_payload = variants.values().any(|fields| !fields.is_empty()); + if has_payload && callable_kind != "function" { + return Err(CompileError::new( + format!( + "{} '{}' cannot return a payload enum across an entry ABI; payload enum returns are pure-helper ABI values", + callable_kind, callable_name + ), + span, + )); + } + if has_payload { + let width = self.payload_type_runtime_width(return_type, &mut HashSet::new()).ok_or_else(|| { + CompileError::new(format!("function '{}' payload enum return has no fixed-width ABI layout", callable_name), span) + })?; + if width > 16 { return Err(CompileError::new( format!( - "{} '{}' cannot return a payload enum across an entry ABI; payload enum returns are pure-helper ABI values", - callable_kind, callable_name + "function '{}' payload enum return is {} bytes; 0.22 register-pair return ABI supports at most 16 bytes", + callable_name, width ), span, )); } - if has_payload { - let width = self.payload_type_runtime_width(return_type, &mut HashSet::new()).ok_or_else(|| { - CompileError::new( - format!("function '{}' payload enum return has no fixed-width ABI layout", callable_name), - span, - ) - })?; - if width > 16 { - return Err(CompileError::new( - format!( - "function '{}' payload enum return is {} bytes; 0.22 register-pair return ABI supports at most 16 bytes", - callable_name, width - ), - span, - )); - } - } } } Ok(()) @@ -2642,13 +2633,13 @@ impl<'a> TypeChecker<'a> { param.span, )); } - if let Some(collection) = parse_bounded_collection_type(¶m.ty) { - if collection.kind != BoundedCollectionKind::List { - return Err(CompileError::new( - format!("witness parameter '{}' may use BoundedList, not BoundedCellSet", param.name), - param.span, - )); - } + if let Some(collection) = parse_bounded_collection_type(¶m.ty) + && collection.kind != BoundedCollectionKind::List + { + return Err(CompileError::new( + format!("witness parameter '{}' may use BoundedList, not BoundedCellSet", param.name), + param.span, + )); } } else if callable_kind != "lock" { return Err(CompileError::new( @@ -2784,9 +2775,10 @@ impl<'a> TypeChecker<'a> { param.span, )); } - if let Type::Ref(inner) | Type::MutRef(inner) = ¶m.ty { - if self.reference_target_is_cell_backed_aggregate(inner) { - return Err(CompileError::new( + if let Type::Ref(inner) | Type::MutRef(inner) = ¶m.ty + && self.reference_target_is_cell_backed_aggregate(inner) + { + return Err(CompileError::new( format!( "parameter '{}' in {} '{}' cannot use reference to aggregate containing cell-backed values {}; use a direct '&T' helper view or named action outputs instead", param.name, @@ -2796,7 +2788,6 @@ impl<'a> TypeChecker<'a> { ), param.span, )); - } } Ok(()) } @@ -3197,10 +3188,10 @@ impl<'a> TypeChecker<'a> { return; } - if let BindingPattern::Name(name) = &let_stmt.pattern { - if let Some(fd_key) = self.spawn_ipc_fd_key(&let_stmt.value, state) { - self.register_spawn_ipc_fd_alias(name, fd_key, state); - } + if let BindingPattern::Name(name) = &let_stmt.pattern + && let Some(fd_key) = self.spawn_ipc_fd_key(&let_stmt.value, state) + { + self.register_spawn_ipc_fd_alias(name, fd_key, state); } } @@ -3436,18 +3427,18 @@ impl<'a> TypeChecker<'a> { if let Some(declared_ty) = &let_stmt.ty { return self.infer_expr_with_expected_type(env, &let_stmt.value, declared_ty, let_stmt.span); } - if let Expr::Array(elems) = &let_stmt.value { - if elems.is_empty() { - return match &let_stmt.ty { - Some(declared @ Type::Array(_, 0)) => Ok(declared.clone()), - Some(Type::Array(_, size)) => Err(CompileError::new( - format!("empty array literal cannot initialize non-empty array of length {}", size), - let_stmt.span, - )), - Some(_) => Err(CompileError::new("empty array literal requires an array type annotation", let_stmt.span)), - None => Err(CompileError::new("empty array literal requires an explicit array type annotation", let_stmt.span)), - }; - } + if let Expr::Array(elems) = &let_stmt.value + && elems.is_empty() + { + return match &let_stmt.ty { + Some(declared @ Type::Array(_, 0)) => Ok(declared.clone()), + Some(Type::Array(_, size)) => Err(CompileError::new( + format!("empty array literal cannot initialize non-empty array of length {}", size), + let_stmt.span, + )), + Some(_) => Err(CompileError::new("empty array literal requires an array type annotation", let_stmt.span)), + None => Err(CompileError::new("empty array literal requires an explicit array type annotation", let_stmt.span)), + }; } self.infer_expr(env, &let_stmt.value) } @@ -3505,28 +3496,28 @@ impl<'a> TypeChecker<'a> { expected_ty: &Type, span: Span, ) -> Result { - if let Type::Named(name) = expected_ty { - if let Some(item_ty) = self.parse_named_collection_item_type(name) { - for elem in elems { - let actual_ty = self.infer_expr_with_expected_type(env, elem, &item_ty, expr_span(elem))?; - if self.type_contains_reference(&actual_ty) { - return Err(CompileError::new( - format!( - "Vec literal cannot store reference type {}; Vec values must use owned non-reference items", - type_repr(&actual_ty) - ), - expr_span(elem), - )); - } - if !self.types_equal(&actual_ty, &item_ty) { - return Err(CompileError::new( - format!("Vec literal type mismatch: expected {:?}, found {:?}", item_ty, actual_ty), - expr_span(elem), - )); - } + if let Type::Named(name) = expected_ty + && let Some(item_ty) = self.parse_named_collection_item_type(name) + { + for elem in elems { + let actual_ty = self.infer_expr_with_expected_type(env, elem, &item_ty, expr_span(elem))?; + if self.type_contains_reference(&actual_ty) { + return Err(CompileError::new( + format!( + "Vec literal cannot store reference type {}; Vec values must use owned non-reference items", + type_repr(&actual_ty) + ), + expr_span(elem), + )); + } + if !self.types_equal(&actual_ty, &item_ty) { + return Err(CompileError::new( + format!("Vec literal type mismatch: expected {:?}, found {:?}", item_ty, actual_ty), + expr_span(elem), + )); } - return Ok(expected_ty.clone()); } + return Ok(expected_ty.clone()); } if let Type::Array(item_ty, expected_len) = expected_ty { @@ -3788,10 +3779,10 @@ impl<'a> TypeChecker<'a> { if !self.is_bool_type(&cond_ty) { return Err(CompileError::new("require condition must be boolean", require_expr.span)); } - if let Some(message) = &require_expr.message { - if !matches!(message.as_ref(), Expr::String(_)) { - return Err(CompileError::new("require message must be a string literal", expr_span(message))); - } + if let Some(message) = &require_expr.message + && !matches!(message.as_ref(), Expr::String(_)) + { + return Err(CompileError::new("require message must be a string literal", expr_span(message))); } Ok(Type::Bool) } @@ -4891,15 +4882,15 @@ impl<'a> TypeChecker<'a> { if self.types_equal(left_ty, right_ty) { return Ok(true); } - if let Expr::Integer(value) = left { - if Self::is_integer_literal_target_type(right_ty) { - return Self::integer_literal_type_for_expected(*value, right_ty, span).map(|ty| ty.is_some()); - } + if let Expr::Integer(value) = left + && Self::is_integer_literal_target_type(right_ty) + { + return Self::integer_literal_type_for_expected(*value, right_ty, span).map(|ty| ty.is_some()); } - if let Expr::Integer(value) = right { - if Self::is_integer_literal_target_type(left_ty) { - return Self::integer_literal_type_for_expected(*value, left_ty, span).map(|ty| ty.is_some()); - } + if let Expr::Integer(value) = right + && Self::is_integer_literal_target_type(left_ty) + { + return Self::integer_literal_type_for_expected(*value, left_ty, span).map(|ty| ty.is_some()); } Ok(false) } @@ -4908,17 +4899,17 @@ impl<'a> TypeChecker<'a> { if self.types_equal(left_ty, right_ty) { return Ok(left_ty.clone()); } - if let Expr::Integer(value) = left { - if Self::is_integer_literal_target_type(right_ty) { - Self::integer_literal_type_for_expected(*value, right_ty, span)?; - return Ok(right_ty.clone()); - } + if let Expr::Integer(value) = left + && Self::is_integer_literal_target_type(right_ty) + { + Self::integer_literal_type_for_expected(*value, right_ty, span)?; + return Ok(right_ty.clone()); } - if let Expr::Integer(value) = right { - if Self::is_integer_literal_target_type(left_ty) { - Self::integer_literal_type_for_expected(*value, left_ty, span)?; - return Ok(left_ty.clone()); - } + if let Expr::Integer(value) = right + && Self::is_integer_literal_target_type(left_ty) + { + Self::integer_literal_type_for_expected(*value, left_ty, span)?; + return Ok(left_ty.clone()); } if let Some(ty) = Self::unsigned_widening_result_type(left_ty, right_ty) { return Ok(ty); @@ -5111,10 +5102,10 @@ impl<'a> TypeChecker<'a> { match expr { Expr::Identifier(name) => { self.reject_active_borrow_root_lifecycle(name, "move", expr_span(expr))?; - if let Some(ty) = env.lookup(name).cloned() { - if self.is_linear_type(&ty) { - env.consume(name)?; - } + if let Some(ty) = env.lookup(name).cloned() + && self.is_linear_type(&ty) + { + env.consume(name)?; } Ok(()) } @@ -5265,18 +5256,17 @@ impl<'a> TypeChecker<'a> { fn reject_stored_linear_reference_alias(&self, env: &TypeEnv, expr: &Expr, span: Span) -> Result<()> { match expr { Expr::Unary(unary) if matches!(unary.op, UnaryOp::Ref) => { - if let Some(root) = assignment_root_name(&unary.expr) { - if let Some(root_ty) = env.lookup(root) { - if self.is_linear_type(root_ty) { - return Err(CompileError::new( + if let Some(root) = assignment_root_name(&unary.expr) + && let Some(root_ty) = env.lookup(root) + && self.is_linear_type(root_ty) + { + return Err(CompileError::new( format!( "local binding cannot store a read-only reference rooted at linear/resource value '{}'; pass the reference directly to a helper call", root ), span, )); - } - } } Ok(()) } @@ -5322,13 +5312,13 @@ impl<'a> TypeChecker<'a> { } fn reject_unrooted_linear_reference_type(&self, ty: &Type, span: Span) -> Result<()> { - if let Type::Ref(inner) = ty { - if self.is_linear_type(inner) { - return Err(CompileError::new( + if let Type::Ref(inner) = ty + && self.is_linear_type(inner) + { + return Err(CompileError::new( "local binding cannot store a read-only reference to a linear/resource value; bind the cell value itself or pass the reference directly", span, )); - } } Ok(()) } @@ -5618,10 +5608,10 @@ impl<'a> TypeChecker<'a> { _ => Err(CompileError::new(format!("unknown ScriptArgs field '{}'; expected len or is_empty", field), span)), }; } - if let Some(fields) = self.resolve_named_type_fields(base_name) { - if let Some(field_ty) = fields.get(field) { - return Ok(field_ty.clone()); - } + if let Some(fields) = self.resolve_named_type_fields(base_name) + && let Some(field_ty) = fields.get(field) + { + return Ok(field_ty.clone()); } Err(CompileError::new(format!("unknown field '{}' on type '{}'", field, base_name), span)) } @@ -5870,13 +5860,13 @@ impl<'a> TypeChecker<'a> { return Ok(self.function_return_type(&function).unwrap_or(Type::Unit)); } if let Some((prefix, suffix)) = name.rsplit_once("::") { - if self.current_module.as_deref() == Some(prefix) { - if let Some(signature) = self.functions.get(suffix).cloned() { - self.validate_call_allowed(name, signature.kind, signature.effect, call.span)?; - self.validate_borrow_call(name, signature.kind, signature.effect, &signature.params, call)?; - self.validate_call_args(name, &signature.params, arg_types, &call.args, call.span)?; - return Ok(signature.return_type.unwrap_or(Type::Unit)); - } + if self.current_module.as_deref() == Some(prefix) + && let Some(signature) = self.functions.get(suffix).cloned() + { + self.validate_call_allowed(name, signature.kind, signature.effect, call.span)?; + self.validate_borrow_call(name, signature.kind, signature.effect, &signature.params, call)?; + self.validate_call_args(name, &signature.params, arg_types, &call.args, call.span)?; + return Ok(signature.return_type.unwrap_or(Type::Unit)); } self.reject_borrow_view_in_builtin_call(name, call)?; return Ok(match (prefix, suffix) { @@ -7206,18 +7196,18 @@ impl<'a> TypeChecker<'a> { Span::default(), )); } - if base_name == "Vec" && name.contains('<') { - if let Some(item_ty) = self.parse_named_collection_item_type(name) { - if Self::base_type_name(&item_ty).and_then(|item| self.resolve_cell_type_kind(item)).is_some() { - return Err(CompileError::new( - format!( - "type '{}' cannot store a cell-backed resource; use a source-aware BoundedCellSet with explicit ownership", - name - ), - Span::default(), - )); - } - } + if base_name == "Vec" + && name.contains('<') + && let Some(item_ty) = self.parse_named_collection_item_type(name) + && Self::base_type_name(&item_ty).and_then(|item| self.resolve_cell_type_kind(item)).is_some() + { + return Err(CompileError::new( + format!( + "type '{}' cannot store a cell-backed resource; use a source-aware BoundedCellSet with explicit ownership", + name + ), + Span::default(), + )); } if base_name == "Vec" && name.contains('<') && self.named_type_contains_reference(name) { return Err(CompileError::new( @@ -7528,10 +7518,10 @@ impl<'a> TypeChecker<'a> { return Ok(()); } - if let Expr::Integer(value) = expr { - if Self::integer_literal_fits_expected_type(*value, target) { - return Ok(()); - } + if let Expr::Integer(value) = expr + && Self::integer_literal_fits_expected_type(*value, target) + { + return Ok(()); } if self.is_numeric_type(source) && self.is_numeric_type(target) { @@ -8008,10 +7998,10 @@ fn action_param_owned_named_type<'a>(action: &'a ActionDef, name: &str) -> Optio } fn action_param_output_named_type<'a>(action: &'a ActionDef, name: &str) -> Option<&'a str> { - if let Some(output) = action.outputs.iter().find(|output| output.name == name) { - if let Type::Named(type_name) = &output.ty { - return Some(type_name.split('<').next().unwrap_or(type_name.as_str())); - } + if let Some(output) = action.outputs.iter().find(|output| output.name == name) + && let Type::Named(type_name) = &output.ty + { + return Some(type_name.split('<').next().unwrap_or(type_name.as_str())); } None } @@ -8225,10 +8215,10 @@ fn collect_consumed_bindings_from_expr(expr: &Expr, bindings: &mut HashSet { - if !call.args.is_empty() { - if let Expr::Identifier(name) = &call.args[0] { - bindings.insert(name.clone()); - } + if !call.args.is_empty() + && let Expr::Identifier(name) = &call.args[0] + { + bindings.insert(name.clone()); } } _ => {} @@ -8566,10 +8556,10 @@ action main(a: TokenA) -> u64 { for stmt in &action.body { checker.check_stmt(&mut env, stmt).unwrap(); - if let Stmt::Let(let_stmt) = stmt { - if matches!(&let_stmt.pattern, BindingPattern::Tuple(_)) { - break; - } + if let Stmt::Let(let_stmt) = stmt + && matches!(&let_stmt.pattern, BindingPattern::Tuple(_)) + { + break; } } diff --git a/src/verified_artifact.rs b/src/verified_artifact.rs new file mode 100644 index 00000000..225522ae --- /dev/null +++ b/src/verified_artifact.rs @@ -0,0 +1,510 @@ +use crate::ast; +use crate::codegen::{MachineEdgeKindEvidence, MachineLayoutEvidence, MachineTerminatorEvidence}; +use crate::error::{CompileError, Result, Span}; +use crate::{CompileMetadata, ParamMetadata}; +use cellscript_artifact_checker::{ + canonical_hash, check_bundle_values, domain_hash_bytes, parse_elf, CheckerBudgets, CompatibilityProfileIdentity, EdgeKind, + EntryKind, LoweringBlock, LoweringEdge, LoweringEntry, MachineRange, MachineTerminator, ProofRecord, RuntimeErrorExit, + SourceArtifactMap, SourceMapCoverageClaim, SourceMapInterval, StorageClass, SyscallSite, TypedParameter, VerificationClaim, + VerifiedArtifactMetadata, VerifiedArtifactState, VerifiedLoweringRecord, CHECKER_POLICY_SCHEMA, CHECKER_VERSION, + LOWERING_RECORD_SCHEMA, LOWERING_RECORD_VERSION, SOURCE_MAP_SCHEMA, SOURCE_MAP_VERSION, +}; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Debug, Clone)] +pub(crate) struct VerifiedArtifactDraft { + pub machine_layout: MachineLayoutEvidence, + pub source_spans: BTreeMap, +} + +impl VerifiedArtifactDraft { + pub(crate) fn new(machine_layout: MachineLayoutEvidence, module: &ast::Module) -> Self { + let source_spans = module + .items + .iter() + .filter_map(|item| match item { + ast::Item::Action(action) => Some((action.name.clone(), action.span)), + ast::Item::Function(function) => Some((function.name.clone(), function.span)), + ast::Item::Lock(lock) => Some((lock.name.clone(), lock.span)), + _ => None, + }) + .collect(); + Self { machine_layout, source_spans } + } +} + +pub(crate) fn build_verified_artifact_boundary( + artifact: &[u8], + metadata: &CompileMetadata, + draft: &VerifiedArtifactDraft, +) -> Result<(VerifiedLoweringRecord, SourceArtifactMap, VerifiedArtifactMetadata)> { + let budgets = CheckerBudgets::default(); + let elf = parse_elf(artifact, budgets.instructions).map_err(|error| boundary_error(error.to_string()))?; + let compatibility_profile = compatibility_profile_identity(metadata); + let compatibility_profile_hash = canonical_hash("cellscript-compatibility-profile-identity-v1", &compatibility_profile) + .map_err(|error| boundary_error(error.to_string()))?; + let source_identity = metadata + .source_content_hash + .clone() + .ok_or_else(|| boundary_error("verified artifact boundary requires source_content_hash before emission"))?; + + let owners = block_owners(&draft.machine_layout)?; + let frame_sizes = complete_frame_sizes(&draft.machine_layout, &elf, &owners)?; + let mut entries = build_entries(metadata, &frame_sizes, &owners)?; + let owner_ids = entries.iter().map(|entry| (entry.name.clone(), entry.id.clone())).collect::>(); + let entry_proofs = build_proof_records(metadata, &owner_ids); + let proof_ids_by_entry = entry_proofs.iter().fold(BTreeMap::>::new(), |mut map, proof| { + map.entry(proof.entry_id.clone()).or_default().push(proof.id.clone()); + map + }); + for entry in &mut entries { + entry.proof_ids = proof_ids_by_entry.get(&entry.id).cloned().unwrap_or_default(); + } + + let mut blocks = Vec::with_capacity(draft.machine_layout.blocks.len()); + for (index, machine) in draft.machine_layout.blocks.iter().enumerate() { + let owner_name = owners.get(index).ok_or_else(|| boundary_error("machine block owner map is incomplete"))?; + let owner_entry = owner_ids + .get(owner_name) + .cloned() + .ok_or_else(|| boundary_error(format!("machine owner '{owner_name}' has no lowering entry")))?; + let range = MachineRange { start: machine.start, end: machine.end }; + let machine_bytes = elf.bytes_for_range(artifact, range).map_err(|error| boundary_error(error.to_string()))?; + let frame_size_bytes = frame_sizes.get(owner_name).copied().unwrap_or(0); + let proof_ids = proof_ids_by_entry.get(&owner_entry).cloned().unwrap_or_default(); + let entry_effect = entries + .iter() + .find(|entry| entry.id == owner_entry) + .map(|entry| entry.effect.clone()) + .unwrap_or_else(|| "runtime".to_string()); + blocks.push(LoweringBlock { + id: machine_block_id(index), + owner_entry, + reachable: true, + lowering_block_id: lowering_block_id(machine.label.as_deref(), owner_name), + machine_label: machine.label.clone(), + terminator: match machine.terminator { + MachineTerminatorEvidence::Fallthrough => MachineTerminator::Fallthrough, + MachineTerminatorEvidence::Jump => MachineTerminator::Jump, + MachineTerminatorEvidence::ConditionalBranch => MachineTerminator::ConditionalBranch, + MachineTerminatorEvidence::Return => MachineTerminator::Return, + }, + range, + byte_digest: domain_hash_bytes("cellscript-machine-block-v1", machine_bytes), + frame_size_bytes, + outgoing_argument_bytes: entries + .iter() + .find(|entry| entry.name == *owner_name) + .map(|entry| entry.outgoing_argument_bytes) + .unwrap_or(0), + stack_slots: Vec::new(), + scratch_register_avoid: Vec::new(), + effect: entry_effect, + capabilities: Vec::new(), + proof_ids, + }); + } + + let mut edges = draft + .machine_layout + .edges + .iter() + .map(|edge| LoweringEdge { + from: machine_block_id(edge.from), + to: machine_block_id(edge.to), + kind: match edge.kind { + MachineEdgeKindEvidence::Fallthrough => EdgeKind::Fallthrough, + MachineEdgeKindEvidence::Jump => EdgeKind::Jump, + MachineEdgeKindEvidence::ConditionalTaken => EdgeKind::ConditionalTaken, + MachineEdgeKindEvidence::ConditionalFallthrough => EdgeKind::ConditionalFallthrough, + MachineEdgeKindEvidence::Call => EdgeKind::Call, + }, + }) + .collect::>(); + edges.sort_by(|a, b| (&a.from, &a.kind, &a.to).cmp(&(&b.from, &b.kind, &b.to))); + mark_reachable_blocks(&entries, &mut blocks, &edges); + + let syscall_sites = elf + .syscall_addresses + .iter() + .copied() + .filter(|address| draft.machine_layout.text_start <= *address && *address < draft.machine_layout.text_end) + .map(|address| { + let block = blocks + .iter() + .find(|block| block.range.contains(address)) + .ok_or_else(|| boundary_error(format!("decoded syscall {address:#x} is outside machine blocks")))?; + Ok(SyscallSite { + block_id: block.id.clone(), + address, + syscall_number: None, + contract: "ckb-vm-ecall-a7-v1".to_string(), + source_domain: "entry-runtime-metadata".to_string(), + index_domain: "entry-runtime-metadata".to_string(), + return_code_checked: true, + buffer_limit_bytes: block.frame_size_bytes.max(1), + }) + }) + .collect::>>()?; + let runtime_error_exits = runtime_error_exits(&draft.machine_layout, &blocks); + + let mut record = VerifiedLoweringRecord { + schema: LOWERING_RECORD_SCHEMA.to_string(), + version: LOWERING_RECORD_VERSION, + compiler_version: metadata.compiler_version.clone(), + module: metadata.module.clone(), + edition: metadata.edition.as_str().to_string(), + target_profile: metadata.target_profile.name.clone(), + compatibility_profile, + compatibility_profile_hash, + source_set_hash: source_identity.clone(), + source_content_hash: source_identity.clone(), + artifact_format: metadata.artifact_format.clone(), + artifact_hash: metadata.artifact_hash.clone().ok_or_else(|| boundary_error("metadata artifact hash is missing"))?, + artifact_size_bytes: artifact.len() as u64, + text_range: MachineRange { start: draft.machine_layout.text_start, end: draft.machine_layout.text_end }, + entries, + blocks, + edges, + proof_records: entry_proofs, + syscall_sites, + runtime_error_exits, + limits: budgets.as_declared_limits(), + claim: VerificationClaim { + lowering_record: "binding-verified".to_string(), + machine_code: "structurally-verified".to_string(), + semantic_equivalence: false, + }, + }; + record.canonicalize(); + let record_hash = canonical_hash(LOWERING_RECORD_SCHEMA, &record).map_err(|error| boundary_error(error.to_string()))?; + + let source_path = stable_entry_source_path(metadata); + let mut intervals = record + .blocks + .iter() + .filter_map(|block| { + let lowering_block_id = block.lowering_block_id?; + let entry = record.entries.iter().find(|entry| entry.id == block.owner_entry)?; + let span = draft.source_spans.get(&entry.name).copied().unwrap_or_default(); + let runtime_error_codes = + record.runtime_error_exits.iter().filter(|exit| exit.block_id == block.id).map(|exit| exit.code).collect(); + Some(SourceMapInterval { + source_path: source_path.clone(), + source_start: u32::try_from(span.start).unwrap_or(u32::MAX), + source_end: u32::try_from(span.end).unwrap_or(u32::MAX), + entry_id: block.owner_entry.clone(), + block_id: block.id.clone(), + lowering_block_id: Some(lowering_block_id), + machine_range: block.range, + proof_ids: block.proof_ids.clone(), + runtime_error_codes, + }) + }) + .collect::>(); + intervals.sort_by_key(|interval| interval.machine_range.start); + let mut source_map = SourceArtifactMap { + schema: SOURCE_MAP_SCHEMA.to_string(), + version: SOURCE_MAP_VERSION, + module: metadata.module.clone(), + artifact_hash: record.artifact_hash.clone(), + lowering_record_hash: record_hash.clone(), + source_set_hash: source_identity, + text_range: record.text_range, + intervals, + coverage_claim: SourceMapCoverageClaim { + mapped_instruction_ranges_only: true, + complete_text_coverage: false, + source_semantic_equivalence: false, + }, + }; + source_map.canonicalize(); + let source_map_hash = canonical_hash(SOURCE_MAP_SCHEMA, &source_map).map_err(|error| boundary_error(error.to_string()))?; + let boundary_metadata = VerifiedArtifactMetadata { + boundary_schema: "cellscript-verified-artifact-boundary-v1".to_string(), + state: VerifiedArtifactState::Emitted, + checker_name: "cellscript-artifact-checker".to_string(), + checker_version: CHECKER_VERSION.to_string(), + checker_policy_schema: CHECKER_POLICY_SCHEMA.to_string(), + lowering_record_schema: LOWERING_RECORD_SCHEMA.to_string(), + lowering_record_hash: Some(record_hash), + source_map_schema: SOURCE_MAP_SCHEMA.to_string(), + source_map_hash: Some(source_map_hash), + claim: "binding-verified+structurally-verified;semantic-equivalence-not-claimed".to_string(), + }; + Ok((record, source_map, boundary_metadata)) +} + +fn mark_reachable_blocks(entries: &[LoweringEntry], blocks: &mut [LoweringBlock], edges: &[LoweringEdge]) { + let mut reachable = BTreeSet::new(); + let mut pending = entries.iter().map(|entry| entry.entry_block.as_str()).collect::>(); + while let Some(block_id) = pending.pop() { + if !reachable.insert(block_id) { + continue; + } + pending.extend(edges.iter().filter(|edge| edge.from == block_id).map(|edge| edge.to.as_str())); + } + for block in blocks { + block.reachable = reachable.contains(block.id.as_str()); + } +} + +pub(crate) fn validate_boundary_values( + artifact: &[u8], + metadata: &CompileMetadata, + record: &VerifiedLoweringRecord, + source_map: &SourceArtifactMap, +) -> Result<()> { + let metadata_value = serde_json::to_value(metadata) + .map_err(|error| boundary_error(format!("failed to serialize metadata for checker: {error}")))?; + check_bundle_values(artifact, &metadata_value, record, source_map, &CheckerBudgets::default()) + .map_err(|error| boundary_error(error.to_string()))?; + Ok(()) +} + +fn build_entries(metadata: &CompileMetadata, frame_sizes: &BTreeMap, owners: &[String]) -> Result> { + let first_block_by_owner = owners.iter().enumerate().fold(BTreeMap::::new(), |mut map, (index, owner)| { + map.entry(owner.clone()).or_insert(index); + map + }); + let mut entries = Vec::new(); + for owner in first_block_by_owner.keys() { + let (kind, params, return_type, effect) = if let Some(action) = metadata.actions.iter().find(|entry| entry.name == *owner) { + (EntryKind::Action, action.params.as_slice(), "unit".to_string(), action.effect_class.clone()) + } else if let Some(lock) = metadata.locks.iter().find(|entry| entry.name == *owner) { + (EntryKind::Lock, lock.params.as_slice(), "bool".to_string(), "lock-predicate".to_string()) + } else if let Some(function) = metadata.functions.iter().find(|entry| entry.name == *owner) { + ( + EntryKind::Helper, + function.params.as_slice(), + function.return_type.clone().unwrap_or_else(|| "unit".to_string()), + function.effect_class.clone(), + ) + } else if owner == "_cellscript_entry" { + (EntryKind::Wrapper, &[][..], "i32".to_string(), "entry-wrapper".to_string()) + } else { + (EntryKind::Runtime, &[][..], "i32".to_string(), "runtime-helper".to_string()) + }; + let id = entry_id(kind, owner); + let frame_size_bytes = frame_sizes.get(owner).copied().unwrap_or(0); + let outgoing_argument_bytes = u32::try_from(params.len().saturating_sub(8).saturating_mul(8)).unwrap_or(u32::MAX); + if outgoing_argument_bytes > frame_size_bytes && frame_size_bytes != 0 { + return Err(boundary_error(format!("entry '{owner}' outgoing ABI exceeds its captured frame"))); + } + entries.push(LoweringEntry { + id, + kind, + name: owner.clone(), + entry_block: machine_block_id(first_block_by_owner[owner]), + params: params.iter().enumerate().map(|(index, param)| typed_parameter(index, param)).collect(), + return_type, + effect, + capabilities: Vec::new(), + proof_ids: Vec::new(), + frame_size_bytes, + outgoing_argument_bytes: outgoing_argument_bytes.min(frame_size_bytes), + }); + } + entries.sort_by(|a, b| a.id.cmp(&b.id)); + Ok(entries) +} + +fn complete_frame_sizes( + layout: &MachineLayoutEvidence, + elf: &cellscript_artifact_checker::ParsedElf, + owners: &[String], +) -> Result> { + let mut sizes = layout.entry_frame_sizes.clone(); + let mut negative_adjustments = BTreeMap::::new(); + for (index, block) in layout.blocks.iter().enumerate() { + let owner = owners.get(index).ok_or_else(|| boundary_error("machine block owner map is incomplete"))?; + let total = elf + .stack_adjustments + .iter() + .filter(|adjustment| block.start <= adjustment.address && adjustment.address < block.end && adjustment.delta < 0) + .try_fold(0_u64, |total, adjustment| total.checked_add(adjustment.delta.unsigned_abs())) + .ok_or_else(|| boundary_error(format!("captured frame size for '{owner}' overflows u64")))?; + let accumulated = negative_adjustments.entry(owner.clone()).or_default(); + *accumulated = accumulated + .checked_add(total) + .ok_or_else(|| boundary_error(format!("captured frame size for '{owner}' overflows u64")))?; + } + for (owner, inferred) in negative_adjustments { + let inferred = + u32::try_from(inferred).map_err(|_| boundary_error(format!("captured frame size for '{owner}' exceeds u32")))?; + sizes.entry(owner).and_modify(|size| *size = (*size).max(inferred)).or_insert(inferred); + } + Ok(sizes) +} + +fn build_proof_records(metadata: &CompileMetadata, owner_ids: &BTreeMap) -> Vec { + let mut records = Vec::new(); + for action in &metadata.actions { + append_entry_proofs(&mut records, owner_ids.get(&action.name), &action.proof_plan); + } + for lock in &metadata.locks { + append_entry_proofs(&mut records, owner_ids.get(&lock.name), &lock.proof_plan); + } + for function in &metadata.functions { + append_entry_proofs(&mut records, owner_ids.get(&function.name), &function.proof_plan); + } + records.sort_by(|a, b| a.id.cmp(&b.id)); + records +} + +fn append_entry_proofs(output: &mut Vec, entry_id: Option<&String>, plans: &[crate::ProofPlanMetadata]) { + let Some(entry_id) = entry_id else { + return; + }; + for (index, plan) in plans.iter().enumerate() { + output.push(ProofRecord { + id: format!("proof:{entry_id}:{index:05}"), + entry_id: entry_id.clone(), + obligation: format!("{}:{}:{}", plan.name, plan.category, plan.status), + evidence_tier: plan.evidence_tier.as_str().to_string(), + }); + } +} + +fn block_owners(layout: &MachineLayoutEvidence) -> Result> { + let text_globals = layout.globals.iter().filter(|name| layout.symbols.contains_key(*name)).cloned().collect::>(); + let mut current = None::; + let mut owners = Vec::with_capacity(layout.blocks.len()); + for block in &layout.blocks { + if let Some(label) = block.label.as_ref().filter(|label| text_globals.contains(*label)) { + current = Some(label.clone()); + } + let owner = current + .clone() + .ok_or_else(|| boundary_error(format!("machine block {} precedes every global entry label", block.index)))?; + owners.push(owner); + } + Ok(owners) +} + +fn runtime_error_exits(layout: &MachineLayoutEvidence, blocks: &[LoweringBlock]) -> Vec { + let mut exits = layout + .blocks + .iter() + .enumerate() + .flat_map(|(index, machine)| { + machine.runtime_error_codes.iter().filter_map(move |code| { + let raw_code = *code; + let code = i32::try_from(raw_code).ok()?; + let block = blocks.get(index)?; + let error = crate::runtime_errors::CellScriptRuntimeError::from_code(raw_code)?; + Some(RuntimeErrorExit { block_id: block.id.clone(), address: block.range.start, code, name: error.name().to_string() }) + }) + }) + .chain(layout.symbols.iter().filter_map(|(label, address)| { + let (_, code) = label.rsplit_once("_fail_")?; + let code = code.parse::().ok()?; + let block = blocks.iter().find(|block| block.range.contains(*address))?; + Some(RuntimeErrorExit { + block_id: block.id.clone(), + address: *address, + code, + name: format!("cellscript-runtime-error-{code}"), + }) + })) + .collect::>(); + exits.sort_by(|a, b| (&a.block_id, a.code, a.address).cmp(&(&b.block_id, b.code, b.address))); + exits.dedup_by(|a, b| a.block_id == b.block_id && a.code == b.code && a.address == b.address); + exits +} + +fn compatibility_profile_identity(metadata: &CompileMetadata) -> CompatibilityProfileIdentity { + let profile = &metadata.compatibility_profile; + CompatibilityProfileIdentity { + schema: profile.schema.clone(), + id: profile.id.clone(), + edition: profile.edition.as_str().to_string(), + source_semantics: profile.source_semantics.clone(), + target_profile: profile.target_profile.clone(), + primitive_assurance: profile.primitive_assurance.clone(), + metadata_schema_version: profile.metadata_schema_version, + source_metadata_schema_version: profile.source_metadata_schema_version, + artifact_metadata_schema_version: profile.artifact_metadata_schema_version, + constraints_metadata_schema_version: profile.constraints_metadata_schema_version, + entry_witness_payload_abi: profile.entry_witness_payload_abi.clone(), + entry_witness_placement_abi: profile.entry_witness_placement_abi.clone(), + entry_witness_placement_field: profile.entry_witness_placement_field.clone(), + entry_witness_placement_source: profile.entry_witness_placement_source.clone(), + raw_entry_witness_payload_compatible: profile.raw_entry_witness_payload_compatible, + } +} + +fn typed_parameter(index: usize, param: &ParamMetadata) -> TypedParameter { + let (storage, width_bytes, alignment_bytes) = parameter_storage(param); + TypedParameter { + index: u32::try_from(index).unwrap_or(u32::MAX), + name: param.name.clone(), + ty: param.ty.clone(), + storage, + width_bytes, + alignment_bytes, + } +} + +fn parameter_storage(param: &ParamMetadata) -> (StorageClass, u32, u32) { + if param.schema_pointer_abi { + return (StorageClass::SchemaPointer, 8, 8); + } + if param.is_ref { + return (StorageClass::Reference, 8, 8); + } + if param.fixed_byte_pointer_abi { + let width = u32::try_from(param.fixed_byte_len.unwrap_or(8)).unwrap_or(u32::MAX); + return (StorageClass::FixedBytes, width.max(1), width.next_power_of_two().min(16)); + } + let width = match param.ty.as_str() { + "u8" | "bool" => 1, + "u16" => 2, + "u32" | "i32" => 4, + "u128" => 16, + "address" | "hash" => 32, + _ => 8, + }; + let storage = if width > 8 { StorageClass::FixedBytes } else { StorageClass::Scalar }; + (storage, width, width.next_power_of_two().min(16)) +} + +fn entry_id(kind: EntryKind, name: &str) -> String { + let prefix = match kind { + EntryKind::Action => "action", + EntryKind::Lock => "lock", + EntryKind::Helper => "helper", + EntryKind::Runtime => "runtime", + EntryKind::Wrapper => "wrapper", + }; + format!("{prefix}:{name}") +} + +fn machine_block_id(index: usize) -> String { + format!("mb{index:06}") +} + +fn lowering_block_id(label: Option<&str>, owner: &str) -> Option { + let prefix = format!(".L{owner}_block_"); + label?.strip_prefix(&prefix)?.parse().ok() +} + +fn stable_entry_source_path(metadata: &CompileMetadata) -> String { + let unit = metadata + .source_units + .iter() + .find(|unit| matches!(unit.role.as_str(), "entry" | "memory")) + .or_else(|| metadata.source_units.first()); + let Some(unit) = unit else { + return "".to_string(); + }; + if unit.path == "" { + return unit.path.clone(); + } + let file_name = unit.path.rsplit(['/', '\\']).next().filter(|name| !name.is_empty()).unwrap_or("module.cell"); + format!("source/{file_name}") +} + +fn boundary_error(message: impl Into) -> CompileError { + CompileError::without_span(format!("verified artifact boundary: {}", message.into())).with_code("E2400") +} diff --git a/tests/artifact_checker.rs b/tests/artifact_checker.rs new file mode 100644 index 00000000..496c91de --- /dev/null +++ b/tests/artifact_checker.rs @@ -0,0 +1,307 @@ +use cellscript::{compile, CompileOptions, CompileResult}; +use cellscript_artifact_checker::{ + canonical_bytes, canonical_hash, check_bundle, check_bundle_values, parse_elf, CheckerBudgets, CheckerRejectionCode, EdgeKind, + SourceArtifactMap, VerifiedLoweringRecord, LOWERING_RECORD_SCHEMA, SOURCE_MAP_SCHEMA, +}; +use serde_json::Value; + +const FIXTURE_SOURCE: &str = r#" +module artifact_checker_fixture + +fn increment(value: u64) -> u64 { + return value + 1 +} + +action main(value: u64) -> u64 { + verification + return increment(value) +} +"#; + +#[derive(Clone)] +struct Fixture { + artifact: Vec, + metadata: Value, + record: VerifiedLoweringRecord, + source_map: SourceArtifactMap, +} + +impl Fixture { + fn new() -> Self { + let result = + compile(FIXTURE_SOURCE, CompileOptions { target: Some("riscv64-elf".to_string()), ..CompileOptions::default() }).unwrap(); + Self::from_result(result) + } + + fn from_result(result: CompileResult) -> Self { + let fixture = Self { + artifact: result.artifact_bytes, + metadata: serde_json::to_value(result.metadata).unwrap(), + record: result.verified_lowering_record.unwrap(), + source_map: result.source_artifact_map.unwrap(), + }; + fixture.check().unwrap(); + fixture + } + + fn check(&self) -> Result<(), CheckerRejectionCode> { + check_bundle_values(&self.artifact, &self.metadata, &self.record, &self.source_map, &CheckerBudgets::default()) + .map(|_| ()) + .map_err(|error| error.code) + } + + fn rebind_sidecars(&mut self) { + let record_hash = canonical_hash(LOWERING_RECORD_SCHEMA, &self.record).unwrap(); + self.source_map.lowering_record_hash = record_hash.clone(); + let source_map_hash = canonical_hash(SOURCE_MAP_SCHEMA, &self.source_map).unwrap(); + self.metadata["verified_artifact"]["lowering_record_hash"] = Value::String(record_hash); + self.metadata["verified_artifact"]["source_map_hash"] = Value::String(source_map_hash); + } + + fn bind_artifact_identity(&mut self) { + let artifact_hash = cellscript_artifact_checker::hex_encode(&cellscript_artifact_checker::ckb_blake2b256(&self.artifact)); + self.record.artifact_hash.clone_from(&artifact_hash); + self.record.artifact_size_bytes = self.artifact.len() as u64; + self.source_map.artifact_hash.clone_from(&artifact_hash); + self.metadata["artifact_hash"] = Value::String(artifact_hash); + self.metadata["artifact_size_bytes"] = Value::from(self.artifact.len() as u64); + self.rebind_sidecars(); + } +} + +fn assert_code(fixture: &Fixture, expected: CheckerRejectionCode) { + match fixture.check() { + Ok(()) => panic!("mutation unexpectedly passed; expected {}", expected.as_str()), + Err(actual) => assert_eq!(actual, expected), + } +} + +#[test] +fn verified_artifact_sidecars_are_deterministic_and_canonical() { + let first = Fixture::new(); + let second = Fixture::new(); + assert_eq!(first.artifact, second.artifact); + assert_eq!(canonical_bytes(&first.record).unwrap(), canonical_bytes(&second.record).unwrap()); + assert_eq!(canonical_bytes(&first.source_map).unwrap(), canonical_bytes(&second.source_map).unwrap()); + assert!(first.source_map.intervals.iter().all(|interval| interval.source_path == "")); +} + +#[test] +fn stable_rejection_codes_cover_json_budget_graph_abi_proof_and_binding_mutations() { + let valid = Fixture::new(); + let budgets = CheckerBudgets::default(); + let metadata_bytes = serde_json::to_vec(&valid.metadata).unwrap(); + let record_bytes = canonical_bytes(&valid.record).unwrap(); + let source_map_bytes = canonical_bytes(&valid.source_map).unwrap(); + + let mut tiny = budgets.clone(); + tiny.artifact_bytes = 1; + assert_eq!( + check_bundle(&valid.artifact, &metadata_bytes, &record_bytes, &source_map_bytes, &tiny).unwrap_err().code, + CheckerRejectionCode::V2400BudgetExceeded, + ); + assert_eq!( + check_bundle(&valid.artifact, &metadata_bytes, b"{", &source_map_bytes, &budgets).unwrap_err().code, + CheckerRejectionCode::V2401MalformedJson, + ); + assert_eq!( + check_bundle( + &valid.artifact, + &metadata_bytes, + &serde_json::to_vec_pretty(&valid.record).unwrap(), + &source_map_bytes, + &budgets, + ) + .unwrap_err() + .code, + CheckerRejectionCode::V2402NonCanonicalJson, + ); + + let mut changed = valid.clone(); + changed.record.schema = "future-schema".to_string(); + assert_code(&changed, CheckerRejectionCode::V2403UnsupportedSchema); + + let mut changed = valid.clone(); + changed.record.entries[0].id = "zz-noncanonical".to_string(); + changed.rebind_sidecars(); + assert_code(&changed, CheckerRejectionCode::V2404CanonicalOrder); + + let mut changed = valid.clone(); + changed.record.entries[0].entry_block = "missing:block".to_string(); + changed.rebind_sidecars(); + assert_code(&changed, CheckerRejectionCode::V2405ReferentialIntegrity); + + let mut changed = valid.clone(); + let index = changed + .record + .blocks + .iter() + .position(|block| changed.record.edges.iter().any(|edge| edge.from == block.id && edge.kind != EdgeKind::Call)) + .expect("fixture must contain a non-return CFG edge"); + changed.record.blocks[index].terminator = cellscript_artifact_checker::MachineTerminator::Return; + changed.rebind_sidecars(); + assert_code(&changed, CheckerRejectionCode::V2406CfgInvalid); + + let mut changed = valid.clone(); + changed.record.runtime_error_exits.push(cellscript_artifact_checker::RuntimeErrorExit { + block_id: changed.record.blocks[0].id.clone(), + address: changed.record.blocks[0].range.end, + code: 5, + name: "assertion-failed".to_string(), + }); + changed.record.canonicalize(); + changed.rebind_sidecars(); + assert_code(&changed, CheckerRejectionCode::V2406CfgInvalid); + + let mut changed = valid.clone(); + changed.record.blocks[0].reachable = !changed.record.blocks[0].reachable; + changed.rebind_sidecars(); + assert_code(&changed, CheckerRejectionCode::V2406CfgInvalid); + + let mut changed = valid.clone(); + let param = changed.record.entries.iter_mut().find_map(|entry| entry.params.first_mut()).unwrap(); + param.alignment_bytes = 3; + changed.rebind_sidecars(); + assert_code(&changed, CheckerRejectionCode::V2407AbiOrStackInvalid); + + let mut changed = valid.clone(); + let framed_entry = + changed.record.entries.iter().position(|entry| entry.frame_size_bytes > 0).expect("fixture must contain a stack-framed entry"); + let owner = changed.record.entries[framed_entry].id.clone(); + changed.record.entries[framed_entry].frame_size_bytes = 0; + changed.record.entries[framed_entry].outgoing_argument_bytes = 0; + for block in changed.record.blocks.iter_mut().filter(|block| block.owner_entry == owner) { + block.frame_size_bytes = 0; + block.outgoing_argument_bytes = 0; + block.stack_slots.clear(); + } + changed.rebind_sidecars(); + assert_code(&changed, CheckerRejectionCode::V2407AbiOrStackInvalid); + + let mut changed = valid.clone(); + changed.record.entries[0].proof_ids.push("zz-missing-proof".to_string()); + changed.record.entries[0].proof_ids.sort(); + changed.rebind_sidecars(); + assert_code(&changed, CheckerRejectionCode::V2408ProofCoverageInvalid); + + let mut changed = valid.clone(); + changed.artifact[0] ^= 1; + assert_code(&changed, CheckerRejectionCode::V2409ArtifactIdentityMismatch); + + let mut changed = valid.clone(); + changed.metadata["module"] = Value::String("tampered".to_string()); + assert_code(&changed, CheckerRejectionCode::V2410MetadataBindingMismatch); + + let mut changed = valid.clone(); + if let Some(interval) = changed.source_map.intervals.first_mut() { + interval.source_path = "../escape.cell".to_string(); + } else { + changed.source_map.schema = "bad-map".to_string(); + } + changed.rebind_sidecars(); + assert_code(&changed, CheckerRejectionCode::V2416SourceMapInvalid); + + let mut changed = valid.clone(); + if let Some(site) = changed.record.syscall_sites.first_mut() { + site.contract.clear(); + } else { + changed.record.syscall_sites.push(cellscript_artifact_checker::SyscallSite { + block_id: changed.record.blocks[0].id.clone(), + address: changed.record.blocks[0].range.start, + syscall_number: None, + contract: "declared-but-not-present".to_string(), + source_domain: "test".to_string(), + index_domain: "test".to_string(), + return_code_checked: true, + buffer_limit_bytes: 1, + }); + } + changed.rebind_sidecars(); + assert_code(&changed, CheckerRejectionCode::V2417SyscallContractInvalid); + + let mut changed = valid.clone(); + let distinct = changed + .record + .entries + .iter() + .enumerate() + .find_map(|(left, a)| changed.record.entries.iter().enumerate().find(|(_, b)| b.id != a.id).map(|(right, _)| (left, right))) + .unwrap(); + let left = changed.record.entries[distinct.0].entry_block.clone(); + let right = changed.record.entries[distinct.1].entry_block.clone(); + changed.record.edges.push(cellscript_artifact_checker::LoweringEdge { + from: left.clone(), + to: right.clone(), + kind: EdgeKind::Call, + }); + changed.record.edges.push(cellscript_artifact_checker::LoweringEdge { from: right, to: left, kind: EdgeKind::Call }); + changed.record.canonicalize(); + changed.rebind_sidecars(); + assert_code(&changed, CheckerRejectionCode::V2418RecursionPolicyInvalid); +} + +#[test] +fn stable_rejection_codes_cover_elf_sections_instructions_flow_and_digests() { + let valid = Fixture::new(); + + let mut changed = valid.clone(); + changed.artifact[0] = 0; + changed.bind_artifact_identity(); + assert_code(&changed, CheckerRejectionCode::V2411ElfFormatInvalid); + + let mut changed = valid.clone(); + let section_table = u64::from_le_bytes(changed.artifact[40..48].try_into().unwrap()) as usize; + let rodata_type = section_table + 2 * 64 + 4; + changed.artifact[rodata_type..rodata_type + 4].copy_from_slice(&6_u32.to_le_bytes()); + changed.bind_artifact_identity(); + assert_code(&changed, CheckerRejectionCode::V2412ElfSectionInvalid); + + let elf = parse_elf(&valid.artifact, CheckerBudgets::default().instructions).unwrap(); + let text_offset = elf.text.offset as usize; + + let mut changed = valid.clone(); + changed.artifact[text_offset..text_offset + 4].copy_from_slice(&u32::MAX.to_le_bytes()); + changed.bind_artifact_identity(); + assert_code(&changed, CheckerRejectionCode::V2413InstructionInvalid); + + let mut changed = valid.clone(); + changed.artifact[text_offset..text_offset + 4].copy_from_slice(&encode_jal(1_048_574).to_le_bytes()); + changed.bind_artifact_identity(); + assert_code(&changed, CheckerRejectionCode::V2414ControlFlowInvalid); + + let mut changed = valid.clone(); + let candidate = elf + .instructions + .windows(2) + .find(|pair| { + let word = pair[0].word; + let rd = (word >> 7) & 0x1f; + let next = pair[1].word; + let next_uses_rd_for_sp = + next & 0x7f == 0x33 && (next >> 7) & 0x1f == 2 && (next >> 15) & 0x1f == 2 && (next >> 20) & 0x1f == rd; + valid.record.text_range.contains(pair[0].address) + && word & 0x7f == 0x13 + && rd != 2 + && !next_uses_rd_for_sp + && valid + .record + .blocks + .iter() + .any(|block| block.range.contains(pair[0].address) && pair[0].address + 4 < block.range.end) + }) + .map(|pair| pair[0]) + .expect("fixture must contain a non-terminating add-immediate instruction"); + let block_offset = elf.text.offset as usize + (candidate.address - elf.text.address) as usize; + changed.artifact[block_offset..block_offset + 4].copy_from_slice(&(candidate.word ^ (1 << 20)).to_le_bytes()); + changed.bind_artifact_identity(); + assert_code(&changed, CheckerRejectionCode::V2415BlockDigestMismatch); +} + +fn encode_jal(offset: i32) -> u32 { + let immediate = offset as u32; + (((immediate >> 20) & 1) << 31) + | (((immediate >> 1) & 0x03ff) << 21) + | (((immediate >> 11) & 1) << 20) + | (((immediate >> 12) & 0x00ff) << 12) + | 0x6f +} diff --git a/tests/artifact_size.rs b/tests/artifact_size.rs index a2539e40..a51eaf0b 100644 --- a/tests/artifact_size.rs +++ b/tests/artifact_size.rs @@ -9,7 +9,8 @@ use cellscript::{compile_file_with_entry_action, ArtifactFormat, CompileOptions} const RUST_CKB_TARGET: &str = "riscv64imac-unknown-none-elf"; const RUST_REFERENCE_PACKAGE: &str = "rust-ckb-token-transfer"; -const TOKEN_TRANSFER_MAX_CELLSCRIPT_BYTES: usize = 7 * 1024; +const TOKEN_TRANSFER_MAX_CELLSCRIPT_LOAD_BYTES: usize = 7 * 1024; +const TOKEN_TRANSFER_MAX_VERIFIED_ELF_OVERHEAD_BYTES: usize = 320; const TOKEN_TRANSFER_MAX_RUST_STRIPPED_BYTES: usize = 3 * 1024; #[test] @@ -58,10 +59,17 @@ fn token_transfer_cellscript_artifact_is_compared_against_equivalent_rust_ckb_co ); assert!( - cellscript_bytes <= TOKEN_TRANSFER_MAX_CELLSCRIPT_BYTES, - "CellScript transfer_token ELF grew past budget: {} > {} bytes", - cellscript_bytes, - TOKEN_TRANSFER_MAX_CELLSCRIPT_BYTES + cellscript_load_bytes <= TOKEN_TRANSFER_MAX_CELLSCRIPT_LOAD_BYTES, + "CellScript transfer_token executable LOAD bytes grew past budget: {} > {} bytes", + cellscript_load_bytes, + TOKEN_TRANSFER_MAX_CELLSCRIPT_LOAD_BYTES + ); + let verified_elf_overhead = cellscript_bytes.saturating_sub(cellscript_load_bytes); + assert!( + verified_elf_overhead <= TOKEN_TRANSFER_MAX_VERIFIED_ELF_OVERHEAD_BYTES, + "CellScript transfer_token verified ELF headers grew past budget: {} > {} bytes", + verified_elf_overhead, + TOKEN_TRANSFER_MAX_VERIFIED_ELF_OVERHEAD_BYTES ); assert!( rust_stripped_bytes <= TOKEN_TRANSFER_MAX_RUST_STRIPPED_BYTES, diff --git a/tests/backend_shape_baseline.json b/tests/backend_shape_baseline.json index 05025800..e3e83627 100644 --- a/tests/backend_shape_baseline.json +++ b/tests/backend_shape_baseline.json @@ -1,110 +1,110 @@ [ { "example": "amm_pool.cell", - "line_count": 19915, - "text_size": 83512, + "line_count": 20136, + "text_size": 84380, "relaxed_branch_count": 1, "max_cond_branch_abs_distance": 4680, - "machine_block_count": 2325, + "machine_block_count": 2361, "max_machine_block_size": 352, - "machine_cfg_edge_count": 4400, + "machine_cfg_edge_count": 4462, "machine_call_edge_count": 994, "unreachable_machine_block_count": 2054 }, { "example": "atomic_swap.cell", - "line_count": 11283, - "text_size": 46992, + "line_count": 11504, + "text_size": 47860, "relaxed_branch_count": 2, "max_cond_branch_abs_distance": 5628, - "machine_block_count": 989, + "machine_block_count": 1025, "max_machine_block_size": 20252, - "machine_cfg_edge_count": 1844, + "machine_cfg_edge_count": 1906, "machine_call_edge_count": 421, "unreachable_machine_block_count": 866 }, { "example": "launch.cell", - "line_count": 6948, - "text_size": 28836, + "line_count": 7169, + "text_size": 29704, "relaxed_branch_count": 2, "max_cond_branch_abs_distance": 5492, - "machine_block_count": 576, + "machine_block_count": 612, "max_machine_block_size": 1924, - "machine_cfg_edge_count": 997, + "machine_cfg_edge_count": 1059, "machine_call_edge_count": 179, "unreachable_machine_block_count": 144 }, { "example": "multi_phase_dao.cell", - "line_count": 12260, - "text_size": 49624, + "line_count": 12481, + "text_size": 50492, "relaxed_branch_count": 2, "max_cond_branch_abs_distance": 5140, - "machine_block_count": 1732, + "machine_block_count": 1768, "max_machine_block_size": 252, - "machine_cfg_edge_count": 3217, + "machine_cfg_edge_count": 3279, "machine_call_edge_count": 712, "unreachable_machine_block_count": 1663 }, { "example": "multisig.cell", - "line_count": 23738, - "text_size": 93408, + "line_count": 23959, + "text_size": 94276, "relaxed_branch_count": 4, "max_cond_branch_abs_distance": 7608, - "machine_block_count": 3499, + "machine_block_count": 3535, "max_machine_block_size": 300, - "machine_cfg_edge_count": 5540, + "machine_cfg_edge_count": 5602, "machine_call_edge_count": 358, "unreachable_machine_block_count": 3324 }, { "example": "nft.cell", - "line_count": 19681, - "text_size": 79944, + "line_count": 19902, + "text_size": 80812, "relaxed_branch_count": 1, "max_cond_branch_abs_distance": 11188, - "machine_block_count": 2925, + "machine_block_count": 2961, "max_machine_block_size": 376, - "machine_cfg_edge_count": 5168, + "machine_cfg_edge_count": 5230, "machine_call_edge_count": 850, "unreachable_machine_block_count": 2764 }, { "example": "timelock.cell", - "line_count": 18764, - "text_size": 75456, + "line_count": 18985, + "text_size": 76324, "relaxed_branch_count": 1, "max_cond_branch_abs_distance": 4404, - "machine_block_count": 2135, + "machine_block_count": 2171, "max_machine_block_size": 20252, - "machine_cfg_edge_count": 3744, + "machine_cfg_edge_count": 3806, "machine_call_edge_count": 578, "unreachable_machine_block_count": 2060 }, { "example": "token.cell", - "line_count": 2956, - "text_size": 11764, + "line_count": 3177, + "text_size": 12632, "relaxed_branch_count": 0, "max_cond_branch_abs_distance": 1260, - "machine_block_count": 414, + "machine_block_count": 450, "max_machine_block_size": 212, - "machine_cfg_edge_count": 723, + "machine_cfg_edge_count": 785, "machine_call_edge_count": 123, "unreachable_machine_block_count": 226 }, { "example": "vesting.cell", - "line_count": 8995, - "text_size": 36048, + "line_count": 9223, + "text_size": 36948, "relaxed_branch_count": 2, - "max_cond_branch_abs_distance": 7184, - "machine_block_count": 1077, + "max_cond_branch_abs_distance": 7216, + "machine_block_count": 1115, "max_machine_block_size": 356, - "machine_cfg_edge_count": 1965, + "machine_cfg_edge_count": 2031, "machine_call_edge_count": 412, - "unreachable_machine_block_count": 1000 + "unreachable_machine_block_count": 1002 } ] diff --git a/tests/benchmarks b/tests/benchmarks index 82129ff1..0e18ccd9 160000 --- a/tests/benchmarks +++ b/tests/benchmarks @@ -1 +1 @@ -Subproject commit 82129ff1b102b9333f98afc5089935b4a29c2bb8 +Subproject commit 0e18ccd97bd75cac7de9211dc8d344c0bc08942f diff --git a/tests/cli.rs b/tests/cli.rs index c26df4f4..089ecb48 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -1,41 +1,14 @@ mod common; +use base64::Engine as _; use common::cellc_command; +use sha2::{Digest as _, Sha256}; use std::io::{Read, Write}; use std::net::TcpListener; use std::process::{Command, Stdio}; use std::time::Duration; use unicode_width::UnicodeWidthStr; -fn git_init(repo_dir: &std::path::Path) { - let status = Command::new("git").args(["init"]).current_dir(repo_dir).status().expect("git init"); - assert!(status.success()); -} - -fn git_add_all(repo_dir: &std::path::Path) { - let status = Command::new("git").args(["add", "."]).current_dir(repo_dir).status().expect("git add"); - assert!(status.success()); -} - -fn git_commit(repo_dir: &std::path::Path, msg: &str) { - git_add_all(repo_dir); - let status = Command::new("git") - .args(["-c", "commit.gpgsign=false", "commit", "-m", msg, "--author=test "]) - .env("GIT_AUTHOR_DATE", "2026-01-01T00:00:00+00:00") - .env("GIT_COMMITTER_NAME", "test") - .env("GIT_COMMITTER_EMAIL", "test@test.com") - .env("GIT_COMMITTER_DATE", "2026-01-01T00:00:00+00:00") - .current_dir(repo_dir) - .status() - .expect("git commit"); - assert!(status.success()); -} - -fn git_tag(repo_dir: &std::path::Path, tag: &str) { - let status = Command::new("git").args(["-c", "tag.gpgSign=false", "tag", tag]).current_dir(repo_dir).status().expect("git tag"); - assert!(status.success()); -} - fn hex_lower(bytes: &[u8]) -> String { bytes.iter().map(|byte| format!("{byte:02x}")).collect() } @@ -45,6 +18,11 @@ fn hash_json_for_test(value: &T) -> String { hex_lower(&cellscript::ckb_blake2b256(&bytes)) } +fn lock_package(root: &std::path::Path) { + let output = Command::new(env!("CARGO_BIN_EXE_cellc")).current_dir(root).arg("lock").output().unwrap(); + assert!(output.status.success(), "lock failed: {}", String::from_utf8_lossy(&output.stderr)); +} + fn ckb_script_hash_for_test(code_hash: &str, hash_type: &str, args: &str) -> String { let code_hash_bytes = hex::decode(code_hash.trim_start_matches("0x")).unwrap(); let hash_type_byte = match hash_type { @@ -174,6 +152,7 @@ fn cellc_auth_help_hides_legacy_login_alias() { let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("capability"), "unexpected auth help: {stdout}"); + assert!(stdout.contains("reproducer"), "unexpected auth help: {stdout}"); assert!(!stdout.contains("login"), "legacy auth login alias should be hidden from auth help: {stdout}"); } @@ -319,6 +298,7 @@ fn cellscript_mcp_check_tool_preserves_structured_boundaries() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "mcp-demo" version = "0.1.0" "#, @@ -1109,6 +1089,7 @@ fn cellc_check_multiple_diagnostics_prints_each_source_context() { std::fs::write( temp.path().join("Cell.toml"), r#"[package] +edition = "2026" name = "bad" version = "0.1.0" entry = "src/main.cell" @@ -1179,6 +1160,58 @@ fn cellc_auth_login_outputs_capability_authorisation_payload() { assert!(payload["cli_version"].as_str().is_some()); } +#[test] +fn cellc_auth_capability_create_infers_only_the_exact_publish_scope() { + let temp = tempfile::tempdir().unwrap(); + std::fs::write( + temp.path().join("Cell.toml"), + r#"[package] +edition = "2026" +name = "amm" +version = "0.1.0" +namespace = "cellscript" +"#, + ) + .unwrap(); + + let output = cellc_command() + .args(["auth", "capability", "create"]) + .arg("--principal-id") + .arg("0xjoyidprincipal") + .arg("--capability-pubkey") + .arg("0xcapabilitypubkey") + .arg("--json") + .current_dir(temp.path()) + .output() + .unwrap(); + + assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr)); + let payload: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(payload["requested_scopes"], serde_json::json!(["publish:cellscript/amm"])); +} + +#[test] +fn cellc_auth_capability_create_rejects_unknown_or_duplicate_scopes() { + for scopes in [vec!["admin:cellscript/amm"], vec!["publish:cellscript/amm", "publish:cellscript/amm"]] { + let mut command = cellc_command(); + command + .args(["auth", "capability", "create"]) + .arg("--principal-id") + .arg("0xjoyidprincipal") + .arg("--capability-pubkey") + .arg("0xcapabilitypubkey") + .arg("--json"); + for scope in scopes { + command.arg("--scope").arg(scope); + } + let output = command.output().unwrap(); + assert!(!output.status.success(), "unexpected success: {}", String::from_utf8_lossy(&output.stdout)); + let failure: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + let message = failure["diagnostics"][0]["message"].as_str().unwrap_or_default(); + assert!(message.contains("capability scope"), "unexpected failure: {failure}"); + } +} + #[test] fn cellc_auth_capability_create_requires_principal_id() { let output = cellc_command() @@ -1202,12 +1235,98 @@ fn cellc_auth_capability_create_requires_principal_id() { assert!(message.contains("--principal-id"), "unexpected failure: {failure}"); } +#[cfg(unix)] +#[test] +fn cellc_auth_reproducer_create_keeps_private_key_out_of_public_enrollment() { + let temp = tempfile::tempdir().unwrap(); + let private_key_path = temp.path().join("builder-private.pkcs8.b64"); + let output = cellc_command() + .args(["auth", "reproducer", "create"]) + .arg("--builder-id") + .arg("independent-builder-a") + .arg("--trust-domain") + .arg("independent-org-a") + .arg("--private-key-output") + .arg(&private_key_path) + .arg("--json") + .output() + .unwrap(); + + assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr)); + let enrollment: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(enrollment["schema"], "cellscript-reproducer-builder-enrollment-v1"); + assert_eq!(enrollment["builder_id"], "independent-builder-a"); + assert_eq!(enrollment["trust_domain"], "independent-org-a"); + assert_eq!(enrollment["policy_builder"]["builder_id"], "independent-builder-a"); + assert_eq!(enrollment["policy_builder"]["trust_domain"], "independent-org-a"); + assert_eq!(enrollment["private_key_storage"]["kind"], "pkcs8_base64_file"); + + let public_key = enrollment["builder_public_key"].as_str().unwrap(); + assert!(public_key.starts_with("p256-spki:")); + let spki = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(public_key.trim_start_matches("p256-spki:")).unwrap(); + assert_eq!(spki.len(), 91); + let expected_key_id = format!("cap_{}", &hex::encode(Sha256::digest(public_key.as_bytes()))[..32]); + assert_eq!(enrollment["builder_key_id"], expected_key_id); + assert_eq!(enrollment["policy_builder"]["public_key"], public_key); + + let private_key_secret = std::fs::read_to_string(&private_key_path).unwrap(); + let private_key = base64::engine::general_purpose::STANDARD.decode(private_key_secret.trim()).unwrap(); + assert!(private_key.len() > 100); + assert!(!String::from_utf8_lossy(&output.stdout).contains(private_key_secret.trim())); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt as _; + assert_eq!(std::fs::metadata(&private_key_path).unwrap().permissions().mode() & 0o777, 0o600); + } + + let second = cellc_command() + .args(["auth", "reproducer", "create"]) + .arg("--builder-id") + .arg("independent-builder-a") + .arg("--trust-domain") + .arg("independent-org-a") + .arg("--private-key-output") + .arg(&private_key_path) + .arg("--json") + .output() + .unwrap(); + assert!(!second.status.success(), "existing private-key file must not be overwritten"); + assert_eq!(std::fs::read_to_string(&private_key_path).unwrap(), private_key_secret); +} + +#[cfg(not(unix))] +#[test] +fn cellc_auth_reproducer_create_rejects_private_key_file_without_unix_permissions() { + let temp = tempfile::tempdir().unwrap(); + let private_key_path = temp.path().join("builder-private.pkcs8.b64"); + let output = cellc_command() + .args(["auth", "reproducer", "create"]) + .arg("--builder-id") + .arg("independent-builder-a") + .arg("--trust-domain") + .arg("independent-org-a") + .arg("--private-key-output") + .arg(&private_key_path) + .arg("--json") + .output() + .unwrap(); + + assert!(!output.status.success()); + assert!( + String::from_utf8_lossy(&output.stderr).contains("requires Unix mode-0600 permission semantics"), + "unexpected stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(!private_key_path.exists()); +} + fn write_publish_fixture_package(root: &std::path::Path) { std::fs::create_dir_all(root.join("src")).unwrap(); std::fs::write( root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "1.2.3" namespace = "cellscript" @@ -1232,6 +1351,154 @@ action identity(value: u64) -> u64 { .unwrap(); } +fn write_declared_artifact_fixture(root: &std::path::Path) { + std::fs::write( + root.join("Artifact.toml"), + r#"schema = "cellscript-registry-artifact" +namespace = "cellscript" +name = "rust-contract" +release = "1.0.0" +kind = "deployable_contract" +language = "rust" +bundle = "artifact-bundle.json" +description = "Rust CKB contract" +repository = "https://example.com/cellscript/rust-contract" +"#, + ) + .unwrap(); + let abi_hash = hex::encode(cellscript::ckb_blake2b256(b"abi")); + let profile_contract = serde_json::json!({ + "schema": "cellscript-registry-profile-contract-v1", + "artifact_kind": "deployable_contract", + "profile": "ckb_executable", + "build": { + "target": "riscv64imac-unknown-none-elf", + "toolchain": "rustc 1.97.1", + "profile": "release", + "source_revision": "0123456789abcdef", + "reproducible": false + }, + "security": { "status": "review_required" }, + "ckb": { + "vm_version": "2", + "script_role": "type", + "hash_type": "data1", + "dep_type": "code", + "abi_hash": abi_hash + } + }); + let bundle = serde_json::json!({ + "schema": "cellscript-registry-bundle", + "namespace": "cellscript", + "name": "rust-contract", + "release": "1.0.0", + "profile": "ckb_executable", + "manifest_json": cellscript::package::registry::canonical_artifact_contract_json(&profile_contract).unwrap(), + "objects": [ + {"role":"source","content_base64":"c291cmNl"}, + {"role":"executable","content_base64":"ZWxm"}, + {"role":"abi","content_base64":"YWJp"} + ] + }); + std::fs::write(root.join("artifact-bundle.json"), serde_json::to_vec_pretty(&bundle).unwrap()).unwrap(); +} + +#[test] +fn cellc_ls_idl_validate_bind_and_bundle_preserve_raw_digest() { + let temp = tempfile::tempdir().unwrap(); + let idl = br#"{"idl_version":"0.1","name":"demo_lock","witness":[{"name":"signature","type":"secp256k1_sig","required":true}]}"#; + let idl_path = temp.path().join("idl.json"); + let executable_path = temp.path().join("lock"); + let bound_path = temp.path().join("lock.ls-idl"); + let source_path = temp.path().join("lock.rs"); + std::fs::write(&idl_path, idl).unwrap(); + std::fs::write(&executable_path, b"\x7fELFdemo-lock").unwrap(); + std::fs::write(&source_path, b"fn main() {}").unwrap(); + + let validate = cellc_command().args(["artifact", "ls-idl", "validate", "--idl"]).arg(&idl_path).arg("--json").output().unwrap(); + assert!(validate.status.success(), "stderr: {}", String::from_utf8_lossy(&validate.stderr)); + + let bind = cellc_command() + .args(["artifact", "ls-idl", "bind", "--idl"]) + .arg(&idl_path) + .arg("--executable") + .arg(&executable_path) + .arg("--output") + .arg(&bound_path) + .arg("--json") + .output() + .unwrap(); + assert!(bind.status.success(), "stderr: {}", String::from_utf8_lossy(&bind.stderr)); + let bound = std::fs::read(&bound_path).unwrap(); + let digest: [u8; 32] = Sha256::digest(idl).into(); + assert_eq!(&bound[bound.len() - 32..], digest); + + let bundle = cellc_command() + .args(["artifact", "ls-idl", "bundle", "--idl"]) + .arg(&idl_path) + .arg("--executable") + .arg(&bound_path) + .arg("--source") + .arg(&source_path) + .args([ + "--namespace", + "cellscript", + "--name", + "demo-ls-idl-lock", + "--release", + "0.1.0", + "--language", + "rust", + "--hash-type", + "data1", + "--dep-type", + "code", + "--toolchain", + "rustc-1.97.1", + "--source-revision", + "0123456789abcdef0123456789abcdef01234567", + "--output", + "artifact.bundle.json", + "--artifact-manifest-output", + "Artifact.toml", + "--json", + ]) + .current_dir(temp.path()) + .output() + .unwrap(); + assert!(bundle.status.success(), "stderr: {}", String::from_utf8_lossy(&bundle.stderr)); + + let publish = cellc_command() + .args(["publish", "--artifact-manifest", "Artifact.toml", "--dry-run", "--json"]) + .current_dir(temp.path()) + .output() + .unwrap(); + assert!(publish.status.success(), "stderr: {}", String::from_utf8_lossy(&publish.stderr)); +} + +#[test] +fn cellc_publish_dry_run_validates_declared_non_cellscript_artifact() { + let temp = tempfile::tempdir().unwrap(); + write_declared_artifact_fixture(temp.path()); + let output = cellc_command() + .arg("publish") + .arg("--artifact-manifest") + .arg("Artifact.toml") + .arg("--dry-run") + .arg("--json") + .current_dir(temp.path()) + .output() + .unwrap(); + assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr)); + let result: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(result["status"], "valid"); + assert_eq!(result["coordinate"], "cellscript/rust-contract@1.0.0"); + assert_eq!(result["artifact"]["kind"], "deployable_contract"); + assert_eq!(result["artifact"]["profile"], "ckb_executable"); + assert_eq!(result["artifact"]["consumption_mode"], "deployment"); + assert_eq!(result["source_hash"].as_str().unwrap().len(), 64); +} + #[test] fn cellc_publish_default_requires_capability_inputs_without_writing_registry_json() { let temp = tempfile::tempdir().unwrap(); @@ -1241,8 +1508,9 @@ fn cellc_publish_default_requires_capability_inputs_without_writing_registry_jso assert!(!output.status.success(), "unexpected success: {}", String::from_utf8_lossy(&output.stdout)); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("capability key id is required for public publish"), "unexpected stderr: {stderr}"); - assert!(stderr.contains("cellc auth capability create --principal-id "), "unexpected stderr: {stderr}"); + assert!(stderr.contains("wallet-authorised publishing key"), "unexpected stderr: {stderr}"); + assert!(stderr.contains("cellc publish --authorise"), "unexpected stderr: {stderr}"); + assert!(stderr.contains("--capability-key-id"), "unexpected stderr: {stderr}"); assert!(!temp.path().join("registry.json").exists(), "default public publish must not silently write offline registry.json"); } @@ -1263,7 +1531,7 @@ fn cellc_publish_print_payload_outputs_signable_registry_publish_payload() { assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr)); let envelope: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); - assert_eq!(envelope["endpoint"], "https://api.registry.cellscript.dev/v1/packages/cellscript/demo/versions"); + assert_eq!(envelope["endpoint"], "https://api.registry.cellscript.dev/v1/artifacts/cellscript/demo/releases"); assert_eq!(envelope["payload"]["protocol"], "cellscript-registry-publish-v1"); assert_eq!(envelope["payload"]["action"], "publish"); assert_eq!(envelope["payload"]["registry_origin"], "https://api.registry.cellscript.dev"); @@ -1271,13 +1539,38 @@ fn cellc_publish_print_payload_outputs_signable_registry_publish_payload() { assert_eq!(envelope["payload"]["name"], "demo"); assert_eq!(envelope["payload"]["version"], "1.2.3"); assert_eq!(envelope["payload"]["capability_key_id"], "cap_test"); - assert_eq!(envelope["payload"]["registry_entry"]["versions"][0]["status"], "source_published"); + assert_eq!(envelope["payload"]["artifact"]["kind"], "source_library"); + assert_eq!(envelope["payload"]["registry_entry"]["versions"][0]["verification_status"], "pending"); let canonical_payload = envelope["canonical_payload"].as_str().expect("canonical payload"); let canonical_json: serde_json::Value = serde_json::from_str(canonical_payload).unwrap(); assert_eq!(canonical_json, envelope["payload"]); assert!(!temp.path().join("registry.json").exists(), "payload preview must not write offline registry.json"); } +#[test] +fn cellc_publish_profile_library_preserves_the_declared_artifact_kind() { + let temp = tempfile::tempdir().unwrap(); + write_publish_fixture_package(temp.path()); + + let output = cellc_command() + .arg("publish") + .arg("--artifact-kind") + .arg("profile_library") + .arg("--capability-key-id") + .arg("cap_test") + .arg("--print-payload") + .arg("--json") + .current_dir(temp.path()) + .output() + .unwrap(); + + assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr)); + let envelope: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(envelope["payload"]["artifact"]["kind"], "profile_library"); + assert_eq!(envelope["payload"]["artifact"]["profile"], "cellscript_source"); + assert_eq!(envelope["payload"]["registry_entry"]["artifact"]["kind"], "profile_library"); +} + #[test] fn cellc_publish_posts_signed_request_to_registry_api() { let temp = tempfile::tempdir().unwrap(); @@ -1285,8 +1578,10 @@ fn cellc_publish_posts_signed_request_to_registry_api() { let (api_url, request_rx) = start_mock_registry_api_capture_request(serde_json::json!({ "request_id": "req_test", - "status": "source_published", - "direct_url": "https://registry.cellscript.dev/packages/cellscript/demo/versions/1.2.3.json", + "verification_status": "pending", + "deployment_status": "not_applicable", + "availability_status": "active", + "direct_url": "https://registry.cellscript.dev/artifacts/cellscript/demo/releases/1.2.3.json", "snapshot_hash": "sha256:test", "verification": "queued" })); @@ -1322,9 +1617,10 @@ fn cellc_publish_posts_signed_request_to_registry_api() { assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr)); let response: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); - assert_eq!(response["status"], "source_published"); + assert_eq!(response["verification_status"], "pending"); + assert_eq!(response["deployment_status"], "not_applicable"); let request = request_rx.recv_timeout(Duration::from_secs(5)).expect("registry API request"); - assert_eq!(request.path, "/v1/packages/cellscript/demo/versions"); + assert_eq!(request.path, "/v1/artifacts/cellscript/demo/releases"); assert!( request .header("idempotency-key") @@ -1350,8 +1646,10 @@ fn cellc_publish_honors_explicit_idempotency_key() { let (api_url, request_rx) = start_mock_registry_api_capture_request(serde_json::json!({ "request_id": "req_test", - "status": "source_published", - "direct_url": "https://registry.cellscript.dev/packages/cellscript/demo/versions/1.2.3.json", + "verification_status": "pending", + "deployment_status": "not_applicable", + "availability_status": "active", + "direct_url": "https://registry.cellscript.dev/artifacts/cellscript/demo/releases/1.2.3.json", "snapshot_hash": "sha256:test", "verification": "queued" })); @@ -1398,8 +1696,10 @@ fn cellc_publish_retries_transient_registry_error_with_same_idempotency_key() { let (api_url, request_rx) = start_mock_registry_api_retry_then_success(serde_json::json!({ "request_id": "req_retry", - "status": "source_published", - "direct_url": "https://registry.cellscript.dev/packages/cellscript/demo/versions/1.2.3.json", + "verification_status": "pending", + "deployment_status": "not_applicable", + "availability_status": "active", + "direct_url": "https://registry.cellscript.dev/artifacts/cellscript/demo/releases/1.2.3.json", "snapshot_hash": "sha256:test", "verification": "queued" })); @@ -1446,7 +1746,7 @@ fn cellc_publish_retries_transient_registry_error_with_same_idempotency_key() { } #[test] -fn cellc_auth_capability_submit_posts_joyid_signature_to_registry_api() { +fn cellc_auth_capability_submit_posts_ckb_wallet_signature_to_registry_api() { let temp = tempfile::tempdir().unwrap(); let (api_url, request_rx) = start_mock_registry_api_expect_path( "/v1/capabilities", @@ -1463,7 +1763,9 @@ fn cellc_auth_capability_submit_posts_joyid_signature_to_registry_api() { .arg("--registry-origin") .arg(&api_url) .arg("--principal-id") - .arg("0x1111111111111111111111111111111111111111") + .arg(format!("0x{}", "11".repeat(32))) + .arg("--principal-type") + .arg("ckb_secp256k1") .arg("--capability-pubkey") .arg("p256-spki:test") .arg("--scope") @@ -1474,6 +1776,71 @@ fn cellc_auth_capability_submit_posts_joyid_signature_to_registry_api() { assert!(create.status.success(), "stderr: {}", String::from_utf8_lossy(&create.stderr)); let payload: serde_json::Value = serde_json::from_slice(&create.stdout).unwrap(); let payload_path = temp.path().join("capability-payload.json"); + let signature_path = temp.path().join("wallet-signature.json"); + std::fs::write(&payload_path, serde_json::to_vec_pretty(&payload).unwrap()).unwrap(); + std::fs::write( + &signature_path, + serde_json::to_vec_pretty(&serde_json::json!({ + "scheme": "ckb_secp256k1", + "challenge": serde_json::to_string(&payload).unwrap(), + "signature": format!("0x{}", "22".repeat(65)), + "public_key": format!("0x02{}", "33".repeat(32)) + })) + .unwrap(), + ) + .unwrap(); + + let output = cellc_command() + .arg("auth") + .arg("capability") + .arg("submit") + .arg("--api-url") + .arg(&api_url) + .arg("--payload") + .arg(&payload_path) + .arg("--wallet-signature") + .arg(&signature_path) + .arg("--json") + .output() + .unwrap(); + + assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr)); + let response: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(response["status"], "active"); + let request = request_rx.recv_timeout(Duration::from_secs(5)).expect("capability request"); + assert_eq!(request["payload"], payload); + assert_eq!(request["wallet_signature"]["scheme"], "ckb_secp256k1"); +} + +#[test] +fn cellc_auth_namespace_claim_posts_signed_capability_payload_to_registry_api() { + let temp = tempfile::tempdir().unwrap(); + let (api_url, request_rx) = start_mock_registry_api_expect_path( + "/v1/namespaces/claim", + serde_json::json!({ + "request_id": "req_namespace", + "namespace": "exampleorg", + "status": "active" + }), + ); + let create = cellc_command() + .arg("auth") + .arg("capability") + .arg("create") + .arg("--registry-origin") + .arg(&api_url) + .arg("--principal-id") + .arg("0x1111111111111111111111111111111111111111") + .arg("--capability-pubkey") + .arg("p256-spki:test") + .arg("--scope") + .arg("publish:exampleorg/demo") + .arg("--json") + .output() + .unwrap(); + assert!(create.status.success(), "stderr: {}", String::from_utf8_lossy(&create.stderr)); + let payload: serde_json::Value = serde_json::from_slice(&create.stdout).unwrap(); + let payload_path = temp.path().join("capability-payload.json"); let signature_path = temp.path().join("joyid-signature.json"); std::fs::write(&payload_path, serde_json::to_vec_pretty(&payload).unwrap()).unwrap(); std::fs::write( @@ -1492,10 +1859,12 @@ fn cellc_auth_capability_submit_posts_joyid_signature_to_registry_api() { let output = cellc_command() .arg("auth") - .arg("capability") - .arg("submit") + .arg("namespace") + .arg("claim") .arg("--api-url") .arg(&api_url) + .arg("--namespace") + .arg("exampleorg") .arg("--payload") .arg(&payload_path) .arg("--joyid-signature") @@ -1507,9 +1876,10 @@ fn cellc_auth_capability_submit_posts_joyid_signature_to_registry_api() { assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr)); let response: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(response["status"], "active"); - let request = request_rx.recv_timeout(Duration::from_secs(5)).expect("capability request"); + let request = request_rx.recv_timeout(Duration::from_secs(5)).expect("namespace claim request"); + assert_eq!(request["namespace"], "exampleorg"); assert_eq!(request["payload"], payload); - assert_eq!(request["joyid_signature"]["signature"], "sig"); + assert_eq!(request["wallet_signature"]["signature"], "sig"); } #[test] @@ -1605,6 +1975,8 @@ fn cellc_publish_offline_writes_source_published_registry_fixture() { fn locked_build_from_metadata_for_test(metadata: &cellscript::CompileMetadata) -> cellscript::package::LockedBuildInfo { let abi = serde_json::json!({ + "edition": metadata.edition, + "compatibility_profile": &metadata.compatibility_profile, "metadata_schema_version": metadata.metadata_schema_version, "metadata_schema_versions": { "metadata": metadata.metadata_schema_version, @@ -1621,6 +1993,8 @@ fn locked_build_from_metadata_for_test(metadata: &cellscript::CompileMetadata) - "cell_data_codec_manifest": &metadata.cell_data_codec_manifest, }); cellscript::package::LockedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: hash_json_for_test(&metadata.compatibility_profile), compiler_version: Some(metadata.compiler_version.clone()), target_profile: Some(metadata.target_profile.name.clone()), artifact_hash: metadata.artifact_hash.clone(), @@ -1780,7 +2154,7 @@ fn read_http_request_path_headers_and_body(stream: &mut std::net::TcpStream) -> let (name, value) = line.split_once(':')?; name.eq_ignore_ascii_case("content-length").then(|| value.trim().parse::().unwrap()) }) - .unwrap(); + .unwrap_or(0); let body_start = header_end + 4; while request.len() < body_start + content_length { let read = stream.read(&mut buffer).unwrap(); @@ -1800,6 +2174,7 @@ fn write_live_registry_fixture_with(root: &std::path::Path, data_hash: &str, cod let out_point = "0xaaaa:0".to_string(); let mut lockfile = cellscript::package::Lockfile::new(); lockfile.package = cellscript::package::LockfilePackageInfo { + edition: cellscript::CURRENT_EDITION, name: "token".to_string(), version: "1.0.0".to_string(), namespace: Some("cellscript".to_string()), @@ -1807,6 +2182,8 @@ fn write_live_registry_fixture_with(root: &std::path::Path, data_hash: &str, cod compiler_source_hash: None, }; lockfile.package_build = Some(cellscript::package::LockedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), compiler_version: Some("0.20.0".to_string()), target_profile: Some("ckb".to_string()), artifact_hash: Some("artifact_hash".to_string()), @@ -1829,14 +2206,17 @@ fn write_live_registry_fixture_with(root: &std::path::Path, data_hash: &str, cod lockfile.write_to_root(root).unwrap(); let deployed = cellscript::package::DeployedManifest { - version: 1, - schema: None, + version: cellscript::package::DeployedManifest::CURRENT_VERSION, + schema: cellscript::package::DEPLOYED_MANIFEST_SCHEMA.to_string(), package: cellscript::package::DeployedPackageInfo { + edition: cellscript::CURRENT_EDITION, name: "token".to_string(), version: "1.0.0".to_string(), source_hash: Some("source_hash".to_string()), }, build: Some(cellscript::package::DeployedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), compiler_version: Some("0.20.0".to_string()), artifact_hash: Some("artifact_hash".to_string()), metadata_hash: Some("metadata_hash".to_string()), @@ -1846,6 +2226,8 @@ fn write_live_registry_fixture_with(root: &std::path::Path, data_hash: &str, cod constraints_hash: Some("constraints_hash".to_string()), }), deployments: vec![cellscript::package::DeploymentRecord { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), network: "aggron4".to_string(), chain_id: "ckb-testnet".to_string(), tx_hash: "0xaaaa".to_string(), @@ -1947,6 +2329,49 @@ action add(x: u64, y: u64) -> u64 { assert!(metadata.contains("\"constraints_metadata_schema_version\"")); } +#[test] +fn cellc_direct_elf_build_writes_and_reports_verified_sidecars() { + let dir = tempfile::tempdir().unwrap(); + let input = dir.path().join("sample.cell"); + let output = dir.path().join("sample.elf"); + std::fs::write( + &input, + r#" +module test + +action ping() -> u64 { + verification + 1 +} +"#, + ) + .unwrap(); + + let result = Command::new(env!("CARGO_BIN_EXE_cellc")) + .arg(&input) + .args(["--target", "riscv64-elf", "--json", "-o"]) + .arg(&output) + .output() + .unwrap(); + assert!(result.status.success(), "{}", String::from_utf8_lossy(&result.stderr)); + + let payload: serde_json::Value = serde_json::from_slice(&result.stdout).unwrap(); + let lowering = dir.path().join("sample.elf.lowering.json"); + let source_map = dir.path().join("sample.elf.sourcemap.json"); + assert_eq!(payload["lowering_record"], lowering.to_string_lossy().as_ref()); + assert_eq!(payload["source_map"], source_map.to_string_lossy().as_ref()); + assert!(lowering.is_file()); + assert!(source_map.is_file()); + + let verify = Command::new(env!("CARGO_BIN_EXE_cellc")) + .arg("verify-artifact") + .arg(&output) + .args(["--expect-target-profile", "ckb", "--json"]) + .output() + .unwrap(); + assert!(verify.status.success(), "{}", String::from_utf8_lossy(&verify.stderr)); +} + #[test] fn cellc_verify_ckb_fixtures_accepts_standard_manifest() { let manifest = @@ -2146,6 +2571,7 @@ fn cellc_constraints_subcommand_surfaces_ckb_deployment_manifest() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -2196,7 +2622,7 @@ action main(value: u64) -> u64 { assert_eq!(dep["tx_hash"], "0x1111111111111111111111111111111111111111111111111111111111111111"); assert_eq!(dep["index"], 0); assert_eq!(dep["hash_type"], "type"); - assert_eq!(ckb["profile_abi_contract"]["witness_abi"], "ckb-molecule-witness-args+cellscript-entry-witness-v1"); + assert_eq!(ckb["profile_abi_contract"]["witness_abi"], "ckb-molecule-witness-args-input-type-v2+cellscript-entry-witness-v1"); assert_eq!(ckb["profile_abi_contract"]["lock_args_abi"], "ckb-script-args-typed-fixed-bytes"); assert_eq!(ckb["profile_abi_contract"]["source_encoding"], "ckb-source-group-high-bit"); assert_eq!(ckb["profile_abi_contract"]["cell_dep_abi"], "ckb-cell-dep-outpoint-and-dep-group"); @@ -2279,7 +2705,7 @@ action swap(input: Pool) -> output: Pool { .unwrap(); assert!(receipt_output.status.success(), "{}", String::from_utf8_lossy(&receipt_output.stderr)); let receipt_json: serde_json::Value = serde_json::from_slice(&std::fs::read(&receipt).unwrap()).unwrap(); - assert_eq!(receipt_json["schema"], "cellscript-compile-receipt-v1"); + assert_eq!(receipt_json["schema"], "cellscript-compile-receipt-v2"); assert_eq!(receipt_json["artifact_hash"], metadata["artifact_hash"]); assert!(receipt_json["template_layout_hash"].as_str().is_some_and(|hash| hash.len() == 64)); @@ -2649,6 +3075,7 @@ fn cellc_compiles_package_with_local_path_dependency() { dep_root.join("Cell.toml"), r#" [package] +edition = "2026" name = "dep_pkg" version = "0.1.0" "#, @@ -2670,6 +3097,7 @@ resource Token has store, replace, relock, consume, burn { app_root.join("Cell.toml"), r#" [package] +edition = "2026" name = "app_pkg" version = "0.1.0" @@ -2695,6 +3123,7 @@ action pass_through(token: Token) -> Token { ) .unwrap(); + lock_package(&app_root); let output = app_root.join("build").join("main.s"); let status = Command::new(env!("CARGO_BIN_EXE_cellc")).arg(&app_root).status().unwrap(); @@ -2716,6 +3145,7 @@ fn cellc_rejects_registry_dependency_without_namespace() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -2737,28 +3167,27 @@ action ping() -> u64 { ) .unwrap(); - let output = Command::new(env!("CARGO_BIN_EXE_cellc")).arg(root).output().unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_cellc")).current_dir(root).arg("lock").output().unwrap(); assert!(!output.status.success(), "unexpected success: {}", String::from_utf8_lossy(&output.stdout)); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("registry dependency 'remote' requires a namespace"), "unexpected stderr: {}", stderr); - assert!(!root.join("build").join("main.s").exists()); - assert!(!root.join("build").join("main.s.meta.json").exists()); + assert!(!root.join("Cell.lock").exists()); } #[test] -fn cellc_build_resolves_registry_dependency_and_writes_phase1_lockfile() { +fn cellc_build_resolves_artifact_api_dependency_and_writes_lockfile() { let temp = tempfile::tempdir().unwrap(); let root = temp.path(); let dep_root = root.join("token"); let app_root = root.join("app"); - let registry_root = root.join("registry"); std::fs::create_dir_all(dep_root.join("src")).unwrap(); std::fs::write( dep_root.join("Cell.toml"), r#" [package] +edition = "2026" name = "token" version = "0.3.0" namespace = "cellscript" @@ -2777,11 +3206,13 @@ resource Token has store, replace, relock, consume, burn { ) .unwrap(); let source_hash = cellscript::package::registry::compute_source_hash(&dep_root).unwrap(); - cellscript::package::registry::RegistryIndex::append_version( - &dep_root, - "token", - "cellscript", - cellscript::package::registry::RegistryVersion { + let registry_entry = cellscript::package::registry::RegistryIndex { + schema_version: cellscript::package::registry::RegistryIndex::CURRENT_SCHEMA_VERSION, + name: "token".to_string(), + namespace: "cellscript".to_string(), + versions: vec![cellscript::package::registry::RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.3.0".to_string(), tag: "v0.3.0".to_string(), source_hash: source_hash.clone(), @@ -2797,30 +3228,100 @@ resource Token has store, replace, relock, consume, burn { yanked_reason: None, replaced_by: None, audit: None, - }, - ) - .unwrap(); - git_init(&dep_root); - git_add_all(&dep_root); - git_commit(&dep_root, "publish token"); - git_tag(&dep_root, "v0.3.0"); + }], + }; - std::fs::create_dir_all(registry_root.join("cellscript")).unwrap(); - git_init(®istry_root); - let entry = cellscript::package::registry::DiscoveryEntry { - name: "token".to_string(), - namespace: "cellscript".to_string(), - source: dep_root.to_string_lossy().to_string(), + let snapshot_file = |path: &str, content: &[u8]| { + serde_json::json!({ + "path": path, + "blake2b256": hex_lower(&cellscript::ckb_blake2b256(content)), + "content_base64": base64::engine::general_purpose::STANDARD.encode(content), + }) }; - std::fs::write(registry_root.join("cellscript/token.json"), serde_json::to_string_pretty(&entry).unwrap()).unwrap(); - git_add_all(®istry_root); - git_commit(®istry_root, "add token"); + let manifest_bytes = std::fs::read(dep_root.join("Cell.toml")).unwrap(); + let source_bytes = std::fs::read(dep_root.join("src/token.cell")).unwrap(); + let snapshot_bytes = serde_json::to_vec(&serde_json::json!({ + "schema": "cellscript-source-snapshot-v1", + "package": { "namespace": "cellscript", "name": "token", "version": "0.3.0" }, + "files": [ + snapshot_file("Cell.toml", &manifest_bytes), + snapshot_file("src/token.cell", &source_bytes), + ], + })) + .unwrap(); + let snapshot_digest = Sha256::digest(&snapshot_bytes); + let snapshot_hash = format!("sha256:{}", hex_lower(&snapshot_digest)); + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = listener.local_addr().unwrap(); + let api_origin = format!("http://{address}"); + let snapshot_path = "/source-snapshots/cellscript/token/0.3.0/token.json"; + let api_body = serde_json::json!({ + "schema": "cellscript-registry-artifact", + "namespace": "cellscript", + "name": "token", + "repository": "https://example.test/cellscript/token", + "artifact": { + "kind": "source_library", + "profile": "cellscript_source", + "consumption_mode": "dependency", + "language": "cellscript" + }, + "releases": [{ + "release": "0.3.0", + "verification_status": "verified", + "availability_status": "active", + "registry_entry": registry_entry, + "immutable_bundle": { + "schema": "cellscript-registry-immutable-bundle", + "url": format!("{api_origin}{snapshot_path}"), + "snapshot_hash": snapshot_hash, + "source_hash": source_hash, + "size_bytes": snapshot_bytes.len(), + "content_type": "application/vnd.cellscript.source-snapshot+json" + } + }] + }) + .to_string(); + let served_snapshot = snapshot_bytes.clone(); + listener.set_nonblocking(true).unwrap(); + let stop_server = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let server_stop = std::sync::Arc::clone(&stop_server); + let server = std::thread::spawn(move || { + while !server_stop.load(std::sync::atomic::Ordering::Acquire) { + match listener.accept() { + Ok((mut stream, _)) => { + stream.set_nonblocking(false).unwrap(); + let (path, _) = read_http_request_path_and_body(&mut stream); + assert!(matches!( + path.as_str(), + "/v1/artifacts/cellscript/token" | "/source-snapshots/cellscript/token/0.3.0/token.json" + )); + let (body, content_type) = if path == snapshot_path { + (served_snapshot.as_slice(), "application/vnd.cellscript.source-snapshot+json") + } else { + (api_body.as_bytes(), "application/json") + }; + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + stream.write_all(response.as_bytes()).unwrap(); + stream.write_all(body).unwrap(); + } + Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { + std::thread::sleep(std::time::Duration::from_millis(5)); + } + Err(error) => panic!("artifact API mock failed: {error}"), + } + } + }); std::fs::create_dir_all(app_root.join("src")).unwrap(); std::fs::write( app_root.join("Cell.toml"), r#" [package] +edition = "2026" name = "app" version = "0.1.0" namespace = "cellscript" @@ -2846,14 +3347,23 @@ action pass_through(token: Token) -> Token { ) .unwrap(); + let lock = Command::new(env!("CARGO_BIN_EXE_cellc")) + .arg("lock") + .env(cellscript::package::registry::REGISTRY_API_URL_ENV, &api_origin) + .current_dir(&app_root) + .output() + .unwrap(); + assert!(lock.status.success(), "stderr: {}", String::from_utf8_lossy(&lock.stderr)); + let output = Command::new(env!("CARGO_BIN_EXE_cellc")) .arg("build") - .env(cellscript::package::registry::REGISTRY_URL_ENV, ®istry_root) + .arg("--locked") + .env(cellscript::package::registry::REGISTRY_API_URL_ENV, &api_origin) .current_dir(&app_root) .output() .unwrap(); - assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr)); + let lockfile: cellscript::package::Lockfile = toml::from_str(&std::fs::read_to_string(app_root.join("Cell.lock")).unwrap()).unwrap(); assert!(lockfile.package.source_hash.is_some()); @@ -2865,16 +3375,19 @@ action pass_through(token: Token) -> Token { assert!(build.schema_hash.is_some()); assert!(build.abi_hash.is_some()); assert!(build.constraints_hash.is_some()); - let token = lockfile.dependencies.get("token").expect("locked registry dependency"); + let token_node = lockfile.root.dependencies.get("token").expect("locked registry root edge"); + let token = lockfile.dependencies.get(token_node).expect("locked registry dependency"); assert_eq!(token.source_hash.as_deref(), Some(source_hash.as_str())); let verify = Command::new(env!("CARGO_BIN_EXE_cellc")) .arg("package") .arg("verify") - .env(cellscript::package::registry::REGISTRY_URL_ENV, ®istry_root) + .env(cellscript::package::registry::REGISTRY_API_URL_ENV, &api_origin) .current_dir(&app_root) .output() .unwrap(); + stop_server.store(true, std::sync::atomic::Ordering::Release); + server.join().unwrap(); assert!(verify.status.success(), "stderr: {}", String::from_utf8_lossy(&verify.stderr)); } @@ -2886,6 +3399,8 @@ fn cellc_registry_edit_yanks_existing_version() { "token", "cellscript", cellscript::package::registry::RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "1.0.0".to_string(), tag: "v1.0.0".to_string(), source_hash: "abc123".to_string(), @@ -2953,6 +3468,7 @@ fn cellc_registry_verify_json_fails_closed_for_missing_deployment_ref() { let mut lockfile = cellscript::package::Lockfile::new(); lockfile.package = cellscript::package::LockfilePackageInfo { + edition: cellscript::CURRENT_EDITION, name: "token".to_string(), version: "1.0.0".to_string(), namespace: Some("cellscript".to_string()), @@ -2960,6 +3476,8 @@ fn cellc_registry_verify_json_fails_closed_for_missing_deployment_ref() { compiler_source_hash: None, }; lockfile.package_build = Some(cellscript::package::LockedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), compiler_version: Some("0.19.0".to_string()), target_profile: Some("ckb".to_string()), artifact_hash: Some("artifact_hash".to_string()), @@ -2972,14 +3490,17 @@ fn cellc_registry_verify_json_fails_closed_for_missing_deployment_ref() { lockfile.write_to_root(root).unwrap(); let deployed = cellscript::package::DeployedManifest { - version: 1, - schema: None, + version: cellscript::package::DeployedManifest::CURRENT_VERSION, + schema: cellscript::package::DEPLOYED_MANIFEST_SCHEMA.to_string(), package: cellscript::package::DeployedPackageInfo { + edition: cellscript::CURRENT_EDITION, name: "token".to_string(), version: "1.0.0".to_string(), source_hash: Some("source_hash".to_string()), }, build: Some(cellscript::package::DeployedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), compiler_version: Some("0.19.0".to_string()), artifact_hash: Some("artifact_hash".to_string()), metadata_hash: Some("metadata_hash".to_string()), @@ -2989,6 +3510,8 @@ fn cellc_registry_verify_json_fails_closed_for_missing_deployment_ref() { constraints_hash: Some("constraints_hash".to_string()), }), deployments: vec![cellscript::package::DeploymentRecord { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), network: "aggron4".to_string(), chain_id: "ckb-testnet".to_string(), tx_hash: "0xaaaa".to_string(), @@ -3035,6 +3558,7 @@ fn write_offline_fixture_with_lineage(root: &std::path::Path, lineage: Option<&s let out_point = "0xbbbb:0".to_string(); let mut lockfile = cellscript::package::Lockfile::new(); lockfile.package = cellscript::package::LockfilePackageInfo { + edition: cellscript::CURRENT_EDITION, name: "token".to_string(), version: "1.0.0".to_string(), namespace: Some("cellscript".to_string()), @@ -3042,6 +3566,8 @@ fn write_offline_fixture_with_lineage(root: &std::path::Path, lineage: Option<&s compiler_source_hash: None, }; lockfile.package_build = Some(cellscript::package::LockedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), compiler_version: Some("0.20.0".to_string()), target_profile: Some("ckb".to_string()), artifact_hash: Some("artifact_hash".to_string()), @@ -3064,14 +3590,17 @@ fn write_offline_fixture_with_lineage(root: &std::path::Path, lineage: Option<&s lockfile.write_to_root(root).unwrap(); let deployed = cellscript::package::DeployedManifest { - version: 1, - schema: None, + version: cellscript::package::DeployedManifest::CURRENT_VERSION, + schema: cellscript::package::DEPLOYED_MANIFEST_SCHEMA.to_string(), package: cellscript::package::DeployedPackageInfo { + edition: cellscript::CURRENT_EDITION, name: "token".to_string(), version: "1.0.0".to_string(), source_hash: Some("source_hash".to_string()), }, build: Some(cellscript::package::DeployedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), compiler_version: Some("0.20.0".to_string()), artifact_hash: Some("artifact_hash".to_string()), metadata_hash: Some("metadata_hash".to_string()), @@ -3081,6 +3610,8 @@ fn write_offline_fixture_with_lineage(root: &std::path::Path, lineage: Option<&s constraints_hash: Some("constraints_hash".to_string()), }), deployments: vec![cellscript::package::DeploymentRecord { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), network: "aggron4".to_string(), chain_id: "ckb-testnet".to_string(), tx_hash: "0xbbbb".to_string(), @@ -3402,6 +3933,7 @@ fn cellc_rejects_underdeclared_effects_from_path_dependency_calls() { dep_root.join("Cell.toml"), r#" [package] +edition = "2026" name = "dep_pkg" version = "0.1.0" "#, @@ -3431,6 +3963,7 @@ action issue(amount: u64) -> Token { app_root.join("Cell.toml"), r#" [package] +edition = "2026" name = "app_pkg" version = "0.1.0" @@ -3456,6 +3989,7 @@ action wrapper(amount: u64) -> Token { ) .unwrap(); + lock_package(&app_root); let output = Command::new(env!("CARGO_BIN_EXE_cellc")).arg(&app_root).output().unwrap(); assert!(!output.status.success(), "unexpected success: {}", String::from_utf8_lossy(&output.stdout)); let stderr = String::from_utf8_lossy(&output.stderr); @@ -3499,6 +4033,7 @@ fn cellc_compiles_external_dependency_function_calls() { dep_root.join("Cell.toml"), r#" [package] +edition = "2026" name = "dep_pkg" version = "0.1.0" "#, @@ -3520,6 +4055,7 @@ fn add_one(x: u64) -> u64 { app_root.join("Cell.toml"), r#" [package] +edition = "2026" name = "app_pkg" version = "0.1.0" @@ -3541,6 +4077,7 @@ action run(x: u64) -> u64 { ) .unwrap(); + lock_package(&app_root); let output = Command::new(env!("CARGO_BIN_EXE_cellc")).arg(&app_root).output().unwrap(); assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr)); @@ -3564,6 +4101,7 @@ fn cellc_compiles_aliased_external_dependency_function_calls() { dep_root.join("Cell.toml"), r#" [package] +edition = "2026" name = "dep_pkg" version = "0.1.0" "#, @@ -3585,6 +4123,7 @@ fn add_one(x: u64) -> u64 { app_root.join("Cell.toml"), r#" [package] +edition = "2026" name = "app_pkg" version = "0.1.0" @@ -3609,6 +4148,7 @@ action run(x: u64) -> u64 { ) .unwrap(); + lock_package(&app_root); let output = Command::new(env!("CARGO_BIN_EXE_cellc")).arg(&app_root).output().unwrap(); assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr)); @@ -3640,6 +4180,7 @@ fn cellc_compiles_same_basename_external_dependency_function_calls_without_colli format!( r#" [package] +edition = "2026" name = "{package}" version = "0.1.0" "# @@ -3665,6 +4206,7 @@ fn add_one(x: u64) -> u64 {{ app_root.join("Cell.toml"), r#" [package] +edition = "2026" name = "app_pkg" version = "0.1.0" @@ -3687,6 +4229,7 @@ action run(x: u64) -> u64 { ) .unwrap(); + lock_package(&app_root); let output = Command::new(env!("CARGO_BIN_EXE_cellc")).arg(&app_root).output().unwrap(); assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr)); @@ -3716,6 +4259,7 @@ fn cellc_compiles_transitive_external_dependency_function_calls() { dep_root.join("Cell.toml"), r#" [package] +edition = "2026" name = "dep_pkg" version = "0.1.0" "#, @@ -3741,6 +4285,7 @@ fn add_two(x: u64) -> u64 { app_root.join("Cell.toml"), r#" [package] +edition = "2026" name = "app_pkg" version = "0.1.0" @@ -3762,6 +4307,7 @@ action run(x: u64) -> u64 { ) .unwrap(); + lock_package(&app_root); let output = Command::new(env!("CARGO_BIN_EXE_cellc")).arg(&app_root).output().unwrap(); assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr)); @@ -3785,6 +4331,7 @@ fn cellc_uses_manifest_build_out_dir_for_package_input() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -3826,6 +4373,7 @@ fn cellc_cli_target_overrides_manifest_build_target() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -3868,6 +4416,7 @@ fn cellc_uses_manifest_build_target_by_default() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -3910,6 +4459,7 @@ fn cellc_build_and_check_subcommands_use_package_flow() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -3972,6 +4522,7 @@ fn cellc_check_all_targets_checks_asm_and_elf_without_writing_artifacts() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -4032,6 +4583,7 @@ fn cellc_check_json_reports_multiple_compile_diagnostics() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -4077,6 +4629,7 @@ fn cellc_check_json_reports_multiple_parse_diagnostics() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -4121,6 +4674,7 @@ fn cellc_check_json_reports_diagnostics_on_stdout() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -4189,6 +4743,7 @@ fn cellc_check_json_reports_multiple_ir_diagnostics() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -4251,6 +4806,7 @@ fn cellc_build_accepts_pure_ckb_target_profile_without_vm_abi_trailer() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -4322,6 +4878,7 @@ fn cellc_check_accepts_pure_ckb_target_profile() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -4369,6 +4926,7 @@ fn cellc_check_accepts_ckb_profile_timepoint() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -4403,6 +4961,7 @@ fn cellc_check_production_rejects_fail_closed_runtime_paths() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -4452,6 +5011,7 @@ fn cellc_errors_include_runtime_ecode_when_policy_failure_maps_to_runtime_regist root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -4491,6 +5051,7 @@ fn cellc_check_production_rejects_incomplete_output_verification() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -4540,6 +5101,7 @@ fn cellc_check_can_reject_runtime_required_obligations() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -4611,6 +5173,7 @@ fn cellc_check_reports_transaction_invariant_checked_subconditions() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -4768,6 +5331,7 @@ fn cellc_check_reports_resource_conservation_blocker_class() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -4838,6 +5402,7 @@ fn cellc_check_reports_explicit_output_binding_without_mutable_state_blockers() root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -4894,6 +5459,7 @@ fn cellc_check_reports_settle_finalization_blocker_class() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -4965,6 +5531,7 @@ fn cellc_check_rejects_cell_backed_vec_with_source_aware_guidance() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -5022,6 +5589,7 @@ fn cellc_check_accepts_u128_mutable_state_transition_with_u64_delta() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -5074,6 +5642,7 @@ fn cellc_check_rejects_undeclared_flow_edge() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -5126,6 +5695,7 @@ fn cellc_check_accepts_declared_cyclic_flow_edge() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -5177,6 +5747,7 @@ fn cellc_check_accepts_declared_linear_flow_edge() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -5223,6 +5794,7 @@ fn cellc_check_rejects_flow_create_missing_state_field() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -5266,6 +5838,7 @@ fn cellc_check_rejects_initial_flow_create_non_static_state() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -5314,6 +5887,7 @@ fn cellc_check_rejects_flow_state_index_out_of_range() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -5359,6 +5933,7 @@ fn cellc_check_rejects_duplicate_flow_edge() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -5407,6 +5982,7 @@ fn cellc_check_rejects_transition_on_type_without_flow_block() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -5450,6 +6026,7 @@ fn cellc_check_rejects_aggregate_invariant_scope_mismatch() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -5507,6 +6084,7 @@ fn cellc_check_reports_claim_source_predicate_blocker_class() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -5586,6 +6164,7 @@ fn cellc_check_reports_pool_invariant_policy_families() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -5681,6 +6260,7 @@ fn cellc_check_reports_amm_pool_without_runtime_blockers() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -5737,6 +6317,7 @@ fn cellc_check_uses_manifest_policy_defaults() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -5788,6 +6369,7 @@ fn cellc_build_uses_manifest_policy_before_writing_artifacts() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -5843,6 +6425,7 @@ fn cellc_test_subcommand_compiles_test_sources() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -5907,6 +6490,7 @@ fn cellc_test_subcommand_supports_expected_compile_failures() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -5957,6 +6541,7 @@ fn cellc_test_subcommand_rejects_missing_expected_error_text() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -6006,6 +6591,7 @@ fn cellc_test_subcommand_supports_target_directive() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -6055,6 +6641,7 @@ fn cellc_test_subcommand_supports_policy_directives() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -6117,6 +6704,7 @@ fn cellc_test_subcommand_supports_runtime_metadata_directives() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -6183,6 +6771,7 @@ fn cellc_test_subcommand_rejects_missing_runtime_metadata() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -6232,6 +6821,7 @@ fn cellc_test_subcommand_supports_entrypoint_metadata_directives() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -6289,6 +6879,7 @@ fn cellc_test_subcommand_rejects_missing_entrypoint_metadata() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -6338,6 +6929,7 @@ fn cellc_test_subcommand_rejects_unknown_directives() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -6388,6 +6980,7 @@ fn cellc_test_subcommand_rejects_conflicting_expectations() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -6437,6 +7030,7 @@ fn cellc_doc_subcommand_generates_markdown_docs() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -6597,7 +7191,7 @@ fn cellc_explain_profile_reports_ckb_v0_14_contract() { let summary: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(summary["profile"], "ckb"); - assert_eq!(summary["witness_abi"], "ckb-molecule-witness-args+cellscript-entry-witness-v1"); + assert_eq!(summary["witness_abi"], "ckb-molecule-witness-args-input-type-v2+cellscript-entry-witness-v1"); assert_eq!(summary["lock_args_abi"], "ckb-script-args-typed-fixed-bytes"); assert_eq!(summary["source_encoding"], "ckb-source-group-high-bit"); assert_eq!(summary["spawn_ipc_abi"], "ckb-vm-v2-spawn-ipc-syscalls-2601-2608"); @@ -6841,6 +7435,7 @@ fn cellc_check_denies_metadata_only_declared_invariant() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -6889,6 +7484,7 @@ fn cellc_check_production_rejects_metadata_only_executable_claim() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -6954,6 +7550,7 @@ fn cellc_info_subcommand_supports_json_summary() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" authors = ["Audit Bot"] @@ -6986,11 +7583,23 @@ deny_fail_closed = true fn cellc_add_and_remove_subcommands_honor_dev_path_and_json() { let temp = tempfile::tempdir().unwrap(); let root = temp.path(); + std::fs::create_dir_all(root.join("src")).unwrap(); + std::fs::create_dir_all(root.join("math/src")).unwrap(); + std::fs::create_dir_all(root.join("contracts")).unwrap(); + std::fs::create_dir_all(root.join("shared")).unwrap(); + std::fs::write(root.join("src/main.cell"), "module demo;\n").unwrap(); + std::fs::write( + root.join("math/Cell.toml"), + "[package]\nedition = \"2026\"\nname = \"math\"\nversion = \"0.1.0\"\nentry = \"src/lib.cell\"\n", + ) + .unwrap(); + std::fs::write(root.join("math/src/lib.cell"), "module math;\n").unwrap(); std::fs::write( root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" entry = "src/main.cell" @@ -7009,25 +7618,30 @@ out_dir = "artifacts" .arg("add") .arg("--dev") .arg("--path") - .arg("../math") + .arg("math") .arg("--json") .arg("math") .output() .unwrap(); - assert!(add_output.status.success(), "stderr: {}", String::from_utf8_lossy(&add_output.stderr)); + assert!( + add_output.status.success(), + "stdout: {} stderr: {}", + String::from_utf8_lossy(&add_output.stdout), + String::from_utf8_lossy(&add_output.stderr) + ); let add_summary: serde_json::Value = serde_json::from_slice(&add_output.stdout).unwrap(); assert_eq!(add_summary["status"], "ok"); assert_eq!(add_summary["target"], "dev-dependencies"); assert_eq!(add_summary["added"][0], "math"); - assert_eq!(add_summary["dependency"]["path"], "../math"); + assert_eq!(add_summary["dependency"]["path"], "math"); let manifest: toml::Value = std::fs::read_to_string(root.join("Cell.toml")).unwrap().parse().unwrap(); assert_eq!(manifest["package"]["source_roots"].as_array().unwrap().len(), 2); assert_eq!(manifest["build"]["target"].as_str().unwrap(), "riscv64-elf"); assert_eq!(manifest["build"]["target_profile"].as_str().unwrap(), "ckb"); assert_eq!(manifest["build"]["out_dir"].as_str().unwrap(), "artifacts"); - assert_eq!(manifest["dev_dependencies"]["math"]["path"].as_str().unwrap(), "../math"); + assert_eq!(manifest["dev_dependencies"]["math"]["path"].as_str().unwrap(), "math"); assert!(manifest.get("dependencies").and_then(|value| value.get("math")).is_none()); let remove_output = Command::new(env!("CARGO_BIN_EXE_cellc")) @@ -7063,10 +7677,13 @@ fn cellc_install_path_updates_lockfile_and_remove_prunes_it() { std::fs::create_dir_all(dep_root.join("src")).unwrap(); std::fs::create_dir_all(util_root.join("src")).unwrap(); + std::fs::create_dir_all(root.join("src")).unwrap(); + std::fs::write(root.join("src/main.cell"), "module demo;\n").unwrap(); std::fs::write( root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -7076,8 +7693,10 @@ version = "0.1.0" dep_root.join("Cell.toml"), r#" [package] +edition = "2026" name = "math" version = "0.2.0" +entry = "src/lib.cell" [dependencies.util] version = "0.1.0" @@ -7085,15 +7704,19 @@ path = "../util" "#, ) .unwrap(); + std::fs::write(dep_root.join("src/lib.cell"), "module math;\n").unwrap(); std::fs::write( util_root.join("Cell.toml"), r#" [package] +edition = "2026" name = "util" version = "0.1.0" +entry = "src/lib.cell" "#, ) .unwrap(); + std::fs::write(util_root.join("src/lib.cell"), "module util;\n").unwrap(); let install = Command::new(env!("CARGO_BIN_EXE_cellc")) .current_dir(root) @@ -7109,24 +7732,220 @@ version = "0.1.0" assert_eq!(manifest["dependencies"]["math"]["path"].as_str().unwrap(), "math"); let lockfile: cellscript::package::Lockfile = toml::from_str(&std::fs::read_to_string(root.join("Cell.lock")).unwrap()).unwrap(); - let locked = lockfile.dependencies.get("math").expect("math should be locked"); + let math_node = lockfile.root.dependencies.get("math").expect("math root edge should be locked"); + let locked = lockfile.dependencies.get(math_node).expect("math should be locked"); assert_eq!(locked.version, "0.2.0"); assert!(matches!(&locked.source, cellscript::package::LockedSource::Path { path } if path == "math")); - let util = lockfile.dependencies.get("util").expect("transitive util should be locked"); + let util_node = locked.dependencies.get("util").expect("math should have a util edge"); + let util = lockfile.dependencies.get(util_node).expect("transitive util should be locked"); assert_eq!(util.version, "0.1.0"); let update = Command::new(env!("CARGO_BIN_EXE_cellc")).current_dir(root).arg("update").output().unwrap(); assert!(update.status.success(), "stderr: {}", String::from_utf8_lossy(&update.stderr)); let update_stdout = String::from_utf8_lossy(&update.stdout); - assert!(update_stdout.contains("Updated 2 dependencies"), "{update_stdout}"); + assert!(update_stdout.contains("Updated 2 dependency nodes"), "{update_stdout}"); assert!(!update_stdout.contains("Warning: lockfile is not consistent"), "{update_stdout}"); let remove = Command::new(env!("CARGO_BIN_EXE_cellc")).current_dir(root).arg("remove").arg("math").output().unwrap(); assert!(remove.status.success(), "stderr: {}", String::from_utf8_lossy(&remove.stderr)); let pruned: cellscript::package::Lockfile = toml::from_str(&std::fs::read_to_string(root.join("Cell.lock")).unwrap()).unwrap(); - assert!(!pruned.dependencies.contains_key("math")); - assert!(!pruned.dependencies.contains_key("util")); + assert!(pruned.root.dependencies.is_empty()); + assert!(pruned.dependencies.is_empty()); +} + +#[test] +fn cellc_build_uses_authoritative_lock_and_frozen_is_offline_and_read_only() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path(); + std::fs::create_dir_all(root.join("src")).unwrap(); + std::fs::create_dir_all(root.join("math/src")).unwrap(); + std::fs::write( + root.join("Cell.toml"), + r#" +[package] +edition = "2026" +name = "demo" +version = "0.1.0" + +[dependencies.math] +path = "math" +version = "^1.2.0" +"#, + ) + .unwrap(); + std::fs::write( + root.join("src/main.cell"), + r#" +module demo::main + +action ping(value: u64) -> u64 { + verification + value +} +"#, + ) + .unwrap(); + std::fs::write( + root.join("math/Cell.toml"), + r#" +[package] +edition = "2026" +name = "math" +version = "1.2.3" +"#, + ) + .unwrap(); + std::fs::write(root.join("math/src/lib.cell"), "module math;\n").unwrap(); + + let missing = Command::new(env!("CARGO_BIN_EXE_cellc")).current_dir(root).arg("build").output().unwrap(); + assert!(!missing.status.success()); + assert!(String::from_utf8_lossy(&missing.stderr).contains("Cell.lock is missing")); + + let lock = Command::new(env!("CARGO_BIN_EXE_cellc")).current_dir(root).arg("lock").arg("--json").output().unwrap(); + assert!(lock.status.success(), "stderr: {}", String::from_utf8_lossy(&lock.stderr)); + let summary: serde_json::Value = serde_json::from_slice(&lock.stdout).unwrap(); + assert_eq!(summary["schema"], cellscript::package::Lockfile::CURRENT_SCHEMA); + assert_eq!(summary["dependency_nodes"], 1); + + let build = Command::new(env!("CARGO_BIN_EXE_cellc")).current_dir(root).arg("build").arg("--locked").output().unwrap(); + assert!(build.status.success(), "stderr: {}", String::from_utf8_lossy(&build.stderr)); + let before_frozen = std::fs::read(root.join("Cell.lock")).unwrap(); + let frozen = + Command::new(env!("CARGO_BIN_EXE_cellc")).current_dir(root).arg("build").arg("--frozen").arg("--offline").output().unwrap(); + assert!(frozen.status.success(), "stderr: {}", String::from_utf8_lossy(&frozen.stderr)); + assert_eq!(std::fs::read(root.join("Cell.lock")).unwrap(), before_frozen); + + std::fs::write(root.join("math/src/lib.cell"), "module math;\n// source drift\n").unwrap(); + let drift = Command::new(env!("CARGO_BIN_EXE_cellc")).current_dir(root).arg("build").arg("--locked").output().unwrap(); + assert!(!drift.status.success()); + assert!(String::from_utf8_lossy(&drift.stderr).contains("source hash mismatch")); + + let update = Command::new(env!("CARGO_BIN_EXE_cellc")).current_dir(root).arg("update").output().unwrap(); + assert!(update.status.success(), "stderr: {}", String::from_utf8_lossy(&update.stderr)); + let rebuilt = Command::new(env!("CARGO_BIN_EXE_cellc")).current_dir(root).arg("build").arg("--locked").output().unwrap(); + assert!(rebuilt.status.success(), "stderr: {}", String::from_utf8_lossy(&rebuilt.stderr)); +} + +#[test] +fn bundled_scenario_basics_executes_positive_and_exact_negative_cases_on_both_backends() { + let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/scenario_basics"); + let lock_before = std::fs::read(root.join("Cell.lock")).expect("scenario example must carry a tracked lockfile"); + + let graph_only_verify = + Command::new(env!("CARGO_BIN_EXE_cellc")).current_dir(&root).args(["package", "verify", "--json"]).output().unwrap(); + assert!(!graph_only_verify.status.success()); + assert!( + String::from_utf8_lossy(&graph_only_verify.stdout).contains("Cell.lock has no [package.build]") + || String::from_utf8_lossy(&graph_only_verify.stderr).contains("Cell.lock has no [package.build]") + ); + + let output = Command::new(env!("CARGO_BIN_EXE_cellc")) + .current_dir(&root) + .args(["test", "--frozen", "--offline", "--backend", "all", "--json"]) + .output() + .unwrap(); + assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr)); + + let report: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(report["status"], "ok"); + assert_eq!(report["scenario_files"], 2); + assert_eq!(report["scenario_runs"], 4); + let scenarios = report["scenarios"].as_array().expect("scenario reports"); + assert!(scenarios.iter().any(|row| { + row["scenario"] == "bundled-positive-entry" + && row["backend"] == "simulator" + && row["evidence_tier"] == "development-non-consensus" + })); + assert!(scenarios.iter().any(|row| { + row["scenario"] == "bundled-positive-entry" && row["backend"] == "ckb-vm" && row["evidence_tier"] == "authoritative-runtime" + })); + let exact_negative = scenarios.iter().filter(|row| row["scenario"] == "bundled-exact-runtime-error").collect::>(); + assert_eq!(exact_negative.len(), 2, "exact-negative scenario should run once per backend"); + assert!(exact_negative.iter().all(|row| { + row["steps"][0]["status"] == "expected-runtime-error" + && row["steps"][0]["runtime_error"]["code"] == 5 + && row["steps"][0]["runtime_error"]["name"] == "assertion-failed" + })); + + let build = Command::new(env!("CARGO_BIN_EXE_cellc")) + .current_dir(&root) + .args(["build", "--frozen", "--offline", "--json"]) + .output() + .unwrap(); + assert!(build.status.success(), "stderr: {}", String::from_utf8_lossy(&build.stderr)); + for file in ["main.elf", "main.elf.meta.json", "main.elf.lowering.json", "main.elf.sourcemap.json"] { + assert!(root.join("build").join(file).is_file(), "verified-artifact example should emit {file}"); + } + let verify = Command::new(env!("CARGO_BIN_EXE_cellc")) + .current_dir(&root) + .args(["verify-artifact", "build/main.elf", "--verify-sources", "--json"]) + .output() + .unwrap(); + assert!(verify.status.success(), "stderr: {}", String::from_utf8_lossy(&verify.stderr)); + let verify_report: serde_json::Value = serde_json::from_slice(&verify.stdout).unwrap(); + assert_eq!(verify_report["status"], "ok"); + assert_eq!(verify_report["structural_verification"], "verified"); + assert_eq!(verify_report["sources_verified"], true); + std::fs::remove_dir_all(root.join("build")).expect("remove generated bundled-example artifacts"); + + assert_eq!( + std::fs::read(root.join("Cell.lock")).unwrap(), + lock_before, + "frozen scenario execution must not rewrite the tracked graph" + ); +} + +#[test] +fn bundled_package_graph_exercises_alias_features_test_scope_and_ckb_environments() { + let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/package_graph"); + let manifest = std::fs::read_to_string(root.join("Cell.toml")).expect("package graph manifest"); + for needle in [ + "package = \"canonical_math\"", + "version = \"^1.2.0\"", + "optional = true", + "[dev_dependencies.test_support]", + "auditing = [\"dep:audit\"]", + "full = [\"auditing\"]", + "[environments.mainnet]", + "[environments.testnet]", + "[dependency_overrides.testnet.contracts]", + ] { + assert!(manifest.contains(needle), "package graph example should contain `{needle}`"); + } + + let lock_before = std::fs::read(root.join("Cell.lock")).expect("package graph example must carry a tracked lockfile"); + let lock_text = String::from_utf8(lock_before.clone()).unwrap(); + for needle in [ + "schema = \"cellscript-lock-v0.24-graph-v1\"", + "[environments.mainnet.dependencies]", + "[environments.mainnet.dev_dependencies]", + "[environments.testnet.dependencies]", + "[environments.testnet.dev_dependencies]", + "network_contracts@1.0.0|path:deps/contracts-mainnet|env=mainnet", + "network_contracts@2.0.0|path:deps/contracts-testnet|env=testnet", + ] { + assert!(lock_text.contains(needle), "package graph lock should contain `{needle}`"); + } + + let missing_environment = + Command::new(env!("CARGO_BIN_EXE_cellc")).current_dir(&root).args(["check", "--frozen", "--offline"]).output().unwrap(); + assert!(!missing_environment.status.success()); + assert!(String::from_utf8_lossy(&missing_environment.stderr).contains("--environment")); + + for args in [ + vec!["check", "--frozen", "--offline", "--environment", "mainnet"], + vec!["check", "--frozen", "--offline", "--environment", "testnet", "--features", "full"], + vec!["test", "--no-run", "--frozen", "--offline", "--environment", "testnet", "--all-features"], + ] { + let output = Command::new(env!("CARGO_BIN_EXE_cellc")).current_dir(&root).args(&args).output().unwrap(); + assert!(output.status.success(), "cellc {} failed: {}", args.join(" "), String::from_utf8_lossy(&output.stderr)); + } + assert_eq!( + std::fs::read(root.join("Cell.lock")).unwrap(), + lock_before, + "frozen package-graph commands must not rewrite the tracked graph" + ); } #[test] @@ -7139,6 +7958,7 @@ fn cellc_metadata_subcommand_emits_lowering_runtime_json() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -7196,6 +8016,7 @@ fn cellc_metadata_reports_multiple_compile_diagnostics() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -7234,6 +8055,7 @@ fn cellc_explain_generics_reports_checked_vec_instantiations() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -7348,6 +8170,7 @@ fn cellc_action_build_emits_builder_plan_json() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -7437,7 +8260,10 @@ action mint(amount: u64) -> Token { assert_eq!(plan["adapter_contract"]["accepted_output_state"], "AcceptedActionTx"); assert_eq!(plan["adapter_contract"]["must_not_infer_protocol_semantics_from_action_name"], true); assert_eq!(plan["adapter_contract"]["witness_policy"]["entry_payload_abi"], "cellscript-entry-witness-v1"); + assert_eq!(plan["adapter_contract"]["witness_policy"]["placement_abi"], "cellscript-witnessargs-input-type-v2"); assert_eq!(plan["adapter_contract"]["witness_policy"]["default_action_payload_field"], "input_type"); + assert_eq!(plan["adapter_contract"]["witness_policy"]["runtime_source"], "group-input-0-then-group-output-0"); + assert_eq!(plan["adapter_contract"]["witness_policy"]["raw_v1_compatible"], false); assert_eq!(plan["adapter_contract"]["witness_policy"]["lock_signature_policy"], "explicit-adapter-owned-do-not-overwrite"); assert!(plan["adapter_contract"]["resolved_tx_required_fields"] .as_array() @@ -7464,6 +8290,7 @@ fn cellc_action_build_emits_runtime_required_scan_selectors() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -7534,6 +8361,7 @@ fn cellc_action_build_emits_cellfabric_intent_envelope() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -7665,6 +8493,7 @@ fn write_xudt_package(root: &std::path::Path, source: &str) { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -7886,6 +8715,7 @@ fn cellc_atomic_swap_full_lifecycle_build_check_audit_receipt() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -7990,6 +8820,7 @@ fn cellc_multi_phase_dao_flow_lifecycle_build_check_audit_receipt() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -8135,6 +8966,7 @@ fn cellc_multi_phase_dao_rejects_undeclared_state_transition() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -8186,7 +9018,14 @@ fn cellc_cross_module_launch_composition_distributes_correctly() { // audit lifecycle and the eight-output distribution shape. let launch_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples").join("launch"); - let build = Command::new(env!("CARGO_BIN_EXE_cellc")).arg(&launch_path).output().unwrap(); + let lock_before = std::fs::read(launch_path.join("Cell.lock")).expect("bundled launch package must carry a tracked lockfile"); + let build = Command::new(env!("CARGO_BIN_EXE_cellc")) + .current_dir(&launch_path) + .arg("build") + .arg("--frozen") + .arg("--offline") + .output() + .unwrap(); assert!(build.status.success(), "build failed: {}", String::from_utf8_lossy(&build.stderr)); let metadata: serde_json::Value = @@ -8218,6 +9057,11 @@ fn cellc_cross_module_launch_composition_distributes_correctly() { let bundle: serde_json::Value = serde_json::from_slice(&std::fs::read(audit_dir.path().join("audit-bundle.json")).unwrap()).unwrap(); assert_eq!(bundle["protocol_graph"]["schema"], "cellscript-protocol-graph-v0.22"); + assert_eq!( + std::fs::read(launch_path.join("Cell.lock")).unwrap(), + lock_before, + "frozen/offline build and audit must not rewrite the tracked dependency graph" + ); } #[test] @@ -8230,6 +9074,7 @@ fn cellc_gen_builder_typescript_emits_package_scaffold() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -8303,7 +9148,7 @@ action mint(amount: u64, owner: Address) -> Token { let manifest: serde_json::Value = serde_json::from_slice(&std::fs::read(output_dir.join("cellscript-builder-manifest.json")).unwrap()).unwrap(); - assert_eq!(manifest["schema"], "cellscript-generated-action-builder-v0.20"); + assert_eq!(manifest["schema"], "cellscript-generated-action-builder-v0.23-edition-2026"); assert_eq!(manifest["target"], "typescript"); assert_eq!(manifest["actions"][0]["name"], "mint"); assert_eq!(manifest["cell_data_codec_manifest"]["schema"], "cellscript-cell-data-codec-manifest-v1"); @@ -8396,6 +9241,7 @@ fn cellc_gen_builder_typescript_declares_raw_cell_data_codec_manifest() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "raw-codec-demo" version = "0.1.0" @@ -8479,6 +9325,7 @@ fn cellc_gen_builder_lockfile_identity_fails_closed() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -8521,15 +9368,19 @@ action mint(amount: u64, owner: Address) -> Token { let deployment_out_point = "0xaaaa:0"; let package_source_hash = "package-registry-source-hash".to_string(); let mut lockfile = cellscript::package::Lockfile { - version: 1, + version: cellscript::package::Lockfile::CURRENT_VERSION, + schema: cellscript::package::Lockfile::CURRENT_SCHEMA.to_string(), package: cellscript::package::LockfilePackageInfo { + edition: cellscript::CURRENT_EDITION, name: "demo".to_string(), version: "0.1.0".to_string(), namespace: None, source_hash: Some(package_source_hash.clone()), compiler_source_hash: metadata.source_hash.clone(), }, + root: Default::default(), dependencies: Default::default(), + environments: Default::default(), package_build: Some(build_info.clone()), deployment: Default::default(), }; @@ -8547,14 +9398,17 @@ action mint(amount: u64, owner: Address) -> Token { std::fs::write(&lockfile_path, toml::to_string_pretty(&lockfile).unwrap()).unwrap(); let deployed = cellscript::package::DeployedManifest { - version: 1, - schema: None, + version: cellscript::package::DeployedManifest::CURRENT_VERSION, + schema: cellscript::package::DEPLOYED_MANIFEST_SCHEMA.to_string(), package: cellscript::package::DeployedPackageInfo { + edition: cellscript::CURRENT_EDITION, name: "demo".to_string(), version: "0.1.0".to_string(), source_hash: Some(package_source_hash.clone()), }, build: Some(cellscript::package::DeployedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: build_info.compatibility_profile_hash.clone(), compiler_version: build_info.compiler_version.clone(), artifact_hash: build_info.artifact_hash.clone(), metadata_hash: build_info.metadata_hash.clone(), @@ -8564,6 +9418,8 @@ action mint(amount: u64, owner: Address) -> Token { constraints_hash: build_info.constraints_hash.clone(), }), deployments: vec![cellscript::package::DeploymentRecord { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: build_info.compatibility_profile_hash.clone(), network: deployment_network.to_string(), chain_id: "ckb-testnet".to_string(), tx_hash: "0xaaaa".to_string(), @@ -8787,6 +9643,7 @@ fn cellc_entry_witness_subcommand_emits_parameterized_witness_json() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -8823,6 +9680,10 @@ action main(amount: u64) -> u64 { let stdout: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(stdout["status"], "ok"); assert_eq!(stdout["abi"], "cellscript-entry-witness-v1"); + assert_eq!(stdout["placement_abi"], "cellscript-witnessargs-input-type-v2"); + assert_eq!(stdout["witness_args_field"], "input_type"); + assert_eq!(stdout["witness_source"], "group-input-0-then-group-output-0"); + assert_eq!(stdout["raw_v1_compatible"], false); assert_eq!(stdout["entry_kind"], "action"); assert_eq!(stdout["entry"], "main"); assert_eq!(stdout["witness_hex"], "43534152477631004d00000000000000"); @@ -8955,6 +9816,7 @@ fn cellc_abi_subcommand_explains_entry_witness_layout() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -9009,6 +9871,7 @@ fn cellc_scheduler_plan_consumes_shared_touch_hints() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -9098,6 +9961,10 @@ fn cellc_ckb_std_compat_reports_runtime_boundary() { assert_eq!(report["ckb_std_refs"]["type_id"], "ckb_std::type_id"); assert_eq!(report["inline_abi"]["fields"]["cell_occupied_capacity"], 6); assert_eq!(report["witness_args_policy"]["entry_payload_abi"], "cellscript-entry-witness-v1"); + assert_eq!(report["witness_args_policy"]["placement_abi"], "cellscript-witnessargs-input-type-v2"); + assert_eq!(report["witness_args_policy"]["default_action_payload_field"], "input_type"); + assert_eq!(report["witness_args_policy"]["runtime_source"], "group-input-0-then-group-output-0"); + assert_eq!(report["witness_args_policy"]["raw_v1_compatible"], false); assert_eq!(report["witness_args_policy"]["final_witness_args_owner"], "adapter"); assert_eq!(report["witness_args_policy"]["lock_signature_policy"], "explicit-adapter-owned-do-not-overwrite"); assert_eq!(report["adapter_boundary"]["transaction_realizer"], "ckb-sdk-rust-or-CCC-adapter"); @@ -9170,6 +10037,7 @@ fn cellc_entry_witness_subcommand_encodes_schema_backed_params() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -9222,6 +10090,7 @@ fn cellc_entry_witness_subcommand_rejects_wrong_width_fixed_bytes() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -9268,6 +10137,7 @@ fn cellc_fmt_subcommand_formats_sources() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -9315,7 +10185,7 @@ fn cellc_run_simulate_json_reports_steps_and_null_cycles() { let temp = tempfile::tempdir().unwrap(); let root = temp.path(); std::fs::create_dir_all(root.join("src")).unwrap(); - std::fs::write(root.join("Cell.toml"), "[package]\nname = \"demo\"\nversion = \"0.1.0\"\n").unwrap(); + std::fs::write(root.join("Cell.toml"), "[package]\nedition = \"2026\"\nname = \"demo\"\nversion = \"0.1.0\"\n").unwrap(); std::fs::write(root.join("src/main.cell"), "module demo::main\naction main() -> u64 {\n verification\n 0\n}\n").unwrap(); let output = Command::new(env!("CARGO_BIN_EXE_cellc")).current_dir(root).args(["run", "--simulate", "--json"]).output().unwrap(); @@ -9344,6 +10214,7 @@ fn cellc_run_subcommand_executes_pure_elf_package() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -9388,6 +10259,7 @@ fn cellc_run_subcommand_rejects_parameterized_schema_elf() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -9429,6 +10301,7 @@ fn cellc_run_subcommand_rejects_ckb_runtime_elf() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" "#, @@ -9479,6 +10352,7 @@ members = ["pkg_a", "pkg_b"] std::fs::write( pkg_a.join("Cell.toml"), r#"[package] +edition = "2026" name = "pkg_a" version = "0.1.0" "#, @@ -9502,6 +10376,7 @@ action hello() -> u64 { std::fs::write( pkg_b.join("Cell.toml"), r#"[package] +edition = "2026" name = "pkg_b" version = "0.1.0" "#, @@ -9545,6 +10420,7 @@ members = ["alpha", "beta"] std::fs::write( alpha.join("Cell.toml"), r#"[package] +edition = "2026" name = "alpha" version = "0.1.0" "#, @@ -9564,6 +10440,7 @@ action run() -> u64 { verification let x: u64 = 1 return x } std::fs::write( beta.join("Cell.toml"), r#"[package] +edition = "2026" name = "beta" version = "0.1.0" "#, @@ -9610,6 +10487,7 @@ members = ["lib_a"] std::fs::write( lib_a.join("Cell.toml"), r#"[package] +edition = "2026" name = "lib_a" version = "0.1.0" "#, @@ -9649,6 +10527,7 @@ members = ["shared_types", "app"] std::fs::write( shared.join("Cell.toml"), r#"[package] +edition = "2026" name = "shared_types" version = "0.1.0" entry = "src/types.cell" @@ -9671,6 +10550,7 @@ resource Token has store, replace, relock, consume, burn { std::fs::write( app.join("Cell.toml"), r#"[package] +edition = "2026" name = "app" version = "0.1.0" @@ -9693,6 +10573,7 @@ action passthrough(token: Token) -> Token { ) .unwrap(); + lock_package(&app); let output = Command::new(env!("CARGO_BIN_EXE_cellc")).current_dir(root).arg("build").arg("-p").arg("app").arg("--json").output().unwrap(); assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr)); @@ -9716,6 +10597,7 @@ fn cellc_incremental_cache_hit_on_second_build() { std::fs::write( root.join("Cell.toml"), r#"[package] +edition = "2026" name = "cache_test" version = "0.1.0" "#, @@ -9758,6 +10640,7 @@ fn cellc_incremental_cache_invalidated_on_source_change() { std::fs::write( root.join("Cell.toml"), r#"[package] +edition = "2026" name = "inval_test" version = "0.1.0" "#, @@ -9804,6 +10687,7 @@ fn cellc_clean_cache_flag_removes_incremental_cache() { std::fs::write( root.join("Cell.toml"), r#"[package] +edition = "2026" name = "clean_test" version = "0.1.0" "#, @@ -9852,6 +10736,7 @@ fn cellc_entry_action_bypasses_incremental_cache() { std::fs::write( root.join("Cell.toml"), r#"[package] +edition = "2026" name = "entry_bypass" version = "0.1.0" "#, @@ -9898,6 +10783,7 @@ fn cellc_install_rejects_self_path_dependency() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -9950,6 +10836,7 @@ fn cellc_install_rejects_self_name_dependency() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -10005,6 +10892,7 @@ fn cellc_add_rejects_self_name_dependency() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -10041,6 +10929,7 @@ fn cellc_build_writes_lockfile_deployment_ref_from_deployed_toml() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -10081,18 +10970,22 @@ action mint(amount: u64) -> Token { let cell_data_codec_manifest_hash = lockfile.package_build.as_ref().unwrap().cell_data_codec_manifest_hash.as_deref().unwrap(); let abi_hash = lockfile.package_build.as_ref().unwrap().abi_hash.as_deref().unwrap(); let constraints_hash = lockfile.package_build.as_ref().unwrap().constraints_hash.as_deref().unwrap(); + let compatibility_profile_hash = lockfile.package_build.as_ref().unwrap().compatibility_profile_hash.as_str(); let source_hash = lockfile.package.source_hash.as_deref().unwrap(); let compiler_version = lockfile.package_build.as_ref().unwrap().compiler_version.as_deref().unwrap(); let deployed = format!( - r#"version = 1 -schema = "cellscript-ckb-deployment-manifest-v0.19" + r#"version = 2 +schema = "cellscript-deployed-v0.23-edition-2026" [package] +edition = "2026" name = "demo" version = "0.1.0" source_hash = "{source_hash}" [build] +edition = "2026" +compatibility_profile_hash = "{compatibility_profile_hash}" compiler_version = "{compiler_version}" artifact_hash = "{artifact_hash}" metadata_hash = "{metadata_hash}" @@ -10102,6 +10995,8 @@ abi_hash = "{abi_hash}" constraints_hash = "{constraints_hash}" [[deployments]] +edition = "2026" +compatibility_profile_hash = "{compatibility_profile_hash}" name = "demo-mock" status = "active" network = "devnet" @@ -10165,6 +11060,7 @@ fn cellc_build_omits_lockfile_deployment_when_artifact_hash_mismatches() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "demo" version = "0.1.0" @@ -10195,15 +11091,18 @@ action mint(amount: u64) -> Token { // Deployed.toml with a wrong artifact_hash. The record field still points // at the out_point, but the code/out_point/data/record_hash fields must // be left None so the verifier can surface the build-identity mismatch. - let deployed = r#"version = 1 -schema = "cellscript-ckb-deployment-manifest-v0.19" + let deployed = r#"version = 2 +schema = "cellscript-deployed-v0.23-edition-2026" [package] +edition = "2026" name = "demo" version = "0.1.0" source_hash = "fake" [build] +edition = "2026" +compatibility_profile_hash = "mismatched-profile" compiler_version = "0.17.0" artifact_hash = "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" metadata_hash = "0x00" @@ -10212,6 +11111,8 @@ abi_hash = "0x00" constraints_hash = "0x00" [[deployments]] +edition = "2026" +compatibility_profile_hash = "mismatched-profile" name = "demo-mock" status = "active" network = "devnet" diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 59124764..500ff6b4 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,18 +1,14 @@ -// Keep the Edition 2024 let-chain cleanup separate from the toolchain migration. -#![allow(clippy::collapsible_if)] - use std::{path::PathBuf, process::Command}; pub fn cellc_bin() -> PathBuf { let path = PathBuf::from(env!("CARGO_BIN_EXE_cellc")); let path = if path.is_absolute() { path } else { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path) }; - if path.file_name().and_then(|name| name.to_str()) == Some("cellc") { - if let Some(debug_dir) = path.parent() { - if let Some(candidate) = newest_hashed_cellc_bin(&debug_dir.join("deps")) { - return candidate; - } - } + if path.file_name().and_then(|name| name.to_str()) == Some("cellc") + && let Some(debug_dir) = path.parent() + && let Some(candidate) = newest_hashed_cellc_bin(&debug_dir.join("deps")) + { + return candidate; } path } @@ -34,7 +30,6 @@ fn newest_hashed_cellc_bin(deps_dir: &std::path::Path) -> Option { newest.map(|(_, path)| path) } -#[allow(dead_code)] pub fn cellc_command() -> Command { Command::new(cellc_bin()) } diff --git a/tests/compat/ls_idl/README.md b/tests/compat/ls_idl/README.md new file mode 100644 index 00000000..3a1866e7 --- /dev/null +++ b/tests/compat/ls_idl/README.md @@ -0,0 +1,53 @@ +# Pinned upstream LS-IDL compatibility fixtures + +These fixtures preserve the current public LS-IDL inputs used by the projects +linked from the Nervos Talk proposal. They are test evidence, not a fork of the +protocol and not an endorsement of the example Lock Scripts. + +Pinned repositories: + +- [`OWK50GA/ckb-idl-derive`](https://github.com/OWK50GA/ckb-idl-derive) at + `e7ee35766b9084099e9d840ccd37d2b5d40074a1`; +- [`OWK50GA/ckb-idl-client`](https://github.com/OWK50GA/ckb-idl-client) at + `7d883e0abccba56d423449b673567ee817747936`; and +- [`OWK50GA/ckb_sudt_script`](https://github.com/OWK50GA/ckb_sudt_script) at + `c20ce3f4813100b78076fd447a0234bb5ad46bbb`. + +Raw-byte SHA-256 pins: + +| Fixture | SHA-256 | +| --- | --- | +| `ckb-idl-client/test-vectors.json` | `a9a6dca4fd0c5fcd2ca7aea6468784be7fdb29d6274049f07090cbab0ce9c1bb` | +| derive `multisig-2of2-nonce/idl.json` | `587098bbe12e37a7394d06ff711a59242f033759e9ba7f5b62b8f6a234275063` | +| derive `pow-lock/idl.json` | `d551803734459f28b2849f13b2111778d3753b518701a86a434e9438df86e2d6` | +| derive `schnorr-pubkey-recovery/idl.json` | `b37329b5fb13b25de94ef068724839f356096bc3516dda461b516ee983a8d371` | +| derive `secp256k1-timelock/idl.json` | `056bc4f2b11bc7f0dfead9f2dcc0ec5097b42b353d4577b3836ef872b121710f` | +| derive `simple-lock/idl.json` | `d28abead992546908eb483c24667e58302f193c00e08f6cbed1a6302995ca1c0` | +| script `simple-lock/idl.json` | `6fd2ab0171167c6862582c4e95a6de7b1cd153f77a936af7e52be6599ddddd31` | +| script `timelock-lock/idl.json` | `18ae57828b5fbd0c8df0900eed1153e7585587d4049900c50729616227a9beda` | + +The three upstream files without a final newline are stored as Base64 so Git +and patch tooling cannot silently change the bytes under test. The Rust test +decodes them before hashing or validating them. + +`tests/ls_idl_upstream.rs` admits every current upstream IDL document, pins all +17 client vectors, covers all seven current wire types, and confirms that the +one unknown-type vector still fails closed at Registry schema admission. The +separate `scripts/cellscript_ls_idl_upstream_acceptance.sh` test uses clean +checkouts at these commits and runs the actual upstream Rust client against +the Registry compatibility handler. It then creates a disposable worktree, +builds all three example contracts from the unmodified merged upstream source, +binds the two Lock Script ELFs to their exact IDL bytes, and runs all 25 +upstream CKB-VM tests against the bound artifacts. + +[Upstream PR #7](https://github.com/OWK50GA/ckb_sudt_script/pull/7) merged the +two runtime fixes first identified by this acceptance work: the contract +Makefiles enable CKB's `lower-atomic` LLVM pass, and the timelock reads the +transaction `HeaderDep` included by its tests. CellScript no longer applies a +compatibility overlay for these paths. + +This evidence establishes schema compatibility, exact-byte preservation, the +SHA-256 suffix contract, and local CKB-VM execution of the unmodified upstream +examples. +It does not establish signature correctness, production transaction validity, +or a security audit. diff --git a/tests/compat/ls_idl/ckb-idl-client-test-vectors.json.b64 b/tests/compat/ls_idl/ckb-idl-client-test-vectors.json.b64 new file mode 100644 index 00000000..6caa62d0 --- /dev/null +++ b/tests/compat/ls_idl/ckb-idl-client-test-vectors.json.b64 @@ -0,0 +1,199 @@ +ewogICJfY29tbWVudCI6ICJDS0IgSURMIHdpcmUgZm9ybWF0IHRlc3QgdmVjdG9ycy4gRWFjaCBj +YXNlIHNwZWNpZmllcyBhbiBJREwgZmllbGQgbGlzdCBhbmQgYSBoZXgtZW5jb2RlZCB3aXJlIGJ1 +ZmZlci4gVGhlICdleHBlY3QnIGZpZWxkIGlzIGVpdGhlciAndmFsaWQnIChkZWNvZGUgc3VjY2Vl +ZHMpIG9yICdlcnJvcicgKGRlY29kZSBmYWlscykuIFRoZXNlIHZlY3RvcnMgYXJlIGNhbm9uaWNh +bCBcdTIwMTQgYW55IHJlaW1wbGVtZW50YXRpb24gb2YgdGhlIGNrYi1pZGwgd2lyZSBmb3JtYXQg +TVVTVCBwcm9kdWNlIHRoZSBzYW1lIHJlc3VsdHMuIiwKICAiX3dpcmVfZm9ybWF0IjogewogICAg +InVpbnQ4IjogIjEgYnl0ZSIsCiAgICAidWludDMyIjogIjQgYnl0ZXMsIGxpdHRsZS1lbmRpYW4i +LAogICAgInVpbnQ2NCI6ICI4IGJ5dGVzLCBsaXR0bGUtZW5kaWFuIiwKICAgICJzZWNwMjU2azFf +c2lnIjogIjY1IGJ5dGVzLCBmaXhlZCIsCiAgICAic2VjcDI1NmsxX3B1YmtleSI6ICIzMyBieXRl +cywgZml4ZWQiLAogICAgInNjaG5vcnJfc2lnIjogIjY0IGJ5dGVzLCBmaXhlZCIsCiAgICAiYnl0 +ZXMiOiAiNC1ieXRlIExFIGxlbmd0aCBwcmVmaXgsIHRoZW4gdGhhdCBtYW55IGJ5dGVzIgogIH0s +CiAgInZlY3RvcnMiOiBbCiAgICB7CiAgICAgICJpZCI6ICJlbXB0eS1pZGwtZW1wdHktYnVmIiwK +ICAgICAgImRlc2NyaXB0aW9uIjogIlplcm8gZmllbGRzLCB6ZXJvIGJ5dGVzIFx1MjAxNCB0cml2 +aWFsbHkgdmFsaWQiLAogICAgICAiZmllbGRzIjogW10sCiAgICAgICJ3aXJlX2hleCI6ICIiLAog +ICAgICAiZXhwZWN0IjogInZhbGlkIiwKICAgICAgImRlY29kZWQiOiBbXQogICAgfSwKICAgIHsK +ICAgICAgImlkIjogInNpbXBsZS1sb2NrLWhlbGxvIiwKICAgICAgImRlc2NyaXB0aW9uIjogInNp +bXBsZS1sb2NrOiBwcmVpbWFnZSA9ICdoZWxsbycgKDB4Njg2NTZjNmM2ZiksIGNvcnJlY3RseSBs +ZW5ndGgtcHJlZml4ZWQiLAogICAgICAiZmllbGRzIjogWwogICAgICAgIHsKICAgICAgICAgICJu +YW1lIjogInByZWltYWdlIiwKICAgICAgICAgICJ0eXBlIjogImJ5dGVzIiwKICAgICAgICAgICJy +ZXF1aXJlZCI6IHRydWUKICAgICAgICB9CiAgICAgIF0sCiAgICAgICJ3aXJlX2hleCI6ICIwNTAw +MDAwMCA2ODY1NmM2YzZmIiwKICAgICAgImV4cGVjdCI6ICJ2YWxpZCIsCiAgICAgICJkZWNvZGVk +IjogWwogICAgICAgIHsKICAgICAgICAgICJuYW1lIjogInByZWltYWdlIiwKICAgICAgICAgICJ0 +eXBlIjogImJ5dGVzIiwKICAgICAgICAgICJ2YWx1ZV9oZXgiOiAiNjg2NTZjNmM2ZiIKICAgICAg +ICB9CiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJpZCI6ICJzaW1wbGUtbG9jay1lbXB0eS1w +cmVpbWFnZSIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJzaW1wbGUtbG9jazogemVyby1sZW5ndGgg +cHJlaW1hZ2UgXHUyMDE0IHN0cnVjdHVyYWxseSB2YWxpZCAoc2VtYW50aWMgY2hlY2sgaXMgaW4g +dGhlIFZNKSIsCiAgICAgICJmaWVsZHMiOiBbCiAgICAgICAgewogICAgICAgICAgIm5hbWUiOiAi +cHJlaW1hZ2UiLAogICAgICAgICAgInR5cGUiOiAiYnl0ZXMiLAogICAgICAgICAgInJlcXVpcmVk +IjogdHJ1ZQogICAgICAgIH0KICAgICAgXSwKICAgICAgIndpcmVfaGV4IjogIjAwMDAwMDAwIiwK +ICAgICAgImV4cGVjdCI6ICJ2YWxpZCIsCiAgICAgICJkZWNvZGVkIjogWwogICAgICAgIHsKICAg +ICAgICAgICJuYW1lIjogInByZWltYWdlIiwKICAgICAgICAgICJ0eXBlIjogImJ5dGVzIiwKICAg +ICAgICAgICJ2YWx1ZV9oZXgiOiAiIgogICAgICAgIH0KICAgICAgXQogICAgfSwKICAgIHsKICAg +ICAgImlkIjogInNpbXBsZS1sb2NrLW5vLWxlbmd0aC1wcmVmaXgiLAogICAgICAiZGVzY3JpcHRp +b24iOiAic2ltcGxlLWxvY2s6IHJhdyBieXRlcyB3aXRoIG5vIGxlbmd0aCBwcmVmaXggXHUyMDE0 +IHRvbyBzaG9ydCBmb3IgNC1ieXRlIHByZWZpeCIsCiAgICAgICJmaWVsZHMiOiBbCiAgICAgICAg +ewogICAgICAgICAgIm5hbWUiOiAicHJlaW1hZ2UiLAogICAgICAgICAgInR5cGUiOiAiYnl0ZXMi +LAogICAgICAgICAgInJlcXVpcmVkIjogdHJ1ZQogICAgICAgIH0KICAgICAgXSwKICAgICAgIndp +cmVfaGV4IjogIjYxNjI2MyIsCiAgICAgICJleHBlY3QiOiAiZXJyb3IiLAogICAgICAiZXJyb3Ii +OiAiRmllbGRUb29TaG9ydCIsCiAgICAgICJlcnJvcl9kZXRhaWwiOiB7CiAgICAgICAgImZpZWxk +IjogInByZWltYWdlIiwKICAgICAgICAiZXhwZWN0ZWQiOiA0LAogICAgICAgICJnb3QiOiAzCiAg +ICAgIH0KICAgIH0sCiAgICB7CiAgICAgICJpZCI6ICJzaW1wbGUtbG9jay10cmFpbGluZy1ieXRl +cyIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJzaW1wbGUtbG9jazogdmFsaWQgcHJlaW1hZ2UgZm9s +bG93ZWQgYnkgNCBleHRyYSBieXRlcyBcdTIwMTQgcmVqZWN0ZWQiLAogICAgICAiZmllbGRzIjog +WwogICAgICAgIHsKICAgICAgICAgICJuYW1lIjogInByZWltYWdlIiwKICAgICAgICAgICJ0eXBl +IjogImJ5dGVzIiwKICAgICAgICAgICJyZXF1aXJlZCI6IHRydWUKICAgICAgICB9CiAgICAgIF0s +CiAgICAgICJ3aXJlX2hleCI6ICIwNTAwMDAwMCA2ODY1NmM2YzZmIDQ0MzMyMjExIiwKICAgICAg +ImV4cGVjdCI6ICJlcnJvciIsCiAgICAgICJlcnJvciI6ICJUcmFpbGluZ0J5dGVzIiwKICAgICAg +ImVycm9yX2RldGFpbCI6IHsKICAgICAgICAidHJhaWxpbmciOiA0CiAgICAgIH0KICAgIH0sCiAg +ICB7CiAgICAgICJpZCI6ICJ1aW50OC1yb3VuZHRyaXAiLAogICAgICAiZGVzY3JpcHRpb24iOiAi +U2luZ2xlIHVpbnQ4IGZpZWxkLCB2YWx1ZSA0MiIsCiAgICAgICJmaWVsZHMiOiBbCiAgICAgICAg +ewogICAgICAgICAgIm5hbWUiOiAiZGlmZmljdWx0eSIsCiAgICAgICAgICAidHlwZSI6ICJ1aW50 +OCIsCiAgICAgICAgICAicmVxdWlyZWQiOiB0cnVlCiAgICAgICAgfQogICAgICBdLAogICAgICAi +d2lyZV9oZXgiOiAiMmEiLAogICAgICAiZXhwZWN0IjogInZhbGlkIiwKICAgICAgImRlY29kZWQi +OiBbCiAgICAgICAgewogICAgICAgICAgIm5hbWUiOiAiZGlmZmljdWx0eSIsCiAgICAgICAgICAi +dHlwZSI6ICJ1aW50OCIsCiAgICAgICAgICAidmFsdWVfdTY0IjogNDIKICAgICAgICB9CiAgICAg +IF0KICAgIH0sCiAgICB7CiAgICAgICJpZCI6ICJ1aW50MzItcm91bmR0cmlwIiwKICAgICAgImRl +c2NyaXB0aW9uIjogIlNpbmdsZSB1aW50MzIgZmllbGQsIHZhbHVlIDB4REVBREJFRUYgbGl0dGxl +LWVuZGlhbiIsCiAgICAgICJmaWVsZHMiOiBbCiAgICAgICAgewogICAgICAgICAgIm5hbWUiOiAi +bm9uY2UiLAogICAgICAgICAgInR5cGUiOiAidWludDMyIiwKICAgICAgICAgICJyZXF1aXJlZCI6 +IHRydWUKICAgICAgICB9CiAgICAgIF0sCiAgICAgICJ3aXJlX2hleCI6ICJlZmJlYWRkZSIsCiAg +ICAgICJleHBlY3QiOiAidmFsaWQiLAogICAgICAiZGVjb2RlZCI6IFsKICAgICAgICB7CiAgICAg +ICAgICAibmFtZSI6ICJub25jZSIsCiAgICAgICAgICAidHlwZSI6ICJ1aW50MzIiLAogICAgICAg +ICAgInZhbHVlX3U2NCI6IDM3MzU5Mjg1NTkKICAgICAgICB9CiAgICAgIF0KICAgIH0sCiAgICB7 +CiAgICAgICJpZCI6ICJ1aW50NjQtcm91bmR0cmlwIiwKICAgICAgImRlc2NyaXB0aW9uIjogIlNp +bmdsZSB1aW50NjQgZmllbGQsIHZhbHVlIDE3MDAwMDAwMDAwMDAgbGl0dGxlLWVuZGlhbiIsCiAg +ICAgICJmaWVsZHMiOiBbCiAgICAgICAgewogICAgICAgICAgIm5hbWUiOiAidW5sb2NrX2FmdGVy +X21zIiwKICAgICAgICAgICJ0eXBlIjogInVpbnQ2NCIsCiAgICAgICAgICAicmVxdWlyZWQiOiB0 +cnVlCiAgICAgICAgfQogICAgICBdLAogICAgICAid2lyZV9oZXgiOiAiMDA2OGU1Y2Y4YjAxMDAw +MCIsCiAgICAgICJleHBlY3QiOiAidmFsaWQiLAogICAgICAiZGVjb2RlZCI6IFsKICAgICAgICB7 +CiAgICAgICAgICAibmFtZSI6ICJ1bmxvY2tfYWZ0ZXJfbXMiLAogICAgICAgICAgInR5cGUiOiAi +dWludDY0IiwKICAgICAgICAgICJ2YWx1ZV91NjQiOiAxNzAwMDAwMDAwMDAwCiAgICAgICAgfQog +ICAgICBdCiAgICB9LAogICAgewogICAgICAiaWQiOiAic2VjcDI1NmsxLXNpZy1hbGwtemVyb3Mi +LAogICAgICAiZGVzY3JpcHRpb24iOiAic2VjcDI1NmsxX3NpZyBmaWVsZCwgNjUgemVybyBieXRl +cyBcdTIwMTQgc3RydWN0dXJhbGx5IHZhbGlkIChzZW1hbnRpYyBjaGVjayBpcyBpbiB0aGUgVk0p +IiwKICAgICAgImZpZWxkcyI6IFsKICAgICAgICB7CiAgICAgICAgICAibmFtZSI6ICJzaWduYXR1 +cmUiLAogICAgICAgICAgInR5cGUiOiAic2VjcDI1NmsxX3NpZyIsCiAgICAgICAgICAicmVxdWly +ZWQiOiB0cnVlCiAgICAgICAgfQogICAgICBdLAogICAgICAid2lyZV9oZXgiOiAiMDAwMDAwMDAw +MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw +MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw +MDAwMDAwMCIsCiAgICAgICJleHBlY3QiOiAidmFsaWQiLAogICAgICAiZGVjb2RlZCI6IFsKICAg +ICAgICB7CiAgICAgICAgICAibmFtZSI6ICJzaWduYXR1cmUiLAogICAgICAgICAgInR5cGUiOiAi +c2VjcDI1NmsxX3NpZyIsCiAgICAgICAgICAidmFsdWVfaGV4IjogIjAwMDAwMDAwMDAwMDAwMDAw +MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw +MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAi +CiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgewogICAgICAiaWQiOiAic2VjcDI1NmsxLXNp +Zy10b28tc2hvcnQiLAogICAgICAiZGVzY3JpcHRpb24iOiAic2VjcDI1NmsxX3NpZyBmaWVsZCwg +b25seSAxMCBieXRlcyBwcm92aWRlZCBcdTIwMTQgcmVqZWN0ZWQiLAogICAgICAiZmllbGRzIjog +WwogICAgICAgIHsKICAgICAgICAgICJuYW1lIjogInNpZ25hdHVyZSIsCiAgICAgICAgICAidHlw +ZSI6ICJzZWNwMjU2azFfc2lnIiwKICAgICAgICAgICJyZXF1aXJlZCI6IHRydWUKICAgICAgICB9 +CiAgICAgIF0sCiAgICAgICJ3aXJlX2hleCI6ICIwMTAyMDMwNDA1MDYwNzA4MDkxMCIsCiAgICAg +ICJleHBlY3QiOiAiZXJyb3IiLAogICAgICAiZXJyb3IiOiAiRmllbGRUb29TaG9ydCIsCiAgICAg +ICJlcnJvcl9kZXRhaWwiOiB7CiAgICAgICAgImZpZWxkIjogInNpZ25hdHVyZSIsCiAgICAgICAg +ImV4cGVjdGVkIjogNjUsCiAgICAgICAgImdvdCI6IDEwCiAgICAgIH0KICAgIH0sCiAgICB7CiAg +ICAgICJpZCI6ICJ0aW1lbG9jay1mdWxsLXdpdG5lc3Mtbm8tZXh0cmEiLAogICAgICAiZGVzY3Jp +cHRpb24iOiAidGltZWxvY2stbG9jazogdmFsaWQgMy1maWVsZCB3aXRuZXNzLCBlbXB0eSBleHRy +YSBwYXlsb2FkIiwKICAgICAgImZpZWxkcyI6IFsKICAgICAgICB7CiAgICAgICAgICAibmFtZSI6 +ICJzaWduYXR1cmUiLAogICAgICAgICAgInR5cGUiOiAic2VjcDI1NmsxX3NpZyIsCiAgICAgICAg +ICAicmVxdWlyZWQiOiB0cnVlCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAibmFtZSI6 +ICJ1bmxvY2tfYWZ0ZXJfbXMiLAogICAgICAgICAgInR5cGUiOiAidWludDY0IiwKICAgICAgICAg +ICJyZXF1aXJlZCI6IHRydWUKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICJuYW1lIjog +ImV4dHJhIiwKICAgICAgICAgICJ0eXBlIjogImJ5dGVzIiwKICAgICAgICAgICJyZXF1aXJlZCI6 +IGZhbHNlCiAgICAgICAgfQogICAgICBdLAogICAgICAid2lyZV9oZXgiOiAiMDEwMTAxMDEwMTAx +MDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEw +MTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAx +MDEwMSA0MDQyMGYwMDAwMDAwMDAwIDAwMDAwMDAwIiwKICAgICAgImV4cGVjdCI6ICJ2YWxpZCIs +CiAgICAgICJkZWNvZGVkIjogWwogICAgICAgIHsKICAgICAgICAgICJuYW1lIjogInNpZ25hdHVy +ZSIsCiAgICAgICAgICAidHlwZSI6ICJzZWNwMjU2azFfc2lnIiwKICAgICAgICAgICJ2YWx1ZV9o +ZXgiOiAiMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEw +MTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAx +MDEwMTAxMDEwMTAxMDEwMTAxMDEwMSIKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICJu +YW1lIjogInVubG9ja19hZnRlcl9tcyIsCiAgICAgICAgICAidHlwZSI6ICJ1aW50NjQiLAogICAg +ICAgICAgInZhbHVlX3U2NCI6IDEwMDAwMDAKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAg +ICJuYW1lIjogImV4dHJhIiwKICAgICAgICAgICJ0eXBlIjogImJ5dGVzIiwKICAgICAgICAgICJ2 +YWx1ZV9oZXgiOiAiIgogICAgICAgIH0KICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImlkIjog +InRpbWVsb2NrLWZ1bGwtd2l0bmVzcy13aXRoLWV4dHJhIiwKICAgICAgImRlc2NyaXB0aW9uIjog +InRpbWVsb2NrLWxvY2s6IHZhbGlkIDMtZmllbGQgd2l0bmVzcywgbm9uLWVtcHR5IGV4dHJhIHBh +eWxvYWQiLAogICAgICAiZmllbGRzIjogWwogICAgICAgIHsKICAgICAgICAgICJuYW1lIjogInNp +Z25hdHVyZSIsCiAgICAgICAgICAidHlwZSI6ICJzZWNwMjU2azFfc2lnIiwKICAgICAgICAgICJy +ZXF1aXJlZCI6IHRydWUKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICJuYW1lIjogInVu +bG9ja19hZnRlcl9tcyIsCiAgICAgICAgICAidHlwZSI6ICJ1aW50NjQiLAogICAgICAgICAgInJl +cXVpcmVkIjogdHJ1ZQogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgIm5hbWUiOiAiZXh0 +cmEiLAogICAgICAgICAgInR5cGUiOiAiYnl0ZXMiLAogICAgICAgICAgInJlcXVpcmVkIjogZmFs +c2UKICAgICAgICB9CiAgICAgIF0sCiAgICAgICJ3aXJlX2hleCI6ICIwMTAxMDEwMTAxMDEwMTAx +MDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEw +MTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAx +IDAwMDAwMDAwMDAwMDAwMDAgMDYwMDAwMDAgNjg2NTZjNmM2ZjIxIiwKICAgICAgImV4cGVjdCI6 +ICJ2YWxpZCIsCiAgICAgICJkZWNvZGVkIjogWwogICAgICAgIHsKICAgICAgICAgICJuYW1lIjog +InNpZ25hdHVyZSIsCiAgICAgICAgICAidHlwZSI6ICJzZWNwMjU2azFfc2lnIiwKICAgICAgICAg +ICJ2YWx1ZV9oZXgiOiAiMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAx +MDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEw +MTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMSIKICAgICAgICB9LAogICAgICAgIHsKICAg +ICAgICAgICJuYW1lIjogInVubG9ja19hZnRlcl9tcyIsCiAgICAgICAgICAidHlwZSI6ICJ1aW50 +NjQiLAogICAgICAgICAgInZhbHVlX3U2NCI6IDAKICAgICAgICB9LAogICAgICAgIHsKICAgICAg +ICAgICJuYW1lIjogImV4dHJhIiwKICAgICAgICAgICJ0eXBlIjogImJ5dGVzIiwKICAgICAgICAg +ICJ2YWx1ZV9oZXgiOiAiNjg2NTZjNmM2ZjIxIgogICAgICAgIH0KICAgICAgXQogICAgfSwKICAg +IHsKICAgICAgImlkIjogInRpbWVsb2NrLXRydW5jYXRlZC1hZnRlci1zaWciLAogICAgICAiZGVz +Y3JpcHRpb24iOiAidGltZWxvY2stbG9jazogYnVmZmVyIGVuZHMgYWZ0ZXIgNjUtYnl0ZSBzaWdu +YXR1cmUsIG5vIHRpbWVzdGFtcCBcdTIwMTQgcmVqZWN0ZWQiLAogICAgICAiZmllbGRzIjogWwog +ICAgICAgIHsKICAgICAgICAgICJuYW1lIjogInNpZ25hdHVyZSIsCiAgICAgICAgICAidHlwZSI6 +ICJzZWNwMjU2azFfc2lnIiwKICAgICAgICAgICJyZXF1aXJlZCI6IHRydWUKICAgICAgICB9LAog +ICAgICAgIHsKICAgICAgICAgICJuYW1lIjogInVubG9ja19hZnRlcl9tcyIsCiAgICAgICAgICAi +dHlwZSI6ICJ1aW50NjQiLAogICAgICAgICAgInJlcXVpcmVkIjogdHJ1ZQogICAgICAgIH0sCiAg +ICAgICAgewogICAgICAgICAgIm5hbWUiOiAiZXh0cmEiLAogICAgICAgICAgInR5cGUiOiAiYnl0 +ZXMiLAogICAgICAgICAgInJlcXVpcmVkIjogZmFsc2UKICAgICAgICB9CiAgICAgIF0sCiAgICAg +ICJ3aXJlX2hleCI6ICIwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEw +MTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAx +MDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxIiwKICAgICAgImV4cGVjdCI6ICJlcnJvciIs +CiAgICAgICJlcnJvciI6ICJGaWVsZFRvb1Nob3J0IiwKICAgICAgImVycm9yX2RldGFpbCI6IHsK +ICAgICAgICAiZmllbGQiOiAidW5sb2NrX2FmdGVyX21zIiwKICAgICAgICAiZXhwZWN0ZWQiOiA4 +LAogICAgICAgICJnb3QiOiAwCiAgICAgIH0KICAgIH0sCiAgICB7CiAgICAgICJpZCI6ICJ0aW1l +bG9jay10cnVuY2F0ZWQtdGltZXN0YW1wLTMtYnl0ZXMiLAogICAgICAiZGVzY3JpcHRpb24iOiAi +dGltZWxvY2stbG9jazogdGltZXN0YW1wIGZpZWxkIGlzIG9ubHkgMyBieXRlcywgbmVlZCA4IFx1 +MjAxNCByZWplY3RlZCIsCiAgICAgICJmaWVsZHMiOiBbCiAgICAgICAgewogICAgICAgICAgIm5h +bWUiOiAic2lnbmF0dXJlIiwKICAgICAgICAgICJ0eXBlIjogInNlY3AyNTZrMV9zaWciLAogICAg +ICAgICAgInJlcXVpcmVkIjogdHJ1ZQogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgIm5h +bWUiOiAidW5sb2NrX2FmdGVyX21zIiwKICAgICAgICAgICJ0eXBlIjogInVpbnQ2NCIsCiAgICAg +ICAgICAicmVxdWlyZWQiOiB0cnVlCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAibmFt +ZSI6ICJleHRyYSIsCiAgICAgICAgICAidHlwZSI6ICJieXRlcyIsCiAgICAgICAgICAicmVxdWly +ZWQiOiBmYWxzZQogICAgICAgIH0KICAgICAgXSwKICAgICAgIndpcmVfaGV4IjogIjAxMDEwMTAx +MDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEw +MTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAx +MDEwMTAxMDEgMDAwMTAyIiwKICAgICAgImV4cGVjdCI6ICJlcnJvciIsCiAgICAgICJlcnJvciI6 +ICJGaWVsZFRvb1Nob3J0IiwKICAgICAgImVycm9yX2RldGFpbCI6IHsKICAgICAgICAiZmllbGQi +OiAidW5sb2NrX2FmdGVyX21zIiwKICAgICAgICAiZXhwZWN0ZWQiOiA4LAogICAgICAgICJnb3Qi +OiAzCiAgICAgIH0KICAgIH0sCiAgICB7CiAgICAgICJpZCI6ICJ0aW1lbG9jay10cmFpbGluZy1i +eXRlcyIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJ0aW1lbG9jay1sb2NrOiBjb21wbGV0ZSB2YWxp +ZCB3aXRuZXNzIGZvbGxvd2VkIGJ5IDUgZXh0cmEgYnl0ZXMgXHUyMDE0IHJlamVjdGVkIiwKICAg +ICAgImZpZWxkcyI6IFsKICAgICAgICB7CiAgICAgICAgICAibmFtZSI6ICJzaWduYXR1cmUiLAog +ICAgICAgICAgInR5cGUiOiAic2VjcDI1NmsxX3NpZyIsCiAgICAgICAgICAicmVxdWlyZWQiOiB0 +cnVlCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAibmFtZSI6ICJ1bmxvY2tfYWZ0ZXJf +bXMiLAogICAgICAgICAgInR5cGUiOiAidWludDY0IiwKICAgICAgICAgICJyZXF1aXJlZCI6IHRy +dWUKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICJuYW1lIjogImV4dHJhIiwKICAgICAg +ICAgICJ0eXBlIjogImJ5dGVzIiwKICAgICAgICAgICJyZXF1aXJlZCI6IGZhbHNlCiAgICAgICAg +fQogICAgICBdLAogICAgICAid2lyZV9oZXgiOiAiMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEw +MTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAx +MDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMSAwMDAwMDAwMDAw +MDAwMDAwIDAwMDAwMDAwIDQxNDI0MzQ0NDUiLAogICAgICAiZXhwZWN0IjogImVycm9yIiwKICAg +ICAgImVycm9yIjogIlRyYWlsaW5nQnl0ZXMiLAogICAgICAiZXJyb3JfZGV0YWlsIjogewogICAg +ICAgICJ0cmFpbGluZyI6IDUKICAgICAgfQogICAgfSwKICAgIHsKICAgICAgImlkIjogInVua25v +d24tdHlwZS1yZWplY3RlZCIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJBIGZpZWxkIHdpdGggYW4g +dW5yZWNvZ25pc2VkIHR5cGUgc3RyaW5nIGlzIHJlamVjdGVkIGltbWVkaWF0ZWx5IiwKICAgICAg +ImZpZWxkcyI6IFsKICAgICAgICB7CiAgICAgICAgICAibmFtZSI6ICJteXN0ZXJ5IiwKICAgICAg +ICAgICJ0eXBlIjogIm1vbGVjdWxlX2J5dGVzIiwKICAgICAgICAgICJyZXF1aXJlZCI6IHRydWUK +ICAgICAgICB9CiAgICAgIF0sCiAgICAgICJ3aXJlX2hleCI6ICIwMDAwMDAwMDAwMDAwMDAwIiwK +ICAgICAgImV4cGVjdCI6ICJlcnJvciIsCiAgICAgICJlcnJvciI6ICJVbmtub3duVHlwZSIsCiAg +ICAgICJlcnJvcl9kZXRhaWwiOiB7CiAgICAgICAgImZpZWxkIjogIm15c3RlcnkiLAogICAgICAg +ICJ0eXBlIjogIm1vbGVjdWxlX2J5dGVzIgogICAgICB9CiAgICB9LAogICAgewogICAgICAiaWQi +OiAic2Nobm9yci1zaWctcm91bmR0cmlwIiwKICAgICAgImRlc2NyaXB0aW9uIjogInNjaG5vcnJf +c2lnIGZpZWxkLCA2NCBieXRlcyBvZiAweEFCIiwKICAgICAgImZpZWxkcyI6IFsKICAgICAgICB7 +CiAgICAgICAgICAibmFtZSI6ICJzaWciLAogICAgICAgICAgInR5cGUiOiAic2Nobm9ycl9zaWci +LAogICAgICAgICAgInJlcXVpcmVkIjogdHJ1ZQogICAgICAgIH0KICAgICAgXSwKICAgICAgIndp +cmVfaGV4IjogImFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJh +YmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFi +YWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiIiwKICAgICAgImV4cGVjdCI6ICJ2YWxpZCIsCiAgICAg +ICJkZWNvZGVkIjogWwogICAgICAgIHsKICAgICAgICAgICJuYW1lIjogInNpZyIsCiAgICAgICAg +ICAidHlwZSI6ICJzY2hub3JyX3NpZyIsCiAgICAgICAgICAidmFsdWVfaGV4IjogImFiYWJhYmFi +YWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJh +YmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFi +YWJhYmFiIgogICAgICAgIH0KICAgICAgXQogICAgfQogIF0KfQ== diff --git a/tests/compat/ls_idl/derive/multisig-2of2-nonce.json b/tests/compat/ls_idl/derive/multisig-2of2-nonce.json new file mode 100644 index 00000000..52ccb16b --- /dev/null +++ b/tests/compat/ls_idl/derive/multisig-2of2-nonce.json @@ -0,0 +1,22 @@ +{ + "witness": [ + { + "name": "sig_a", + "type": "secp256k1_sig", + "required": true, + "description": "Signature from the first co-signer" + }, + { + "name": "sig_b", + "type": "secp256k1_sig", + "required": true, + "description": "Signature from the second co-signer" + }, + { + "name": "nonce", + "type": "uint32", + "required": true, + "description": "Replay-protection nonce; must match the value stored in cell data" + } + ] +} diff --git a/tests/compat/ls_idl/derive/pow-lock.json b/tests/compat/ls_idl/derive/pow-lock.json new file mode 100644 index 00000000..7fef14df --- /dev/null +++ b/tests/compat/ls_idl/derive/pow-lock.json @@ -0,0 +1,16 @@ +{ + "witness": [ + { + "name": "difficulty", + "type": "uint8", + "required": true, + "description": "Required difficulty level (leading zero bits); must match args[0]" + }, + { + "name": "proof", + "type": "bytes", + "required": true, + "description": "Variable-length proof-of-work nonce bytes" + } + ] +} diff --git a/tests/compat/ls_idl/derive/schnorr-pubkey-recovery.json b/tests/compat/ls_idl/derive/schnorr-pubkey-recovery.json new file mode 100644 index 00000000..c3eb38c8 --- /dev/null +++ b/tests/compat/ls_idl/derive/schnorr-pubkey-recovery.json @@ -0,0 +1,16 @@ +{ + "witness": [ + { + "name": "signature", + "type": "schnorr_sig", + "required": true, + "description": "64-byte Schnorr signature (R || s)" + }, + { + "name": "pubkey", + "type": "secp256k1_pubkey", + "required": false, + "description": "Compressed secp256k1 public key (33 bytes); required only when not stored in args" + } + ] +} diff --git a/tests/compat/ls_idl/derive/secp256k1-timelock.json b/tests/compat/ls_idl/derive/secp256k1-timelock.json new file mode 100644 index 00000000..792b3c8f --- /dev/null +++ b/tests/compat/ls_idl/derive/secp256k1-timelock.json @@ -0,0 +1,16 @@ +{ + "witness": [ + { + "name": "signature", + "type": "secp256k1_sig", + "required": true, + "description": "65-byte secp256k1 ECDSA signature (r || s || v)" + }, + { + "name": "unlock_time", + "type": "uint64", + "required": true, + "description": "Unix timestamp (u64 LE) before which the cell cannot be spent" + } + ] +} diff --git a/tests/compat/ls_idl/derive/simple-lock.json b/tests/compat/ls_idl/derive/simple-lock.json new file mode 100644 index 00000000..80f1c857 --- /dev/null +++ b/tests/compat/ls_idl/derive/simple-lock.json @@ -0,0 +1,10 @@ +{ + "witness": [ + { + "description": "Preimage whose blake2b-256 hash must match the hash in script args", + "name": "preimage", + "required": true, + "type": "bytes" + } + ] +} diff --git a/tests/compat/ls_idl/scripts/simple-lock.idl.json.b64 b/tests/compat/ls_idl/scripts/simple-lock.idl.json.b64 new file mode 100644 index 00000000..35a822b4 --- /dev/null +++ b/tests/compat/ls_idl/scripts/simple-lock.idl.json.b64 @@ -0,0 +1,3 @@ +eyJ3aXRuZXNzIjpbeyJkZXNjcmlwdGlvbiI6IlByZWltYWdlIHdob3NlIGJsYWtlMmItMjU2IGhh +c2ggbXVzdCBtYXRjaCB0aGUgaGFzaCBpbiBzY3JpcHQgYXJncyIsIm5hbWUiOiJwcmVpbWFnZSIs +InJlcXVpcmVkIjp0cnVlLCJ0eXBlIjoiYnl0ZXMifV19 diff --git a/tests/compat/ls_idl/scripts/timelock-lock.idl.json.b64 b/tests/compat/ls_idl/scripts/timelock-lock.idl.json.b64 new file mode 100644 index 00000000..07f4898e --- /dev/null +++ b/tests/compat/ls_idl/scripts/timelock-lock.idl.json.b64 @@ -0,0 +1,8 @@ +eyJ3aXRuZXNzIjpbeyJkZXNjcmlwdGlvbiI6InNlY3AyNTZrMSBFQ0RTQSBzaWduYXR1cmUgYXV0 +aG9yaXNpbmcgdGhlIHNwZW5kIiwibmFtZSI6InNpZ25hdHVyZSIsInJlcXVpcmVkIjp0cnVlLCJ0 +eXBlIjoic2VjcDI1NmsxX3NpZyJ9LHsiZGVzY3JpcHRpb24iOiJVbml4IHRpbWVzdGFtcCBpbiBt +aWxsaXNlY29uZHM7IGNlbGwgY2Fubm90IGJlIHNwZW50IGJlZm9yZSB0aGlzIiwibmFtZSI6InVu +bG9ja19hZnRlcl9tcyIsInJlcXVpcmVkIjp0cnVlLCJ0eXBlIjoidWludDY0In0seyJkZXNjcmlw +dGlvbiI6Ik9wdGlvbmFsIGF1eGlsaWFyeSBwYXlsb2FkOyBoYXNoIG11c3QgbWF0Y2ggY29tbWl0 +bWVudCBpbiBhcmdzWzMzLi42NV0iLCJuYW1lIjoiZXh0cmEiLCJyZXF1aXJlZCI6ZmFsc2UsInR5 +cGUiOiJieXRlcyJ9XX0= diff --git a/tests/crypto_primitives.rs b/tests/crypto_primitives.rs index 3606962b..584edcb1 100644 --- a/tests/crypto_primitives.rs +++ b/tests/crypto_primitives.rs @@ -1,11 +1,12 @@ -#![allow(dead_code)] - +use cellscript_ckb_adapter::{place_entry_witness_payload_before_signing, EntryWitnessPlacementAbi}; use ckb_testtool::ckb_hash::blake2b_256; use ckb_testtool::ckb_types::bytes::Bytes; use ckb_testtool::ckb_types::packed; +use ckb_testtool::ckb_types::prelude::{Builder, Entity}; use sha2::{Digest, Sha256}; #[path = "support/ckb_script_runner.rs"] +#[allow(dead_code)] mod ckb_script_runner; use ckb_script_runner::{build_simple_fixture, compile_cellscript_source_to_elf, execute_cellscript_script, FixtureCell}; @@ -50,7 +51,14 @@ fn sha256d(bytes: &[u8]) -> [u8; 32] { sha256(&sha256(bytes)) } -fn sha256_merkle_witness(expected_root: [u8; 32]) -> Vec { +fn canonical_entry_witness(payload: Vec) -> Bytes { + let base = packed::WitnessArgs::new_builder().build(); + place_entry_witness_payload_before_signing(&base, EntryWitnessPlacementAbi::WitnessArgsInputTypeV2, Bytes::from(payload)) + .expect("place CellScript entry payload in WitnessArgs.input_type") + .as_bytes() +} + +fn sha256_merkle_witness(expected_root: [u8; 32]) -> Bytes { let leaf = std::array::from_fn::<_, 32, _>(|index| index as u8); let other = std::array::from_fn::<_, 32, _>(|index| (0xff - index) as u8); let expected_sha256 = sha256(&leaf); @@ -69,7 +77,7 @@ fn sha256_merkle_witness(expected_root: [u8; 32]) -> Vec { witness.extend_from_slice(&expected_sha256d); witness.extend_from_slice(&expected_pair); witness.extend_from_slice(&expected_root); - witness + canonical_entry_witness(witness) } #[test] @@ -84,8 +92,8 @@ fn bounded_sha256_sha256d_and_merkle_execute_in_ckb_vm() { let witness = sha256_merkle_witness(expected_pair); let elf = compile_cellscript_source_to_elf(SHA256_MERKLE_PROGRAM, "verify", None); - let mut fixture = build_simple_fixture(Bytes::default(), 1, 1, true, None); - fixture.witnesses = vec![Bytes::from(witness)]; + let mut fixture = build_simple_fixture(Bytes::default(), 1, 1); + fixture.witnesses = vec![witness]; let result = execute_cellscript_script(&elf, &fixture); assert_eq!(result.exit_code, 0, "bounded SHA-256/SHA256d/Merkle helpers failed in CKB VM: {:?}", result.captured_debug); @@ -95,8 +103,8 @@ fn bounded_sha256_sha256d_and_merkle_execute_in_ckb_vm() { #[test] fn bounded_merkle_rejects_wrong_root_in_ckb_vm() { let elf = compile_cellscript_source_to_elf(SHA256_MERKLE_PROGRAM, "verify", None); - let mut fixture = build_simple_fixture(Bytes::default(), 1, 1, true, None); - fixture.witnesses = vec![Bytes::from(sha256_merkle_witness([0x5a; 32]))]; + let mut fixture = build_simple_fixture(Bytes::default(), 1, 1); + fixture.witnesses = vec![sha256_merkle_witness([0x5a; 32])]; let result = execute_cellscript_script(&elf, &fixture); assert_eq!(result.exit_code, 64, "wrong Merkle root must fail closed with the stable runtime code: {:?}", result.captured_debug); @@ -110,9 +118,9 @@ fn bounded_cell_dep_scan_and_exact_identity_execute_in_ckb_vm() { witness.extend_from_slice(&expected_data_hash); let elf = compile_cellscript_source_to_elf(BOUNDED_CELL_DEP_PROGRAM, "verify", None); - let mut fixture = build_simple_fixture(Bytes::default(), 1, 1, true, None); - fixture.witnesses = vec![Bytes::from(witness)]; - fixture.cell_deps.push(FixtureCell { capacity: 0, lock: packed::Script::default(), type_script: None, data: dep_data }); + let mut fixture = build_simple_fixture(Bytes::default(), 1, 1); + fixture.witnesses = vec![canonical_entry_witness(witness)]; + fixture.cell_deps.push(FixtureCell { capacity: 0, type_script: None, data: dep_data }); let result = execute_cellscript_script(&elf, &fixture); assert_eq!(result.exit_code, 0, "bounded CellDep scan/exact identity helpers failed in CKB VM: {:?}", result.captured_debug); @@ -125,8 +133,8 @@ fn bounded_cell_dep_scan_rejects_missing_dep_in_ckb_vm() { witness.extend_from_slice(&expected_data_hash); let elf = compile_cellscript_source_to_elf(BOUNDED_CELL_DEP_PROGRAM, "verify", None); - let mut fixture = build_simple_fixture(Bytes::default(), 1, 1, true, None); - fixture.witnesses = vec![Bytes::from(witness)]; + let mut fixture = build_simple_fixture(Bytes::default(), 1, 1); + fixture.witnesses = vec![canonical_entry_witness(witness)]; let result = execute_cellscript_script(&elf, &fixture); assert_eq!(result.exit_code, 63, "missing CellDep must fail closed with the stable runtime code: {:?}", result.captured_debug); diff --git a/tests/e2e_registry_devnet.rs b/tests/e2e_registry_devnet.rs index dce3dd50..e5552525 100644 --- a/tests/e2e_registry_devnet.rs +++ b/tests/e2e_registry_devnet.rs @@ -1,19 +1,17 @@ -//! End-to-end integration tests for the CellScript two-tier Git registry -//! with CKB devnet deployment and multi-scenario verification. +//! End-to-end integration tests for CellScript registry data, the public +//! artifact API resolver, and CKB devnet deployment. //! //! ## Test Layers //! -//! 1. **Offline Git registry** (always runs): Two-tier discovery + registry.json, -//! source hash verification, publish/install/verify lifecycle. +//! 1. **Registry data** (always runs): Explicit offline Git editing fixtures +//! plus public artifact API resolution, immutable snapshots, source hash +//! verification, and publish/install/verify lifecycle. //! 2. **Headless CKB deploy** (always runs): Build deploy transactions without RPC, //! compute on-chain identity fields (data_hash, code_hash, TYPE_ID), //! write Deployed.toml + Cell.lock, cross-verify three identity layers. //! 3. **Live devnet deploy** (`#[ignore]`): Submit real transactions to a CKB devnet, //! query on-chain state, verify Cell.lock ↔ Deployed.toml ↔ on-chain consistency. -// Keep the Edition 2024 let-chain cleanup separate from the toolchain migration. -#![allow(clippy::collapsible_if)] - //! //! ## Running //! @@ -25,6 +23,7 @@ //! cargo test --locked -p cellscript --test e2e_registry_devnet -- --ignored //! ``` +use base64::Engine as _; use blake2b_simd::Params as Blake2bParams; use cellscript::package::registry::{ compute_source_hash, git_checkout, git_clone, git_list_tags, git_revision, DiscoveryEntry, DiscoveryIndex, RegistryAuditInfo, @@ -43,11 +42,16 @@ use ckb_testtool::ckb_types::{ prelude::*, }; use ckb_testtool::context::Context; +use sha2::{Digest as _, Sha256}; use std::collections::BTreeMap; +use std::io::{Read, Write}; +use std::net::TcpListener; use std::path::Path; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; // --------------------------------------------------------------------------- -// Registry env guard (serialises CELLSCRIPT_REGISTRY_URL across tests in this binary) +// Registry env guard (serialises the public artifact API origin across tests) // --------------------------------------------------------------------------- use std::ffi::OsString; @@ -61,14 +65,14 @@ struct RegistryEnvGuard { } impl RegistryEnvGuard { - fn new(url: &Path) -> Self { + fn new(url: &str) -> Self { // Recover from a poisoned mutex so one test panicking while holding the // lock does not cascade into every later test in this binary. let guard = REGISTRY_ENV_LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); - let previous = std::env::var_os(cellscript::package::registry::REGISTRY_URL_ENV); + let previous = std::env::var_os(cellscript::package::registry::REGISTRY_API_URL_ENV); // SAFETY: CI runs tests with one test thread, and this guard serializes // registry URL changes within this test binary. - unsafe { std::env::set_var(cellscript::package::registry::REGISTRY_URL_ENV, url) }; + unsafe { std::env::set_var(cellscript::package::registry::REGISTRY_API_URL_ENV, url) }; Self { previous, _guard: guard } } } @@ -77,14 +81,169 @@ impl Drop for RegistryEnvGuard { fn drop(&mut self) { if let Some(previous) = &self.previous { // SAFETY: See `RegistryEnvGuard::new`; the guard still owns the lock. - unsafe { std::env::set_var(cellscript::package::registry::REGISTRY_URL_ENV, previous) }; + unsafe { std::env::set_var(cellscript::package::registry::REGISTRY_API_URL_ENV, previous) }; } else { // SAFETY: See `RegistryEnvGuard::new`; the guard still owns the lock. - unsafe { std::env::remove_var(cellscript::package::registry::REGISTRY_URL_ENV) }; + unsafe { std::env::remove_var(cellscript::package::registry::REGISTRY_API_URL_ENV) }; + } + } +} + +struct SourceSnapshotFixture { + release: String, + source_hash: String, + bytes: Vec, + snapshot_hash: String, +} + +struct ArtifactPackageFixture { + namespace: String, + name: String, + repository: String, + index: RegistryIndex, + snapshots: BTreeMap, +} + +struct ArtifactApiServer { + origin: String, + stop: Arc, + handle: Option>, +} + +impl Drop for ArtifactApiServer { + fn drop(&mut self) { + self.stop.store(true, Ordering::Release); + if let Some(handle) = self.handle.take() { + handle.join().unwrap(); + } + } +} + +fn source_snapshot_fixture(root: &Path, namespace: &str, name: &str, release: &str, source_hash: &str) -> SourceSnapshotFixture { + let files = ["Cell.toml", "src/main.cell"] + .into_iter() + .map(|path| { + let content = std::fs::read(root.join(path)).unwrap(); + serde_json::json!({ + "path": path, + "blake2b256": hex::encode(cellscript::ckb_blake2b256(&content)), + "content_base64": base64::engine::general_purpose::STANDARD.encode(content), + }) + }) + .collect::>(); + let bytes = serde_json::to_vec(&serde_json::json!({ + "schema": "cellscript-source-snapshot-v1", + "package": { "namespace": namespace, "name": name, "version": release }, + "files": files, + })) + .unwrap(); + let snapshot_hash = format!("sha256:{}", hex::encode(Sha256::digest(&bytes))); + SourceSnapshotFixture { release: release.to_string(), source_hash: source_hash.to_string(), bytes, snapshot_hash } +} + +fn artifact_package_fixture(repo: &Path, snapshots: Vec) -> ArtifactPackageFixture { + let index = RegistryIndex::read_from_repo(repo).unwrap(); + ArtifactPackageFixture { + namespace: index.namespace.clone(), + name: index.name.clone(), + repository: format!("https://example.test/{}/{}", index.namespace, index.name), + index, + snapshots: snapshots.into_iter().map(|snapshot| (snapshot.release.clone(), snapshot)).collect(), + } +} + +fn read_mock_http_path(stream: &mut std::net::TcpStream) -> String { + let mut request = Vec::new(); + let mut buffer = [0_u8; 1024]; + loop { + let read = stream.read(&mut buffer).unwrap(); + assert_ne!(read, 0, "artifact API request ended before headers"); + request.extend_from_slice(&buffer[..read]); + if let Some(header_end) = request.windows(4).position(|window| window == b"\r\n\r\n") { + let headers = String::from_utf8_lossy(&request[..header_end]); + return headers.lines().next().and_then(|line| line.split_whitespace().nth(1)).unwrap_or("/").to_string(); } } } +fn start_artifact_api(packages: Vec) -> ArtifactApiServer { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + listener.set_nonblocking(true).unwrap(); + let origin = format!("http://{}", listener.local_addr().unwrap()); + let mut api_responses = BTreeMap::>::new(); + let mut snapshot_responses = BTreeMap::>::new(); + for package in packages { + let releases = package + .index + .versions + .iter() + .map(|version| { + let snapshot = package.snapshots.get(&version.version).expect("snapshot for every Registry release"); + let path = format!("/source-snapshots/{}/{}/{}/fixture.json", package.namespace, package.name, version.version); + snapshot_responses.insert(path.clone(), snapshot.bytes.clone()); + serde_json::json!({ + "release": version.version, + "verification_status": "verified", + "availability_status": "active", + "registry_entry": &package.index, + "immutable_bundle": { + "schema": "cellscript-registry-immutable-bundle", + "url": format!("{origin}{path}"), + "snapshot_hash": snapshot.snapshot_hash, + "source_hash": snapshot.source_hash, + "size_bytes": snapshot.bytes.len(), + "content_type": "application/vnd.cellscript.source-snapshot+json" + } + }) + }) + .collect::>(); + let response = serde_json::to_vec(&serde_json::json!({ + "schema": "cellscript-registry-artifact", + "namespace": package.namespace, + "name": package.name, + "repository": package.repository, + "artifact": { + "kind": "source_library", + "profile": "cellscript_source", + "consumption_mode": "dependency", + "language": "cellscript" + }, + "releases": releases + })) + .unwrap(); + api_responses.insert(format!("/v1/artifacts/{}/{}", package.index.namespace, package.index.name), response); + } + let stop = Arc::new(AtomicBool::new(false)); + let server_stop = Arc::clone(&stop); + let handle = std::thread::spawn(move || { + while !server_stop.load(Ordering::Acquire) { + match listener.accept() { + Ok((mut stream, _)) => { + let path = read_mock_http_path(&mut stream); + let (status, content_type, body) = if let Some(body) = api_responses.get(&path) { + ("200 OK", "application/json", body.as_slice()) + } else if let Some(body) = snapshot_responses.get(&path) { + ("200 OK", "application/vnd.cellscript.source-snapshot+json", body.as_slice()) + } else { + ("404 Not Found", "application/json", b"{}".as_slice()) + }; + let headers = format!( + "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + stream.write_all(headers.as_bytes()).unwrap(); + stream.write_all(body).unwrap(); + } + Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { + std::thread::sleep(std::time::Duration::from_millis(5)); + } + Err(error) => panic!("artifact API fixture failed: {error}"), + } + } + }); + ArtifactApiServer { origin, stop, handle: Some(handle) } +} + // --------------------------------------------------------------------------- // Git fixture helpers // --------------------------------------------------------------------------- @@ -142,7 +301,7 @@ fn git_config_user(repo_dir: &Path) { fn create_package(dir: &Path, name: &str, version: &str, namespace: Option<&str>) { std::fs::create_dir_all(dir.join("src")).unwrap(); - let mut toml = String::from("[package]\n"); + let mut toml = String::from("[package]\nedition = \"2026\"\n"); toml.push_str(&format!("name = \"{}\"\n", name)); toml.push_str(&format!("version = \"{}\"\n", version)); if let Some(ns) = namespace { @@ -166,7 +325,7 @@ fn create_package_with_dep( ) { std::fs::create_dir_all(dir.join("src")).unwrap(); - let mut toml = String::from("[package]\n"); + let mut toml = String::from("[package]\nedition = \"2026\"\n"); toml.push_str(&format!("name = \"{}\"\n", name)); toml.push_str(&format!("version = \"{}\"\n", version)); if let Some(ns) = namespace { @@ -189,6 +348,8 @@ fn init_source_repo(repo_dir: &Path, name: &str, version: &str, namespace: &str) let source_hash = compute_source_hash(repo_dir).unwrap(); let version_entry = RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: version.to_string(), tag: format!("v{}", version), source_hash: source_hash.clone(), @@ -248,6 +409,8 @@ fn sample_deployment_record( out_point: &str, ) -> DeploymentRecord { DeploymentRecord { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), network: network.to_string(), chain_id: chain_id.to_string(), tx_hash: tx_hash.to_string(), @@ -279,39 +442,39 @@ fn cross_verify_lockfile_deployed(lockfile: &Lockfile, deployed: &DeployedManife let mut violations = Vec::new(); // Check build hashes - if let Some(build) = &lockfile.package_build { - if let Some(deployed_build) = &deployed.build { - if let (Some(lk), Some(dp)) = (&build.artifact_hash, &deployed_build.artifact_hash) { - if lk != dp { - violations.push(format!("artifact_hash mismatch: Cell.lock='{}', Deployed.toml='{}'", lk, dp)); - } - } - if let (Some(lk), Some(dp)) = (&build.schema_hash, &deployed_build.schema_hash) { - if lk != dp { - violations.push(format!("schema_hash mismatch: Cell.lock='{}', Deployed.toml='{}'", lk, dp)); - } - } + if let Some(build) = &lockfile.package_build + && let Some(deployed_build) = &deployed.build + { + if let (Some(lk), Some(dp)) = (&build.artifact_hash, &deployed_build.artifact_hash) + && lk != dp + { + violations.push(format!("artifact_hash mismatch: Cell.lock='{}', Deployed.toml='{}'", lk, dp)); + } + if let (Some(lk), Some(dp)) = (&build.schema_hash, &deployed_build.schema_hash) + && lk != dp + { + violations.push(format!("schema_hash mismatch: Cell.lock='{}', Deployed.toml='{}'", lk, dp)); } } // Check deployment records for deployment in &deployed.deployments { if let Some(deployment_ref) = lockfile.deployment.get(&deployment.network) { - if let Some(ref code_hash) = deployment_ref.code_hash { - if code_hash != &deployment.code_hash { - violations.push(format!( - "code_hash mismatch for network '{}': Cell.lock='{}', Deployed.toml='{}'", - deployment.network, code_hash, deployment.code_hash - )); - } + if let Some(ref code_hash) = deployment_ref.code_hash + && code_hash != &deployment.code_hash + { + violations.push(format!( + "code_hash mismatch for network '{}': Cell.lock='{}', Deployed.toml='{}'", + deployment.network, code_hash, deployment.code_hash + )); } - if let Some(ref data_hash) = deployment_ref.data_hash { - if data_hash != &deployment.data_hash { - violations.push(format!( - "data_hash mismatch for network '{}': Cell.lock='{}', Deployed.toml='{}'", - deployment.network, data_hash, deployment.data_hash - )); - } + if let Some(ref data_hash) = deployment_ref.data_hash + && data_hash != &deployment.data_hash + { + violations.push(format!( + "data_hash mismatch for network '{}': Cell.lock='{}', Deployed.toml='{}'", + deployment.network, data_hash, deployment.data_hash + )); } } } @@ -383,6 +546,7 @@ fn e2e_publish_install_verify_offline_git() { let mut lockfile = Lockfile::new(); lockfile.package = LockfilePackageInfo { + edition: cellscript::CURRENT_EDITION, name: "app".to_string(), version: "0.1.0".to_string(), namespace: Some("cellscript".to_string()), @@ -392,6 +556,8 @@ fn e2e_publish_install_verify_offline_git() { lockfile.dependencies.insert( "token".to_string(), LockedDependency { + name: "token".to_string(), + namespace: Some("cellscript".to_string()), version: "0.3.0".to_string(), source: LockedSource::Registry { registry: "https://github.com/cellscript/cellscript-registry".to_string(), @@ -401,6 +567,8 @@ fn e2e_publish_install_verify_offline_git() { version: "0.3.0".to_string(), }, source_hash: Some(source_hash_v030.clone()), + manifest_digest: "sha256:test-token-manifest".to_string(), + dependencies: BTreeMap::new(), build: None, }, ); @@ -439,6 +607,8 @@ fn e2e_multi_package_dependency_chain() { // Build registry entry for lib-b with dependency reference let version_entry_b = RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.1.0".to_string(), tag: "v0.1.0".to_string(), source_hash: hash_b.clone(), @@ -521,6 +691,7 @@ fn e2e_multi_package_dependency_chain() { let mut lockfile = Lockfile::new(); lockfile.package = LockfilePackageInfo { + edition: cellscript::CURRENT_EDITION, name: "app".to_string(), version: "0.1.0".to_string(), namespace: Some("cellscript".to_string()), @@ -530,6 +701,8 @@ fn e2e_multi_package_dependency_chain() { lockfile.dependencies.insert( "lib-a".to_string(), LockedDependency { + name: "lib-a".to_string(), + namespace: Some("cellscript".to_string()), version: "0.1.0".to_string(), source: LockedSource::Registry { registry: "https://github.com/cellscript/cellscript-registry".to_string(), @@ -539,12 +712,16 @@ fn e2e_multi_package_dependency_chain() { version: "0.1.0".to_string(), }, source_hash: Some(hash_a), + manifest_digest: "sha256:test-lib-a-manifest".to_string(), + dependencies: BTreeMap::new(), build: None, }, ); lockfile.dependencies.insert( "lib-b".to_string(), LockedDependency { + name: "lib-b".to_string(), + namespace: Some("cellscript".to_string()), version: "0.1.0".to_string(), source: LockedSource::Registry { registry: "https://github.com/cellscript/cellscript-registry".to_string(), @@ -554,6 +731,8 @@ fn e2e_multi_package_dependency_chain() { version: "0.1.0".to_string(), }, source_hash: Some(hash_b), + manifest_digest: "sha256:test-lib-b-manifest".to_string(), + dependencies: BTreeMap::new(), build: None, }, ); @@ -567,13 +746,12 @@ fn e2e_multi_package_dependency_chain() { } // =========================================================================== -// SCENARIO 2b: Diamond dependency — unified (single-version) resolution +// SCENARIO 2b: Diamond dependency — canonical multi-version graph // =========================================================================== // -// Two consumers of a shared package must agree on a single version. The -// resolver picks one version per package; if the diamond's two version -// requirements cannot both be satisfied by that one version, resolution -// fails closed instead of silently keeping whichever was resolved first. +// Compatible consumers reuse one canonical node. Incompatible requirements +// remain distinct graph nodes so the lockfile records the complete decision; +// compiler-level module and type identity collisions still fail closed. /// Rewrite the `version = "..."` line in a package's Cell.toml in place. fn bump_manifest_version(repo_dir: &Path, new_version: &str) { @@ -605,6 +783,8 @@ fn publish_version_with_deps( deps: &[(String, String, String)], // (dep_name, dep_namespace, dep_version) ) { let version_entry = RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: version.to_string(), tag: format!("v{}", version), source_hash: source_hash.to_string(), @@ -642,11 +822,13 @@ fn e2e_diamond_dependency_compatible_versions_unify() { // ── 1. Shared package "token" with two compatible versions ── let token_repo = temp.path().join("source-repos/cellscript-token"); - let _hash_030 = init_source_repo(&token_repo, "token", "0.3.0", "cellscript"); + let hash_030 = init_source_repo(&token_repo, "token", "0.3.0", "cellscript"); + let token_snapshot_030 = source_snapshot_fixture(&token_repo, "cellscript", "token", "0.3.0", &hash_030); // Bump Cell.toml version to 0.3.2 and change source so the hashes differ. bump_manifest_version(&token_repo, "0.3.2"); std::fs::write(token_repo.join("src/main.cell"), "module token;\n// v0.3.2\n").unwrap(); let hash_032 = compute_source_hash(&token_repo).unwrap(); + let token_snapshot_032 = source_snapshot_fixture(&token_repo, "cellscript", "token", "0.3.2", &hash_032); publish_version_with_deps(&token_repo, "token", "cellscript", "0.3.2", &hash_032, &[]); // ── 2. amm depends on token ^0.3.0, vesting depends on token ^0.3.0 ── @@ -657,6 +839,7 @@ fn e2e_diamond_dependency_compatible_versions_unify() { let amm_repo = temp.path().join("source-repos/cellscript-amm"); create_package_with_dep(&amm_repo, "amm", "0.1.0", Some("cellscript"), "token", "0.3.0", Some("cellscript")); let amm_hash = compute_source_hash(&amm_repo).unwrap(); + let amm_snapshot = source_snapshot_fixture(&amm_repo, "cellscript", "amm", "0.1.0", &amm_hash); publish_version_with_deps( &amm_repo, "amm", @@ -669,6 +852,7 @@ fn e2e_diamond_dependency_compatible_versions_unify() { let vesting_repo = temp.path().join("source-repos/cellscript-vesting"); create_package_with_dep(&vesting_repo, "vesting", "0.1.0", Some("cellscript"), "token", "0.3.0", Some("cellscript")); let vesting_hash = compute_source_hash(&vesting_repo).unwrap(); + let vesting_snapshot = source_snapshot_fixture(&vesting_repo, "cellscript", "vesting", "0.1.0", &vesting_hash); publish_version_with_deps( &vesting_repo, "vesting", @@ -678,28 +862,23 @@ fn e2e_diamond_dependency_compatible_versions_unify() { &[("token".into(), "cellscript".into(), "0.3.0".into())], ); - // ── 3. Discovery index with all three packages ── - let discovery_repo = temp.path().join("discovery-index"); - init_discovery_repo( - &discovery_repo, - &[ - ("cellscript", "token", &token_repo.to_string_lossy()), - ("cellscript", "amm", &amm_repo.to_string_lossy()), - ("cellscript", "vesting", &vesting_repo.to_string_lossy()), - ], - ); + // ── 3. Public artifact API with immutable source snapshots ── + let api = start_artifact_api(vec![ + artifact_package_fixture(&token_repo, vec![token_snapshot_030, token_snapshot_032]), + artifact_package_fixture(&amm_repo, vec![amm_snapshot]), + artifact_package_fixture(&vesting_repo, vec![vesting_snapshot]), + ]); // ── 4. Consumer "app" depends on both amm and vesting (the diamond) ── let app_dir = temp.path().join("consumer-app"); std::fs::create_dir_all(app_dir.join("src")).unwrap(); - let mut toml = String::from("[package]\nname = \"app\"\nversion = \"0.1.0\"\nnamespace = \"cellscript\"\n"); + let mut toml = String::from("[package]\nedition = \"2026\"\nname = \"app\"\nversion = \"0.1.0\"\nnamespace = \"cellscript\"\n"); toml.push_str("\n[dependencies.amm]\nversion = \"0.1.0\"\nnamespace = \"cellscript\"\n"); toml.push_str("\n[dependencies.vesting]\nversion = \"0.1.0\"\nnamespace = \"cellscript\"\n"); std::fs::write(app_dir.join("Cell.toml"), toml).unwrap(); std::fs::write(app_dir.join("src/main.cell"), "module app;\n").unwrap(); - // Point the resolver at our local discovery index (serialised across tests). - let _env = RegistryEnvGuard::new(&discovery_repo); + let _env = RegistryEnvGuard::new(&api.origin); let mut pm = PackageManager::new(&app_dir); // Resolution should succeed: both amm and vesting require token ^0.3.0, @@ -707,29 +886,35 @@ fn e2e_diamond_dependency_compatible_versions_unify() { pm.resolve_dependencies().expect("compatible diamond must resolve to a single token version"); let resolved = pm.get_resolved(); - let token = resolved.get("token").expect("token must be resolved transitively"); + let tokens = resolved.values().filter(|package| package.name == "token").collect::>(); + assert_eq!(tokens.len(), 1, "compatible diamond should reuse one canonical token node: {resolved:#?}"); + let token = tokens[0]; // The resolver picks the latest satisfying version, which is 0.3.2. assert_eq!(token.version, "0.3.2", "unified resolution should select the latest satisfying version"); } #[test] -fn e2e_diamond_dependency_conflicting_versions_fails_closed() { +fn e2e_diamond_dependency_incompatible_versions_use_distinct_nodes() { let temp = tempfile::tempdir().unwrap(); // ── 1. Shared package "token" with 0.3.x and 0.4.x lines ── let token_repo = temp.path().join("source-repos/cellscript-token"); - let _hash_030 = init_source_repo(&token_repo, "token", "0.3.0", "cellscript"); + let hash_030 = init_source_repo(&token_repo, "token", "0.3.0", "cellscript"); + let token_snapshot_030 = source_snapshot_fixture(&token_repo, "cellscript", "token", "0.3.0", &hash_030); bump_manifest_version(&token_repo, "0.4.0"); std::fs::write(token_repo.join("src/main.cell"), "module token;\n// v0.4.0\n").unwrap(); let hash_040 = compute_source_hash(&token_repo).unwrap(); + let token_snapshot_040 = source_snapshot_fixture(&token_repo, "cellscript", "token", "0.4.0", &hash_040); publish_version_with_deps(&token_repo, "token", "cellscript", "0.4.0", &hash_040, &[]); // ── 2. amm pins token to ^0.3.0, vesting pins token to ^0.4.0 ── - // No single token version can satisfy both "^0.3.0" and "^0.4.0", so the - // dependency graph is unsatisfiable and resolution must fail closed. + // No single token version can satisfy both "^0.3.0" and "^0.4.0". The v3 + // graph therefore retains two canonical nodes; compilation still fails + // closed later if their exported module/type identities collide. let amm_repo = temp.path().join("source-repos/cellscript-amm"); create_package_with_dep(&amm_repo, "amm", "0.1.0", Some("cellscript"), "token", "0.3.0", Some("cellscript")); let amm_hash = compute_source_hash(&amm_repo).unwrap(); + let amm_snapshot = source_snapshot_fixture(&amm_repo, "cellscript", "amm", "0.1.0", &amm_hash); publish_version_with_deps( &amm_repo, "amm", @@ -742,6 +927,7 @@ fn e2e_diamond_dependency_conflicting_versions_fails_closed() { let vesting_repo = temp.path().join("source-repos/cellscript-vesting"); create_package_with_dep(&vesting_repo, "vesting", "0.1.0", Some("cellscript"), "token", "0.4.0", Some("cellscript")); let vesting_hash = compute_source_hash(&vesting_repo).unwrap(); + let vesting_snapshot = source_snapshot_fixture(&vesting_repo, "cellscript", "vesting", "0.1.0", &vesting_hash); publish_version_with_deps( &vesting_repo, "vesting", @@ -751,32 +937,34 @@ fn e2e_diamond_dependency_conflicting_versions_fails_closed() { &[("token".into(), "cellscript".into(), "0.4.0".into())], ); - // ── 3. Discovery index ── - let discovery_repo = temp.path().join("discovery-index"); - init_discovery_repo( - &discovery_repo, - &[ - ("cellscript", "token", &token_repo.to_string_lossy()), - ("cellscript", "amm", &amm_repo.to_string_lossy()), - ("cellscript", "vesting", &vesting_repo.to_string_lossy()), - ], - ); + // ── 3. Public artifact API ── + let api = start_artifact_api(vec![ + artifact_package_fixture(&token_repo, vec![token_snapshot_030, token_snapshot_040]), + artifact_package_fixture(&amm_repo, vec![amm_snapshot]), + artifact_package_fixture(&vesting_repo, vec![vesting_snapshot]), + ]); // ── 4. Consumer "app" forms the conflicting diamond ── let app_dir = temp.path().join("consumer-app"); std::fs::create_dir_all(app_dir.join("src")).unwrap(); - let mut toml = String::from("[package]\nname = \"app\"\nversion = \"0.1.0\"\nnamespace = \"cellscript\"\n"); + let mut toml = String::from("[package]\nedition = \"2026\"\nname = \"app\"\nversion = \"0.1.0\"\nnamespace = \"cellscript\"\n"); toml.push_str("\n[dependencies.amm]\nversion = \"0.1.0\"\nnamespace = \"cellscript\"\n"); toml.push_str("\n[dependencies.vesting]\nversion = \"0.1.0\"\nnamespace = \"cellscript\"\n"); std::fs::write(app_dir.join("Cell.toml"), toml).unwrap(); std::fs::write(app_dir.join("src/main.cell"), "module app;\n").unwrap(); - let _env = RegistryEnvGuard::new(&discovery_repo); + let _env = RegistryEnvGuard::new(&api.origin); let mut pm = PackageManager::new(&app_dir); - let err = pm.resolve_dependencies().expect_err("conflicting diamond must fail closed"); - let msg = err.to_string(); - assert!(msg.contains("version conflict") && msg.contains("token"), "expected a token version-conflict error, got: {msg}"); + pm.resolve_dependencies().expect("incompatible requirements should resolve to distinct graph nodes"); + let mut token_versions = pm + .get_resolved() + .values() + .filter(|package| package.name == "token") + .map(|package| package.version.as_str()) + .collect::>(); + token_versions.sort_unstable(); + assert_eq!(token_versions, ["0.3.0", "0.4.0"]); } #[test] @@ -873,6 +1061,8 @@ fn e2e_version_upgrade_yank_semver() { let hash_010 = compute_source_hash(&repo).unwrap(); let v010 = RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.1.0".to_string(), tag: "v0.1.0".to_string(), source_hash: hash_010.clone(), @@ -903,6 +1093,8 @@ fn e2e_version_upgrade_yank_semver() { assert_ne!(hash_010, hash_020, "source hash must change when code changes"); let v020 = RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.2.0".to_string(), tag: "v0.2.0".to_string(), source_hash: hash_020.clone(), @@ -931,6 +1123,8 @@ fn e2e_version_upgrade_yank_semver() { assert_ne!(hash_020, hash_030); let v030 = RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.3.0".to_string(), tag: "v0.3.0".to_string(), source_hash: hash_030.clone(), @@ -968,6 +1162,8 @@ fn e2e_version_upgrade_yank_semver() { // ── 6. Yank v0.2.0 (critical security issue) ── let v020_yanked = RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.2.0".to_string(), tag: "v0.2.0".to_string(), source_hash: hash_020.clone(), @@ -1094,14 +1290,17 @@ fn e2e_headless_deploy_deployed_toml_three_layer_identity() { // ── 4. Write Deployed.toml with deployment facts ── let deployed = DeployedManifest { - version: 1, - schema: Some(DEPLOYED_MANIFEST_SCHEMA.to_string()), + version: DeployedManifest::CURRENT_VERSION, + schema: DEPLOYED_MANIFEST_SCHEMA.to_string(), package: DeployedPackageInfo { + edition: cellscript::CURRENT_EDITION, name: "my-contract".to_string(), version: "0.1.0".to_string(), source_hash: Some(source_hash.clone()), }, build: Some(DeployedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), compiler_version: Some("0.19.0".to_string()), artifact_hash: Some(artifact_hash_hex.clone()), metadata_hash: None, @@ -1111,6 +1310,8 @@ fn e2e_headless_deploy_deployed_toml_three_layer_identity() { constraints_hash: None, }), deployments: vec![DeploymentRecord { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), network: "aggron4".to_string(), chain_id: "ckb-testnet".to_string(), tx_hash: tx_hash_hex.clone(), @@ -1142,6 +1343,7 @@ fn e2e_headless_deploy_deployed_toml_three_layer_identity() { // ── 5. Write Cell.lock with build identity ── let mut lockfile = Lockfile::new(); lockfile.package = LockfilePackageInfo { + edition: cellscript::CURRENT_EDITION, name: "my-contract".to_string(), version: "0.1.0".to_string(), namespace: Some("cellscript".to_string()), @@ -1149,6 +1351,8 @@ fn e2e_headless_deploy_deployed_toml_three_layer_identity() { compiler_source_hash: None, }; lockfile.package_build = Some(LockedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), compiler_version: Some("0.19.0".to_string()), target_profile: Some("ckb-release".to_string()), artifact_hash: Some(artifact_hash_hex.clone()), @@ -1224,6 +1428,8 @@ fn e2e_headless_deploy_with_cell_deps_and_multi_network() { let _secp256k1_data_out_point = format!("{}:2", secp256k1_data_tx_hash); let mainnet_record = DeploymentRecord { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), network: "ckb-mainnet".to_string(), chain_id: "ckb-mainnet".to_string(), tx_hash: "0xaaaa0000111122223333444455556666777788889999000011112222333344445555".to_string(), @@ -1258,6 +1464,8 @@ fn e2e_headless_deploy_with_cell_deps_and_multi_network() { }; let testnet_record = DeploymentRecord { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), network: "aggron4".to_string(), chain_id: "ckb-testnet".to_string(), tx_hash: "0xeeee3333444455556666777788889999000011112222333344445555666677778888".to_string(), @@ -1305,14 +1513,17 @@ fn e2e_headless_deploy_with_cell_deps_and_multi_network() { // ── 2. Write Deployed.toml with both deployments ── let source_hash = compute_source_hash(&pkg_dir).unwrap(); let deployed = DeployedManifest { - version: 1, - schema: Some(DEPLOYED_MANIFEST_SCHEMA.to_string()), + version: DeployedManifest::CURRENT_VERSION, + schema: DEPLOYED_MANIFEST_SCHEMA.to_string(), package: DeployedPackageInfo { + edition: cellscript::CURRENT_EDITION, name: "multi-deploy".to_string(), version: "0.2.0".to_string(), source_hash: Some(source_hash.clone()), }, build: Some(DeployedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), compiler_version: Some("0.19.0".to_string()), artifact_hash: Some("artifact_hash_mainnet".to_string()), metadata_hash: None, @@ -1328,6 +1539,7 @@ fn e2e_headless_deploy_with_cell_deps_and_multi_network() { // ── 3. Write Cell.lock with both network deployment refs ── let mut lockfile = Lockfile::new(); lockfile.package = LockfilePackageInfo { + edition: cellscript::CURRENT_EDITION, name: "multi-deploy".to_string(), version: "0.2.0".to_string(), namespace: Some("cellscript".to_string()), @@ -1335,6 +1547,8 @@ fn e2e_headless_deploy_with_cell_deps_and_multi_network() { compiler_source_hash: None, }; lockfile.package_build = Some(LockedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), compiler_version: Some("0.19.0".to_string()), target_profile: Some("ckb-release".to_string()), artifact_hash: Some("artifact_hash_mainnet".to_string()), @@ -1407,6 +1621,7 @@ fn e2e_fail_closed_three_layer_identity_verification() { // ── 1. Write correct Cell.lock + Deployed.toml ── let mut lockfile = Lockfile::new(); lockfile.package = LockfilePackageInfo { + edition: cellscript::CURRENT_EDITION, name: "fail-closed".to_string(), version: "0.1.0".to_string(), namespace: Some("cellscript".to_string()), @@ -1414,6 +1629,8 @@ fn e2e_fail_closed_three_layer_identity_verification() { compiler_source_hash: None, }; lockfile.package_build = Some(LockedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), compiler_version: Some("0.19.0".to_string()), artifact_hash: Some(artifact_hash.clone()), ..Default::default() @@ -1431,14 +1648,17 @@ fn e2e_fail_closed_three_layer_identity_verification() { lockfile.write_to_root(&pkg_dir).unwrap(); let deployed = DeployedManifest { - version: 1, - schema: Some(DEPLOYED_MANIFEST_SCHEMA.to_string()), + version: DeployedManifest::CURRENT_VERSION, + schema: DEPLOYED_MANIFEST_SCHEMA.to_string(), package: DeployedPackageInfo { + edition: cellscript::CURRENT_EDITION, name: "fail-closed".to_string(), version: "0.1.0".to_string(), source_hash: Some(source_hash.clone()), }, build: Some(DeployedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), compiler_version: Some("0.19.0".to_string()), artifact_hash: Some(artifact_hash.clone()), ..Default::default() @@ -1518,8 +1738,7 @@ const ALWAYS_SUCCESS_HASH_TYPE: &str = "data"; // ── Devnet lifecycle helpers ── struct CkbDevnet { - #[allow(dead_code)] - ckb_dir: tempfile::TempDir, + _ckb_dir: tempfile::TempDir, rpc_url: String, ckb_pid: std::process::Child, } @@ -1558,18 +1777,17 @@ impl CkbDevnet { "id": 1, "jsonrpc": "2.0", "method": "get_tip_header", - "params": serde_json::Value::Array(vec![]), + "params": serde_json::Value::Array(vec![]), }); if let Ok(output) = std::process::Command::new("curl") .args(["-s", "-H", "Content-Type: application/json", "-d", &body.to_string(), &rpc_url]) .output() + && let Ok(response) = serde_json::from_slice::(&output.stdout) + && response.get("result").is_some() + && response.get("error").is_none() { - if let Ok(response) = serde_json::from_slice::(&output.stdout) { - if response.get("result").is_some() && response.get("error").is_none() { - ready = true; - break; - } - } + ready = true; + break; } if let Ok(Some(status)) = ckb_pid.try_wait() { let log_content = std::fs::read_to_string(&ckb_log_path).unwrap_or_default(); @@ -1597,7 +1815,7 @@ impl CkbDevnet { .output(); } - Self { ckb_dir, rpc_url, ckb_pid } + Self { _ckb_dir: ckb_dir, rpc_url, ckb_pid } } fn find_ckb_repo() -> std::path::PathBuf { @@ -1714,32 +1932,6 @@ impl CkbDevnet { panic!("{} was not committed after 64 blocks", label); } - /// Find a spendable cellbase output and return (tx_hash, index, capacity_hex). - #[allow(dead_code)] - fn find_spendable_cellbase(&self) -> (String, u32, String) { - for _ in 0..64 { - let block_hash = self.generate_block().as_str().unwrap().to_string(); - let block = self.rpc("get_block", serde_json::json!([block_hash])); - let cellbase = &block["transactions"][0]; - let tx_hash = cellbase["hash"].as_str().unwrap().to_string(); - let outputs = cellbase["outputs"].as_array().unwrap(); - for (index, output) in outputs.iter().enumerate() { - let capacity = output["capacity"].as_str().unwrap(); - if u64::from_str_radix(capacity.trim_start_matches("0x"), 16).unwrap() > 0 { - // Wait for it to be live - for _ in 0..20 { - let live = self.get_live_cell(&tx_hash, index as u32); - if live["status"].as_str() == Some("live") { - return (tx_hash, index as u32, capacity.to_string()); - } - std::thread::sleep(std::time::Duration::from_millis(50)); - } - } - } - } - panic!("no spendable cellbase found after 64 blocks"); - } - /// Collect enough capacity for deploying `artifact_size` bytes. /// Generates blocks, collects cellbase outputs, then merges them into a single /// cell via a consolidation transaction. Returns (tx_hash, index, capacity_hex) @@ -1983,6 +2175,7 @@ fn e2e_live_devnet_deploy_and_verify() { pkg_dir.join("Cell.toml"), r#" [package] +edition = "2026" name = "devnet-contract" version = "0.1.0" namespace = "cellscript" @@ -2133,14 +2326,17 @@ action ping(value: u64) -> u64 { let data_hash_hex = computed_data_hash_hex.clone(); let deployed = DeployedManifest { - version: 1, - schema: Some(DEPLOYED_MANIFEST_SCHEMA.to_string()), + version: DeployedManifest::CURRENT_VERSION, + schema: DEPLOYED_MANIFEST_SCHEMA.to_string(), package: DeployedPackageInfo { + edition: cellscript::CURRENT_EDITION, name: "devnet-contract".to_string(), version: "0.1.0".to_string(), source_hash: Some(source_hash.clone()), }, build: Some(DeployedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), compiler_version: Some(cellscript::VERSION.to_string()), artifact_hash: Some(artifact_hash_hex.clone()), metadata_hash: None, @@ -2150,6 +2346,8 @@ action ping(value: u64) -> u64 { constraints_hash: None, }), deployments: vec![DeploymentRecord { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), network: "ckb-devnet".to_string(), chain_id: "ckb-integration".to_string(), tx_hash: tx_hash.clone(), @@ -2180,6 +2378,7 @@ action ping(value: u64) -> u64 { // ── 14. Write Cell.lock ── let mut lockfile = Lockfile::new(); lockfile.package = LockfilePackageInfo { + edition: cellscript::CURRENT_EDITION, name: "devnet-contract".to_string(), version: "0.1.0".to_string(), namespace: Some("cellscript".to_string()), @@ -2187,6 +2386,8 @@ action ping(value: u64) -> u64 { compiler_source_hash: None, }; lockfile.package_build = Some(LockedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), compiler_version: Some(cellscript::VERSION.to_string()), target_profile: Some("ckb".to_string()), artifact_hash: Some(artifact_hash_hex.clone()), @@ -2255,6 +2456,7 @@ fn e2e_live_devnet_publish_deploy_verify_full_lifecycle() { app_repo.join("Cell.toml"), r#" [package] +edition = "2026" name = "app-contract" version = "0.1.0" namespace = "cellscript" @@ -2281,6 +2483,8 @@ action verify(amount: u64) -> u64 { let hash_app = compute_source_hash(&app_repo).unwrap(); let version_entry_app = RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.1.0".to_string(), tag: "v0.1.0".to_string(), source_hash: hash_app.clone(), @@ -2427,6 +2631,7 @@ action verify(amount: u64) -> u64 { // ── 11. Write Cell.lock with real on-chain facts ── let mut lockfile = Lockfile::new(); lockfile.package = LockfilePackageInfo { + edition: cellscript::CURRENT_EDITION, name: "app-contract".to_string(), version: "0.1.0".to_string(), namespace: Some("cellscript".to_string()), @@ -2434,6 +2639,8 @@ action verify(amount: u64) -> u64 { compiler_source_hash: None, }; lockfile.package_build = Some(LockedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), compiler_version: Some(cellscript::VERSION.to_string()), target_profile: Some("ckb".to_string()), artifact_hash: Some(artifact_hash_hex.clone()), @@ -2453,10 +2660,17 @@ action verify(amount: u64) -> u64 { // ── 12. Write Deployed.toml with on-chain facts ── let deployed = DeployedManifest { - version: 1, - schema: Some(DEPLOYED_MANIFEST_SCHEMA.to_string()), - package: DeployedPackageInfo { name: "app-contract".to_string(), version: "0.1.0".to_string(), source_hash: Some(hash_app) }, + version: DeployedManifest::CURRENT_VERSION, + schema: DEPLOYED_MANIFEST_SCHEMA.to_string(), + package: DeployedPackageInfo { + edition: cellscript::CURRENT_EDITION, + name: "app-contract".to_string(), + version: "0.1.0".to_string(), + source_hash: Some(hash_app), + }, build: Some(DeployedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), compiler_version: Some(cellscript::VERSION.to_string()), artifact_hash: Some(artifact_hash_hex), metadata_hash: None, @@ -2466,6 +2680,8 @@ action verify(amount: u64) -> u64 { constraints_hash: None, }), deployments: vec![DeploymentRecord { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), network: "ckb-devnet".to_string(), chain_id: "ckb-integration".to_string(), tx_hash: tx_hash.clone(), @@ -2604,6 +2820,7 @@ fn e2e_source_hash_cross_platform_determinism() { std::fs::write( pkg_dir.join("Cell.toml"), r#"[package] +edition = "2026" name = "multi-file" version = "0.1.0" namespace = "cellscript" @@ -2632,6 +2849,7 @@ namespace = "cellscript" std::fs::write( pkg_dir.join("Cell.toml"), r#"[package] +edition = "2026" name = "multi-file" version = "0.2.0" namespace = "cellscript" @@ -2653,6 +2871,7 @@ namespace = "cellscript" std::fs::write( pkg_dir2.join("Cell.toml"), r#"[package] +edition = "2026" name = "det-check" version = "0.1.0" "#, @@ -2681,6 +2900,8 @@ fn e2e_registry_json_append_update_idempotency() { // ── 1. Append first version ── let v1 = RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.1.0".to_string(), tag: "v0.1.0".to_string(), source_hash: "hash_v1".to_string(), @@ -2705,6 +2926,8 @@ fn e2e_registry_json_append_update_idempotency() { // ── 2. Append second version ── let v2 = RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.2.0".to_string(), tag: "v0.2.0".to_string(), source_hash: "hash_v2".to_string(), @@ -2728,6 +2951,8 @@ fn e2e_registry_json_append_update_idempotency() { // ── 3. Re-append v0.1.0 (update semantics — should replace, not duplicate) ── let v1_updated = RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.1.0".to_string(), tag: "v0.1.0".to_string(), source_hash: "hash_v1_updated".to_string(), @@ -2814,6 +3039,7 @@ fn e2e_package_manager_registry_resolution_with_local_git() { let mut lockfile = Lockfile::new(); lockfile.package = LockfilePackageInfo { + edition: cellscript::CURRENT_EDITION, name: "consumer".to_string(), version: "0.1.0".to_string(), namespace: Some("cellscript".to_string()), @@ -2823,6 +3049,8 @@ fn e2e_package_manager_registry_resolution_with_local_git() { lockfile.dependencies.insert( "math-lib".to_string(), LockedDependency { + name: "math-lib".to_string(), + namespace: Some("cellscript".to_string()), version: "0.1.0".to_string(), source: LockedSource::Registry { registry: "https://github.com/cellscript/cellscript-registry".to_string(), @@ -2832,9 +3060,13 @@ fn e2e_package_manager_registry_resolution_with_local_git() { version: "0.1.0".to_string(), }, source_hash: Some(hash_math), + manifest_digest: "sha256:test-math-manifest".to_string(), + dependencies: BTreeMap::new(), build: None, }, ); + lockfile.root.manifest_digest = cellscript::package::compute_manifest_digest(&consumer_dir).unwrap(); + lockfile.root.dependencies.insert("math-lib".to_string(), "math-lib".to_string()); lockfile.write_to_root(&consumer_dir).unwrap(); // ── 6. Verify Cell.lock consistency ── diff --git a/tests/entry_witness_abi.rs b/tests/entry_witness_abi.rs new file mode 100644 index 00000000..0e6a1e6c --- /dev/null +++ b/tests/entry_witness_abi.rs @@ -0,0 +1,363 @@ +use cellscript_ckb_adapter::{place_entry_witness_payload_before_signing, EntryWitnessPlacementAbi}; +use ckb_sdk::{ + constants::MultisigScript, + traits::SecpCkbRawKeySigner, + types::ScriptGroup, + unlock::{MultisigConfig, ScriptSignError, ScriptSigner, SecpMultisigScriptSigner}, + SECP256K1, +}; +use ckb_testtool::{ + ckb_hash::blake2b_256, + ckb_types::{ + bytes::Bytes, + core::{DepType, TransactionBuilder}, + packed, + prelude::{Builder, Entity, Pack}, + H160, + }, + context::Context, +}; +use secp256k1::{PublicKey, SecretKey}; + +#[path = "support/ckb_script_runner.rs"] +#[allow(dead_code)] +mod ckb_script_runner; + +use ckb_script_runner::{build_simple_fixture, compile_cellscript_source_to_elf, execute_cellscript_script}; + +const PARAMETERIZED_ENTRY: &str = r#" +module entry_witness_abi + +action verify(witness expected: u64) -> u64 { + verification + require expected == 42 + return 0 +} +"#; + +const ALWAYS_SUCCESS_LOCK: &str = r#" +module entry_witness_always_success + +action always_success() -> u64 { + verification + return 0 +} +"#; + +const U128_DYNAMIC_SCHEMA_ENTRY: &str = r#" +module entry_witness_u128_dynamic_schema + +struct Entry { + amount: u128, + note: Vec, +} + +action verify(witness left: Entry, witness right: Entry, witness expected_add: u128) -> u64 { + verification + require left.amount + right.amount == expected_add + return 0 +} +"#; + +const U128_U64_ADD_ENTRY: &str = r#" +module entry_witness_u128_u64_add + +action verify_add(witness wide: u128, witness delta: u64, witness expected: u128) -> u64 { + verification + require wide + delta == expected + return 0 +} +"#; + +const U128_U64_SUB_ENTRY: &str = r#" +module entry_witness_u128_u64_sub + +action verify_sub(witness wide: u128, witness delta: u64, witness expected: u128) -> u64 { + verification + require wide - delta == expected + return 0 +} +"#; + +const SIGNED_TX_MAX_CYCLES: u64 = 70_000_000; + +fn canonical_multisig_v2_witness(entry_payload: Bytes) -> packed::WitnessArgs { + let signer_a = H160::from_slice(&[0x11; 20]).expect("20-byte signer hash"); + let signer_b = H160::from_slice(&[0x22; 20]).expect("20-byte signer hash"); + let config = + MultisigConfig::new_with(MultisigScript::V2, vec![signer_a, signer_b], 0, 2).expect("canonical 2-of-2 multisig-v2 config"); + + place_entry_witness_payload_before_signing( + &config.placeholder_witness(), + EntryWitnessPlacementAbi::WitnessArgsInputTypeV2, + entry_payload, + ) + .expect("place CellScript payload before signing") +} + +fn signer_id(secret_key: &SecretKey) -> H160 { + let public_key = PublicKey::from_secret_key(&SECP256K1, secret_key); + H160::from_slice(&blake2b_256(public_key.serialize())[..20]).expect("20-byte signer hash") +} + +fn raw_entry_payload(value: u64) -> Bytes { + let mut payload = b"CSARGv1\0".to_vec(); + payload.extend_from_slice(&value.to_le_bytes()); + Bytes::from(payload) +} + +/// Encode a dynamic-layout `Entry` as the Molecule table shape the runtime +/// decodes: ``. +/// The dynamic `Vec` field forces table decoding for the `u128` field. +fn molecule_dynamic_entry_bytes(amount: u128, note: &[u8], total_override: Option) -> Vec { + let amount_offset = 4 + 4 * 2; + let note_offset = amount_offset + 16; + let total = note_offset + note.len(); + let mut bytes = Vec::with_capacity(total); + bytes.extend_from_slice(&total_override.unwrap_or(total as u32).to_le_bytes()); + bytes.extend_from_slice(&(amount_offset as u32).to_le_bytes()); + bytes.extend_from_slice(&(note_offset as u32).to_le_bytes()); + bytes.extend_from_slice(&amount.to_le_bytes()); + bytes.extend_from_slice(note); + bytes +} + +fn execute_u128_dynamic_schema_add( + elf: &[u8], + left: u128, + right: u128, + expected_add: u128, + total_override: Option, +) -> ckb_script_runner::CkbScriptExecutionResult { + let mut payload = b"CSARGv1\0".to_vec(); + for value in [left, right] { + let entry = molecule_dynamic_entry_bytes(value, b"audit", total_override); + payload.extend_from_slice(&(entry.len() as u32).to_le_bytes()); + payload.extend_from_slice(&entry); + } + payload.extend_from_slice(&expected_add.to_le_bytes()); + let witness = canonical_multisig_v2_witness(Bytes::from(payload)); + let mut fixture = build_simple_fixture(Bytes::default(), 1, 1); + fixture.witnesses = vec![witness.as_bytes()]; + execute_cellscript_script(elf, &fixture) +} + +fn execute_u128_u64_arithmetic(elf: &[u8], wide: u128, delta: u64, expected: u128) -> ckb_script_runner::CkbScriptExecutionResult { + let mut payload = b"CSARGv1\0".to_vec(); + payload.extend_from_slice(&wide.to_le_bytes()); + payload.extend_from_slice(&delta.to_le_bytes()); + payload.extend_from_slice(&expected.to_le_bytes()); + let witness = canonical_multisig_v2_witness(Bytes::from(payload)); + let mut fixture = build_simple_fixture(Bytes::default(), 1, 1); + fixture.witnesses = vec![witness.as_bytes()]; + execute_cellscript_script(elf, &fixture) +} + +fn execute_on_second_group_input(witness: Bytes) -> ckb_script_runner::CkbScriptExecutionResult { + let elf = compile_cellscript_source_to_elf(PARAMETERIZED_ENTRY, "verify", None); + let mut fixture = build_simple_fixture(Bytes::default(), 2, 1); + fixture.current_type_script_input_indices = vec![1]; + fixture.witnesses = vec![Bytes::from_static(b"unrelated-global-input-zero"), witness]; + execute_cellscript_script(&elf, &fixture) +} + +fn execute_on_output_only_group(witness: Bytes) -> ckb_script_runner::CkbScriptExecutionResult { + let elf = compile_cellscript_source_to_elf(PARAMETERIZED_ENTRY, "verify", None); + let mut fixture = build_simple_fixture(Bytes::default(), 1, 1); + fixture.current_type_script_input_indices.clear(); + fixture.witnesses = vec![witness]; + execute_cellscript_script(&elf, &fixture) +} + +#[test] +fn signed_multisig_v2_lock_and_cellscript_type_execute_in_ckb_vm() -> Result<(), ScriptSignError> { + let key_a = SecretKey::from_slice(&[0x11; 32]).expect("valid signer A key"); + let key_b = SecretKey::from_slice(&[0x22; 32]).expect("valid signer B key"); + let config = MultisigConfig::new_with(MultisigScript::V2, vec![signer_id(&key_a), signer_id(&key_b)], 0, 2)?; + + let mut context = Context::new_with_deterministic_rng(); + let multisig_v2 = ckb_system_scripts_v0_6_0::BUNDLED_CELL + .get("specs/cells/secp256k1_blake160_multisig_all") + .expect("bundled multisig-v2 script"); + context.deploy_cell(Bytes::copy_from_slice(&multisig_v2)); + let secp256k1_data = ckb_system_scripts_v0_6_0::BUNDLED_CELL.get("specs/cells/secp256k1_data").expect("bundled secp256k1 data"); + let secp256k1_data_out_point = context.deploy_cell(Bytes::copy_from_slice(&secp256k1_data)); + + let always_success_elf = compile_cellscript_source_to_elf(ALWAYS_SUCCESS_LOCK, "always_success", None); + let always_success_out_point = context.deploy_cell(Bytes::from(always_success_elf)); + let always_success_lock = context.build_script(&always_success_out_point, Bytes::default()).expect("build always-success lock"); + + let cellscript_elf = compile_cellscript_source_to_elf(PARAMETERIZED_ENTRY, "verify", None); + let cellscript_out_point = context.deploy_cell(Bytes::from(cellscript_elf)); + let cellscript_type = context.build_script(&cellscript_out_point, Bytes::default()).expect("build CellScript type script"); + let multisig_lock: packed::Script = (&config).into(); + + let unrelated_input = context.create_cell( + packed::CellOutput::new_builder() + .capacity::(100_000_000_000u64.pack()) + .lock(always_success_lock.clone()) + .build(), + Bytes::default(), + ); + let multisig_input = context.create_cell( + packed::CellOutput::new_builder() + .capacity::(100_000_000_000u64.pack()) + .lock(multisig_lock.clone()) + .type_(Some(cellscript_type.clone()).pack()) + .build(), + Bytes::default(), + ); + let output = packed::CellOutput::new_builder() + .capacity::(190_000_000_000u64.pack()) + .lock(always_success_lock) + .type_(Some(cellscript_type).pack()) + .build(); + + let unsigned_witness = place_entry_witness_payload_before_signing( + &config.placeholder_witness(), + EntryWitnessPlacementAbi::WitnessArgsInputTypeV2, + raw_entry_payload(42), + ) + .expect("place entry payload before signing"); + let tx = TransactionBuilder::default() + .inputs([ + packed::CellInput::new_builder().previous_output(unrelated_input).build(), + packed::CellInput::new_builder().previous_output(multisig_input).build(), + ]) + .output(output) + .output_data(Bytes::default().pack()) + .cell_dep(packed::CellDep::new_builder().out_point(secp256k1_data_out_point).dep_type(DepType::Code).build()) + .witnesses([Bytes::from_static(b"unrelated-global-input-zero"), unsigned_witness.as_bytes()].pack()) + .build(); + let tx = context.complete_tx(tx); + + let raw_signer = SecpCkbRawKeySigner::new_with_secret_keys(vec![key_a, key_b]); + let signer = SecpMultisigScriptSigner::new(Box::new(raw_signer), config); + let mut lock_group = ScriptGroup::from_lock_script(&multisig_lock); + lock_group.input_indices.push(1); + let signed_tx = signer.sign_tx(&tx, &lock_group)?; + + let signed_witness = packed::WitnessArgs::from_slice(signed_tx.witnesses().get(1).expect("multisig witness").raw_data().as_ref()) + .expect("signed WitnessArgs"); + let lock = signed_witness.lock().to_opt().expect("signed multisig lock").raw_data(); + let signature_offset = 4 + 2 * 20; + assert_eq!(&lock[..4], &[0, 0, 2, 2], "canonical 2-of-2 multisig header"); + assert!(lock[signature_offset..].iter().any(|byte| *byte != 0), "multisig signatures must be populated"); + context.verify_tx(&signed_tx, SIGNED_TX_MAX_CYCLES).expect("multisig-v2 lock and CellScript type script must both pass"); + + // A valid, otherwise unused output_type mutation keeps the CellScript + // input_type payload valid, but must invalidate the multisig signature. + let tampered_witness = signed_witness.as_builder().output_type(Some(Bytes::from_static(b"post-signing-mutation")).pack()).build(); + let mut witnesses: Vec = signed_tx.witnesses().into_iter().collect(); + witnesses[1] = tampered_witness.as_bytes().pack(); + let tampered_tx = signed_tx.as_advanced_builder().set_witnesses(witnesses).build(); + assert!( + context.verify_tx(&tampered_tx, SIGNED_TX_MAX_CYCLES).is_err(), + "mutating WitnessArgs after signing must invalidate multisig-v2" + ); + + Ok(()) +} + +#[test] +fn raw_v1_group_input_payload_is_rejected_by_placement_abi_v2() { + let result = execute_on_second_group_input(raw_entry_payload(42)); + assert_eq!( + result.exit_code, 25, + "placement ABI v2 must require WitnessArgs.input_type instead of accepting a raw payload alias: {:?}", + result.captured_debug + ); +} + +#[test] +fn witnessargs_input_type_falls_back_to_group_output_zero() { + let witness = canonical_multisig_v2_witness(raw_entry_payload(42)); + let result = execute_on_output_only_group(witness.as_bytes()); + assert_eq!(result.exit_code, 0, "an output-only type group must resolve GroupOutput#0: {:?}", result.captured_debug); +} + +#[test] +fn witnessargs_output_type_is_not_an_entry_payload_alias() { + let witness = canonical_multisig_v2_witness(raw_entry_payload(42)) + .as_builder() + .input_type(None::.pack()) + .output_type(Some(raw_entry_payload(42)).pack()) + .build(); + let result = execute_on_second_group_input(witness.as_bytes()); + assert_eq!(result.exit_code, 25, "wrong WitnessArgs field must fail closed: {:?}", result.captured_debug); +} + +#[test] +fn malformed_witnessargs_input_type_length_fails_closed() { + let witness = canonical_multisig_v2_witness(raw_entry_payload(42)); + let mut encoded = witness.as_slice().to_vec(); + let input_type_offset = u32::from_le_bytes(encoded[8..12].try_into().expect("input_type table offset")) as usize; + let declared_len = + u32::from_le_bytes(encoded[input_type_offset..input_type_offset + 4].try_into().expect("input_type Bytes length")); + encoded[input_type_offset..input_type_offset + 4].copy_from_slice(&(declared_len + 1).to_le_bytes()); + + let result = execute_on_second_group_input(Bytes::from(encoded)); + assert_eq!(result.exit_code, 25, "malformed Molecule must fail closed: {:?}", result.captured_debug); +} + +#[test] +fn malformed_unselected_witnessargs_field_still_fails_closed() { + let witness = canonical_multisig_v2_witness(raw_entry_payload(42)) + .as_builder() + .output_type(Some(Bytes::from_static(b"protocol-output-data")).pack()) + .build(); + let mut encoded = witness.as_slice().to_vec(); + let output_type_offset = u32::from_le_bytes(encoded[12..16].try_into().expect("output_type table offset")) as usize; + let declared_len = + u32::from_le_bytes(encoded[output_type_offset..output_type_offset + 4].try_into().expect("output_type Bytes length")); + encoded[output_type_offset..output_type_offset + 4].copy_from_slice(&(declared_len + 1).to_le_bytes()); + + let result = execute_on_second_group_input(Bytes::from(encoded)); + assert_eq!(result.exit_code, 25, "the placement ABI must validate the whole WitnessArgs table: {:?}", result.captured_debug); +} + +#[test] +fn u128_add_on_dynamic_schema_fields_executes_exactly_in_ckb_vm() { + let elf = compile_cellscript_source_to_elf(U128_DYNAMIC_SCHEMA_ENTRY, "verify", None); + let vectors: [(u128, u128); 3] = [ + (0x00ff_ffee_ddcc_bbaa_55aa_55aa_55aa_55aa, 0x55aa_55aa_55aa_55aa_aa55_aa55_aa55_aa55), + (0x0123_4567_89ab_cdef_fedc_ba98_7654_3210, 0xf0f0_0f0f_f0f0_0f0f_aaaa_5555_aaaa_5555), + (1, u128::MAX - 1), + ]; + for (left, right) in vectors { + let expected_add = left.checked_add(right).expect("vector must not overflow"); + let result = execute_u128_dynamic_schema_add(&elf, left, right, expected_add, None); + assert_eq!( + result.exit_code, 0, + "u128 addition over Molecule-table-decoded fields must execute exactly in CKB-VM: {:?}", + result.captured_debug + ); + } + + let wrong = execute_u128_dynamic_schema_add(&elf, 1, 2, 4, None); + assert_eq!(wrong.exit_code, 5, "a wrong expected sum must fail the assertion: {:?}", wrong.captured_debug); + + let malformed = execute_u128_dynamic_schema_add(&elf, 8, 1, 9, Some(255)); + assert_eq!( + malformed.exit_code, 2, + "a mismatched Molecule table length must fail the bounds check: {:?}", + malformed.captured_debug + ); +} + +#[test] +fn u128_u64_arithmetic_checks_carry_overflow_and_underflow_in_ckb_vm() { + let add_elf = compile_cellscript_source_to_elf(U128_U64_ADD_ENTRY, "verify_add", None); + let carried = execute_u128_u64_arithmetic(&add_elf, u64::MAX as u128, 1, 1u128 << 64); + assert_eq!(carried.exit_code, 0, "u128 + u64 carry must execute exactly: {:?}", carried.captured_debug); + + let overflow = execute_u128_u64_arithmetic(&add_elf, u128::MAX, 1, 0); + assert_eq!(overflow.exit_code, 49, "u128 + u64 overflow must fail closed: {:?}", overflow.captured_debug); + + let sub_elf = compile_cellscript_source_to_elf(U128_U64_SUB_ENTRY, "verify_sub", None); + let borrowed = execute_u128_u64_arithmetic(&sub_elf, 1u128 << 64, 1, u64::MAX as u128); + assert_eq!(borrowed.exit_code, 0, "u128 - u64 borrow must execute exactly: {:?}", borrowed.captured_debug); + + let underflow = execute_u128_u64_arithmetic(&sub_elf, 0, 1, 0); + assert_eq!(underflow.exit_code, 49, "u128 - u64 underflow must fail closed: {:?}", underflow.captured_debug); +} diff --git a/tests/examples.rs b/tests/examples.rs index 99bb0441..85cdc38d 100644 --- a/tests/examples.rs +++ b/tests/examples.rs @@ -1,5 +1,3 @@ -#![allow(clippy::too_many_arguments)] - use camino::{Utf8Path, Utf8PathBuf}; use cellscript::{ codegen::{analyze_backend_shape, BackendShapeMetrics}, @@ -173,7 +171,9 @@ const BUNDLED_EXAMPLE_ASM_SHAPE_BUDGETS: [(&str, AssemblyShapeBudget); 9] = [ max_lines: 24_500, max_fail_handlers: 64, max_shared_epilogues: 20, - max_text_bytes: 92 * 1024, + // The v2 placement parser adds 68 bytes to the full multisig text + // surface while the focused transfer entry remains below 7 KiB. + max_text_bytes: 93 * 1024, max_relaxed_branches: 4, max_cond_branch_abs_distance: 7_700, max_machine_blocks: 3_600, @@ -500,7 +500,7 @@ fn docs_examples_cellscript_blocks_match_declared_compile_boundary() { let rejected_collections = write_wrapped_doc_snippet(&temp_root, "collections_rejected", &collections[2]); let rejected_source = std::fs::read_to_string(&rejected_collections).expect("rejected collection snippet should be readable"); - let rejected_report = compile_metadata_with_diagnostics(&rejected_source, None); + let rejected_report = compile_metadata_with_diagnostics(&rejected_source, cellscript::CURRENT_EDITION, None); assert!( rejected_report.diagnostics.iter().any(|diagnostic| { diagnostic.message.contains("type 'Vec' cannot store a cell-backed resource") @@ -522,6 +522,8 @@ fn token_amm_bootstrap_docs_cover_builder_friction_boundary() { "launch_token` materialises the Pool and LP receipt topology directly", "Do not rely on \"the first action runs on creation\" as a protocol rule", "Cell-bound inputs and outputs are transaction Cells, not witness payload args", + "place the payload in `input_type` before any lock-script signer runs", + "Never submit the raw `CSARGv1` payload as a transaction witness", "Strict v0.16 ProofPlan checks compile the bundled token, AMM, and launch actions as original scoped entries", ] { assert!(bootstrap_text.contains(needle), "bootstrap guide should contain `{needle}`"); @@ -534,6 +536,10 @@ fn token_amm_bootstrap_docs_cover_builder_friction_boundary() { ] { assert!(bootstrap.contains(needle), "bootstrap guide should contain `{needle}`"); } + assert!( + !bootstrap.contains("Do not wrap it in `WitnessArgs.input_type`"), + "bootstrap guide must not reintroduce the retired raw-witness placement guidance" + ); let flows = std::fs::read_to_string( Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("docs").join("CELLSCRIPT_EXAMPLE_BUSINESS_FLOWS.md"), diff --git a/tests/fuzzy_debug.rs b/tests/fuzzy_debug.rs index e6bec871..181fca8b 100644 --- a/tests/fuzzy_debug.rs +++ b/tests/fuzzy_debug.rs @@ -367,6 +367,7 @@ fn fuzzy_unicode_hex_inputs_are_controlled_errors() { root.join("Cell.toml"), r#" [package] +edition = "2026" name = "fuzzy_cli_hex" version = "0.1.0" "#, diff --git a/tests/ickb_diff.rs b/tests/ickb_diff.rs index 58c220a1..496ccae8 100644 --- a/tests/ickb_diff.rs +++ b/tests/ickb_diff.rs @@ -3078,8 +3078,6 @@ fn cellscript_ckb_script_executes_pass_with_syscall_and_fails_with_reject() { Bytes::default(), // empty script args 1, // one input cell 1, // one output cell (gets type script under test) - true, // expected pass - None, // no failure mode ); let pass_result = execute_cellscript_script(&pass_elf, &pass_fixture); assert_eq!( @@ -3096,8 +3094,6 @@ fn cellscript_ckb_script_executes_pass_with_syscall_and_fails_with_reject() { Bytes::default(), // empty script args 1, // one input cell 1, // one output cell - false, // expected fail - Some("always_reject".to_string()), ); let fail_result = execute_cellscript_script(&fail_elf, &fail_fixture); assert_eq!( @@ -3169,8 +3165,6 @@ fn cellscript_dao_accumulated_rate_passes_with_valid_header() { 10000, // accumulated rate in the header DAO field 1, // one input cell (linked to the header) 1, // one output cell - true, // expected pass - None, // no failure mode ); let result = execute_cellscript_script(&elf, &fixture); assert_eq!( @@ -3193,8 +3187,6 @@ fn cellscript_dao_accumulated_rate_fails_without_header_dep() { Bytes::default(), // empty script args 1, // one input cell 1, // one output cell - false, // expected fail - Some("dao_missing_header_dep".to_string()), ); let result = execute_cellscript_script(&elf, &fixture); assert_ne!( @@ -3231,8 +3223,6 @@ fn cellscript_dao_is_deposit_data_passes_with_deposit_cell() { Bytes::default(), // empty script args vec![deposit_data], // one input with deposit data 1, // one output cell - true, // expected pass - None, // no failure mode ); let result = execute_cellscript_script(&elf, &fixture); assert_eq!( @@ -3256,8 +3246,6 @@ fn cellscript_dao_is_withdrawal_request_data_passes_with_withdrawal_cell() { Bytes::default(), // empty script args vec![withdrawal_data], // one input with withdrawal data 1, // one output cell - true, // expected pass - None, // no failure mode ); let result = execute_cellscript_script(&elf, &fixture); assert_eq!( @@ -3278,8 +3266,6 @@ fn cellscript_cell_capacity_passes_with_nonzero_capacity() { Bytes::default(), // empty script args vec![Bytes::default()], // one input with empty data (capacity is set by fixture) 1, // one output cell - true, // expected pass - None, // no failure mode ); let result = execute_cellscript_script(&elf, &fixture); assert_eq!( @@ -3301,8 +3287,6 @@ fn cellscript_dao_has_dao_type_returns_false_for_non_dao_cell() { Bytes::default(), // empty script args vec![Bytes::default()], // one input with empty data and no type script 1, // one output cell - true, // expected pass (has_dao_type returns false, script returns 0) - None, // no failure mode ); let result = execute_cellscript_script(&elf, &fixture); assert_eq!( @@ -3334,8 +3318,6 @@ fn cellscript_cell_occupied_capacity_passes_with_lock_script() { Bytes::default(), // empty script args vec![Bytes::default()], // one input with empty data 1, // one output cell - true, // expected pass - None, // no failure mode ); let result = execute_cellscript_script(&elf, &fixture); assert_eq!( @@ -3357,8 +3339,6 @@ fn cellscript_cell_data_size_passes_with_data() { Bytes::default(), // empty script args vec![data], // one input with 8 bytes of data 1, // one output cell - true, // expected pass - None, // no failure mode ); let result = execute_cellscript_script(&elf, &fixture); assert_eq!( @@ -3378,15 +3358,8 @@ fn cellscript_cell_dep_data_size_passes_with_fixture_cell_dep() { Bytes::default(), // empty script args 1, // one input cell 1, // one output cell - true, // expected pass - None, // no failure mode - ); - fixture.cell_deps.push(FixtureCell { - capacity: 0, - lock: packed::Script::default(), - type_script: None, - data: Bytes::from(vec![1, 2, 3, 4]), - }); + ); + fixture.cell_deps.push(FixtureCell { capacity: 0, type_script: None, data: Bytes::from(vec![1, 2, 3, 4]) }); let result = execute_cellscript_script(&elf, &fixture); assert_eq!( @@ -3401,7 +3374,7 @@ fn cellscript_cell_dep_data_size_passes_with_fixture_cell_dep() { fn cellscript_witness_args_empty_lock_passes_in_ckb_vm() { let elf = compile_cellscript_source_to_elf(VM_HARNESS_WITNESS_ARGS_PROGRAM, VM_HARNESS_WITNESS_ARGS_ACTION, None); - let mut fixture = build_simple_fixture(Bytes::default(), 1, 1, true, None); + let mut fixture = build_simple_fixture(Bytes::default(), 1, 1); fixture.witnesses = vec![molecule_witness_args(None, None, None)]; let result = execute_cellscript_script(&elf, &fixture); @@ -3418,7 +3391,7 @@ fn cellscript_require_witness_size_at_least_rejects_too_small_in_ckb_vm() { let elf = compile_cellscript_source_to_elf(VM_HARNESS_WITNESS_SIZE_TOO_SMALL_PROGRAM, VM_HARNESS_WITNESS_SIZE_TOO_SMALL_ACTION, None); - let mut fixture = build_simple_fixture(Bytes::default(), 1, 1, false, Some("witness_size_too_small".to_string())); + let mut fixture = build_simple_fixture(Bytes::default(), 1, 1); fixture.witnesses = vec![molecule_witness_args(None, None, None)]; let result = execute_cellscript_script(&elf, &fixture); @@ -3435,7 +3408,7 @@ fn cellscript_require_witness_size_at_least_rejects_too_small_in_ckb_vm() { fn cellscript_witness_args_short_lock_is_zero_padded_in_ckb_vm() { let elf = compile_cellscript_source_to_elf(VM_HARNESS_WITNESS_SHORT_LOCK_PROGRAM, VM_HARNESS_WITNESS_SHORT_LOCK_ACTION, None); - let mut fixture = build_simple_fixture(Bytes::default(), 1, 1, true, None); + let mut fixture = build_simple_fixture(Bytes::default(), 1, 1); fixture.witnesses = vec![molecule_witness_args(Some(&[0u8][..]), None, None)]; let result = execute_cellscript_script(&elf, &fixture); @@ -3454,7 +3427,7 @@ fn cellscript_witness_args_lock_input_type_output_type_are_isolated_in_ckb_vm() let lock = [0x11u8; 32]; let input_type = [0x22u8; 32]; let output_type = [0x33u8; 32]; - let mut fixture = build_simple_fixture(Bytes::default(), 1, 1, true, None); + let mut fixture = build_simple_fixture(Bytes::default(), 1, 1); fixture.witnesses = vec![ckb_packed_witness_args(Some(&lock), Some(&input_type), Some(&output_type))]; let result = execute_cellscript_script(&elf, &fixture); @@ -3470,7 +3443,7 @@ fn cellscript_witness_args_lock_input_type_output_type_are_isolated_in_ckb_vm() fn cellscript_witness_args_total_size_mismatch_rejects_in_ckb_vm() { let elf = compile_cellscript_source_to_elf(VM_HARNESS_WITNESS_MALFORMED_PROGRAM, VM_HARNESS_WITNESS_MALFORMED_ACTION, None); - let mut fixture = build_simple_fixture(Bytes::default(), 1, 1, false, Some("witness_total_size_mismatch".to_string())); + let mut fixture = build_simple_fixture(Bytes::default(), 1, 1); fixture.witnesses = vec![molecule_witness_args_with_header(17, [16, 16, 16], &[])]; let result = execute_cellscript_script(&elf, &fixture); @@ -3487,7 +3460,7 @@ fn cellscript_witness_args_total_size_mismatch_rejects_in_ckb_vm() { fn cellscript_witness_args_reordered_offsets_reject_in_ckb_vm() { let elf = compile_cellscript_source_to_elf(VM_HARNESS_WITNESS_MALFORMED_PROGRAM, VM_HARNESS_WITNESS_MALFORMED_ACTION, None); - let mut fixture = build_simple_fixture(Bytes::default(), 1, 1, false, Some("witness_reordered_offsets".to_string())); + let mut fixture = build_simple_fixture(Bytes::default(), 1, 1); fixture.witnesses = vec![molecule_witness_args_with_header(16, [16, 12, 16], &[])]; let result = execute_cellscript_script(&elf, &fixture); @@ -3504,7 +3477,7 @@ fn cellscript_witness_args_reordered_offsets_reject_in_ckb_vm() { fn cellscript_witness_args_truncated_offsets_reject_in_ckb_vm() { let elf = compile_cellscript_source_to_elf(VM_HARNESS_WITNESS_MALFORMED_PROGRAM, VM_HARNESS_WITNESS_MALFORMED_ACTION, None); - let mut fixture = build_simple_fixture(Bytes::default(), 1, 1, false, Some("witness_truncated_offsets".to_string())); + let mut fixture = build_simple_fixture(Bytes::default(), 1, 1); fixture.witnesses = vec![molecule_witness_args_with_header(16, [16, 16, 17], &[])]; let result = execute_cellscript_script(&elf, &fixture); @@ -3603,8 +3576,6 @@ fn cellscript_ickb_deposit_verification_passes_with_valid_dao_deposit() { 10000, // accumulated rate 1, // one input cell 1, // one output cell - true, // expected pass - None, // no failure mode ); // Set the input cell data to 8 zero bytes (DAO deposit marker). // The build_dao_fixture creates empty data, so we need to set deposit data. diff --git a/tests/ls_idl_upstream.rs b/tests/ls_idl_upstream.rs new file mode 100644 index 00000000..ec524eee --- /dev/null +++ b/tests/ls_idl_upstream.rs @@ -0,0 +1,106 @@ +use std::collections::BTreeSet; + +use base64::Engine as _; +use cellscript::package::registry::validate_ls_idl_document; +use serde_json::{json, Value}; +use sha2::{Digest as _, Sha256}; + +const CLIENT_VECTORS_SHA256: &str = "a9a6dca4fd0c5fcd2ca7aea6468784be7fdb29d6274049f07090cbab0ce9c1bb"; + +fn decode_fixture(encoded: &str) -> Vec { + let compact: String = encoded.chars().filter(|character| !character.is_whitespace()).collect(); + base64::engine::general_purpose::STANDARD.decode(compact).expect("valid fixture Base64") +} + +fn sha256_hex(bytes: &[u8]) -> String { + hex::encode(Sha256::digest(bytes)) +} + +#[test] +fn registry_admits_every_pinned_upstream_idl_document_without_reserializing() { + let fixtures: Vec<(&str, Vec, &str)> = vec![ + ( + "derive/multisig-2of2-nonce", + include_bytes!("compat/ls_idl/derive/multisig-2of2-nonce.json").to_vec(), + "587098bbe12e37a7394d06ff711a59242f033759e9ba7f5b62b8f6a234275063", + ), + ( + "derive/pow-lock", + include_bytes!("compat/ls_idl/derive/pow-lock.json").to_vec(), + "d551803734459f28b2849f13b2111778d3753b518701a86a434e9438df86e2d6", + ), + ( + "derive/schnorr-pubkey-recovery", + include_bytes!("compat/ls_idl/derive/schnorr-pubkey-recovery.json").to_vec(), + "b37329b5fb13b25de94ef068724839f356096bc3516dda461b516ee983a8d371", + ), + ( + "derive/secp256k1-timelock", + include_bytes!("compat/ls_idl/derive/secp256k1-timelock.json").to_vec(), + "056bc4f2b11bc7f0dfead9f2dcc0ec5097b42b353d4577b3836ef872b121710f", + ), + ( + "derive/simple-lock", + include_bytes!("compat/ls_idl/derive/simple-lock.json").to_vec(), + "d28abead992546908eb483c24667e58302f193c00e08f6cbed1a6302995ca1c0", + ), + ( + "scripts/simple-lock", + decode_fixture(include_str!("compat/ls_idl/scripts/simple-lock.idl.json.b64")), + "6fd2ab0171167c6862582c4e95a6de7b1cd153f77a936af7e52be6599ddddd31", + ), + ( + "scripts/timelock-lock", + decode_fixture(include_str!("compat/ls_idl/scripts/timelock-lock.idl.json.b64")), + "18ae57828b5fbd0c8df0900eed1153e7585587d4049900c50729616227a9beda", + ), + ]; + + for (name, bytes, expected_sha256) in fixtures { + assert_eq!(sha256_hex(&bytes), expected_sha256, "raw-byte drift in {name}"); + validate_ls_idl_document(&bytes).unwrap_or_else(|error| panic!("{name}: {error}")); + } +} + +#[test] +fn registry_schema_tracks_the_complete_pinned_upstream_client_vector_corpus() { + let bytes = decode_fixture(include_str!("compat/ls_idl/ckb-idl-client-test-vectors.json.b64")); + assert_eq!(sha256_hex(&bytes), CLIENT_VECTORS_SHA256); + + let document: Value = serde_json::from_slice(&bytes).expect("valid upstream vector JSON"); + let vectors = document["vectors"].as_array().expect("upstream vectors array"); + assert_eq!(vectors.len(), 17, "review the compatibility profile when upstream adds vectors"); + + let mut observed_types = BTreeSet::new(); + let mut rejected_ids = Vec::new(); + for vector in vectors { + let id = vector["id"].as_str().expect("vector id"); + let fields = vector["fields"].as_array().expect("vector fields"); + for field in fields { + observed_types.insert(field["type"].as_str().expect("field type").to_string()); + } + let registry_document = serde_json::to_vec(&json!({ "witness": fields })).expect("serialize Registry schema probe"); + match validate_ls_idl_document(®istry_document) { + Ok(()) => assert_ne!(id, "unknown-type-rejected", "unknown types must fail closed"), + Err(error) => { + assert_eq!(id, "unknown-type-rejected", "unexpected rejection for {id}: {error}"); + assert!(error.contains("must be one of")); + rejected_ids.push(id); + } + } + } + + assert_eq!(rejected_ids, ["unknown-type-rejected"]); + assert_eq!( + observed_types, + BTreeSet::from([ + "bytes".to_string(), + "molecule_bytes".to_string(), + "schnorr_sig".to_string(), + "secp256k1_sig".to_string(), + "uint32".to_string(), + "uint64".to_string(), + "uint8".to_string(), + ]) + ); +} diff --git a/tests/myelin_handoff.rs b/tests/myelin_handoff.rs new file mode 100644 index 00000000..42569f70 --- /dev/null +++ b/tests/myelin_handoff.rs @@ -0,0 +1,48 @@ +use serde_json::Value; + +fn contract() -> Value { + serde_json::from_str(include_str!("../integrations/myelin/cellscript-0.24-handoff-contract.json")).unwrap() +} + +#[test] +fn myelin_handoff_is_ckb_only_versioned_and_rejects_raw_witness_aliases() { + let value = contract(); + assert_eq!(value["schema"], "cellscript-myelin-handoff-contract-v1"); + assert_eq!(value["release_line"], "0.24"); + assert_eq!(value["compiler"]["edition"], "2026"); + assert_eq!(value["compiler"]["target_profile"], "ckb"); + assert_eq!(value["compatibility_profile"]["metadata_schema_version"], 58); + assert_eq!(value["compatibility_profile"]["entry_witness_placement_field"], "input_type"); + assert_eq!(value["compatibility_profile"]["raw_entry_witness_payload_compatible"], false); + assert_eq!(value["allow_legacy_fallback"], false); + assert_eq!(value["verified_artifact"]["semantic_equivalence_claimed"], false); + + let forbidden = value["forbidden_cellscript_profiles"].as_array().unwrap(); + assert!(forbidden.iter().any(|profile| profile == "MyelinExtended")); + assert!(!forbidden.iter().any(|profile| profile == "ckb")); +} + +#[test] +fn myelin_adoption_requires_all_artifact_checker_and_source_bindings() { + let value = contract(); + assert_eq!(value["adoption_state"], "pending-external-release-pin"); + assert_eq!(value["source_revision_policy"], "exact-40-hex-release-commit-required"); + let bindings = value["required_exact_bindings"].as_array().unwrap(); + for required in [ + "compiler_binary_sha256", + "source_revision", + "source_tree_digest", + "artifact_ckb_blake2b256", + "metadata_ckb_blake2b256", + "compatibility_profile_ckb_blake2b256", + "lowering_record_ckb_blake2b256", + "source_map_ckb_blake2b256", + "checker_binary_sha256", + "checker_policy_ckb_blake2b256", + ] { + assert!(bindings.iter().any(|binding| binding == required), "missing required handoff binding {required}"); + } + assert_eq!(value["scheduler_boundary"]["compiler_access_template_authority"], "untrusted-template"); + assert_eq!(value["scheduler_boundary"]["authenticated_concrete_cell_resolution"], "myelin-owned"); + assert_eq!(value["scheduler_boundary"]["scheduler_plan_location"], "sidecar"); +} diff --git a/tests/registry.rs b/tests/registry.rs index 27c28a4b..f7bff950 100644 --- a/tests/registry.rs +++ b/tests/registry.rs @@ -1,4 +1,4 @@ -//! Integration tests for the Phase 1 Registry system. +//! Integration tests for the Registry artifact model and offline index tools. //! //! Tests cover: //! - Source hash computation determinism @@ -6,22 +6,28 @@ //! - Discovery index lookup with local Git fixture //! - Deployed.toml file round-trip //! - Cell.lock new fields (package.build, deployment.*) -//! - Full publish → verify flow with local Git fixtures +//! - Public artifact API dependency resolution with immutable snapshots +//! - Explicit offline Git fixture editing and verification //! - Fail-closed verification on hash mismatch +use base64::Engine as _; use cellscript::package::registry::{ compute_source_hash, DiscoveryEntry, DiscoveryIndex, RegistryAuditInfo, RegistryDependencyRef, RegistryEntryStatus, RegistryIndex, - RegistryResolutionPolicy, RegistryVersion, + RegistryVersion, }; use cellscript::package::{ DeployedBuildInfo, DeployedManifest, DeployedPackageInfo, DeploymentCellDep, DeploymentRecord, DeploymentStatus, LockedBuildInfo, LockedDependency, LockedSource, Lockfile, LockfileDeploymentRef, LockfilePackageInfo, PackageManager, ScriptRole, DEPLOYED_MANIFEST_SCHEMA, }; +use sha2::{Digest as _, Sha256}; use std::collections::BTreeMap; use std::ffi::OsString; +use std::io::{Read, Write}; +use std::net::TcpListener; use std::path::Path; -use std::sync::{Mutex, MutexGuard}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex, MutexGuard}; static REGISTRY_ENV_LOCK: Mutex<()> = Mutex::new(()); @@ -31,12 +37,12 @@ struct RegistryEnvGuard { } impl RegistryEnvGuard { - fn new(url: &Path) -> Self { - let guard = REGISTRY_ENV_LOCK.lock().unwrap(); - let previous = std::env::var_os(cellscript::package::registry::REGISTRY_URL_ENV); + fn new(url: &str) -> Self { + let guard = REGISTRY_ENV_LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + let previous = std::env::var_os(cellscript::package::registry::REGISTRY_API_URL_ENV); // SAFETY: CI runs tests with one test thread, and this guard serializes // registry URL changes within this test binary. - unsafe { std::env::set_var(cellscript::package::registry::REGISTRY_URL_ENV, url) }; + unsafe { std::env::set_var(cellscript::package::registry::REGISTRY_API_URL_ENV, url) }; Self { previous, _guard: guard } } } @@ -45,14 +51,138 @@ impl Drop for RegistryEnvGuard { fn drop(&mut self) { if let Some(previous) = &self.previous { // SAFETY: See `RegistryEnvGuard::new`; the guard still owns the lock. - unsafe { std::env::set_var(cellscript::package::registry::REGISTRY_URL_ENV, previous) }; + unsafe { std::env::set_var(cellscript::package::registry::REGISTRY_API_URL_ENV, previous) }; } else { // SAFETY: See `RegistryEnvGuard::new`; the guard still owns the lock. - unsafe { std::env::remove_var(cellscript::package::registry::REGISTRY_URL_ENV) }; + unsafe { std::env::remove_var(cellscript::package::registry::REGISTRY_API_URL_ENV) }; } } } +struct PackageArtifactApi { + origin: String, + stop: Arc, + handle: Option>, +} + +impl Drop for PackageArtifactApi { + fn drop(&mut self) { + self.stop.store(true, Ordering::Release); + if let Some(handle) = self.handle.take() + && let Err(payload) = handle.join() + && !std::thread::panicking() + { + std::panic::resume_unwind(payload); + } + } +} + +fn read_mock_http_path(stream: &mut std::net::TcpStream) -> String { + let mut request = Vec::new(); + let mut buffer = [0_u8; 1024]; + loop { + let read = match stream.read(&mut buffer) { + Ok(read) => read, + Err(error) if matches!(error.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::Interrupted) => { + std::thread::yield_now(); + continue; + } + Err(error) => panic!("artifact API fixture request read failed: {error}"), + }; + assert_ne!(read, 0, "artifact API request ended before headers"); + request.extend_from_slice(&buffer[..read]); + if let Some(header_end) = request.windows(4).position(|window| window == b"\r\n\r\n") { + let headers = String::from_utf8_lossy(&request[..header_end]); + return headers.lines().next().and_then(|line| line.split_whitespace().nth(1)).unwrap_or("/").to_string(); + } + } +} + +fn start_package_artifact_api(source_root: &Path, verification_status: &str) -> PackageArtifactApi { + let index = RegistryIndex::read_from_repo(source_root).unwrap(); + assert_eq!(index.versions.len(), 1, "single-package API fixture expects one release"); + let version = &index.versions[0]; + let snapshot_files = ["Cell.toml", "src/main.cell"] + .into_iter() + .map(|path| { + let content = std::fs::read(source_root.join(path)).unwrap(); + serde_json::json!({ + "path": path, + "blake2b256": hex::encode(cellscript::ckb_blake2b256(&content)), + "content_base64": base64::engine::general_purpose::STANDARD.encode(content), + }) + }) + .collect::>(); + let snapshot = serde_json::to_vec(&serde_json::json!({ + "schema": "cellscript-source-snapshot-v1", + "package": { "namespace": index.namespace, "name": index.name, "version": version.version }, + "files": snapshot_files, + })) + .unwrap(); + let snapshot_hash = format!("sha256:{}", hex::encode(Sha256::digest(&snapshot))); + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + listener.set_nonblocking(true).unwrap(); + let origin = format!("http://{}", listener.local_addr().unwrap()); + let artifact_path = format!("/v1/artifacts/{}/{}", index.namespace, index.name); + let snapshot_path = format!("/source-snapshots/{}/{}/{}/fixture.json", index.namespace, index.name, version.version); + let artifact = serde_json::to_vec(&serde_json::json!({ + "schema": "cellscript-registry-artifact", + "namespace": index.namespace, + "name": index.name, + "repository": format!("https://example.test/{}/{}", index.namespace, index.name), + "artifact": { + "kind": "source_library", + "profile": "cellscript_source", + "consumption_mode": "dependency", + "language": "cellscript" + }, + "releases": [{ + "release": version.version, + "verification_status": verification_status, + "availability_status": "active", + "registry_entry": index, + "immutable_bundle": { + "schema": "cellscript-registry-immutable-bundle", + "url": format!("{origin}{snapshot_path}"), + "snapshot_hash": snapshot_hash, + "source_hash": version.source_hash, + "size_bytes": snapshot.len(), + "content_type": "application/vnd.cellscript.source-snapshot+json" + } + }] + })) + .unwrap(); + let stop = Arc::new(AtomicBool::new(false)); + let server_stop = Arc::clone(&stop); + let handle = std::thread::spawn(move || { + while !server_stop.load(Ordering::Acquire) { + match listener.accept() { + Ok((mut stream, _)) => { + let path = read_mock_http_path(&mut stream); + let (status, content_type, body) = if path == artifact_path { + ("200 OK", "application/json", artifact.as_slice()) + } else if path == snapshot_path { + ("200 OK", "application/vnd.cellscript.source-snapshot+json", snapshot.as_slice()) + } else { + ("404 Not Found", "application/json", b"{}".as_slice()) + }; + let headers = format!( + "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + stream.write_all(headers.as_bytes()).unwrap(); + stream.write_all(body).unwrap(); + } + Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { + std::thread::sleep(std::time::Duration::from_millis(5)); + } + Err(error) => panic!("artifact API fixture failed: {error}"), + } + } + }); + PackageArtifactApi { origin, stop, handle: Some(handle) } +} + // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- @@ -97,7 +227,7 @@ fn git_tag(repo_dir: &Path, tag: &str) { fn create_minimal_package(dir: &Path, name: &str, version: &str, namespace: Option<&str>) { std::fs::create_dir_all(dir.join("src")).unwrap(); - let mut toml = String::from("[package]\n"); + let mut toml = String::from("[package]\nedition = \"2026\"\n"); toml.push_str(&format!("name = \"{}\"\n", name)); toml.push_str(&format!("version = \"{}\"\n", version)); if let Some(ns) = namespace { @@ -161,6 +291,7 @@ fn compute_source_hash_includes_configured_source_roots() { temp.path().join("Cell.toml"), r#" [package] +edition = "2026" name = "hash-test" version = "0.1.0" entry = "contracts/main.cell" @@ -185,10 +316,12 @@ source_roots = ["contracts"] fn registry_index_write_read_round_trip() { let temp = tempfile::tempdir().unwrap(); let index = RegistryIndex { - schema_version: 1, + schema_version: RegistryIndex::CURRENT_SCHEMA_VERSION, name: "token".to_string(), namespace: "cellscript".to_string(), versions: vec![RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.3.0".to_string(), tag: "v0.3.0".to_string(), source_hash: "abcd1234".to_string(), @@ -210,7 +343,7 @@ fn registry_index_write_read_round_trip() { index.write_to_repo(temp.path()).unwrap(); let read_back = RegistryIndex::read_from_repo(temp.path()).unwrap(); - assert_eq!(read_back.schema_version, 1); + assert_eq!(read_back.schema_version, RegistryIndex::CURRENT_SCHEMA_VERSION); assert_eq!(read_back.name, "token"); assert_eq!(read_back.namespace, "cellscript"); assert_eq!(read_back.versions.len(), 1); @@ -224,6 +357,8 @@ fn registry_index_append_version_creates_new_file() { let temp = tempfile::tempdir().unwrap(); let version = RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.1.0".to_string(), tag: "v0.1.0".to_string(), source_hash: "hash_of_source".to_string(), @@ -255,6 +390,8 @@ fn registry_index_append_version_updates_existing() { let temp = tempfile::tempdir().unwrap(); let v1 = RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.1.0".to_string(), tag: "v0.1.0".to_string(), source_hash: "h1".to_string(), @@ -274,6 +411,8 @@ fn registry_index_append_version_updates_existing() { RegistryIndex::append_version(temp.path(), "pkg", "ns", v1).unwrap(); let v2 = RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.2.0".to_string(), tag: "v0.2.0".to_string(), source_hash: "h2".to_string(), @@ -297,6 +436,8 @@ fn registry_index_append_version_updates_existing() { // Re-appending same version should update (not duplicate) let v1_updated = RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.1.0".to_string(), tag: "v0.1.0".to_string(), source_hash: "h1_updated".to_string(), @@ -332,6 +473,8 @@ fn registry_index_with_dependencies_and_audit() { )]); let version = RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "1.0.0".to_string(), tag: "v1.0.0".to_string(), source_hash: "deadbeef".to_string(), @@ -453,14 +596,17 @@ fn deployed_manifest_file_round_trip() { let temp = tempfile::tempdir().unwrap(); let manifest = DeployedManifest { - version: 1, - schema: Some(DEPLOYED_MANIFEST_SCHEMA.to_string()), + version: DeployedManifest::CURRENT_VERSION, + schema: DEPLOYED_MANIFEST_SCHEMA.to_string(), package: DeployedPackageInfo { + edition: cellscript::CURRENT_EDITION, name: "token".to_string(), version: "1.0.0".to_string(), source_hash: Some("blake2b:0xabc".to_string()), }, build: Some(DeployedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), compiler_version: Some("0.19.0".to_string()), artifact_hash: Some("blake2b:0xdef".to_string()), metadata_hash: None, @@ -470,6 +616,8 @@ fn deployed_manifest_file_round_trip() { constraints_hash: None, }), deployments: vec![DeploymentRecord { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), network: "aggron4".to_string(), chain_id: "ckb-testnet".to_string(), tx_hash: "0xaaaa1111".to_string(), @@ -507,7 +655,7 @@ fn deployed_manifest_file_round_trip() { manifest.write_to_root(temp.path()).unwrap(); let read_back = DeployedManifest::read_from_root(temp.path()).unwrap().unwrap(); - assert_eq!(read_back.version, 1); + assert_eq!(read_back.version, DeployedManifest::CURRENT_VERSION); assert_eq!(read_back.package.name, "token"); assert_eq!(read_back.package.version, "1.0.0"); assert_eq!(read_back.package.source_hash.as_deref(), Some("blake2b:0xabc")); @@ -521,13 +669,14 @@ fn deployed_manifest_file_round_trip() { } #[test] -fn deployed_manifest_backward_compatible_minimal() { +fn deployed_manifest_rejects_legacy_minimal() { let temp = tempfile::tempdir().unwrap(); let toml_str = r#" version = 1 [package] +edition = "2026" name = "minimal" version = "0.1.0" @@ -544,13 +693,12 @@ out_point = "0x1111:0" "#; std::fs::write(temp.path().join("Deployed.toml"), toml_str).unwrap(); - let parsed = DeployedManifest::read_from_root(temp.path()).unwrap().unwrap(); - assert_eq!(parsed.package.name, "minimal"); - assert!(parsed.build.is_none()); - assert_eq!(parsed.deployments.len(), 1); - assert!(parsed.deployments[0].type_id.is_none()); - assert!(parsed.deployments[0].status.is_none()); - assert!(parsed.deployments[0].cell_deps.is_empty()); + let error = DeployedManifest::read_from_root(temp.path()).unwrap_err(); + assert!( + error.message.contains("missing field") || error.message.contains("unsupported Deployed.toml identity"), + "unexpected error: {}", + error.message + ); } // --------------------------------------------------------------------------- @@ -563,6 +711,7 @@ fn lockfile_with_build_and_deployment_round_trip() { let mut lockfile = Lockfile::new(); lockfile.package = LockfilePackageInfo { + edition: cellscript::CURRENT_EDITION, name: "amm_pool".to_string(), version: "1.0.0".to_string(), namespace: Some("cellscript".to_string()), @@ -570,6 +719,8 @@ fn lockfile_with_build_and_deployment_round_trip() { compiler_source_hash: None, }; lockfile.package_build = Some(LockedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), compiler_version: Some("0.19.0".to_string()), target_profile: Some("ckb-release".to_string()), artifact_hash: Some("blake2b:0x1234".to_string()), @@ -591,6 +742,8 @@ fn lockfile_with_build_and_deployment_round_trip() { lockfile.dependencies.insert( "token".to_string(), LockedDependency { + name: "token".to_string(), + namespace: Some("cellscript".to_string()), version: "0.3.0".to_string(), source: LockedSource::Registry { registry: "https://github.com/cellscript/cellscript-registry".to_string(), @@ -600,7 +753,11 @@ fn lockfile_with_build_and_deployment_round_trip() { version: "0.3.0".to_string(), }, source_hash: Some("blake2b:0xaaaa".to_string()), + manifest_digest: "sha256:test-token-manifest".to_string(), + dependencies: BTreeMap::new(), build: Some(LockedBuildInfo { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), artifact_hash: Some("blake2b:0xtoken".to_string()), constraints_hash: Some("blake2b:0xtoken_constraints".to_string()), ..Default::default() @@ -636,6 +793,7 @@ fn lockfile_consistency_with_registry_source() { let manifest: PackageManifest = toml::from_str( r#" [package] +edition = "2026" name = "app" version = "0.1.0" namespace = "cellscript" @@ -651,6 +809,8 @@ namespace = "cellscript" lockfile.dependencies.insert( "token".to_string(), LockedDependency { + name: "token".to_string(), + namespace: Some("cellscript".to_string()), version: "0.3.0".to_string(), source: LockedSource::Registry { registry: "https://github.com/cellscript/cellscript-registry".to_string(), @@ -660,9 +820,12 @@ namespace = "cellscript" version: "0.3.0".to_string(), }, source_hash: None, + manifest_digest: "sha256:test-token-manifest".to_string(), + dependencies: BTreeMap::new(), build: None, }, ); + lockfile.root.dependencies.insert("token".to_string(), "token".to_string()); let issues = lockfile.consistency_issues(&manifest); assert!(issues.is_empty(), "lockfile with matching registry source should be consistent: {issues:?}"); @@ -683,6 +846,8 @@ fn publish_flow_computes_source_hash_and_writes_registry_json() { assert!(!source_hash.is_empty()); let version = RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.1.0".to_string(), tag: "v0.1.0".to_string(), source_hash, @@ -728,6 +893,8 @@ fn full_publish_install_verify_flow_with_local_git() { let source_hash = compute_source_hash(&source_repo).unwrap(); let version = RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.3.0".to_string(), tag: "v0.3.0".to_string(), source_hash: source_hash.clone(), @@ -785,7 +952,7 @@ fn full_publish_install_verify_flow_with_local_git() { } #[test] -fn package_manager_resolves_registry_dependency_with_source_hash_from_local_git_fixture() { +fn package_manager_resolves_artifact_api_dependency_with_source_hash() { let temp = tempfile::tempdir().unwrap(); let source_repo = temp.path().join("source-repo"); @@ -797,6 +964,8 @@ fn package_manager_resolves_registry_dependency_with_source_hash_from_local_git_ "token", "cellscript", RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.3.0".to_string(), tag: "v0.3.0".to_string(), source_hash: source_hash.clone(), @@ -838,6 +1007,7 @@ fn package_manager_resolves_registry_dependency_with_source_hash_from_local_git_ consumer.join("Cell.toml"), r#" [package] +edition = "2026" name = "consumer" version = "0.1.0" namespace = "app" @@ -850,15 +1020,16 @@ namespace = "cellscript" .unwrap(); std::fs::write(consumer.join("src/main.cell"), "module consumer;\n").unwrap(); - let _env = RegistryEnvGuard::new(®istry_repo); + let api = start_package_artifact_api(&source_repo, "verified"); + let _env = RegistryEnvGuard::new(&api.origin); let mut manager = PackageManager::new(&consumer); manager.resolve_dependencies().unwrap(); - let resolved = manager.get_resolved().get("token").unwrap(); + let resolved = manager.get_resolved().values().find(|package| package.name == "token").unwrap(); assert_eq!(resolved.source_hash.as_deref(), Some(source_hash.as_str())); let mut lockfile = Lockfile::new(); lockfile.update_from_resolved(manager.get_resolved()); - let token = lockfile.dependencies.get("token").unwrap(); + let token = lockfile.dependencies.values().find(|package| package.name == "token").unwrap(); assert_eq!(token.source_hash.as_deref(), Some(source_hash.as_str())); assert!( matches!(token.source, LockedSource::Registry { ref namespace, ref version, .. } if namespace == "cellscript" && version == "0.3.0") @@ -878,6 +1049,8 @@ fn package_manager_rejects_unverified_registry_entry_by_default() { "token", "cellscript", RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.3.0".to_string(), tag: "v0.3.0".to_string(), source_hash, @@ -919,6 +1092,7 @@ fn package_manager_rejects_unverified_registry_entry_by_default() { consumer.join("Cell.toml"), r#" [package] +edition = "2026" name = "consumer" version = "0.1.0" namespace = "app" @@ -931,7 +1105,8 @@ namespace = "cellscript" .unwrap(); std::fs::write(consumer.join("src/main.cell"), "module consumer;\n").unwrap(); - let _env = RegistryEnvGuard::new(®istry_repo); + let api = start_package_artifact_api(&source_repo, "pending"); + let _env = RegistryEnvGuard::new(&api.origin); let mut manager = PackageManager::new(&consumer); let err = manager.resolve_dependencies().unwrap_err(); assert!(err.message.contains("status 'source_published'"), "unexpected error: {}", err.message); @@ -939,7 +1114,7 @@ namespace = "cellscript" } #[test] -fn package_manager_allows_unverified_registry_entry_with_explicit_policy() { +fn package_manager_persists_unverified_registry_policy_in_dependency_manifest() { let temp = tempfile::tempdir().unwrap(); let source_repo = temp.path().join("source-repo"); @@ -951,6 +1126,8 @@ fn package_manager_allows_unverified_registry_entry_with_explicit_policy() { "token", "cellscript", RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.3.0".to_string(), tag: "v0.3.0".to_string(), source_hash: source_hash.clone(), @@ -992,25 +1169,26 @@ fn package_manager_allows_unverified_registry_entry_with_explicit_policy() { consumer.join("Cell.toml"), r#" [package] +edition = "2026" name = "consumer" version = "0.1.0" namespace = "app" + +[dependencies.token] +version = "0.3.0" +namespace = "cellscript" +allow_unverified = true "#, ) .unwrap(); std::fs::write(consumer.join("src/main.cell"), "module consumer;\n").unwrap(); - let _env = RegistryEnvGuard::new(®istry_repo); - let manager = PackageManager::new(&consumer); - let resolved = manager - .resolve_from_registry_with_namespace_and_policy( - "token", - "0.3.0", - Some("cellscript"), - RegistryResolutionPolicy { allow_unverified: true, allow_quarantined: false }, - ) - .unwrap(); - assert_eq!(resolved.source_hash.as_deref(), Some(source_hash.as_str())); + let api = start_package_artifact_api(&source_repo, "pending"); + let _env = RegistryEnvGuard::new(&api.origin); + let mut manager = PackageManager::new(&consumer); + manager.resolve_dependencies().unwrap(); + let token = manager.get_resolved().values().find(|package| package.name == "token").unwrap(); + assert_eq!(token.source_hash.as_deref(), Some(source_hash.as_str())); } #[test] @@ -1025,6 +1203,8 @@ fn package_manager_rejects_registry_source_hash_mismatch() { "token", "cellscript", RegistryVersion { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), version: "0.3.0".to_string(), tag: "v0.3.0".to_string(), source_hash: "deliberately_wrong_hash".to_string(), @@ -1066,6 +1246,7 @@ fn package_manager_rejects_registry_source_hash_mismatch() { consumer.join("Cell.toml"), r#" [package] +edition = "2026" name = "consumer" version = "0.1.0" namespace = "app" @@ -1078,10 +1259,15 @@ namespace = "cellscript" .unwrap(); std::fs::write(consumer.join("src/main.cell"), "module consumer;\n").unwrap(); - let _env = RegistryEnvGuard::new(®istry_repo); + let api = start_package_artifact_api(&source_repo, "verified"); + let _env = RegistryEnvGuard::new(&api.origin); let mut manager = PackageManager::new(&consumer); let err = manager.resolve_dependencies().unwrap_err(); - assert!(err.message.contains("source_hash mismatch"), "unexpected error: {}", err.message); + assert!( + err.message.contains("source_hash") && err.message.contains("deliberately_wrong_hash"), + "unexpected error: {}", + err.message + ); } // --------------------------------------------------------------------------- @@ -1100,6 +1286,7 @@ fn package_verify_detects_missing_source_hash() { let mut lockfile = Lockfile::new(); lockfile.package = LockfilePackageInfo { + edition: cellscript::CURRENT_EDITION, name: "verify-test".to_string(), version: "0.1.0".to_string(), namespace: None, @@ -1122,6 +1309,7 @@ fn lockfile_consistency_rejects_wrong_registry_namespace() { let manifest: PackageManifest = toml::from_str( r#" [package] +edition = "2026" name = "app" version = "0.1.0" namespace = "cellscript" @@ -1137,6 +1325,8 @@ namespace = "cellscript" lockfile.dependencies.insert( "token".to_string(), LockedDependency { + name: "token".to_string(), + namespace: Some("other".to_string()), version: "0.3.0".to_string(), source: LockedSource::Registry { registry: "https://github.com/cellscript/cellscript-registry".to_string(), @@ -1146,9 +1336,12 @@ namespace = "cellscript" version: "0.3.0".to_string(), }, source_hash: None, + manifest_digest: "sha256:test-token-manifest".to_string(), + dependencies: BTreeMap::new(), build: None, }, ); + lockfile.root.dependencies.insert("token".to_string(), "token".to_string()); let issues = lockfile.consistency_issues(&manifest); assert!(!issues.is_empty(), "wrong namespace should cause consistency issues: {issues:?}"); @@ -1165,6 +1358,7 @@ fn lockfile_consistency_accepts_matching_registry_source() { let manifest: PackageManifest = toml::from_str( r#" [package] +edition = "2026" name = "app" version = "0.1.0" namespace = "cellscript" @@ -1180,6 +1374,8 @@ namespace = "cellscript" lockfile.dependencies.insert( "token".to_string(), LockedDependency { + name: "token".to_string(), + namespace: Some("cellscript".to_string()), version: "0.3.0".to_string(), source: LockedSource::Registry { registry: "https://github.com/cellscript/cellscript-registry".to_string(), @@ -1189,9 +1385,12 @@ namespace = "cellscript" version: "0.3.0".to_string(), }, source_hash: None, + manifest_digest: "sha256:test-token-manifest".to_string(), + dependencies: BTreeMap::new(), build: None, }, ); + lockfile.root.dependencies.insert("token".to_string(), "token".to_string()); let issues = lockfile.consistency_issues(&manifest); assert!(issues.is_empty(), "matching registry source should have no issues: {issues:?}"); @@ -1206,12 +1405,19 @@ fn deployed_manifest_supports_multiple_deployments() { let temp = tempfile::tempdir().unwrap(); let manifest = DeployedManifest { - version: 1, - schema: Some(DEPLOYED_MANIFEST_SCHEMA.to_string()), - package: DeployedPackageInfo { name: "token".to_string(), version: "1.0.0".to_string(), source_hash: None }, + version: DeployedManifest::CURRENT_VERSION, + schema: DEPLOYED_MANIFEST_SCHEMA.to_string(), + package: DeployedPackageInfo { + edition: cellscript::CURRENT_EDITION, + name: "token".to_string(), + version: "1.0.0".to_string(), + source_hash: None, + }, build: None, deployments: vec![ DeploymentRecord { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), network: "ckb-mainnet".to_string(), chain_id: "ckb-mainnet".to_string(), tx_hash: "0x1111".to_string(), @@ -1237,6 +1443,8 @@ fn deployed_manifest_supports_multiple_deployments() { cell_deps: vec![], }, DeploymentRecord { + edition: cellscript::CURRENT_EDITION, + compatibility_profile_hash: "test-compatibility-profile".to_string(), network: "aggron4".to_string(), chain_id: "ckb-testnet".to_string(), tx_hash: "0x4444".to_string(), diff --git a/tests/scenarios/assertion-failure.cell b/tests/scenarios/assertion-failure.cell new file mode 100644 index 00000000..c2a5dae1 --- /dev/null +++ b/tests/scenarios/assertion-failure.cell @@ -0,0 +1,8 @@ +// cellscript-test: expect-success +// cellscript-test: target: riscv64-elf +module scenario_assertion_failure + +action main() { + verification + require false, "expected failure" +} diff --git a/tests/scenarios/assertion-failure.scenario.json b/tests/scenarios/assertion-failure.scenario.json new file mode 100644 index 00000000..69790bdf --- /dev/null +++ b/tests/scenarios/assertion-failure.scenario.json @@ -0,0 +1,35 @@ +{ + "schema": "cellscript-test-scenario-v1", + "name": "negative-exact-runtime-error", + "source": "assertion-failure.cell", + "target_profile": "ckb", + "entry": { + "kind": "action", + "name": "main", + "args": [] + }, + "initial_cells": [], + "steps": [ + { + "name": "assertion-fails", + "consumes": [], + "outputs": [], + "cell_deps": [], + "header_deps": [], + "since": {}, + "witnesses": [], + "expectation": { + "status": "runtime-error", + "result": null, + "runtime_error": {"code": 5, "name": "assertion-failed"} + } + } + ], + "limits": { + "max_steps": 1000, + "max_cycles": 10000000, + "max_transaction_bytes": 65536, + "minimum_cell_capacity": 100000000 + }, + "oracle": null +} diff --git a/tests/scenarios/positive.cell b/tests/scenarios/positive.cell new file mode 100644 index 00000000..98e98d10 --- /dev/null +++ b/tests/scenarios/positive.cell @@ -0,0 +1,7 @@ +// cellscript-test: expect-success +// cellscript-test: target: riscv64-elf +module scenario_positive + +action main() { + verification +} diff --git a/tests/scenarios/positive.scenario.json b/tests/scenarios/positive.scenario.json new file mode 100644 index 00000000..941f0e06 --- /dev/null +++ b/tests/scenarios/positive.scenario.json @@ -0,0 +1,84 @@ +{ + "schema": "cellscript-test-scenario-v1", + "name": "positive-two-step-cell-replacement", + "source": "positive.cell", + "target_profile": "ckb", + "entry": { + "kind": "action", + "name": "main", + "args": [] + }, + "initial_cells": [ + { + "name": "state-0", + "capacity": 10000000000, + "data": "00", + "lock": { + "code_hash": "1111111111111111111111111111111111111111111111111111111111111111", + "hash_type": "data1", + "args": "" + }, + "type": null, + "prior_output": null + } + ], + "steps": [ + { + "name": "replace-0-with-1", + "consumes": ["state-0"], + "outputs": [ + { + "name": "state-1", + "capacity": 10000000000, + "data": "01", + "lock": { + "code_hash": "1111111111111111111111111111111111111111111111111111111111111111", + "hash_type": "data1", + "args": "" + }, + "type": null, + "prior_output": "state-0" + } + ], + "cell_deps": [], + "header_deps": [], + "since": {"state-0": 0}, + "witnesses": [ + {"input": "state-0", "lock": null, "input_type": "", "output_type": null} + ], + "expectation": {"status": "pass", "result": "()", "runtime_error": null} + }, + { + "name": "replace-1-with-2", + "consumes": ["state-1"], + "outputs": [ + { + "name": "state-2", + "capacity": 10000000000, + "data": "02", + "lock": { + "code_hash": "1111111111111111111111111111111111111111111111111111111111111111", + "hash_type": "data1", + "args": "" + }, + "type": null, + "prior_output": "state-1" + } + ], + "cell_deps": [], + "header_deps": [], + "since": {"state-1": 0}, + "witnesses": [ + {"input": "state-1", "lock": null, "input_type": "", "output_type": null} + ], + "expectation": {"status": "pass", "result": "()", "runtime_error": null} + } + ], + "limits": { + "max_steps": 1000, + "max_cycles": 10000000, + "max_transaction_bytes": 65536, + "minimum_cell_capacity": 100000000 + }, + "oracle": null +} diff --git a/tests/support/ckb_script_runner.rs b/tests/support/ckb_script_runner.rs index 3a8282af..b409d3c7 100644 --- a/tests/support/ckb_script_runner.rs +++ b/tests/support/ckb_script_runner.rs @@ -7,7 +7,6 @@ //! //! This harness is protocol-neutral. It does not contain iCKB-specific logic. -use ckb_testtool::ckb_hash::blake2b_256; use ckb_testtool::ckb_types::{ bytes::Bytes, core::{DepType, HeaderBuilder, TransactionBuilder}, @@ -74,23 +73,6 @@ action test_dao_input_accumulated_rate() -> u64 { } "#; -/// CellScript source that just reads a header-dep SourceView without using DAO. -/// This isolates whether the issue is with source::header_dep() or dao::accumulated_rate(). -#[allow(dead_code)] -pub const VM_HARNESS_HEADER_DEP_PROGRAM: &str = r#" -module vm_harness_header_dep - -action test_header_dep_source() -> u64 { - verification - let header_view = source::header_dep(0) - return 0 -} -"#; - -/// Entry action for the header-dep-only test. -#[allow(dead_code)] -pub const VM_HARNESS_HEADER_DEP_ACTION: &str = "test_header_dep_source"; - /// Entry action name for the DAO pass-case harness test. pub const VM_HARNESS_DAO_PASS_ACTION: &str = "test_dao_input_accumulated_rate"; @@ -274,22 +256,24 @@ pub struct CkbScriptExecutionResult { /// A cell in a CKB transaction fixture. #[derive(Debug, Clone)] -#[allow(dead_code)] pub struct FixtureCell { pub capacity: u64, - pub lock: packed::Script, pub type_script: Option, pub data: Bytes, } /// A complete CKB VM fixture for script execution. #[derive(Debug, Clone)] -#[allow(dead_code)] pub struct CkbVmFixture { /// Script args for the CellScript-compiled type script being tested. pub script_args: Bytes, /// Input cells. pub inputs: Vec, + /// Input indexes that carry the CellScript type script under test. + /// + /// This is separate from `FixtureCell::type_script` so tests can express a + /// real type-script group whose first member is not transaction input 0. + pub current_type_script_input_indices: Vec, /// Output cells. pub outputs: Vec, /// Additional cell deps (beyond the script code cell itself). @@ -302,10 +286,6 @@ pub struct CkbVmFixture { /// Whether to link input cells with their block headers. /// If true, input[i] is linked to header_dao_fields[i]'s block. pub link_inputs_to_headers: bool, - /// Whether this fixture expects the script to pass. - pub expected_pass: bool, - /// Named failure mode for reject cases. - pub failure_mode: Option, } /// Compile a CellScript source string to RISC-V ELF bytes with a specific entry action. @@ -333,27 +313,6 @@ pub fn compile_cellscript_source_to_elf(source: &str, entry_action: &str, primit cellscript::strip_vm_abi_trailer(&result.artifact_bytes).to_vec() } -/// Compile a CellScript .cell file to RISC-V ELF bytes with a specific entry action. -#[allow(dead_code)] -pub fn compile_cellscript_to_elf(cell_path: &str, entry_action: &str, primitive_compat: Option<&str>) -> Vec { - let options = cellscript::CompileOptions { - target: Some("riscv64-elf".to_string()), - target_profile: Some("ckb".to_string()), - primitive_compat: primitive_compat.map(|s| s.to_string()), - ..cellscript::CompileOptions::default() - }; - let result = cellscript::compile_file_with_entry_action(cell_path, options, entry_action) - .unwrap_or_else(|err| panic!("failed to compile {} entry {}: {}", cell_path, entry_action, err.message)); - assert!( - matches!(result.artifact_format, cellscript::ArtifactFormat::RiscvElf), - "expected ELF artifact, got {:?}", - result.artifact_format - ); - // Strip the VM ABI trailer before feeding to ckb-testtool, - // which expects a bare RISC-V ELF. - cellscript::strip_vm_abi_trailer(&result.artifact_bytes).to_vec() -} - /// Execute a CellScript-compiled ELF against a CKB VM fixture. /// /// This deploys the ELF, creates the transaction from the fixture, @@ -388,11 +347,17 @@ pub fn execute_cellscript_script(elf_bytes: &[u8], fixture: &CkbVmFixture) -> Ck let input_out_points: Vec = fixture .inputs .iter() - .map(|cell| { + .enumerate() + .map(|(index, cell)| { + let input_type_script = if fixture.current_type_script_input_indices.contains(&index) { + Some(type_script.clone()) + } else { + cell.type_script.clone() + }; let output = packed::CellOutput::new_builder() .capacity::(cell.capacity.pack()) .lock(always_success_lock.clone()) - .type_(packed::ScriptOpt::from(cell.type_script.clone())) + .type_(packed::ScriptOpt::from(input_type_script)) .build(); context.create_cell(output, cell.data.clone()) }) @@ -492,39 +457,25 @@ fn parse_ckb_script_error_code(error: &str) -> Option { /// Build a simple harness fixture for testing CellScript script execution. /// Uses always_success lock for all cells and the current ELF as type script. -pub fn build_simple_fixture( - script_args: Bytes, - input_count: usize, - output_count: usize, - expected_pass: bool, - failure_mode: Option, -) -> CkbVmFixture { - let inputs = (0..input_count) - .map(|_| FixtureCell { - capacity: 100_000_000_000, - lock: packed::Script::default(), // always_success set by harness - type_script: None, - data: Bytes::default(), - }) - .collect(); +pub fn build_simple_fixture(script_args: Bytes, input_count: usize, output_count: usize) -> CkbVmFixture { + let inputs = + (0..input_count).map(|_| FixtureCell { capacity: 100_000_000_000, type_script: None, data: Bytes::default() }).collect(); let outputs = (0..output_count) .map(|_| FixtureCell { capacity: 100_000_000_000, - lock: packed::Script::default(), // always_success set by harness - type_script: None, // current_under_test set by harness + type_script: None, // current_under_test set by harness data: Bytes::default(), }) .collect(); CkbVmFixture { script_args, inputs, + current_type_script_input_indices: Vec::new(), outputs, cell_deps: Vec::new(), witnesses: Vec::new(), header_dao_fields: Vec::new(), link_inputs_to_headers: false, - expected_pass, - failure_mode, } } @@ -545,31 +496,21 @@ fn build_cell_output( /// Build a DAO fixture for testing DAO accumulated-rate scripts. /// Creates a header with the given DAO accumulated rate and includes it as a header dep. -pub fn build_dao_fixture( - script_args: Bytes, - accumulated_rate: u64, - input_count: usize, - output_count: usize, - expected_pass: bool, - failure_mode: Option, -) -> CkbVmFixture { +pub fn build_dao_fixture(script_args: Bytes, accumulated_rate: u64, input_count: usize, output_count: usize) -> CkbVmFixture { let dao_field = make_dao_field(accumulated_rate); - let inputs = (0..input_count) - .map(|_| FixtureCell { capacity: 100_000_000_000, lock: packed::Script::default(), type_script: None, data: Bytes::default() }) - .collect(); - let outputs = (0..output_count) - .map(|_| FixtureCell { capacity: 100_000_000_000, lock: packed::Script::default(), type_script: None, data: Bytes::default() }) - .collect(); + let inputs = + (0..input_count).map(|_| FixtureCell { capacity: 100_000_000_000, type_script: None, data: Bytes::default() }).collect(); + let outputs = + (0..output_count).map(|_| FixtureCell { capacity: 100_000_000_000, type_script: None, data: Bytes::default() }).collect(); CkbVmFixture { script_args, inputs, + current_type_script_input_indices: Vec::new(), outputs, cell_deps: Vec::new(), witnesses: Vec::new(), header_dao_fields: vec![dao_field], link_inputs_to_headers: true, - expected_pass, - failure_mode, } } @@ -578,30 +519,20 @@ pub fn build_dao_fixture( /// No header deps or DAO accumulated rate — this fixture is for /// `is_deposit_data` / `is_withdrawal_request_data` / `has_dao_type` / `cell_capacity` /// tests that use LOAD_CELL_DATA or LOAD_CELL_BY_FIELD on inputs. -pub fn build_dao_data_fixture( - script_args: Bytes, - input_data: Vec, - output_count: usize, - expected_pass: bool, - failure_mode: Option, -) -> CkbVmFixture { - let inputs: Vec = input_data - .into_iter() - .map(|data| FixtureCell { capacity: 100_000_000_000, lock: packed::Script::default(), type_script: None, data }) - .collect(); - let outputs = (0..output_count) - .map(|_| FixtureCell { capacity: 100_000_000_000, lock: packed::Script::default(), type_script: None, data: Bytes::default() }) - .collect(); +pub fn build_dao_data_fixture(script_args: Bytes, input_data: Vec, output_count: usize) -> CkbVmFixture { + let inputs: Vec = + input_data.into_iter().map(|data| FixtureCell { capacity: 100_000_000_000, type_script: None, data }).collect(); + let outputs = + (0..output_count).map(|_| FixtureCell { capacity: 100_000_000_000, type_script: None, data: Bytes::default() }).collect(); CkbVmFixture { script_args, inputs, + current_type_script_input_indices: Vec::new(), outputs, cell_deps: Vec::new(), witnesses: Vec::new(), header_dao_fields: Vec::new(), link_inputs_to_headers: false, - expected_pass, - failure_mode, } } @@ -618,12 +549,6 @@ pub fn make_dao_field(accumulated_rate: u64) -> [u8; 32] { dao } -/// Compute the blake2b hash of bytes, returning a 32-byte array. -#[allow(dead_code)] -pub fn blake2b_hash(data: &[u8]) -> [u8; 32] { - blake2b_256(data) -} - /// Load an original iCKB script binary from the test fixtures directory. /// Returns the raw ELF bytes for deployment as a CKB code cell. pub fn load_original_ickb_binary(name: &str) -> Vec { diff --git a/tests/syntax_combo/cases.json b/tests/syntax_combo/cases.json new file mode 100644 index 00000000..1ae20831 --- /dev/null +++ b/tests/syntax_combo/cases.json @@ -0,0 +1,2028 @@ +{ + "bug_class_contracts": [ + { + "id": "SCA-BUG-STD-LIFECYCLE-LOCKED-OUTPUT", + "min_mode": "quick", + "name": "stdlib lifecycle pattern must create and lock the declared output", + "release_boundary": "std::lifecycle::transfer(input, output, to) cannot drop to or omit create output with_lock(to)", + "required_cases": [ + "stdlib-transfer" + ], + "required_origins": [ + "generated" + ] + }, + { + "id": "SCA-BUG-PRESERVE-TYPE-EQUIVALENCE", + "min_mode": "quick", + "name": "preserve sugar must be type-equivalent to canonical require equality", + "release_boundary": "preserve output from input { field } must reject field type mismatches", + "required_cases": [ + "reject-preserve-type-mismatch" + ], + "required_origins": [ + "generated" + ] + }, + { + "id": "SCA-BUG-REQUIRE-BLOCK-PURITY", + "min_mode": "quick", + "name": "anonymous require block cannot hide lifecycle or verifier-boundary operations", + "release_boundary": "require { ... } remains pure boolean grouping sugar", + "required_cases": [ + "reject-require-block-lifecycle", + "seed-require-block-lifecycle" + ], + "required_origins": [ + "generated", + "tests/syntax_combo/seeds/require-block-lifecycle.cell" + ] + }, + { + "id": "SCA-BUG-STDLIB-NAMESPACE-FAIL-CLOSED", + "min_mode": "quick", + "name": "unknown stdlib namespaces and helper names fail closed", + "release_boundary": "unsupported std::* calls cannot compile as inert boolean expressions", + "required_cases": [ + "reject-unknown-stdlib" + ], + "required_origins": [ + "generated" + ] + }, + { + "id": "SCA-BUG-SOURCE-QUALIFIER-LINEARITY", + "min_mode": "quick", + "name": "source-qualified values cannot be consumed by lifecycle operations", + "release_boundary": "read/protected/witness/lock_args values do not escape into consume/destroy/stdlib lifecycle", + "required_cases": [ + "reject-consume-read-param" + ], + "required_origins": [ + "generated" + ] + }, + { + "id": "SCA-BUG-RECEIPT-CLAIM-CONTRACT", + "min_mode": "quick", + "name": "receipt claim helpers require receipt inputs and declared claim output type", + "release_boundary": "claim semantics come from stdlib helper validation, not action names", + "required_cases": [ + "reject-claim-without-output-arrow" + ], + "required_origins": [ + "generated" + ] + }, + { + "id": "SCA-BUG-LOCK-SOURCE-QUALIFIERS", + "min_mode": "quick", + "name": "lock protected, witness, and lock_args source qualifiers stay parse/type checked", + "release_boundary": "lock authorization data sources remain explicit in the surface and metadata path", + "required_cases": [ + "lock-source-qualifiers" + ], + "required_origins": [ + "generated" + ] + }, + { + "id": "SCA-BUG-0.22-CELLSET-UNBOUNDED", + "min_mode": "quick", + "name": "transaction-backed Cell collections require an explicit finite maximum cardinality", + "release_boundary": "BoundedCellSet cannot omit N or use an unbounded transaction source", + "required_cases": [ + "seed-bounded-collection-missing-cardinality-reject" + ], + "required_origins": [ + "tests/syntax_combo/seeds/bounded-collection-missing-cardinality-reject.cell" + ] + }, + { + "id": "SCA-BUG-0.22-CELLSET-VEC-RESOURCE", + "min_mode": "quick", + "name": "generic Vec cannot stand in for a source-aware Cell set", + "release_boundary": "transaction Cell membership and ownership are never inferred from local Vec storage", + "required_cases": [ + "seed-bounded-collection-vec-resource-reject" + ], + "required_origins": [ + "tests/syntax_combo/seeds/bounded-collection-vec-resource-reject.cell" + ] + }, + { + "id": "SCA-BUG-0.22-CONSUME-EACH-DUPLICATE", + "min_mode": "quick", + "name": "consume_each consumes one bounded Cell set exactly once", + "release_boundary": "linear bounded input sets cannot be consumed twice or silently partially consumed", + "required_cases": [ + "seed-bounded-collection-duplicate-consume-reject" + ], + "required_origins": [ + "tests/syntax_combo/seeds/bounded-collection-duplicate-consume-reject.cell" + ] + }, + { + "id": "SCA-BUG-0.22-CREATE-EACH-CARDINALITY-MISSING", + "min_mode": "quick", + "name": "create_each carries output cardinality and capacity builder obligations", + "release_boundary": "bounded output plans compile only with metadata and ProofPlan builder-evidence contracts", + "required_cases": [ + "seed-bounded-collection" + ], + "required_origins": [ + "tests/syntax_combo/seeds/bounded-collection.cell" + ] + }, + { + "id": "SCA-BUG-0.22-VALIDITY-EVIDENCE-MISSING", + "min_mode": "quick", + "name": "type validity predicates carry canonical metadata and ProofPlan evidence tiers", + "release_boundary": "every accepted validity predicate is paired with a canonical evidence tier and ProofPlan record", + "required_cases": [ + "seed-type-validity" + ], + "required_origins": [ + "tests/syntax_combo/seeds/type-validity.cell" + ] + }, + { + "id": "SCA-BUG-0.22-VALIDITY-ENV-UNKNOWN", + "min_mode": "quick", + "name": "unknown validity environment reads fail closed", + "release_boundary": "env::block_number is the only approved 0.22 validity environment read", + "required_cases": [ + "seed-type-validity-unknown-env-reject" + ], + "required_origins": [ + "tests/syntax_combo/seeds/type-validity-unknown-env-reject.cell" + ] + }, + { + "id": "SCA-BUG-0.22-BORROW-EFFECT-COMPAT", + "min_mode": "quick", + "name": "borrowed linear views may reach only Pure or ReadOnly helpers with dedicated &T parameters", + "release_boundary": "borrow calls are checked against authenticated callable effects and explicit read-only reference parameters", + "required_cases": [ + "seed-explicit-borrow", + "seed-explicit-borrow-effect-reject" + ], + "required_origins": [ + "tests/syntax_combo/seeds/explicit-borrow.cell", + "tests/syntax_combo/seeds/explicit-borrow-effect-reject.cell" + ] + }, + { + "id": "SCA-BUG-0.22-BORROW-ESCAPE", + "min_mode": "quick", + "name": "borrowed View markers cannot acquire layout, storage, ABI, or return representation", + "release_boundary": "borrow markers cannot escape through local aggregates, assignments, returns, or generic calls", + "required_cases": [ + "seed-explicit-borrow-escape-reject" + ], + "required_origins": [ + "tests/syntax_combo/seeds/explicit-borrow-escape-reject.cell" + ] + }, + { + "id": "SCA-BUG-0.22-BORROW-CROSSES-CONSUME", + "min_mode": "quick", + "name": "borrowed views cannot cross lifecycle discharge of their linear root", + "release_boundary": "every path rejects consume, destroy, transfer, claim, or settle of a root while its borrow block is active", + "required_cases": [ + "seed-explicit-borrow-cross-consume-reject" + ], + "required_origins": [ + "tests/syntax_combo/seeds/explicit-borrow-cross-consume-reject.cell" + ] + }, + { + "id": "SCA-BUG-0.22-CAPABILITY-OVERGRANT", + "min_mode": "quick", + "name": "composite lifecycle authority is derived only by the closed versioned entailment relation", + "release_boundary": "destroy requires consume+burn and replace_unique requires replace plus an exact declared identity condition", + "required_cases": [ + "seed-capability-entailment", + "seed-capability-missing-identity-reject" + ], + "required_origins": [ + "tests/syntax_combo/seeds/capability-entailment.cell", + "tests/syntax_combo/seeds/capability-missing-identity-reject.cell" + ] + }, + { + "id": "SCA-BUG-0.22-CAPABILITY-TRANSITIVE-GRANT", + "min_mode": "quick", + "name": "container capability sets never grant authority over another Cell resource", + "release_boundary": "capability lookup uses the exact lifecycle operand type and does not traverse container-like declarations", + "required_cases": [ + "seed-capability-transitive-grant-reject" + ], + "required_origins": [ + "tests/syntax_combo/seeds/capability-transitive-grant-reject.cell" + ] + }, + { + "id": "SCA-BUG-0.22-PAYLOAD-MATCH-NONEXHAUSTIVE", + "min_mode": "quick", + "name": "payload enum matches remain exhaustive after destructuring", + "release_boundary": "every concrete payload variant is covered exactly once unless a final non-linear wildcard arm is explicit", + "required_cases": [ + "seed-payload-enum", + "seed-payload-enum-nonexhaustive-reject" + ], + "required_origins": [ + "tests/syntax_combo/seeds/payload-enum.cell", + "tests/syntax_combo/seeds/payload-enum-nonexhaustive-reject.cell" + ] + }, + { + "id": "SCA-BUG-0.22-PAYLOAD-DYNAMIC-ACCEPTED", + "min_mode": "quick", + "name": "payload enum layout accepts only concrete fixed-width values", + "release_boundary": "dynamic and generic payload ADTs fail closed before IR, ABI, or metadata claims are emitted", + "required_cases": [ + "seed-payload-enum-dynamic-reject", + "seed-payload-enum-generic-reject" + ], + "required_origins": [ + "tests/syntax_combo/seeds/payload-enum-dynamic-reject.cell", + "tests/syntax_combo/seeds/payload-enum-generic-reject.cell" + ] + }, + { + "id": "SCA-BUG-0.22-PAYLOAD-LINEAR-DROP", + "min_mode": "quick", + "name": "linear Cell payload ownership is discharged inside every match arm", + "release_boundary": "a Cell payload cannot disappear through wildcard binding or implicit arm-local drop", + "required_cases": [ + "seed-payload-enum-linear-drop-reject" + ], + "required_origins": [ + "tests/syntax_combo/seeds/payload-enum-linear-drop-reject.cell" + ] + }, + { + "id": "SCA-BUG-0.22-PROTOCOLGRAPH-ROLE-OVERCLAIM", + "min_mode": "quick", + "name": "field-name role hints remain weak metadata and never authorization evidence", + "release_boundary": "a participant-like Address field records source=field-name, evidence_tier=metadata-only, and authorization_proven=false", + "required_cases": [ + "seed-protocolgraph-role-weak" + ], + "required_origins": [ + "tests/syntax_combo/seeds/protocolgraph-role-weak.cell" + ] + }, + { + "id": "SCA-BUG-0.22-PROTOCOLGRAPH-ROLE-CONFLICT", + "min_mode": "quick", + "name": "conflicting ProtocolGraph role sources remain attributed and deterministically ordered", + "release_boundary": "explicit predicates precede witness/lock_args bindings and weak field names without entering ProofPlan", + "required_cases": [ + "seed-protocolgraph-role-conflict" + ], + "required_origins": [ + "tests/syntax_combo/seeds/protocolgraph-role-conflict.cell" + ] + }, + { + "id": "SCA-BUG-STDLIB-ARGUMENT-VALIDATION", + "min_mode": "ci", + "name": "stdlib lifecycle helpers validate arity, cell kind, lock target, and claim output", + "release_boundary": "stdlib lifecycle patterns fail closed before lowering when arguments, lock targets, or claim outputs are invalid", + "required_cases": [ + "matrix-reject-claim-non-receipt", + "matrix-reject-claim-extra-args", + "matrix-reject-transfer-extra-args", + "matrix-reject-settle-missing-args", + "matrix-reject-claim-output-type-mismatch", + "matrix-reject-settle-lock-target-type" + ], + "required_origins": [ + "matrix:reject/stdlib-lifecycle" + ] + }, + { + "id": "SCA-BUG-METADATA-HELPER-VALIDATION", + "min_mode": "ci", + "name": "cell metadata helpers reject non-cell arguments", + "release_boundary": "std::cell::* metadata helpers cannot be used as generic boolean predicates", + "required_cases": [ + "matrix-reject-cell-metadata-non-cell" + ], + "required_origins": [ + "matrix:reject/metadata" + ] + }, + { + "id": "SCA-BUG-RECEIPT-LIFECYCLE-OUTPUT", + "min_mode": "ci", + "name": "receipt claim and settle helpers emit locked output obligations", + "release_boundary": "claim/settle helpers must lower to explicit consume/create/lock obligations", + "required_cases": [ + "matrix-stdlib-claim-require-block", + "matrix-stdlib-settle-preserve-capacity" + ], + "required_origins": [ + "matrix:receipt/proof", + "matrix:receipt/metadata" + ] + }, + { + "id": "SCA-BUG-DEEP-HIDDEN-LIFECYCLE", + "min_mode": "deep", + "name": "deep reject variants keep stdlib lifecycle out of pure proof positions", + "release_boundary": "release-local deep replay covers hidden lifecycle mutations beyond the quick corpus", + "required_cases": [ + "matrix-deep-reject-require-block-transfer" + ], + "required_origins": [ + "matrix:deep/reject/proof-purity", + "seeded:deep/reject" + ] + }, + { + "id": "SCA-BUG-DEEP-READ-STDLIB-LIFECYCLE", + "min_mode": "deep", + "name": "deep reject variants cover stdlib lifecycle on read parameters", + "release_boundary": "read-param lifecycle rejection is covered for both explicit consume and stdlib lifecycle syntax", + "required_cases": [ + "matrix-deep-reject-transfer-read-param" + ], + "required_origins": [ + "matrix:deep/reject/source-qualifier" + ] + }, + { + "id": "SCA-BUG-DEEP-UNKNOWN-STDLIB", + "min_mode": "deep", + "name": "deep reject variants cover unknown stdlib helper families", + "release_boundary": "unsupported helper families stay rejected under release-local deep replay", + "required_cases": [ + "matrix-deep-reject-unknown-accounting" + ], + "required_origins": [ + "matrix:deep/reject/stdlib-namespace" + ] + }, + { + "id": "SCA-BUG-FLOW-EDGE-UNDECLARED", + "min_mode": "ci", + "name": "flow state transitions must use edges declared in the flow block", + "release_boundary": "transition input.state: A -> output.state: B must fail closed when A -> B is not a declared flow edge", + "required_cases": [ + "reject-flow-undeclared-edge", + "accept-flow-declared-cyclic-edge" + ], + "required_origins": [ + "generated" + ] + }, + { + "id": "SCA-BUG-FLOW-CREATE-STATE-CONTRACT", + "min_mode": "ci", + "name": "initial create of a flow type must set a statically known declared state", + "release_boundary": "flow-typed create must set the state field to a declared state literal, not a runtime value", + "required_cases": [ + "reject-flow-create-missing-state", + "reject-flow-create-non-static-initial" + ], + "required_origins": [ + "generated" + ] + }, + { + "id": "SCA-BUG-AGGREGATE-INVARIANT-CONTRACT", + "min_mode": "ci", + "name": "xUDT group amount conservation invariant must lower to the matching runtime helper", + "release_boundary": "assert_sum(group_outputs.amount) == assert_sum(group_inputs.amount) is recognised as the xUDT conserved aggregate and surfaces the runtime-helper-required gap", + "required_cases": [ + "accept-invariant-xudt-conserved" + ], + "required_origins": [ + "generated" + ] + } + ], + "cases": [ + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "explicit-transfer", + "oracle": { + "action": "transfer_coin", + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [ + "coin" + ], + "create_bindings": [ + "next_coin" + ], + "create_fields": { + "next_coin": [ + "amount", + "nonce" + ] + }, + "locked_outputs": [ + "next_coin" + ], + "obligation_contains": [ + "create-output-lock" + ], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "generated", + "source": "module cellscript::audit::explicit_transfer\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction transfer_coin(coin: Coin, to: Address) -> next_coin: Coin {\n verification\n consume coin\n\n create next_coin = Coin {\n amount: coin.amount,\n nonce: coin.nonce\n } with_lock(to)\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "pure-require-block", + "oracle": { + "action": "keep_fields", + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [ + "coin" + ], + "create_bindings": [ + "next_coin" + ], + "create_fields": { + "next_coin": [ + "amount", + "nonce" + ] + }, + "locked_outputs": [ + "next_coin" + ], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "generated", + "source": "module cellscript::audit::pure_require_block\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction keep_fields(coin: Coin, to: Address) -> next_coin: Coin {\n verification\n consume coin\n\n create next_coin = Coin {\n amount: coin.amount,\n nonce: coin.nonce\n } with_lock(to)\n\n require {\n next_coin.amount == coin.amount\n next_coin.nonce == coin.nonce\n }\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "preserve-sugar", + "oracle": { + "action": "preserve_fields", + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [ + "coin" + ], + "create_bindings": [ + "next_coin" + ], + "create_fields": { + "next_coin": [ + "amount", + "nonce" + ] + }, + "locked_outputs": [ + "next_coin" + ], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "generated", + "source": "module cellscript::audit::preserve_sugar\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction preserve_fields(coin: Coin, to: Address) -> next_coin: Coin {\n verification\n consume coin\n\n create next_coin = Coin {\n amount: coin.amount,\n nonce: coin.nonce\n } with_lock(to)\n\n preserve next_coin from coin {\n amount\n nonce\n }\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "stdlib-transfer", + "oracle": { + "action": "transfer_coin", + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [ + "coin" + ], + "create_bindings": [ + "next_coin" + ], + "create_fields": { + "next_coin": [ + "amount", + "nonce" + ] + }, + "locked_outputs": [ + "next_coin" + ], + "obligation_contains": [ + "create-output-lock", + "consume-input:Coin:coin" + ], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "generated", + "source": "module cellscript::audit::stdlib_transfer\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction transfer_coin(coin: Coin, to: Address) -> next_coin: Coin {\n verification\n std::lifecycle::transfer(coin, next_coin, to) {\n amount\n nonce\n }\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "stdlib-claim", + "oracle": { + "action": "claim_voucher", + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [ + "voucher" + ], + "create_bindings": [ + "coin" + ], + "create_fields": { + "coin": [ + "amount", + "nonce" + ] + }, + "locked_outputs": [ + "coin" + ], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "generated", + "source": "module cellscript::audit::stdlib_claim\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction claim_voucher(voucher: Voucher) -> coin: Coin {\n verification\n std::receipt::claim(voucher, coin, voucher.holder) {\n amount\n nonce\n }\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "stdlib-settle", + "oracle": { + "action": "settle_voucher", + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [ + "voucher" + ], + "create_bindings": [ + "coin" + ], + "create_fields": { + "coin": [ + "amount", + "nonce" + ] + }, + "locked_outputs": [ + "coin" + ], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "generated", + "source": "module cellscript::audit::stdlib_settle\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction settle_voucher(voucher: Voucher) -> coin: Coin {\n verification\n std::lifecycle::settle(voucher, coin, voucher.holder) {\n amount\n nonce\n }\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "cell-metadata-helpers", + "oracle": { + "action": "preserve_boundary", + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [ + "cell-metadata-equality:lock_hash", + "cell-metadata-equality:capacity" + ], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "generated", + "source": "module cellscript::audit::cell_metadata_helpers\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction preserve_boundary(coin_before: Coin) -> coin_after: Coin {\n verification\n std::cell::preserve_type(coin_after, coin_before)\n std::cell::preserve_lock(coin_after, coin_before)\n std::cell::preserve_capacity(coin_after, coin_before)\n std::accounting::conserved(coin_after, coin_before)\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "lock-source-qualifiers", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "generated", + "source": "module cellscript::audit::lock_source_qualifiers\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\nlock owner_only(\n protected wallet: Wallet,\n lock_args owner: Address,\n witness claimed_owner: Address\n) -> bool {\n verification\n require wallet.owner == owner\n require claimed_owner == owner\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "if-tuple-projection", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:edge/tuple-projection", + "source": "module cellscript::audit::if_tuple_projection\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction choose(flag: bool) -> u64 {\n verification\n let pair = if flag { (1, 2) } else { (3, 4) }\n return pair.0\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "match-tuple-projection", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:edge/tuple-projection", + "source": "module cellscript::audit::match_tuple_projection\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\nenum Flag {\n Off,\n On,\n}\n\naction choose(flag: Flag) -> u64 {\n verification\n let pair = match flag {\n Flag::Off => { (1, 2) },\n _ => { (3, 4) },\n }\n return pair.1\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "byte-string-fixed-length", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:edge/bytestring-length", + "source": "module cellscript::audit::byte_string_fixed_length\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction symbol() -> [u8; 4] {\n verification\n return b\"TEST\"\n}\n" + }, + { + "expected": { + "contains": [ + "require block", + "verifier-boundary syntax" + ], + "phase": "reject_compile" + }, + "name": "reject-require-block-lifecycle", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "generated", + "source": "module cellscript::audit::reject_require_block_lifecycle\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction bad(voucher: Voucher) -> coin: Coin {\n verification\n require {\n std::receipt::claim(voucher, coin, voucher.holder) {\n amount\n nonce\n }\n }\n}\n" + }, + { + "expected": { + "contains": [ + "wildcard pattern '_'", + "last match arm" + ], + "phase": "reject_compile" + }, + "name": "reject-wildcard-match-non-last", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:edge/wildcard-match-order", + "source": "module cellscript::audit::reject_wildcard_match_non_last\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\nenum Flag {\n Off,\n On,\n}\n\naction bad(flag: Flag) -> u64 {\n verification\n return match flag {\n _ => { 1 },\n Flag::Off => { 2 },\n }\n}\n" + }, + { + "expected": { + "contains": [ + "type mismatch" + ], + "phase": "reject_compile" + }, + "name": "reject-byte-string-length-mismatch", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:edge/bytestring-length", + "source": "module cellscript::audit::reject_byte_string_length_mismatch\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction bad() -> [u8; 3] {\n verification\n return b\"TEST\"\n}\n" + }, + { + "expected": { + "contains": [ + "type mismatch" + ], + "phase": "reject_compile" + }, + "name": "reject-preserve-type-mismatch", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "generated", + "source": "module cellscript::audit::reject_preserve_type_mismatch\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n}\n\nresource BadCoin has store, create, consume, replace, burn, relock {\n amount: bool,\n}\n\naction bad(coin: Coin) -> bad_coin: BadCoin {\n verification\n preserve bad_coin from coin {\n amount\n }\n}\n" + }, + { + "expected": { + "contains": [ + "missing nonce" + ], + "phase": "reject_compile" + }, + "name": "reject-transfer-missing-field", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "generated", + "source": "module cellscript::audit::reject_transfer_missing_field\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction bad(coin: Coin, to: Address) -> next_coin: Coin {\n verification\n std::lifecycle::transfer(coin, next_coin, to) {\n amount\n }\n}\n" + }, + { + "expected": { + "contains": [ + "cell-backed linear" + ], + "phase": "reject_compile" + }, + "name": "reject-consume-read-param", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "generated", + "source": "module cellscript::audit::reject_consume_read_param\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction bad(read coin: Coin) {\n verification\n consume coin\n}\n" + }, + { + "expected": { + "contains": [ + "unknown stdlib pattern" + ], + "phase": "reject_compile" + }, + "name": "reject-unknown-stdlib", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "generated", + "source": "module cellscript::audit::reject_unknown_stdlib\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction bad(coin_before: Coin) -> coin_after: Coin {\n verification\n std::cell::teleport(coin_after, coin_before)\n}\n" + }, + { + "expected": { + "contains": [ + "declare a claim output type" + ], + "phase": "reject_compile" + }, + "name": "reject-claim-without-output-arrow", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "generated", + "source": "module cellscript::audit::reject_claim_without_output_arrow\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\naction bad(voucher: Voucher) -> coin: Coin {\n verification\n std::receipt::claim(voucher, coin, voucher.holder) {\n amount\n nonce\n }\n}\n" + }, + { + "expected": { + "contains": [ + "is not declared in the flow" + ], + "phase": "reject_compile" + }, + "name": "reject-flow-undeclared-edge", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "generated", + "source": "module cellscript::audit::reject_flow_undeclared_edge\n\nresource Offer has store {\n state: u8\n amount: u64\n}\n\nflow Offer.state {\n Live -> Filled;\n Filled -> Cancelled;\n Cancelled -> Filled;\n}\n\naction cancel(input: Offer) -> output: Offer {\n transition input.state: Live -> output.state: Cancelled\n verification\n require input.amount == output.amount\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "accept-flow-declared-cyclic-edge", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "generated", + "source": "module cellscript::audit::accept_flow_declared_cyclic_edge\n\nresource Pool has store {\n state: u8\n reserve: u64\n}\n\nflow Pool.state {\n Open -> Closed;\n Closed -> Open;\n}\n\naction close(pool_before: Pool) -> pool_after: Pool {\n transition pool_before.state: Open -> pool_after.state: Closed\n verification\n require pool_after.reserve == pool_before.reserve\n}\n\naction reopen(pool_before: Pool) -> pool_after: Pool {\n transition pool_before.state: Closed -> pool_after.state: Open\n verification\n require pool_after.reserve == pool_before.reserve\n}\n" + }, + { + "expected": { + "contains": [ + "must set its state field" + ], + "phase": "reject_compile" + }, + "name": "reject-flow-create-missing-state", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "generated", + "source": "module cellscript::audit::reject_flow_create_missing_state\n\nresource Offer has store, create {\n state: u8\n amount: u64\n}\n\nflow Offer.state {\n Live -> Filled;\n}\n\naction seed(recipient: Address) -> output: Offer {\n verification\n create output = Offer { amount: 0 } with_lock(recipient)\n}\n" + }, + { + "expected": { + "contains": [ + "must use a statically known declared state" + ], + "phase": "reject_compile" + }, + "name": "reject-flow-create-non-static-initial", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "generated", + "source": "module cellscript::audit::reject_flow_create_non_static_initial\n\nresource Offer has store, create {\n state: u8\n amount: u64\n}\n\nflow Offer.state {\n Live -> Filled;\n}\n\naction seed(dynamic_state: u8, recipient: Address) -> output: Offer {\n verification\n create output = Offer { state: dynamic_state, amount: 0 } with_lock(recipient)\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "accept-invariant-xudt-conserved", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "generated", + "source": "module cellscript::audit::accept_invariant_xudt_conserved\n\nresource Token has store, create, consume {\n amount: u128,\n}\n\ninvariant xudt_group_transfer_conservation {\n trigger: type_group\n scope: group\n reads: group_inputs.amount, group_outputs.amount\n assert_sum(group_outputs.amount) == assert_sum(group_inputs.amount)\n}\n\naction transfer(input: Token) -> output: Token {\n verification\n xudt::require_group_amount_conserved()\n preserve output from input {\n amount\n }\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "matrix-cell-helper-preserve_type", + "oracle": { + "action": "matrix_preserve_type", + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:continuity/std-cell", + "source": "module cellscript::audit::matrix_cell_helper_preserve_type\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction matrix_preserve_type(coin_before: Coin) -> coin_after: Coin {\n verification\n std::cell::preserve_type(coin_after, coin_before)\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "matrix-cell-helper-same_lock", + "oracle": { + "action": "matrix_same_lock", + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [ + "cell-metadata-equality:lock_hash" + ], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:continuity/std-cell", + "source": "module cellscript::audit::matrix_cell_helper_same_lock\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction matrix_same_lock(coin_before: Coin) -> coin_after: Coin {\n verification\n std::cell::same_lock(coin_after, coin_before)\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "matrix-cell-helper-preserve_lock", + "oracle": { + "action": "matrix_preserve_lock", + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [ + "cell-metadata-equality:lock_hash" + ], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:continuity/std-cell", + "source": "module cellscript::audit::matrix_cell_helper_preserve_lock\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction matrix_preserve_lock(coin_before: Coin) -> coin_after: Coin {\n verification\n std::cell::preserve_lock(coin_after, coin_before)\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "matrix-cell-helper-preserve_capacity", + "oracle": { + "action": "matrix_preserve_capacity", + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [ + "cell-metadata-equality:capacity" + ], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:continuity/std-cell", + "source": "module cellscript::audit::matrix_cell_helper_preserve_capacity\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction matrix_preserve_capacity(coin_before: Coin) -> coin_after: Coin {\n verification\n std::cell::preserve_capacity(coin_after, coin_before)\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "matrix-cell-helper-conserved", + "oracle": { + "action": "matrix_conserved", + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:continuity/std-cell", + "source": "module cellscript::audit::matrix_cell_helper_conserved\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction matrix_conserved(coin_before: Coin) -> coin_after: Coin {\n verification\n std::accounting::conserved(coin_after, coin_before)\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "matrix-explicit-transfer-branch-require", + "oracle": { + "action": "branch_keep", + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [ + "coin" + ], + "create_bindings": [ + "next_coin" + ], + "create_fields": { + "next_coin": [ + "amount", + "nonce" + ] + }, + "locked_outputs": [ + "next_coin" + ], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:lifecycle/proof/control-flow", + "source": "module cellscript::audit::matrix_explicit_transfer_branch_require\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction branch_keep(coin: Coin, to: Address) -> next_coin: Coin {\n verification\n consume coin\n\n create next_coin = Coin {\n amount: coin.amount,\n nonce: coin.nonce\n } with_lock(to)\n\n if next_coin.amount == coin.amount {\n require next_coin.nonce == coin.nonce\n } else {\n require next_coin.nonce == coin.nonce\n }\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "matrix-explicit-transfer-let-proof", + "oracle": { + "action": "let_keep", + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [ + "coin" + ], + "create_bindings": [ + "next_coin" + ], + "create_fields": { + "next_coin": [ + "amount", + "nonce" + ] + }, + "locked_outputs": [ + "next_coin" + ], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:lifecycle/proof/local-binding", + "source": "module cellscript::audit::matrix_explicit_transfer_let_proof\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction let_keep(coin: Coin, to: Address) -> next_coin: Coin {\n verification\n consume coin\n\n create next_coin = Coin {\n amount: coin.amount,\n nonce: coin.nonce\n } with_lock(to)\n\n let same_amount = next_coin.amount == coin.amount\n require same_amount\n require next_coin.nonce == coin.nonce\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "matrix-stdlib-transfer-require-block", + "oracle": { + "action": "transfer_with_block", + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [ + "coin" + ], + "create_bindings": [ + "next_coin" + ], + "create_fields": { + "next_coin": [ + "amount", + "nonce" + ] + }, + "locked_outputs": [ + "next_coin" + ], + "obligation_contains": [ + "create-output-lock", + "consume-input:Coin:coin" + ], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:stdlib-lifecycle/proof", + "source": "module cellscript::audit::matrix_stdlib_transfer_require_block\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction transfer_with_block(coin: Coin, to: Address) -> next_coin: Coin {\n verification\n std::lifecycle::transfer(coin, next_coin, to) {\n amount\n nonce\n }\n\n require {\n next_coin.amount == coin.amount\n next_coin.nonce == coin.nonce\n }\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "matrix-stdlib-transfer-lock-capacity", + "oracle": { + "action": "transfer_with_metadata", + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [ + "coin" + ], + "create_bindings": [ + "next_coin" + ], + "create_fields": { + "next_coin": [ + "amount", + "nonce" + ] + }, + "locked_outputs": [ + "next_coin" + ], + "obligation_contains": [ + "cell-metadata-equality:lock_hash", + "cell-metadata-equality:capacity" + ], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:stdlib-lifecycle/metadata", + "source": "module cellscript::audit::matrix_stdlib_transfer_lock_capacity\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction transfer_with_metadata(coin: Coin, to: Address) -> next_coin: Coin {\n verification\n std::lifecycle::transfer(coin, next_coin, to) {\n amount\n nonce\n }\n std::cell::preserve_lock(next_coin, coin)\n std::cell::preserve_capacity(next_coin, coin)\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "matrix-stdlib-claim-require-block", + "oracle": { + "action": "claim_with_block", + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [ + "voucher" + ], + "create_bindings": [ + "coin" + ], + "create_fields": { + "coin": [ + "amount", + "nonce" + ] + }, + "locked_outputs": [ + "coin" + ], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:receipt/proof", + "source": "module cellscript::audit::matrix_stdlib_claim_require_block\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction claim_with_block(voucher: Voucher) -> coin: Coin {\n verification\n std::receipt::claim(voucher, coin, voucher.holder) {\n amount\n nonce\n }\n\n require {\n coin.amount == voucher.amount\n coin.nonce == voucher.nonce\n }\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "matrix-stdlib-settle-preserve-capacity", + "oracle": { + "action": "settle_with_capacity", + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [ + "voucher" + ], + "create_bindings": [ + "coin" + ], + "create_fields": { + "coin": [ + "amount", + "nonce" + ] + }, + "locked_outputs": [ + "coin" + ], + "obligation_contains": [ + "cell-metadata-equality:capacity" + ], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:receipt/metadata", + "source": "module cellscript::audit::matrix_stdlib_settle_preserve_capacity\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction settle_with_capacity(voucher: Voucher) -> coin: Coin {\n verification\n std::lifecycle::settle(voucher, coin, voucher.holder) {\n amount\n nonce\n }\n std::cell::preserve_capacity(coin, voucher)\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "matrix-lock-protected-only", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:lock/source-qualifier", + "source": "module cellscript::audit::matrix_lock_protected_only\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\nlock protected_wallet(protected wallet: Wallet) -> bool {\n verification\n require wallet.owner == wallet.owner\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "matrix-lock-witness-only", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:lock/source-qualifier", + "source": "module cellscript::audit::matrix_lock_witness_only\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\nlock witness_owner(witness owner: Address) -> bool {\n verification\n require owner == owner\n}\n" + }, + { + "expected": { + "contains": [], + "phase": "accept" + }, + "name": "matrix-lock-args-only", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:lock/source-qualifier", + "source": "module cellscript::audit::matrix_lock_args_only\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\nlock args_owner(lock_args owner: Address) -> bool {\n verification\n require owner == owner\n}\n" + }, + { + "expected": { + "contains": [ + "require block", + "assignment" + ], + "phase": "reject_compile" + }, + "name": "matrix-reject-require-block-assignment", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:reject/proof-purity", + "source": "module cellscript::audit::matrix_reject_require_block_assignment\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction hidden_mutation(flag: bool) {\n verification\n let mut ok = flag\n require {\n ok = false\n }\n}\n" + }, + { + "expected": { + "contains": [ + "claim requires a receipt" + ], + "phase": "reject_compile" + }, + "name": "matrix-reject-claim-non-receipt", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:reject/stdlib-lifecycle", + "source": "module cellscript::audit::matrix_reject_claim_non_receipt\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction bad_claim(coin: Coin, to: Address) -> next_coin: Coin {\n verification\n std::receipt::claim(coin, next_coin, to) {\n amount\n nonce\n }\n}\n" + }, + { + "expected": { + "contains": [ + "claim expects 3 arguments" + ], + "phase": "reject_compile" + }, + "name": "matrix-reject-claim-extra-args", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:reject/stdlib-lifecycle", + "source": "module cellscript::audit::matrix_reject_claim_extra_args\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction bad_claim(voucher: Voucher) -> coin: Coin {\n verification\n std::receipt::claim(voucher, coin, voucher.holder, voucher.holder) {\n amount\n nonce\n }\n}\n" + }, + { + "expected": { + "contains": [ + "transfer expects 3 arguments" + ], + "phase": "reject_compile" + }, + "name": "matrix-reject-transfer-extra-args", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:reject/stdlib-lifecycle", + "source": "module cellscript::audit::matrix_reject_transfer_extra_args\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction bad_transfer(coin: Coin, to: Address) -> next_coin: Coin {\n verification\n std::lifecycle::transfer(coin, next_coin, to, to) {\n amount\n nonce\n }\n}\n" + }, + { + "expected": { + "contains": [ + "settle expects 3 arguments" + ], + "phase": "reject_compile" + }, + "name": "matrix-reject-settle-missing-args", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:reject/stdlib-lifecycle", + "source": "module cellscript::audit::matrix_reject_settle_missing_args\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction bad_settle(voucher: Voucher) -> coin: Coin {\n verification\n std::lifecycle::settle(voucher, coin) {\n amount\n nonce\n }\n}\n" + }, + { + "expected": { + "contains": [ + "claim output type mismatch" + ], + "phase": "reject_compile" + }, + "name": "matrix-reject-claim-output-type-mismatch", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:reject/stdlib-lifecycle", + "source": "module cellscript::audit::matrix_reject_claim_output_type_mismatch\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\nresource Badge has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\naction bad_claim_output(voucher: Voucher, to: Address) -> badge: Badge {\n verification\n std::receipt::claim(voucher, badge, to) {\n amount\n nonce\n }\n}\n" + }, + { + "expected": { + "contains": [ + "settle lock target must be Address or Hash" + ], + "phase": "reject_compile" + }, + "name": "matrix-reject-settle-lock-target-type", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:reject/stdlib-lifecycle", + "source": "module cellscript::audit::matrix_reject_settle_lock_target_type\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction bad_settle_lock(voucher: Voucher) -> coin: Coin {\n verification\n std::lifecycle::settle(voucher, coin, voucher.amount) {\n amount\n nonce\n }\n}\n" + }, + { + "expected": { + "contains": [ + "preserve_capacity input must be a cell-backed value" + ], + "phase": "reject_compile" + }, + "name": "matrix-reject-cell-metadata-non-cell", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:reject/metadata", + "source": "module cellscript::audit::matrix_reject_cell_metadata_non_cell\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction bad_metadata(amount: u64) -> out: Coin {\n verification\n std::cell::preserve_capacity(out, amount)\n}\n" + }, + { + "expected": { + "contains": [ + "cell-backed linear" + ], + "phase": "reject_compile" + }, + "name": "matrix-deep-reject-transfer-read-param", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:deep/reject/source-qualifier", + "source": "module cellscript::audit::matrix_deep_reject_transfer_read_param\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction bad_transfer(read coin: Coin, to: Address) -> next_coin: Coin {\n verification\n std::lifecycle::transfer(coin, next_coin, to) {\n amount\n nonce\n }\n}\n" + }, + { + "expected": { + "contains": [ + "require block", + "verifier-boundary syntax" + ], + "phase": "reject_compile" + }, + "name": "matrix-deep-reject-require-block-transfer", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:deep/reject/proof-purity", + "source": "module cellscript::audit::matrix_deep_reject_require_block_transfer\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction hidden_transfer(coin: Coin, to: Address) -> next_coin: Coin {\n verification\n require {\n std::lifecycle::transfer(coin, next_coin, to) {\n amount\n nonce\n }\n }\n}\n" + }, + { + "expected": { + "contains": [ + "unknown stdlib pattern" + ], + "phase": "reject_compile" + }, + "name": "matrix-deep-reject-unknown-accounting", + "oracle": { + "action": null, + "borrow_scope": null, + "borrow_view_type": null, + "capability_operation": null, + "capability_type": null, + "consume_bindings": [], + "create_bindings": [], + "create_fields": {}, + "locked_outputs": [], + "obligation_contains": [], + "payload_enum": null, + "protocol_role": null, + "protocol_role_action": null, + "protocol_role_conflict": null, + "protocol_role_source": null, + "validity_tiers": [], + "validity_type": null + }, + "origin": "matrix:deep/reject/stdlib-namespace", + "source": "module cellscript::audit::matrix_deep_reject_unknown_accounting\n\nresource Coin has store, create, consume, replace, burn, relock {\n amount: u64,\n nonce: u64,\n}\n\nreceipt Voucher -> Coin has create, consume, burn {\n amount: u64,\n nonce: u64,\n holder: Address,\n}\n\nresource Wallet has store, create, consume, replace, burn, relock {\n owner: Address,\n}\n\naction bad_accounting(coin_before: Coin) -> coin_after: Coin {\n verification\n std::accounting::minted(coin_after, coin_before)\n}\n" + } + ], + "governance_release_matrix": [ + { + "evidence": "action and lock cases parse, format, and use the verification section", + "gate": "syntax-combo accepted action/lock cases plus VS Code validate/dry-run in release gate", + "layer": "parser_formatter_lsp_docs", + "status": "covered_by_gate", + "track": "canonical_action_lock_surface" + }, + { + "evidence": "preserve and anonymous require-block cases are type/effect checked and metadata-checked", + "gate": "syntax-combo preserve/require-block positive and negative cases", + "layer": "type_lowering_metadata", + "status": "covered_by_gate", + "track": "local_explicit_sugar" + }, + { + "evidence": "transfer/claim/settle emit consume, create, locked output, and field obligations", + "gate": "syntax-combo stdlib lifecycle metadata oracles", + "layer": "type_lowering_metadata_codegen", + "status": "covered_by_gate", + "track": "stdlib_lifecycle_patterns" + }, + { + "evidence": "read/protected/witness/lock_args boundaries reject linear lifecycle misuse", + "gate": "syntax-combo lock source qualifier and read-param reject cases", + "layer": "type_effect", + "status": "covered_by_gate", + "track": "source_qualifier_boundary" + }, + { + "evidence": "unknown stdlib patterns and hidden lifecycle proof forms fail closed", + "gate": "syntax-combo reject seeds and required bug classes", + "layer": "parser_type_policy", + "status": "covered_by_gate", + "track": "deferred_rejected_surfaces" + }, + { + "evidence": "accepted cases compile to non-empty assembly and metadata matches consume/create/lock obligations", + "gate": "syntax-combo metadata/codegen oracles", + "layer": "ir_metadata_codegen", + "status": "covered_by_gate", + "track": "metadata_fidelity" + } + ] +} diff --git a/tests/syntax_combo/matrix.toml b/tests/syntax_combo/matrix.toml index 09ced35a..eca13ed2 100644 --- a/tests/syntax_combo/matrix.toml +++ b/tests/syntax_combo/matrix.toml @@ -1,4 +1,4 @@ -# Matrix metadata for scripts/cellscript_syntax_combo_audit.py. +# Matrix metadata for the Rust `cellscript-tools syntax-combo-audit` runner. # The first runner version keeps generation deterministic and small, while this # file records the axes that must stay covered as the generator grows. @@ -25,6 +25,8 @@ required_origins = [ "tests/syntax_combo/seeds/explicit-borrow-effect-reject.cell", "tests/syntax_combo/seeds/explicit-borrow-escape-reject.cell", "tests/syntax_combo/seeds/explicit-borrow-cross-consume-reject.cell", + "tests/syntax_combo/seeds/field-commas-canonical.cell", + "tests/syntax_combo/seeds/field-commas-compatibility.cell", "tests/syntax_combo/seeds/capability-entailment.cell", "tests/syntax_combo/seeds/capability-missing-identity-reject.cell", "tests/syntax_combo/seeds/capability-transitive-grant-reject.cell", @@ -72,6 +74,8 @@ required_origins = [ "tests/syntax_combo/seeds/explicit-borrow-effect-reject.cell", "tests/syntax_combo/seeds/explicit-borrow-escape-reject.cell", "tests/syntax_combo/seeds/explicit-borrow-cross-consume-reject.cell", + "tests/syntax_combo/seeds/field-commas-canonical.cell", + "tests/syntax_combo/seeds/field-commas-compatibility.cell", "tests/syntax_combo/seeds/capability-entailment.cell", "tests/syntax_combo/seeds/capability-missing-identity-reject.cell", "tests/syntax_combo/seeds/capability-transitive-grant-reject.cell", @@ -122,6 +126,8 @@ required_origins = [ "tests/syntax_combo/seeds/explicit-borrow-effect-reject.cell", "tests/syntax_combo/seeds/explicit-borrow-escape-reject.cell", "tests/syntax_combo/seeds/explicit-borrow-cross-consume-reject.cell", + "tests/syntax_combo/seeds/field-commas-canonical.cell", + "tests/syntax_combo/seeds/field-commas-compatibility.cell", "tests/syntax_combo/seeds/capability-entailment.cell", "tests/syntax_combo/seeds/capability-missing-identity-reject.cell", "tests/syntax_combo/seeds/capability-transitive-grant-reject.cell", diff --git a/tests/syntax_combo/seeds/field-commas-canonical.cell b/tests/syntax_combo/seeds/field-commas-canonical.cell new file mode 100644 index 00000000..2b142fc4 --- /dev/null +++ b/tests/syntax_combo/seeds/field-commas-canonical.cell @@ -0,0 +1,12 @@ +// audit: phase=accept +module cellscript::audit::seed_field_commas_canonical + +struct CanonicalFields { + amount: u64, + enabled: bool, +} + +action inspect(value: CanonicalFields) -> bool { + verification + return value.enabled +} diff --git a/tests/syntax_combo/seeds/field-commas-compatibility.cell b/tests/syntax_combo/seeds/field-commas-compatibility.cell new file mode 100644 index 00000000..16743b61 --- /dev/null +++ b/tests/syntax_combo/seeds/field-commas-compatibility.cell @@ -0,0 +1,12 @@ +// audit: phase=accept +module cellscript::audit::seed_field_commas_compatibility + +struct CompatibilityFields { + amount: u64 + enabled: bool +} + +action inspect(value: CompatibilityFields) -> bool { + verification + return value.enabled +} diff --git a/website b/website index fffdcf6a..9849c0cb 160000 --- a/website +++ b/website @@ -1 +1 @@ -Subproject commit fffdcf6a73427d8bfcae8271cef8702ebcad2cee +Subproject commit 9849c0cb051439901bd3d9c01bd6ba58e8e40751