瀏覽代碼

Implement chain extensions builtin (#1305)

Cyrill Leutwiler 2 年之前
父節點
當前提交
e8cfb9afcb

+ 2 - 2
README.md

@@ -142,9 +142,9 @@ Here is a brief description of what we envision for the next versions.
 | Improvements in overflow checking            | Completed   |
 | Support Solana's Program Derived Addresses   | Completed   |
 | Call Solidity from Solana's Rust contracts   | Not started |
-| Improve developer experience for Substrate   | Complete    |
+| Improve developer experience for Substrate   | Completed   |
 | Tooling for calls between ink! <> solidity   | In progress |
-| Support chain extensions for Substrate       | Not started |
+| Support chain extensions for Substrate       | Completed   |
 | Provide CLI for node interactions            | Not started |
 
 

+ 19 - 0
docs/examples/substrate/call_chain_extension.sol

@@ -0,0 +1,19 @@
+import "substrate";
+
+contract Foo {
+    // Call the "rand-extension" example chain extension demonstrated here:
+    // https://use.ink/macros-attributes/chain-extension
+    //
+    // This chain extension is registered under ID 1101.
+    // It takes a bytes32 as input seed and returns a pseudo random bytes32.
+    function fetch_random(bytes32 _seed) public returns (bytes32) {
+        bytes input = abi.encode(_seed);
+        (uint32 ret, bytes output) = chain_extension(1101, input);
+
+        assert(ret == 0); // The fetch-random chain extension always returns 0
+        bytes32 random = abi.decode(output, (bytes32));
+
+        print("psuedo random bytes: {}".format(random));
+        return random;
+    }
+}

+ 29 - 0
docs/language/builtins.rst

@@ -311,6 +311,35 @@ Its underlying type is ``bytes32``, but it will be reported correctly as the ``H
 .. include:: ../examples/substrate/hash_type.sol
   :code: solidity
 
+chain_extension(uint32 ID, bytes input) returns (uint32, bytes)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+Only available on Substrate. Call the chain extension with the given ``ID`` and ``input`` data.
+Returns the return value from the chain extension and the output data.
+
+This function is a low level interface.
+The caller is responsible for encoding the input and decoding the output correctly.
+We expect parachain authors to write their own higher level libraries on top.
+
+.. warning::
+    This function calls the runtime API `call_chain_extension <https://docs.rs/pallet-contracts/latest/pallet_contracts/api_doc/trait.Version0.html#tymethod.call_chain_extension>`.
+	It assumes that the implementation of the chain extension
+	- reads the input from the ``input_ptr`` parameter, used as a buffer pointer
+	- writes potential output into the buffer found at the ``output_ptr`` pointer
+	- respects the output buffer length in ``output_len_ptr`` to prevent OOB writes. The output buffer is 16KB in size.
+	- writes the amount of bytes written to ``output_ptr`` into the buffer at ``output_len_ptr``
+	
+	Unlike with other runtime API calls, the contracts pallet can not guarantee this behaviour.
+	Instead, it's specific to the targeted chain runtime. Hence, when using this builtin,
+	you must be sure that the implementation being called underneath is compatible.
+
+The following example demonstrates the usage of this builtin function.
+It shows how the chain extension example from the <ink! documentation https://use.ink/macros-attributes/chain-extension/>
+looks like in a solidity contract:
+
+.. include:: ../examples/substrate/call_chain_extension.sol
+  :code: solidity
+
 Cryptography
 ____________
 

+ 1 - 0
docs/targets/substrate.rst

@@ -35,6 +35,7 @@ can be imported via the special import file ``substrate``.
 .. code-block:: solidity
 
     import {Hash} from 'substrate';
+    import {chain_extension} from 'substrate';
 
 Note that ``{Hash}`` can be omitted, renamed or imported via
 import object.

+ 17 - 16
src/emit/instructions.rs

@@ -529,24 +529,25 @@ pub(super) fn process_instruction<'a, T: TargetRuntime<'a> + ?Sized>(
             }
 
             let first_arg_type = bin.llvm_type(&args[0].ty(), ns);
-            let ret = target.builtin_function(bin, function, callee, &parms, first_arg_type, ns);
-
-            let success = bin.builder.build_int_compare(
-                IntPredicate::EQ,
-                ret.into_int_value(),
-                bin.return_values[&ReturnCode::Success],
-                "success",
-            );
-
-            let success_block = bin.context.append_basic_block(function, "success");
-            let bail_block = bin.context.append_basic_block(function, "bail");
-            bin.builder
-                .build_conditional_branch(success, success_block, bail_block);
+            if let Some(ret) =
+                target.builtin_function(bin, function, callee, &parms, first_arg_type, ns)
+            {
+                let success = bin.builder.build_int_compare(
+                    IntPredicate::EQ,
+                    ret.into_int_value(),
+                    bin.return_values[&ReturnCode::Success],
+                    "success",
+                );
+                let success_block = bin.context.append_basic_block(function, "success");
+                let bail_block = bin.context.append_basic_block(function, "bail");
+                bin.builder
+                    .build_conditional_branch(success, success_block, bail_block);
 
-            bin.builder.position_at_end(bail_block);
+                bin.builder.position_at_end(bail_block);
+                bin.builder.build_return(Some(&ret));
 
-            bin.builder.build_return(Some(&ret));
-            bin.builder.position_at_end(success_block);
+                bin.builder.position_at_end(success_block);
+            }
 
             if !res.is_empty() {
                 for (i, v) in callee.returns.iter().enumerate() {

+ 1 - 1
src/emit/mod.rs

@@ -240,7 +240,7 @@ pub trait TargetRuntime<'a> {
         args: &[BasicMetadataValueEnum<'a>],
         first_arg_type: BasicTypeEnum,
         ns: &Namespace,
-    ) -> BasicValueEnum<'a>;
+    ) -> Option<BasicValueEnum<'a>>;
 
     /// Calls constructor
     fn create_contract<'b>(

+ 7 - 5
src/emit/solana/target.rs

@@ -1281,7 +1281,7 @@ impl<'a> TargetRuntime<'a> for SolanaTarget {
         args: &[BasicMetadataValueEnum<'a>],
         first_arg_type: BasicTypeEnum,
         ns: &ast::Namespace,
-    ) -> BasicValueEnum<'a> {
+    ) -> Option<BasicValueEnum<'a>> {
         if builtin_func.name == "create_program_address" {
             let func = binary
                 .module
@@ -1300,7 +1300,7 @@ impl<'a> TargetRuntime<'a> for SolanaTarget {
                 .builder
                 .build_store(address, args[1].into_array_value());
 
-            binary
+            let ret = binary
                 .builder
                 .build_call(
                     func,
@@ -1314,7 +1314,8 @@ impl<'a> TargetRuntime<'a> for SolanaTarget {
                 )
                 .try_as_basic_value()
                 .left()
-                .unwrap()
+                .unwrap();
+            Some(ret)
         } else if builtin_func.name == "try_find_program_address" {
             let func = binary
                 .module
@@ -1333,7 +1334,7 @@ impl<'a> TargetRuntime<'a> for SolanaTarget {
                 .builder
                 .build_store(address, args[1].into_array_value());
 
-            binary
+            let ret = binary
                 .builder
                 .build_call(
                     func,
@@ -1348,7 +1349,8 @@ impl<'a> TargetRuntime<'a> for SolanaTarget {
                 )
                 .try_as_basic_value()
                 .left()
-                .unwrap()
+                .unwrap();
+            Some(ret)
         } else {
             unreachable!();
         }

+ 10 - 0
src/emit/substrate/mod.rs

@@ -156,6 +156,7 @@ impl SubstrateTarget {
         binary.internalize(&[
             "deploy",
             "call",
+            "call_chain_extension",
             "seal_input",
             "seal_set_storage",
             "seal_get_storage",
@@ -250,6 +251,15 @@ impl SubstrateTarget {
             };
         }
 
+        external!(
+            "call_chain_extension",
+            i32_type,
+            u32_val,
+            u8_ptr,
+            u32_val,
+            u8_ptr,
+            u32_ptr
+        );
         external!("seal_input", void_type, u8_ptr, u32_ptr);
         external!("seal_hash_keccak_256", void_type, u8_ptr, u32_val, u8_ptr);
         external!("seal_hash_sha2_256", void_type, u8_ptr, u32_val, u8_ptr);

+ 49 - 5
src/emit/substrate/target.rs

@@ -1580,14 +1580,58 @@ impl<'a> TargetRuntime<'a> for SubstrateTarget {
 
     fn builtin_function(
         &self,
-        _binary: &Binary<'a>,
+        binary: &Binary<'a>,
         _function: FunctionValue<'a>,
-        _builtin_func: &Function,
-        _args: &[BasicMetadataValueEnum<'a>],
+        builtin_func: &Function,
+        args: &[BasicMetadataValueEnum<'a>],
         _first_arg_type: BasicTypeEnum,
         _ns: &Namespace,
-    ) -> BasicValueEnum<'a> {
-        unimplemented!()
+    ) -> Option<BasicValueEnum<'a>> {
+        emit_context!(binary);
+
+        assert_eq!(builtin_func.name, "chain_extension", "unimplemented");
+
+        let input_ptr = binary.vector_bytes(args[1].into_pointer_value().into());
+        let input_len = binary.vector_len(args[1].into_pointer_value().into());
+        let (output_ptr, output_len_ptr) = scratch_buf!();
+        let len = 16384; // 16KB for the output buffer should be enough for virtually any case.
+        binary.builder.build_store(output_len_ptr, i32_const!(len));
+        call!("__bzero8", &[output_ptr.into(), i32_const!(len / 8).into()]);
+        let ret_val = call!(
+            "call_chain_extension",
+            &[
+                args[0].into_int_value().into(),
+                input_ptr.into(),
+                input_len.into(),
+                output_ptr.into(),
+                output_len_ptr.into()
+            ]
+        )
+        .try_as_basic_value()
+        .left()
+        .unwrap()
+        .into_int_value();
+
+        let buf_len = binary
+            .builder
+            .build_load(binary.context.i32_type(), output_len_ptr, "buf_len")
+            .into_int_value();
+        let buf = call!(
+            "vector_new",
+            &[buf_len.into(), i32_const!(1).into(), output_ptr.into(),]
+        )
+        .try_as_basic_value()
+        .left()
+        .unwrap();
+
+        binary
+            .builder
+            .build_store(args[2].into_pointer_value(), ret_val);
+        binary
+            .builder
+            .build_store(args[3].into_pointer_value(), buf.into_pointer_value());
+
+        None
     }
 
     fn storage_subscript(

+ 73 - 8
src/sema/builtin.rs

@@ -1630,6 +1630,12 @@ impl Namespace {
     }
 
     pub fn add_substrate_builtins(&mut self) {
+        let loc = pt::Loc::Builtin;
+        let id = |name: &str| Identifier {
+            name: name.into(),
+            loc,
+        };
+
         let file_no = self.files.len();
         self.files.push(File {
             path: PathBuf::from("substrate"),
@@ -1641,22 +1647,81 @@ impl Namespace {
         let type_no = self.user_types.len();
         self.user_types.push(UserTypeDecl {
             tags: vec![Tag {
-                loc: pt::Loc::Builtin,
+                loc,
                 tag: "notice".into(),
                 no: 0,
                 value: "The Hash type from ink primitives".into(),
             }],
-            loc: pt::Loc::Builtin,
+            loc,
             name: "Hash".into(),
             ty: Type::Bytes(32),
             contract: None,
         });
 
-        let id = pt::Identifier {
-            loc: pt::Loc::Builtin,
-            name: "Hash".into(),
-        };
-        let symbol = Symbol::UserType(pt::Loc::Builtin, type_no);
-        assert!(self.add_symbol(file_no, None, &id, symbol));
+        let symbol = Symbol::UserType(loc, type_no);
+        assert!(self.add_symbol(file_no, None, &id("Hash"), symbol));
+
+        // Chain extensions
+        let mut func = Function::new(
+            loc,
+            "chain_extension".to_string(),
+            None,
+            Vec::new(),
+            pt::FunctionTy::Function,
+            None,
+            pt::Visibility::Public(Some(loc)),
+            vec![
+                Parameter {
+                    loc,
+                    id: Some(id("id")),
+                    ty: Type::Uint(32),
+                    ty_loc: Some(loc),
+                    readonly: false,
+                    indexed: false,
+                    infinite_size: false,
+                    recursive: false,
+                },
+                Parameter {
+                    loc,
+                    id: Some(id("input")),
+                    ty: Type::DynamicBytes,
+                    ty_loc: Some(loc),
+                    readonly: false,
+                    indexed: false,
+                    infinite_size: false,
+                    recursive: false,
+                },
+            ],
+            vec![
+                Parameter {
+                    loc,
+                    id: Some(id("return_value")),
+                    ty: Type::Uint(32),
+                    ty_loc: Some(loc),
+                    readonly: false,
+                    indexed: false,
+                    infinite_size: false,
+                    recursive: false,
+                },
+                Parameter {
+                    loc,
+                    id: Some(id("output")),
+                    ty: Type::DynamicBytes,
+                    ty_loc: Some(loc),
+                    readonly: false,
+                    indexed: false,
+                    infinite_size: false,
+                    recursive: false,
+                },
+            ],
+            self,
+        );
+
+        func.has_body = true;
+        let func_no = self.functions.len();
+        let id = id(&func.name);
+        self.functions.push(func);
+
+        assert!(self.add_symbol(file_no, None, &id, Symbol::Function(vec![(loc, func_no)])));
     }
 }

+ 22 - 0
tests/substrate.rs

@@ -711,6 +711,28 @@ impl Runtime {
 
         Ok(())
     }
+
+    /// Mock chain extension with ID 123 that writes the reversed input to the output buf.
+    /// Returns the sum of the input data.
+    #[seal(0)]
+    fn call_chain_extension(
+        id: u32,
+        input_ptr: u32,
+        input_len: u32,
+        output_ptr: u32,
+        output_len_ptr: u32,
+    ) -> Result<u32, Trap> {
+        assert_eq!(id, 123, "unkown chain extension");
+        assert!(read_len(mem, output_len_ptr) == 16384 && input_len <= 16384);
+
+        let mut data = read_buf(mem, input_ptr, input_len);
+        data.reverse();
+
+        write_buf(mem, output_ptr, &data);
+        write_buf(mem, output_len_ptr, &(data.len() as u32).to_le_bytes());
+
+        Ok(data.iter().map(|i| *i as u32).sum())
+    }
 }
 
 /// Provides a mock implementation of substrates [contracts pallet][1]

+ 20 - 0
tests/substrate_tests/builtins.rs

@@ -755,3 +755,23 @@ fn hash() {
 
     runtime.function("test_encoding", vec![]);
 }
+
+#[test]
+fn call_chain_extension() {
+    let mut runtime = build_solidity(
+        r##"
+        import {chain_extension as ChainExtension} from "substrate";
+
+        contract Foo {
+            function chain_extension(bytes input) public returns (uint32, bytes) {
+                return ChainExtension(123, input);
+            }
+        }"##,
+    );
+
+    let data = 0xdeadbeefu32.to_be_bytes().to_vec();
+    runtime.function("chain_extension", data.encode());
+    let ret = <(u32, Vec<u8>)>::decode(&mut &runtime.output()[..]).unwrap();
+    assert_eq!(ret.0, data.iter().map(|i| *i as u32).sum::<u32>());
+    assert_eq!(ret.1, data.iter().cloned().rev().collect::<Vec<_>>());
+}