Skip to content

Commit 19c5f70

Browse files
committed
feat: generate loader abi for the executable loaders
1 parent 32d1cc9 commit 19c5f70

File tree

1 file changed

+133
-3
lines changed

1 file changed

+133
-3
lines changed

forc-plugins/forc-client/src/op/deploy.rs

+133-3
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,22 @@ use forc_pkg::{manifest::GenericManifestFile, MemberFilter};
1818
use forc_tracing::{println_action_green, println_warning};
1919
use forc_util::default_output_directory;
2020
use forc_wallet::utils::default_wallet_path;
21+
use fuel_abi_types::abi::program::Configurable;
2122
use fuel_core_client::client::types::{ChainInfo, TransactionStatus};
2223
use fuel_core_client::client::FuelClient;
2324
use fuel_crypto::fuel_types::ChainId;
2425
use fuel_tx::{Salt, Transaction};
25-
use fuel_vm::prelude::*;
26+
use fuel_vm::{consts::WORD_SIZE, fuel_asm::op, prelude::*};
2627
use fuels::{
2728
macros::abigen,
2829
programs::{
2930
contract::{LoadConfiguration, StorageConfiguration},
3031
executable::Executable,
3132
},
32-
types::{bech32::Bech32ContractId, transaction_builders::Blob},
33+
types::{
34+
bech32::Bech32ContractId,
35+
transaction_builders::{Blob, BlobId},
36+
},
3337
};
3438
use fuels_accounts::{provider::Provider, Account, ViewOnlyAccount};
3539
use fuels_core::types::{transaction::TxPolicies, transaction_builders::CreateTransactionBuilder};
@@ -43,8 +47,8 @@ use std::{
4347
sync::Arc,
4448
time::Duration,
4549
};
46-
use sway_core::language::parsed::TreeType;
4750
use sway_core::BuildTarget;
51+
use sway_core::{asm_generation::ProgramABI, language::parsed::TreeType};
4852

4953
/// Default maximum contract size allowed for a single contract. If the target
5054
/// contract size is bigger than this amount, forc-deploy will automatically
@@ -356,6 +360,96 @@ pub async fn deploy(command: cmd::Deploy) -> Result<Vec<DeployedPackage>> {
356360
Ok(deployed_packages)
357361
}
358362

