Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add object code deployment methods to PackagePublisher #35

Merged
merged 1 commit into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ examples_cli:
poetry run python -m examples.hello_blockchain
# poetry run python -m examples.large_package_publisher CURRENTLY BROKEN -- OUT OF GAS
poetry run python -m examples.multisig
poetry run python -m examples.object_code_deployment
poetry run python -m examples.your_coin

integration_test:
Expand Down
126 changes: 115 additions & 11 deletions aptos_sdk/package_publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
# SPDX-License-Identifier: Apache-2.0

import os
from typing import List
from enum import Enum
from typing import List, Optional

import tomli

Expand All @@ -20,6 +21,15 @@
"0xfa3911d7715238b2e3bd5b26b6a35e11ffa16cff318bc11471e84eccee8bd291"
)

# Domain separator for the code object address derivation
OBJECT_CODE_DEPLOYMENT_DOMAIN_SEPARATOR = b"aptos_framework::object_code_deployment"


class PublishMode(Enum):
ACCOUNT_DEPLOY = "ACCOUNT_DEPLOY"
OBJECT_DEPLOY = "OBJECT_DEPLOY"
OBJECT_UPGRADE = "OBJECT_UPGRADE"


class PackagePublisher:
"""A wrapper around publishing packages."""
Expand Down Expand Up @@ -51,11 +61,62 @@ async def publish_package(
)
return await self.client.submit_bcs_transaction(signed_transaction)

async def publish_package_to_object(
self, sender: Account, package_metadata: bytes, modules: List[bytes]
) -> str:
transaction_arguments = [
TransactionArgument(package_metadata, Serializer.to_bytes),
TransactionArgument(
modules, Serializer.sequence_serializer(Serializer.to_bytes)
),
]

payload = EntryFunction.natural(
"0x1::object_code_deployment",
"publish",
[],
transaction_arguments,
)

signed_transaction = await self.client.create_bcs_signed_transaction(
sender, TransactionPayload(payload)
)
return await self.client.submit_bcs_transaction(signed_transaction)

async def upgrade_package_object(
self,
sender: Account,
package_metadata: bytes,
modules: List[bytes],
object_address: AccountAddress,
) -> str:
transaction_arguments = [
TransactionArgument(package_metadata, Serializer.to_bytes),
TransactionArgument(
modules, Serializer.sequence_serializer(Serializer.to_bytes)
),
TransactionArgument(object_address, Serializer.struct),
]

payload = EntryFunction.natural(
"0x1::object_code_deployment",
"upgrade",
[],
transaction_arguments,
)

signed_transaction = await self.client.create_bcs_signed_transaction(
sender, TransactionPayload(payload)
)
return await self.client.submit_bcs_transaction(signed_transaction)

