你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

Azure 机密账本中的简单用户定义函数(预览版)

使用 Azure 机密账本中的简单用户定义的函数(UDF),可以创建自定义 JavaScript 函数,这些函数可在账本信任边界内执行。 此功能设计为简单易用,使你可以扩展账本 API 的功能,而无需复杂的应用程序开发。

使用 内置的 JavaScript API,可以运行自定义代码来实现各种任务,例如自定义查询和计算、条件检查、处理后任务等。 此功能适用于需要与现有账本 API 直接集成或在机密环境中运行轻型自定义逻辑的方案。

有关 UDF 的快速概述和演示,请观看以下视频:

重要

用户定义函数目前在 API 版本下处于预览状态 2024-12-09-preview。 可以通过 此注册表单请求对此预览版的访问权限。 有关适用于 Beta 版、预览版或尚未正式发布的 Azure 功能的法律条款,请参阅 适用于 Microsoft azure 预览版的补充使用条款

小窍门

有关更高级的方案(例如自定义 Role-Based 访问控制(RBAC)或与外部机密工作负荷的集成,请参阅 Azure 机密账本中的高级用户定义函数

用例

使用 Azure 机密账本 UDF,可以通过运行自定义逻辑来扩展账本的功能。 UDF 的一些常见用例包括:

  • 自定义计算和查询:根据业务逻辑运行独立的 UDF 以读取或写入任何账本应用程序表中的数据。

  • 数据验证和输入检查:使用 UDF 作为 预挂钩 在账本条目写入账本之前运行预处理作,例如清理输入数据或检查先决条件。

  • 数据扩充和智能合约:使用用户定义函数(UDF)作为后处理钩子在编写账本条目后运行后处理动作,例如,将自定义元数据添加到账本或触发后写入工作流。

编写 UDF

Azure 机密账本 UDF 是存储在账本中具有唯一 ID 的实体,包含调用 UDF 时执行的 JavaScript 代码。 本部分介绍如何编写 UDF 代码并使用 JavaScript API 来实现不同的任务。

函数结构

UDF 的代码需要导出的函数,该函数是执行时脚本的入口点。 基本 UDF 代码模板如下所示:

export function main() {
    // Your JavaScript code here
}

注释

在运行 UDF 时,可以使用参数修改 exportedFunctionName 在执行期间调用的导出入口点函数的名称。 如果未指定,则默认名称为 main.

注释

支持 Lambda 函数,但它们需要显式定义导出的函数名称,并匹配 entrypoint 函数名称。 例如:

export const main = () => { 
    // Your JavaScript code here 
};

函数参数

可以指定 UDF 接受的任何可选运行时参数。 使用 arguments 参数运行 UDF 时,可以在运行时传递参数的值。

参数始终作为字符串数组传递。 用户负责确保 UDF 代码中指定的参数与运行 UDF 时传递的参数匹配。 用户还应确保参数在运行时正确分析到预期的数据类型。

export function main(arg1, arg2) {
    // Your JavaScript code here
}

JavaScript API

UDF 的 JavaScript 代码在提供 有限 API 集的沙盒环境中执行。

可以使用所有 JavaScript 标准全局函数、对象和值 。 调用 ccf 的全局对象可用于访问 机密联盟框架(CCF) 提供的特定功能和实用工具(例如加密帮助程序函数、账本表访问器等)。 此处记录了全局对象的完整 APIccf

还可以使用全局对象访问当前请求的 context 上下文信息。 此对象提供对发起函数执行()的请求元数据和函数调用方的用户 ID(context.requestcontext.userId)的访问权限。 对于事务挂钩,集合 ID 和与写入操作相关的事务内容也分别被添加到 context 对象中(分别是 context.collectionIdcontext.contents)。

以下代码片段演示了 JavaScript API 用法的一些基本示例:

export function main(args) {
    
    // Basic instructions
    const a = 1 + 1;

    // Basic statements
    if (a > 0) {
        console.log("a is positive");
    } else {
        console.log("a is negative or zero");
    }

    // Parse the string argument as a JSON object
    JSON.parse(args);

    // Logging utilities
    console.log("Hello world");
    
    // Math utilities
    Math.random();
    
    // CCF cryptography utilities
    ccf.crypto.digest("SHA-256", ccf.strToBuf("Hello world"));
    
    // Write to a custom ledger table
    ccf.kv["public:mytable"].set(ccf.strToBuf("myKey"), ccf.strToBuf("myValue"));

    // Read from a custom ledger table
    ccf.bufToStr(ccf.kv["public:mytable"].get(ccf.strToBuf("myKey")));

    // Read from the ledger entry table
    ccf.kv["public:confidentialledger.logs"].get(ccf.strToBuf("subledger:0"));

    // Get the request metadata that originated the function execution
    const requestMetadata = context.request;
    
    // Get the collection ID and transaction content (for transaction hooks only)
    const collectionId = context.collectionId;
    const contents = context.contents;

    // Throw exceptions
    throw new Error("MyCustomError");
}

小窍门

有关如何使用账本映射存储和检索数据的详细信息,请参阅 有关 Key-Value 存储 API 的 CCF 文档

注释

UDF 不支持导入模块。 JavaScript 代码必须自包含,不能依赖于外部库或模块。 目前也不支持 Web API

管理用户定义函数 (UDF)

Azure 机密账本应用程序提供专用 CRUD API,用于创建、读取、更新和删除 UDF 实体。 UDF 安全地存储在账本中,只能访问账本应用程序。

注释

简单的用户定义的函数和 高级用户定义函数 是互斥的功能。 如果定义了高级 UDF,则无法创建或运行简单的 UDF,反之亦然。 若要在两者之间切换,请按照 UDF 概述页中的说明作。

创建或更新 UDF

PUT /app/userDefinedFunctions/myFunction
{
    "code": "export function main() { return "Hello World"; }",
}

重要

创建或更新 UDF 需要管理员角色。

获取 UDF

GET /app/userDefinedFunctions/myFunction

列出 UDF

GET /app/userDefinedFunctions

删除 UDF

DELETE /app/userDefinedFunctions/myFunction

重要

删除 UDF 需要管理员角色。

注释

删除 UDF 仅会从当前账本状态中移除此实体。 任何已删除的 UDF 都将永久保留在不可变账本历史记录中(与所有已提交事务相同)。

运行 UDF

创建后,Azure 机密账本用户可以以独立函数或与写操作关联的事务钩子的形式执行用户定义函数(UDF)。 每个 UDF 执行都在单独的运行时环境和沙盒环境中运行,这意味着 UDF 执行与其他 UDF 或其他账本操作相隔离。

可以使用可在请求正文中指定的可选属性来控制 UDF 执行。 当前支持的属性包括:

  • arguments:表示要传递给 UDF 的参数的字符串数组。 参数的传递顺序与在 UDF 代码中定义的顺序相同。 默认值为空数组。

  • exportedFunctionName:在执行期间要调用的导出函数的名称。 如果未指定,默认值为 main

  • runtimeOptions:一个对象,指定 UDF 执行的运行时选项。 可以使用以下选项:

    • max_heap_bytes:最大堆大小(以字节为单位)。 默认值为 10,485,760 (10 MB)。

    • max_stack_bytes:最大堆栈大小(以字节为单位)。 默认值为 1,048,576 (1 MB)。

    • max_execution_time_ms:最大执行时间(以毫秒为单位)。 默认值为 1000(1 秒)。

    • log_exception_details:一个布尔值,该值指定是否记录异常详细信息。 默认值是 true

    • return_exception_details:一个布尔值,该值指定是否在响应中返回异常详细信息。 默认值是 true

独立函数

可以直接使用POST /app/userDefinedFunctions/{functionId}:execute API 执行UDF。

POST /app/userDefinedFunctions/myFunction:execute
{}

请求正文可用于指定可选的执行参数,例如函数参数和 JavaScript 运行时属性。

POST /app/userDefinedFunctions/myFunction:execute
{
    "arguments": ["arg1", "arg2"],
    "exportedFunctionName": "myMainFunction",
    "runtimeOptions": {
        "max_heap_bytes": 5,
        "max_stack_bytes": 1024,
        "max_execution_time_ms": 5000,
        "log_exception_details": true,
        "return_exception_details": true
    }
}

响应指示 UDF 执行的结果(成功或失败)。 如果 UDF 成功,则响应以字符串格式(如果有)包含函数返回的值。

{
    "result": 
        {
            "returnValue": "MyReturnValue"
        }, 
    "status": "Succeeded"
}

如果 UDF 失败,响应将包含包含详细堆栈跟踪的错误消息。

{
    "error": {
        "message": "Error while executing function myFunction: Error: MyCustomError\n    at myMainFunction (myFunction)\n"
    }, 
    "status": "Failed"
}

重要

执行 UDF 需要参与者角色。

事务挂钩

UDF 也可以作为挂钩执行,在条目被写入账本之前(预挂钩)或之后(后挂钩)执行,作为账本写入 API 的一部分(POST /app/transactions)。 挂钩在写入操作的同一上下文中运行,这意味着由挂钩写入账本的所有数据都会自动包含在同一写入事务中。

写入请求的请求正文可用于指定要分别作为前置挂钩和后置挂钩执行的任何 UDF ID。

POST /app/transactions?collectionId=myCollection
{
  "contents": "myValue", 
  "preHooks": [ 
    { 
        "functionId": "myPreHook"
    } 
  ], 
  "postHooks": [ 
    { 
        "functionId": "myPostHook" 
    }
  ] 
} 

重要

挂钩必须在写入操作的请求正文中明确定义。 通常,UDF 在创建后无法自动对每次写入进行运行。

对于每个挂钩,可以指定任何可选的执行属性。 例如:

POST /app/transactions?collectionId=myCollection
{
  "contents": "myValue", 
  "preHooks": [ 
    { 
        "functionId": "myPreHook", 
        "properties": { 
            "arguments": [ 
                "arg1",
                "arg2"
            ], 
            "exportedFunctionName": "myMainFunction", 
            "runtimeOptions": { 
                "max_heap_bytes": 5,
                "max_stack_bytes": 1024,
                "max_execution_time_ms": 5000,
                "log_exception_details": true,
                "return_exception_details": true
            } 
        } 
    } 
  ], 
  "postHooks": [ 
    { 
        "functionId": "myPostHook", 
        "properties": { 
            "arguments": [ 
                "arg1"
            ], 
            "exportedFunctionName": "myMainFunction", 
            "runtimeOptions": { 
                "max_heap_bytes": 5,
                "max_stack_bytes": 1024,
                "max_execution_time_ms": 5000,
                "log_exception_details": true,
                "return_exception_details": true
            } 
        } 
    }
  ] 
} 

在请求正文中可以指定最多 5 个前置钩子和后置钩子,并且可以任意组合。 挂钩始终按请求正文中提供的顺序执行。

如果前置挂钩或后置挂钩失败,则会中止整个事务。 在这种情况下,响应包含错误消息,并显示失败原因:

{
    "error": {
        "code": "InternalError",
        "message": "Error while executing function myPreHook: Error: MyCustomError\n    at myMainFunction (myPreHook)\n",
    }
}

注释

即使多个挂钩成功,如果定义的任何前置挂钩或后置挂钩未成功运行至完成,交易仍会失败。

小窍门

UDF 可以在同一请求中作为预钩子和后钩子重复使用,并多次调用。

例子

本部分逐步讲解如何在 Azure 机密账本中使用 UDF 的一些实际示例。 对于以下示例方案,我们假设使用 Azure 机密账本为不同的银行用户存储银行交易。

上下文

若要为用户存储银行交易,可以使用现有的账本写入 API:交易的金额作为账本条目的内容,用户 ID 可以作为集合或密钥,这个集合或密钥是内容的存储位置。

POST /app/transactions?collectionId=John
{
    "contents": "10"
}

HTTP/1.1 200 OK

由于输入内容没有验证,因此可以将非数值作为内容写入。 例如,即使内容值不是数字,此请求也会成功:

POST /app/transactions?collectionId=Mark
{
    "contents": "This is not a number"
}

HTTP/1.1 200 OK

拥有数据验证的前置挂钩

为了确保事务内容始终为数字,可以创建 UDF 来检查输入内容。 以下前置挂钩检查内容值是否为数字,如果不是,则会提示错误。

PUT /app/userDefinedFunctions/validateTransaction
{
    "code": "export function main() { if (isNaN(context.contents)) { throw new Error('Contents is not a number'); } }"
}

HTTP/1.1 201 CREATED

在写入请求中使用前置钩子,可以确保输入数据符合预期的格式。 以前的请求现在按预期失败:

POST /app/transactions?collectionId=Mark
{
    "contents": "This is not a number",
    "preHooks": [
        {
            "functionId": "validateTransaction"
        }
    ]
}

HTTP/1.1 500 INTERNAL_SERVER_ERROR
{
  "error": {
    "code": "InternalError",
    "message": "Error while executing function validateTransaction: Error: Contents is not a number\n    at main (validateTransaction)\n"
  }
}

包含数值的有效请求将按预期成功:

POST /app/transactions?collectionId=Mark
{
    "contents": "30",
    "preHooks": [
        {
            "functionId": "validateTransaction"
        }
    ]
}

HTTP/1.1 200 OK

用于数据扩充的后置挂钩

当用户执行新的银行交易时,出于审核原因,我们希望在交易高于特定阈值时进行记录。 后置挂钩可用于在写入操作完成后向账本写入自定义元数据,以指示事务是否超过特定阈值。

例如,可以创建 UDF 来检查交易值,并在输入用户的自定义分类账表(payment_metadata)中为高于 50 的值编写一个虚拟消息(高值显示为“警报”,否则为“普通”)。

PUT /app/userDefinedFunctions/detectHighTransaction
{
    "code": "export function main() { let value = 'Normal'; if (context.contents > 50) { value = 'Alert' } ccf.kv['public:payment_metadata'].set(ccf.strToBuf(context.collectionId), ccf.strToBuf(value)); }"
}

HTTP/1.1 201 CREATED

成功创建 UDF 后,可以在新的写入请求中使用后置挂钩:

POST /app/transactions?collectionId=Mark
{
    "contents": "100",
    "preHooks": [
        {
            "functionId": "validateTransaction"
        }
    ],
    "postHooks": [
        {
            "functionId": "detectHighTransaction"
        }
    ]
}

HTTP/1.1 200 OK
POST /app/transactions?collectionId=John
{
    "contents": "20",
    "preHooks": [
        {
            "functionId": "validateTransaction"
        }
    ],
    "postHooks": [
        {
            "functionId": "detectHighTransaction"
        }
    ]
}

HTTP/1.1 200 OK

自定义查询的独立 UDF

若要使用后置挂钩检查写入自定义表 payment_metadata 的最新值,可以创建 UDF 来根据给定的输入用户 ID 从表中读取值:

PUT /app/userDefinedFunctions/checkPaymentMetadataTable
{
    "code": "export function main(user) { const value = ccf.kv['public:payment_metadata'].get(ccf.strToBuf(user)); if (value === undefined) { throw new Error('UnknownUser'); } return ccf.bufToStr(value); }"
}

HTTP/1.1 201 CREATED

通过直接运行 UDF,可以检查给定用户的自定义元数据表中记录的最新值。

对于最近进行大额交易的用户,UDF 将如预期返回值“Alert”。

POST /app/userDefinedFunctions/checkPaymentMetadataTable:execute
{
    "arguments": [
        "Mark"
    ]
}

HTTP/1.1 200 OK
{
  "result": {
    "returnValue": "Alert"
  },
  "status": "Succeeded"
}

对于最近交易量低的用户,UDF 将改为返回值“Normal”。

POST /app/userDefinedFunctions/checkPaymentMetadataTable:execute
{
    "arguments": [
        "John"
    ]
}

HTTP/1.1 200 OK
{
  "result": {
    "returnValue": "Normal"
  },
  "status": "Succeeded"
}

对于自定义表中没有任何条目的用户,UDF 会引发 UDF 代码中定义的错误。

POST /app/userDefinedFunctions/checkPaymentMetadataTable:execute
{
    "arguments": [
        "Jane"
    ]
}

HTTP/1.1 200 OK
{
  "error": {
    "message": "Error while executing function checkPaymentMetadataTable: Error: UnknownUser\n    at main (checkPaymentMetadataTable)\n"
  },
  "status": "Failed"
}

注意事项

  • 目前的事务挂钩仅支持通过 POST /app/transactions API 向账本中添加新条目时触发。

  • 用户定义的函数 (UDF) 和钩子始终在账本的主要副本上执行,以确保事务排序和强一致性。

  • UDF 代码执行始终封装在单个原子事务中。 如果 UDF 中的 JavaScript 逻辑完成且没有任何例外,则 UDF 中的所有作都会提交到账本。 如果引发任何异常,则会回滚所有事务。 同样,前置挂钩和后置挂钩均在其所注册的写入操作上下文中执行。 如果前置挂钩或后置挂钩失败,整个事务将被中止,并且不会向账本中添加任何条目。

  • UDF 只能访问 CCF 应用程序表,并且出于安全原因无法访问 账本的内部表和治理表 或其他 内置表 。 写入条目的账本表(public:confidentialledger.logs 用于公共账本,private:confidentialledger.logs 用于私人账本)是只读的。

  • 可为单个写入事务注册的前置挂钩和后置挂钩的最大数量为 5。

  • UDF 和挂钩执行的时间限制为 5 秒。 如果函数执行时间超过 5 秒,则中止该作并返回错误。