363+
fn loader_data_offset(binary: &[u8], blob_id: &BlobId) -> Result<Option<usize>> {
364+
// The final code is going to have this structure (if the data section is non-empty):
365+
// 1. loader instructions
366+
// 2. blob id
367+
// 3. length_of_data_section
368+
// 4. the data_section (updated with configurables as needed)
369+
const BLOB_ID_SIZE: u16 = 32;
370+
const REG_ADDRESS_OF_DATA_AFTER_CODE: u8 = 0x10;
371+
const REG_START_OF_LOADED_CODE: u8 = 0x11;
372+
const REG_GENERAL_USE: u8 = 0x12;
373+
let get_instructions = |num_of_instructions| {
374+
// There are 3 main steps:
375+
// 1. Load the blob content into memory
376+
// 2. Load the data section right after the blob
377+
// 3. Jump to the beginning of the memory where the blob was loaded
378+
[
379+
// 1. Load the blob content into memory
380+
// Find the start of the hardcoded blob ID, which is located after the loader code ends.
381+
op::move_(REG_ADDRESS_OF_DATA_AFTER_CODE, RegId::PC),
382+
// hold the address of the blob ID.
383+
op::addi(
384+
REG_ADDRESS_OF_DATA_AFTER_CODE,
385+
REG_ADDRESS_OF_DATA_AFTER_CODE,
386+
num_of_instructions * Instruction::SIZE as u16,
387+
),
388+
// The code is going to be loaded from the current value of SP onwards, save
389+
// the location into REG_START_OF_LOADED_CODE so we can jump into it at the end.
390+
op::move_(REG_START_OF_LOADED_CODE, RegId::SP),
391+
// REG_GENERAL_USE to hold the size of the blob.
392+
op::bsiz(REG_GENERAL_USE, REG_ADDRESS_OF_DATA_AFTER_CODE),
393+
// Push the blob contents onto the stack.
394+
op::ldc(REG_ADDRESS_OF_DATA_AFTER_CODE, 0, REG_GENERAL_USE, 1),
395+
// Move on to the data section length
396+
op::addi(
397+
REG_ADDRESS_OF_DATA_AFTER_CODE,
398+
REG_ADDRESS_OF_DATA_AFTER_CODE,
399+
BLOB_ID_SIZE,
400+
),
401+
// load the size of the data section into REG_GENERAL_USE
402+
op::lw(REG_GENERAL_USE, REG_ADDRESS_OF_DATA_AFTER_CODE, 0),
403+
// after we have read the length of the data section, we move the pointer to the actual
404+
// data by skipping WORD_SIZE B.
405+
op::addi(
406+
REG_ADDRESS_OF_DATA_AFTER_CODE,
407+
REG_ADDRESS_OF_DATA_AFTER_CODE,
408+
WORD_SIZE as u16,
409+
),
410+
// load the data section of the executable
411+
op::ldc(REG_ADDRESS_OF_DATA_AFTER_CODE, 0, REG_GENERAL_USE, 2),
412+
// Jump into the memory where the contract is loaded.
413+
// What follows is called _jmp_mem by the sway compiler.
414+
// Subtract the address contained in IS because jmp will add it back.
415+
op::sub(
416+
REG_START_OF_LOADED_CODE,
417+
REG_START_OF_LOADED_CODE,
418+
RegId::IS,
419+
),
420+
// jmp will multiply by 4, so we need to divide to cancel that out.
421+
op::divi(REG_START_OF_LOADED_CODE, REG_START_OF_LOADED_CODE, 4),
422+
// Jump to the start of the contract we loaded.
423+
op::jmp(REG_START_OF_LOADED_CODE),
424+
]
425+
};
426+
427+
let offset = extract_data_offset(&binary)?;
428+
429+
if binary.len() < offset {
430+
anyhow::bail!("data sectio offset is out of bounds");
431+
}
432+
433+
let data_section = binary[offset..].to_vec();
434+
435+
if !data_section.is_empty() {
436+
let num_of_instructions = u16::try_from(get_instructions(0).len())
437+
.expect("to never have more than u16::MAX instructions");
438+
439+
let instruction_bytes = get_instructions(num_of_instructions)
440+
.into_iter()
441+
.flat_map(|instruction| instruction.to_bytes());
442+
443+
let blob_bytes = blob_id.iter().copied();
444+
445+
let loader_offset = instruction_bytes.count() + blob_bytes.count();
446+
447+
Ok(Some(loader_offset))
448+
} else {
449+
Ok(None)
450+
}
451+
}
452+
359453
/// Builds and deploys executable (script and predicate) package(s) as blobs,
360454
/// and generates a loader for each of them.
361455
pub async fn deploy_executables(
@@ -387,6 +481,29 @@ pub async fn deploy_executables(
387481
"Saved",
388482
&format!("loader bytecode at {}", bin_path.display()),
389483
);
484+
if let Some(loader_data_section_offset) =
485+
loader_data_offset(&pkg.bytecode.bytes, &BlobId::default())?
486+
{
487+
if let ProgramABI::Fuel(mut fuel_abi) = pkg.program_abi.clone() {
488+
println_action_green("Generating", "loader abi for the uploaded executable.");
489+
let json_abi_path = out_dir.join(format!("{pkg_name}-loader-abi.json"));
490+
let original_data_section = extract_data_offset(&pkg.bytecode.bytes).unwrap();
491+
let offset_shift = original_data_section - loader_data_section_offset;
492+
// if there are configurables in the abi we need to shift them by `offset_shift`.
493+
let configurables = fuel_abi.configurables.clone().map(|configs| {
494+
configs
495+
.into_iter()
496+
.map(|config| Configurable {
497+
offset: config.offset - offset_shift as u64,
498+
..config.clone()
499+
})
500+
.collect()
501+
});
502+
fuel_abi.configurables = configurables;
503+
let json_string = serde_json::to_string_pretty(&fuel_abi)?;
504+
std::fs::write(json_abi_path, json_string)?;
505+
}
506+
}
390507
// If the executable is a predicate, we also want to display and save the predicate root.
391508
if pkg
392509
.descriptor
@@ -419,6 +536,19 @@ pub async fn deploy_executables(
419536
Ok(deployed_executable)
420537
}
421538

539+
fn extract_data_offset(binary: &[u8]) -> Result<usize> {
540+
if binary.len() < 16 {
541+
anyhow::bail!(
542+
"given binary is too short to contain a data offset, len: {}",
543+
binary.len()
544+
);
545+
}
546+
547+
let data_offset: [u8; 8] = binary[8..16].try_into().expect("checked above");
548+
549+
Ok(u64::from_be_bytes(data_offset) as usize)
550+
}
551+
422552
/// Builds and deploys contract(s). If the given path corresponds to a workspace, all deployable members
423553
/// will be built and deployed.
424554
///

0 commit comments

Comments
 (0)