async def publish_package_in_path(
self,
sender: Account,
package_dir: str,
large_package_address: AccountAddress = MODULE_ADDRESS,
publish_mode: PublishMode = PublishMode.ACCOUNT_DEPLOY,
code_object: Optional[AccountAddress] = None,
) -> List[str]:
with open(os.path.join(package_dir, "Move.toml"), "rb") as f:
data = tomli.load(f)
Expand All @@ -76,16 +137,55 @@ async def publish_package_in_path(
metadata_path = os.path.join(package_build_dir, "package-metadata.bcs")
with open(metadata_path, "rb") as f:
metadata = f.read()
return await self.publish_package_experimental(
sender, metadata, modules, large_package_address

# If the package size is larger than a single transaction limit, use chunked publish.
if self.is_large_package(metadata, modules):
return await self.chunked_package_publish(
sender, metadata, modules, large_package_address, publish_mode
)

# If the deployment can fit into a single transaction, use the normal package publisher
if publish_mode == PublishMode.ACCOUNT_DEPLOY:
txn_hash = await self.publish_package(sender, metadata, modules)
elif publish_mode == PublishMode.OBJECT_DEPLOY:
txn_hash = await self.publish_package_to_object(sender, metadata, modules)
elif publish_mode == PublishMode.OBJECT_UPGRADE:
if code_object is None:
raise ValueError("code_object must be provided for OBJECT_UPGRADE mode")
txn_hash = await self.upgrade_package_object(
sender, metadata, modules, code_object
)
else:
raise ValueError(f"Unexpected publish mode: {publish_mode}")

return [txn_hash]

async def derive_object_address(
self, publisher_address: AccountAddress
) -> AccountAddress:
sequence_number = await self.client.account_sequence_number(publisher_address)
return self.create_object_deployment_address(
publisher_address, sequence_number + 1
)

async def publish_package_experimental(
@staticmethod
def create_object_deployment_address(
creator_address: AccountAddress, creator_sequence_number: int
) -> AccountAddress:
ser = Serializer()
ser.to_bytes(OBJECT_CODE_DEPLOYMENT_DOMAIN_SEPARATOR)
ser.u64(creator_sequence_number)
seed = ser.output()

return AccountAddress.for_named_object(creator_address, seed)

async def chunked_package_publish(
self,
sender: Account,
package_metadata: bytes,
modules: List[bytes],
large_package_address: AccountAddress = MODULE_ADDRESS,
publish_mode: PublishMode = PublishMode.ACCOUNT_DEPLOY,
) -> List[str]:
"""
Chunks the package_metadata and modules across as many transactions as necessary.
Expand All @@ -94,13 +194,6 @@ async def publish_package_experimental(
optimistic transaction batching. The batching tries to place as much data in a transaction
before moving to the chunk to the next transaction.
"""
# If this can fit into a single transaction, use the normal package publisher
total_size = len(package_metadata)
for module in modules:
total_size += len(module)
if total_size < MAX_TRANSACTION_SIZE:
txn_hash = await self.publish_package(sender, package_metadata, modules)
return [txn_hash]

# Chunk the metadata and insert it into payloads. The last chunk may be small enough
# to be placed with other data. This may also be the only chunk.
Expand Down Expand Up @@ -194,6 +287,17 @@ def create_large_package_publishing_payload(

return TransactionPayload(payload)

@staticmethod
def is_large_package(
package_metadata: bytes,
modules: List[bytes],
) -> bool:
total_size = len(package_metadata)
for module in modules:
total_size += len(module)

return total_size >= MAX_TRANSACTION_SIZE

@staticmethod
def create_chunks(data: bytes) -> List[bytes]:
chunks: List[bytes] = []
Expand Down
79 changes: 79 additions & 0 deletions examples/object_code_deployment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Copyright © Aptos Foundation
# SPDX-License-Identifier: Apache-2.0

import asyncio
import os
import sys

from aptos_sdk.account import Account
from aptos_sdk.aptos_cli_wrapper import AptosCLIWrapper
from aptos_sdk.async_client import FaucetClient, RestClient
from aptos_sdk.package_publisher import MODULE_ADDRESS, PackagePublisher, PublishMode

from .common import APTOS_CORE_PATH, FAUCET_URL, NODE_URL


async def main(package_dir):
rest_client = RestClient(NODE_URL)
faucet_client = FaucetClient(FAUCET_URL, rest_client)
package_publisher = PackagePublisher(rest_client)
alice = Account.generate()

print("\n=== Publisher Address ===")
print(f"Alice: {alice.address()}")

await faucet_client.fund_account(alice.address(), 100_000_000)

print("\n=== Initial Coin Balance ===")
alice_balance = await rest_client.account_balance(alice.address())
print(f"Alice: {alice_balance}")

# The object address is derived from publisher's address and sequence number.
code_object_address = await package_publisher.derive_object_address(alice.address())
module_name = "hello_blockchain"

print("\nCompiling package...")
if AptosCLIWrapper.does_cli_exist():
AptosCLIWrapper.compile_package(package_dir, {module_name: code_object_address})
else:
print(f"Address of the object to be created: {code_object_address}")
input(
"\nUpdate the module with the derived code object address, compile, and press enter."
)

# Deploy package to code object.
print("\n=== Object Code Deployment ===")
deploy_txn_hash = await package_publisher.publish_package_in_path(
alice, package_dir, MODULE_ADDRESS, publish_mode=PublishMode.OBJECT_DEPLOY
)

print(f"Tx submitted: {deploy_txn_hash[0]}")
await rest_client.wait_for_transaction(deploy_txn_hash[0])
print(f"Package deployed to object {code_object_address}")

print("\n=== Object Code Upgrade ===")
upgrade_txn_hash = await package_publisher.publish_package_in_path(
alice,
package_dir,
MODULE_ADDRESS,
publish_mode=PublishMode.OBJECT_UPGRADE,
code_object=code_object_address,
)
print(f"Tx submitted: {upgrade_txn_hash[0]}")
await rest_client.wait_for_transaction(upgrade_txn_hash[0])
print(f"Package in object {code_object_address} upgraded")
await rest_client.close()


if __name__ == "__main__":
if len(sys.argv) == 2:
package_dir = sys.argv[1]
else:
package_dir = os.path.join(
APTOS_CORE_PATH,
"aptos-move",
"move-examples",
"hello_blockchain",
)

asyncio.run(main(package_dir))
Loading