Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
An account on the Endless blockchain represents access control over a set of assets including on-chain currency and NFTs. In Endless, these assets are represented by a Move language primitive known as a resource that emphasizes both access control and scarcity.
Each account on the Endless blockchain is identified by a 32-byte. More details, refer:
Account Address FormatDifferent from other blockchains where accounts and addresses are implicit, accounts on Endless are explicit and need to be created before they can execute transactions.
The account can be created explicitly or implicitly by transferring Endless Coin(EDS) there. See the Creating an account section for more details.
In a way, this is similar to other chains where an address needs to be sent funds for gas before it can send transactions.
Explicit accounts support "native" Multisig feature via Authentication key; accounts on Endless support k-of-n multisig using Ed25519 signature schemes when constructing the 32-byte authentication key.
There are three types of accounts on Endless:
Standard account - This is a typical account corresponding to an address with a corresponding pair of public/private keys.
Resource account - An autonomous account without a corresponding private key used by developers to store resources or publish modules on-chain.
Object - A complex set of resources stored within a single address representing a single entity.
When a user requests to create an account, for example by using the Endless SDK, the following steps are executed:
Generate a new private key, public key pair with Ed25519 authentication scheme.
Combine the public key with the public keyβs authentication scheme to generate a 32-byte authentication key and the account address.
The user should use the private key for signing the transactions associated with this account.
The sequence number for an account indicates the number of transactions that have been submitted and committed on-chain from that account. Committed transactions either execute with the resulting state changes committed to the blockchain or abort wherein state changes are discarded and only the transaction is stored.
Every transaction submitted must contain a unique sequence number for the given senderβs account. When the Endless blockchain processes the transaction, it looks at the sequence number in the transaction and compares it with the sequence number in the on-chain account. The transaction is processed only if the sequence number is equal to or larger than the current sequence number. Transactions are only forwarded to other mempools or executed if there is a contiguous series of transactions from the current sequence number. Execution rejects out of order sequence numbers preventing replay attacks of older transactions and guarantees ordering of future transactions.
The initial account address is equal to authentication key during account creation. However, the authentication key content may subsequently change, from one authentication key to a list of authentication keys, for example when you upgrade an account to , adding more accounts as individual signer.
The Endless blockchain supports the following authentication schemes:
Ed25519
K-of-N multi-signatures
The Endless blockchain defaults to Ed25519 signature.
To generate an authentication key and the account address for an Ed25519 signature:
Generate a key-pair: Generate a fresh key-pair (privkey_A, pubkey_A). The Endless blockchain uses the PureEdDSA scheme over the Ed25519 curve, as defined in .
Derive a 32-byte authentication key Derive a 32-byte authentication key from the pubkey_A:
where | denotes concatenation. The 0x00 is the 1-byte single-signature scheme identifier.
Use this initial authentication key as the permanent account address.
With K-of-N multisig authentication, there are a total of N signers for the account, and at least K of those N signatures must be used to authenticate a transaction.
To generate a K-of-N multisig accountβs authentication key and the account address, we may choose via Endless , or Endless CLI.
Here we use Endless CLI to demonstrates:
Generate key-pairs: Generate N ed25519 public keys p_1, β¦, p_n.
Decide on the value of K, the threshold number of signatures needed for authenticating the transaction.
Fund these accounts to ensure accounts are created on chain.
Here we choose p_1 as multisig account, and adding left N-1 accounts into p_1
repeat to add all accounts into p_1's authentication key list. Now p_1 account is Multisig account, and Threshold is 1-of-N
Update Threshold K of p_1.
Now approve any transaction on behaves of p_1 account, K signatures is required at least.
The state of each account comprises both the code (Move modules) and the data (Move resources). An account may contain an arbitrary number of Move modules and Move resources:
Move modules: Move modules contain code, for example, type and procedure declarations; but they do not contain data. A Move module encodes the rules for updating the Endless blockchainβs global state.
Move resources: Move resources contain data but no code. Every resource value has a type that is declared in a module published on the Endless blockchain.
The sender of a transaction is represented by a signer. When a function in a Move module takes signer as an argument, the Endless Move VM translates the identity of the account that signed the transaction into a signer in a Move module entry point. See the below Move example code with signer in the initialize and withdraw functions. When a signer is not specified in a function, for example, the below deposit function, then no signer-based access controls will be provided for this function:
coin.move
auth_key = sha3-256(pubkey_A | 0x00)$ endless account add-authentication-key \
--private-key ${p1-private-key} \
--new-private-key ${p2-private-key} $ endless account update-signature-required \
-n ${K} \
--private-key ${anyone-in-the-list-of-private-key} module Test::Coin {
struct Coin has key { amount: u64 }
public fun initialize(account: &signer) {
move_to(account, Coin { amount: 1000 });
}
public fun withdraw(account: &signer, amount: u64): Coin acquires Coin {
let balance = &mut borrow_global_mut<Coin>(Signer::address_of(account)).amount;
*balance = *balance - amount;
Coin { amount }
}
public fun deposit(account: address, coin: Coin) acquires Coin {
let balance = &mut borrow_global_mut<Coin>(account).amount;
*balance = *balance + coin.amount;
Coin { amount: _ } = coin;
}
}The Endless blockchain account addresses use Base58 encoding and have the following characteristics:
The length of most regular account addresses is between 43 and 44 characters.
You can quickly differentiate between accounts by checking the first and last few characters of the address.
Vanity addresses are supported.
For example, 5SHvmLEaSr76dsKy4XLR5vMht14PRuLzJFx6svJzqorP is an Endless account address.
Currently, addresses in Base58 encoding format are fully supported in both the command line and the block explorer.
Below is an example of how account transactions are displayed on the :
On the backend, Endless addresses are stored as 32-byte arrays. In some cases, you may see this format in the command line or the browser. For instance, the Endless chain has two system account addresses, represented in hexadecimal format:
0x0000000000000000000000000000000000000001, which can be simplified to 0x1. Its role is as the system account, responsible for executing system contracts.
0x0000000000000000000000000000000000000004, which can be simplified to 0x4. Its role is as the system token account, responsible for managing "Tokens" and "NFTs."
To convert between Base58 format and byte format, you can use the following methods:
Online Tools: Search for keywords such as "base58 encoder decoder"
Python Code (requires the base58 library):
Transaction simulation, often referred to as "dry runs," is a method used to predict the outcome of a blockchain transaction before broadcasting it on-chain. This process is especially useful for smart contracts and decentralized applications (dApps) to ensure transactions perform as intended.
Transaction simulation plays a critical role in enhancing security and mitigating malicious activity in the Web3 ecosystem. By simulating a transaction, you can inspect it for unexpected or harmful behavior prior to execution. For instance, when interacting with a new dApp or calling a smart contract function, a simulation may reveal unauthorized token transfers or wallet access that were neither disclosed nor expected. This capability is invaluable for identifying and avoiding scams or malicious contracts designed to compromise user funds.
Sponsored transactions on the Endless chain refer to transactions where the gas fees are paid by the contract invoked by the transaction.
On the Endless blockchain, executing transactions requires a fee, also known as a gas fee, which is typically paid in the native token of the Endless chain (EDS). Gas fees are paid to the validators of the Endless chain to ensure network security. However, for new users, especially those transitioning from Web 2.0, this requirement can increase the barrier to entry.
Sponsored transactions lower the difficulty of interacting with contracts for users, particularly beginners. By leveraging sponsored transaction technology, contracts can make it easier for users to interact while ensuring the contract itself covers the gas fees, thereby maintaining stable chain operations.
import base58
def encode_base58(data: bytes) -> str:
"""Encode bytes into a Base58 string"""
return base58.b58encode(data).decode()
def decode_base58(data: str) -> bytes:
"""Decode a Base58 string into bytes"""
return base58.b58decode(data)Scenario 1 (under construction)
Scenario 2 (under construction)
Sponsored transactions are implemented at the contract level. Any contract function intending to offer this feature can include the sponsored keyword before the function declaration. This instructs the VM to mark the function as a "sponsored contract function."
Below is an example:
When a transaction invokes the fund function, the gas fees for that transaction will be paid by the contract. Ensure the contract account has sufficient funds to cover these fees; otherwise, the user will encounter an error indicating insufficient tokens to pay the gas fees when submitting the transaction.
Although sponsored functions are easy to implement, consider the following points before using them:
Access Control
External Calls Only
Access Control Determine whether the contract function should be accessible to all users. If the function is intended to sponsor only specific users, implement user checks within the contract to exclude unwanted users. Failing to do so may result in unnecessary gas fee expenses.
External Calls Only Mark the contract function as an entry function to allow only direct user-initiated transactions to call it. Prevent external contracts from invoking the sponsored transaction function to guard against potential attacks.
entry sponsored fun fund(dst_addr: &signer) {
...
}Designed Use Cases
Understanding Transaction Outcomes Blockchain transactions, particularly those involving complex smart contracts or DeFi protocols, often yield outcomes that aren't immediately apparent. Simulations clarify the consequences of executing a transaction, providing users with a detailed preview of what they are signing. For instance, in DAO voting or executing intricate financial strategies, transaction simulation can illuminate the implications of a decision, leading to safer and better-informed choices.
Gas Fee Estimation and Cost Savings Ethereum transactions incur gas fees, which can vary significantly based on network congestion and transaction complexity. Simulating transactions provides an accurate gas estimate, helping users avoid issues like overpaying or underpaying gas fees.
Overpaying unnecessarily increases transaction costs. Underpaying risks leaving transactions stuck in the mempool. By simulating a transaction beforehand, users can set appropriate gas limits and prices, minimizing costs and avoiding failures.
While transaction simulation is a powerful tool, it is inherently limitedβit only predicts outcomes based on current blockchain states, without actual execution on-chain. This limitation can be exploited by attackers in what is known as transaction simulation spoofing.
In a typical spoofing scenario:
The attacker lures the victim to a malicious website disguised as a legitimate platform.
transaction simulates outputs, such as a "Claim" function, which deceptively shows that the user will receive a small amount of ETH.
Trusting the wallet's simulation result, the victim signs the transaction, unknowingly allowing the attacker to drain their wallet.
Between the simulation and actual execution, the attacker alters the on-chain contract state, causing the approved transaction to perform a different, malicious action.
A victim signed a fraudulent transaction 30 seconds after an on-chain state change, resulting in the loss of 143.35 ETH.
Phishing transaction hash
This incident highlights the limitations of transaction simulations: on Ethereum and many other blockchains, simulation is not equivalent to "What You See Is What You Sign" (WYSIWYS).
To address the limitations of transaction simulations, Endless Wallet introduces the Safe Transaction feature, equipped with a Safety Switch.
When enabled, the Safety Switch ensures that the simulation result (transaction output) is embedded within the transaction itself.
Here's how it works:
The Safety Switch hashes the user's balance change(specifically refers to user's balance deduction) resulting from the transaction and embeds the hash into a transaction field.
Nodes executing the transaction validate the user's actual balance change by recalculating the hash and comparing it to the embedded value.
If the two hashes do not match, the transaction is reverted.
By default enabling the Safety Switch, users can make their transactions deterministic, effectively mitigating the risk of simulation spoofing.
Any decentralized application (Dapp) that may submit non-deterministic transactions, such as those involving dynamic deduction of the sender's account balance, should be carefully considered. Examples include:
Liquidity pool swaps in Endless Swap, where the results depend on real-time asset ratios.
Any contract interaction that leads to variable deductions from the sender's transaction balance.
It is crucial to address the potential impact of the "Safety Transaction" feature, which may block transaction submissions. Possible solutions include:
Remind the end user to disable the "Safety Transaction" feature when submitting transactions via the wallet.
Alternatively, modify the program logic to adopt a "deterministic transaction" design.


On the Endless chain, all types of tokens adhere to the FungibleAsset(FA) standard. This concept is equivalent to "fungible tokens" on other blockchains, such as Ethereum. For simplicity, in the following sections, "token" and "asset" are used interchangeably.
In simple terms, every token corresponds to:
Token Metadata
FungibleStoreοΌthe storage location for a specific account's token balance:
FungibleAsset(FA), the memory representation of a fungible token in VM enviroment:
FungibleAsset's maximum_supply and FungibleStore's amount are all u128 type.
FungibleAsset only exists in VM memory; at the end of transaction, any FA needs be merged into FungibleStore of corrsponding Account, otherwise FA is burned or dropped.
Endless provides move functions to handle token operations, such as:
public entry fun transfer<T: key>(sender: & signer, from: Object<T>, to: Object<T>, amount: u128)
public fun withdraw<T: key>(owner: & signer, store: Object<T>, amount: u64): FungibleAsset
public fun deposit<T: key>(store: Object<T>, fa: FungibleAsset)
These functions enable token minting, transferring, and burning between accounts.
The metadata for the native token EDS is as follows:
The smallest denomination of EDS is Vein; 1 EDS is equivalent to veins.
Assume Alice has an account on the Endless Chain but does not currently hold any EDS tokens.
If Bob transfers 1 EDS to Alice, the transaction will involve the following steps:
Withdraw
Deduct 1 EDS from Bob's account and create a corresponding FungibleAsset with an amount of 1 EDS.
CreateFungibleStore
Check if Alice's account has an associated storage location for FungibleStore. If not, create a new storage location and link it to Alice's account.
For more details on system functions related to EDS, refer to the
Due to the comparatively massive volumes sold during early-stage rounds, projects may encounter extreme selling pressure after the IDO and after the token has been posted on Centralized Exchange β CEX / Decentralized Exchange β DEX. It significantly lowers the price of tokens by increasing the total amount of tokens in circulation.
For the Token Economy to stay sustainable & effective, the majority of tokens must be kept by investors rather than being sold on the market. Members can stop the value of its token from sinking by locking up tokens. Additionally, it prevents team members from selling their tokens as soon as trade begins, safeguarding holdersβ interests.
Endless's locking_coin_ex
struct Metadata has key {
/// Token name, e.g., "US Dollar Tether"
name: String,
/// More commonly used token symbol, e.g., "USDT"
symbol: String,
/// Token precision, e.g., EDS or Ether
decimals: u8,
/// Token icon URI
icon_uri: String,
/// Project URI
project_uri: String,
}public fun mint_to<T: key>( ref: & MintRef, store: Object<T>, amount: u128)public fun burn_from<T: key>( ref: & BurnRef, store: Object<T>, amount: u128)
...
Call the system function deposit to store the token in Alice's FungibleStore.
The contract allows any user to:
Lock any amount of any fungible token, including native EDS.
Specify the recipient account for unlocked tokens.
Define a two-stage release plan:
Specify a portion of tokens for immediate release at a designated epoch.
Release the remaining tokens linearly over a stipulated epoch interval.
The recipient invokes the do_claim function to fetch the released asset.
The following are the main API interfaces provided by this contract:
total_locks(token_address: address): u128
Function: Query the total locked amount for the specified token address.
Parameters: token_address - The token address.
Return value: Total locked amount.
public fun get_all_stakers(token_address: address): vector<address>
Function: Query all staker addresses for the specified token address.
Parameters: token_address - The token address.
Return value: List of staker addresses.
public fun staking_amount(token_address: address, recipient: address): u128
Function: Query the staking amount for the specified staker at the specified token address.
Parameters: token_address - The token address, recipient - The staker address.
Return value: Staking amount.
the structure contains receiver's address when release, and next release plan(amount and related epoch)
public fun get_all_stakers_unlock_info(token_address: address): vector<UnlockInfo>
Function: Query the unlock information for all stakers at the specified token address.
Parameters: token_address - The token address.
Return value: List of unlock information.
public fun get_unlock_info(token_address: address, sender: address): UnlockInfo
Function: Query the unlock information for the specified staker at the specified token address.
Parameters: token_address - The token address, sender - The staker address.
Return value: Unlock information.
public entry fun add_locking_plan_from_unlocked_balance(sender: &signer, token_address: address, receiver: address, total_coins: u128, first_unlock_bps: u64, first_unlock_epoch: u64, stable_unlock_interval: u64, stable_unlock_periods: u64)
Function: Add a locking plan from the unlocked balance.
Parameters:
sender - The signer,
token_address - The token address,
receiver - The receiver address,
total_coins - The total locked amount,
first_unlock_bps - The first unlock percentage,
first_unlock_epoch - The first unlock epoch,
stable_unlock_interval - The stable unlock interval,
stable_unlock_periods - The number of stable unlock periods.
public entry fun add_locking_plan(sender: &signer, token_address: address, receiver: address, total_coins: u128, first_unlock_bps: u64, first_unlock_epoch: u64, stable_unlock_interval: u64, stable_unlock_periods: u64)
Function: Add a locking plan.
Parameters:
sender - The signer,
token_address - The token address,
receiver - The receiver address,
total_coins - The total locked amount,
first_unlock_bps - The first unlock percentage,
first_unlock_epoch - The first unlock epoch,
stable_unlock_interval - The stable unlock interval,
stable_unlock_periods - The number of stable unlock periods.
public entry fun claim(sender: &signer, token_address: address, amount: u128)
Function: Claim unlocked tokens.
Parameters:
sender - The signer,
token_address - The token address,
amount - The claim amount.

struct FungibleStore has key {
/// Address of the metadata object
metadata: Object<Metadata>,
/// Token balance
balance: Aggregator<u128>,
/// Freeze option
frozen: bool,
}struct FungibleAsset {
// Associated token metadata
metadata: Object<Metadata>,
// Token amount
amount: u128,
}name: string::utf8(b"Endless Coin"),
symbol: string::utf8(EDS_SYMBOL)
decimals: 8,
icon_uri: "https://www.endless.link/eds-icon.svg"
project_uri: "https://www.endless.link"struct UnlockInfo has drop {
address: address,
unlocked: u128,
unlock_list: vector<UnlockAt>,
}With K-of-N multisig authentication, there are a total of N signers for the account, and at least K of those N signatures must be used to authenticate a transaction.
Endless support two method of MultiSig:
On-Chain K-of-N multisig
Off-Chain K-of-N multisig
Here we describe the operations the On-Chain K-of-N multisig and compares with Off-Chain at the end.
Install your preferred SDK from the below list:
TypeScript SDK
Clone the endless-ts-sdk repo and build it:
Navigate to the Typescript examples directory:
Install the necessary dependencies:
Run the example:
First, we will generate accounts for 4 owner accounts and fund them:
First we invoke view function 0x1::multisig_account::get_next_multisig_account_address to get multisig account address
The Multisig Account addresss is derived from owner1 account address.
We build a transaction to create a new Multisig Account, via invoke "0x1::multisig_account::create_with_owners()" and append owner2
On-Chain multisig implement versatile MultiSig based on Move module, which could expand to support different key schemas in one trasaction, and more expandable features, eg. adding Weight to each account to support Weighted-Threashold multisig.
On-Chain multisig account is created on "0x1::multisig_account" module, and dedicated multisig account. it means we cannot convert any pre-exists account with Ed25519 keypair, into an multisig account.
any k-N multisig transaction, go through K transaction committment, K-1 for seperate approve of each owner, 1 transaction is for any owner execute the transaction. the whole process is gas consumption, compared with Off-Chain multisig.
Off-Chain is built on the Endless Account authentication key and off-chain multisig service(Dapp). With well-designed Dapp user interface, users can easily collaborate to sign transactions, while the Dapp handles transaction submission to the Endless network, delivering a smoother operational experience.
Compared to on-chain multisig, off-chain multisig significantly reduces gas consumption.
Due to the structural invariance of Endless Account, off-chain multisig is less scalable and less versatile.
For more details, please refer to the following resources:
: A tutorial demonstrating off-chain multisig.
owner3Thresholdoutput like below:
Any operation on multisig account is done via interact with "0x1::multisig_account" move module by transaction, eg:
create and submit a new transfer EDS multisig transaction
vote Appprove for this transaction
vote Reject for this transaction
execute the transfer EDS multisig transaction
we create a new EDS transfer transaction, from multisig account to recipient, detailed step as below:
generate transaction payload
invoke "0x1::multisig_account::create_transaction" and pass serialized data of payload as input parameter
any owner sign and submit the transaction
Threshold of multisig is 2, means any transaction, at least 2 owners vote for "Approve" to make tranaction validate. eg:
if 2 owners vote for a transcation, one for "Approve", one for "Reject", the transaction execute successfully.
if 3 owners vote for a transcation, two for "Approve", one for "Reject", the transaction execute with failure.
simular output as below:
vote count As shown in the code above, owner1 voted in favor, owner3 voted against.
owner2 executed the transaction, effectively casting a vote in favor.
The vote count is 2 in favor, and this transaction can be executed, for the multi-signature governance module
The transfer transaction fails due to multisig account balance is zero.
0x1::multisig_account module provide owner management functions as add_owner and remove_owner.
we re-use the same process flow: owner1 voted in favor, owner3 voted against, and owner2 executed the transaction
output as below:
We also can modify multisign threshold. At first we set threshold to 2 when create multisign account. now we set threshold to 3
After re-set threshold, we invoke view function num_signatures_required with paramter of multisigAddress to fetch updated threashold
const rejectAndApprove = async (aprroveOwner: Account, rejectOwner: Account, transactionId: number): Promise<void> => {
const rejectTx = await endless.transaction.build.simple({
sender: aprroveOwner.accountAddress,
data: {
function: "0x1::multisig_account::reject_transaction",
functionArguments: [multisigAddress, transactionId],
},
});
const rejectSenderAuthenticator = endless.transaction.sign({ signer: aprroveOwner, transaction: rejectTx });
const rejectTxResponse = await endless.transaction.submit.simple({
senderAuthenticator: rejectSenderAuthenticator,
transaction: rejectTx,
});
await endless.waitForTransaction({ transactionHash: rejectTxResponse.hash });
console.log("reject_transaction:", scan(rejectTxResponse.hash));
const approveTx = await endless.transaction.build.simple({
sender: rejectOwner.accountAddress,
data: {
function: "0x1::multisig_account::approve_transaction",
functionArguments: [multisigAddress, transactionId],
},
});
const approveSenderAuthenticator = endless.transaction.sign({ signer: rejectOwner, transaction: approveTx });
const approveTxResponse = await endless.transaction.submit.simple({
senderAuthenticator: approveSenderAuthenticator,
transaction: approveTx,
});
await endless.waitForTransaction({ transactionHash: approveTxResponse.hash });
console.log("approve_transaction:", scan(approveTxResponse.hash));
};transfer but fail:
Creating a multisig transaction to transfer coins...
create_transaction: https://scan.endless.link/txn/9hamxKBTUEVvKzeA94oxx8aLq62rZJ9AY6qaPaV8WNqL
reject_transaction: https://scan.endless.link/txn/cEpCKXL2SwTiJr6fL1hNQwvTACY1dh5ZGVWSqbLRPMR
approve_transaction: https://scan.endless.link/txn/2YZPh5THyvaazUN7RjYjV3z3kC8GhFWZ6hAYxBrpt4Qfconst executeMultiSigTransferTransaction = async () => {
const rawTransaction = await generateRawTransaction({
endlessConfig: config,
sender: owner2.accountAddress,
payload: transactionPayload,
});
const transaction = new SimpleTransaction(rawTransaction);
const owner2Authenticator = endless.transaction.sign({ signer: owner2, transaction });
const transferTransactionReponse = await endless.transaction.submit.simple({
senderAuthenticator: owner2Authenticator,
transaction,
});
await endless.waitForTransaction({ transactionHash: transferTransactionReponse.hash });
console.log("executeMultiSigTransferTransaction:", scan(transferTransactionReponse.hash));
};const createAddingAnOwnerToMultiSigAccountTransaction = async () => {
// Generate a transaction payload as it is one of the input arguments create_transaction expects
const addOwnerTransactionPayload = await generateTransactionPayload({
multisigAddress,
function: "0x1::multisig_account::add_owner",
functionArguments: [owner4.accountAddress],
endlessConfig: config,
});
// Build create_transaction transaction
const createAddOwnerTransaction = await endless.transaction.build.simple({
sender: owner2.accountAddress,
data: {
function: "0x1::multisig_account::create_transaction",
functionArguments: [multisigAddress, addOwnerTransactionPayload.multiSig.transaction_payload.bcsToBytes()],
},
});
// Owner 2 signs the transaction
const createAddOwnerTxAuthenticator = endless.transaction.sign({
signer: owner2,
transaction: createAddOwnerTransaction,
});
// Submit the transaction to chain
const createAddOwnerTxResponse = await endless.transaction.submit.simple({
senderAuthenticator: createAddOwnerTxAuthenticator,
transaction: createAddOwnerTransaction,
});
await endless.waitForTransaction({ transactionHash: createAddOwnerTxResponse.hash });
console.log("create_transaction:", scan(createAddOwnerTxResponse.hash));
};const executeAddingAnOwnerToMultiSigAccountTransaction = async () => {
const multisigTxExecution3 = await generateRawTransaction({
endlessConfig: config,
sender: owner2.accountAddress,
payload: new TransactionPayloadMultiSig(new MultiSig(AccountAddress.fromString(multisigAddress))),
});
const transaction = new SimpleTransaction(multisigTxExecution3);
const owner2Authenticator3 = endless.transaction.sign({
signer: owner2,
transaction,
});
const multisigTxExecution3Reponse = await endless.transaction.submit.simple({
senderAuthenticator: owner2Authenticator3,
transaction,
});
await endless.waitForTransaction({ transactionHash: multisigTxExecution3Reponse.hash });
console.log("Execution:", scan(multisigTxExecution3Reponse.hash));
};const createChangeSignatureThresholdTransaction = async () => {
const changeSigThresholdPayload = await generateTransactionPayload({
multisigAddress,
function: "0x1::multisig_account::update_signatures_required",
functionArguments: [3],
endlessConfig: config,
});
// Build create_transaction transaction
const changeSigThresholdTx = await endless.transaction.build.simple({
sender: owner2.accountAddress,
data: {
function: "0x1::multisig_account::create_transaction",
functionArguments: [multisigAddress, changeSigThresholdPayload.multiSig.transaction_payload.bcsToBytes()],
},
});
// Owner 2 signs the transaction
const changeSigThresholdAuthenticator = endless.transaction.sign({
signer: owner2,
transaction: changeSigThresholdTx,
});
// Submit the transaction to chain
const changeSigThresholdResponse = await endless.transaction.submit.simple({
senderAuthenticator: changeSigThresholdAuthenticator,
transaction: changeSigThresholdTx,
});
await endless.waitForTransaction({ transactionHash: changeSigThresholdResponse.hash });
console.log("changeSigThreshold:", scan(changeSigThresholdResponse.hash));
};const getSignatureThreshold = async (): Promise<void> => {
const multisigAccountResource = await endless.getAccountResource<{ num_signatures_required: number }>({
accountAddress: multisigAddress,
resourceType: "0x1::multisig_account::MultisigAccount",
});
console.log("Signature Threshold:", multisigAccountResource.num_signatures_required);
};Threshold:
changeSigThreshold: https://scan.endless.link/txn/CNLtLWBVBSS4wVPtvCiF5NQ4R55HEC883D2KpDqdcsrs
reject_transaction: https://scan.endless.link/txn/C8rqvfJFBRjZxeUypqmX14qgUh8YXKHubh7YJmW3Phmk
approve_transaction: https://scan.endless.link/txn/9BtopY8JLjHnMr1JNsRsjGDa5M6boUiPmtE8jGpQXPQg
changeSigThreshold Execution: https://scan.endless.link/txn/8MUyc1d17YPSGtjyyCyRRyAr5FqcgNnRA8y9VDk8dwhC
Signature Threshold: 3
Multisig setup and transactions complete.git clone https://github.com/endless-labs/endless-ts-sdk.git
cd endless-ts-sdk
pnpm install
pnpm buildcd examples/typescript/endlesspnpm installpnpm run onchain_multisigowner1 balance: 1000000000
owner2 balance: 1000000000
owner3 balance: 1000000000
owner4 balance: 1000000000const payload: InputViewFunctionData = {
function: "0x1::multisig_account::get_next_multisig_account_address",
functionArguments: [owner1.accountAddress.toString()],
};
[multisigAddress] = await endless.view<[string]>({ payload });const createMultisig = await endless.transaction.build.simple({
sender: owner1.accountAddress,
data: {
function: "0x1::multisig_account::create_with_owners",
functionArguments: [
[owner2.accountAddress, owner3.accountAddress],
2,
["Example"],
[new MoveString("SDK").bcsToBytes()],
],
},
});
const owner1Authenticator = endless.transaction.sign({ signer: owner1, transaction: createMultisig });
const res = await endless.transaction.submit.simple({
senderAuthenticator: owner1Authenticator,
transaction: createMultisig,
});create_with_owners: https://scan.endless.link/txn/D5jRQVcV8B67UpFjjF74XAXyT7xu6uXbQD9WxJJG7enu
Multisig Account Address: FRPXTpbeaWqALuqLrGMiypiN9MVzmh1NAG7hn4oQTd3yconst createMultiSigTransferTransaction = async () => {
console.log("Creating a multisig transaction to transfer coins...");
transactionPayload = await generateTransactionPayload({
multisigAddress,
function: "0x1::endless_account::transfer",
functionArguments: [recipient.accountAddress, 1_000_000],
endlessConfig: config,
});
// Build create_transaction transaction
const createMultisigTx = await endless.transaction.build.simple({
sender: owner2.accountAddress,
data: {
function: "0x1::multisig_account::create_transaction",
functionArguments: [multisigAddress, transactionPayload.multiSig.transaction_payload.bcsToBytes()],
},
});
// Owner 2 signs the transaction
const createMultisigTxAuthenticator = endless.transaction.sign({ signer: owner2, transaction: createMultisigTx });
// Submit the transaction to chain
const createMultisigTxResponse = await endless.transaction.submit.simple({
senderAuthenticator: createMultisigTxAuthenticator,
transaction: createMultisigTx,
});
await endless.waitForTransaction({ transactionHash: createMultisigTxResponse.hash });
console.log("create_transaction:", scan(createMultisigTxResponse.hash));
};executeMultiSigTransferTransaction: https://scan.endless.link/txn/DfA8L2PC3zP3WLGTBaXQRMrg39sfqcoDrXTpGzE6zupF
recipient balance: 0adding an owner:
create_transaction: https://scan.endless.link/txn/Ear1cyydwSbYm1g7LAXPFJkSzGCgNAVEDoR1wvR3anDD
reject_transaction: https://scan.endless.link/txn/AdwWLqhdmSsQzkHBHYsGySFatdSVPGjzsigYrKRSebL9
approve_transaction: https://scan.endless.link/txn/4mgS4uRf2FBvBeyMATPLkXvW25gGHnEH6HftiASkfcK5
Execution: https://scan.endless.link/txn/B1VbZ3gVxo9n5oBevN4sTQzEnovrEn1SnSCMayWCRP8g
Number of Owners: 4Randomization is an important feature in blockchain. The application of randomization results, such as generated random numbers, can be achieved similarly to, e.g., the election of block nodes, while trusted random numbers can provide support for numerous DApps, such as lotto, games, etc.
In many other chains, the implementation of trusted random number generation requires the use of off-chain random number generation services, such as Ethereum, which requires off-chain beacons such as Chainlink or drand to provide external randomness.
The disadvantages of relying on external beacons are that the randomness is too expensive or too slow to be generated for DApps that require higher speed random numbers; in addition, the external randomness must be sent to the contract through transactions, which complicates DApp development and user interaction.
If a sequence is to be identified as random, it has to possess the following qualities:
Unpredictable β The result must be unknowable ahead of time.
Unbiased β Each outcome must be equally possible.
Provable β The result must be independently verifiable.
Tamper-proof β The process of generating randomness must be resistant to manipulation by any entity.
Here we discuss threshold VUF with an idealized key generation algorithm. The advantages of this approach, rather than immediately considering VUF keying using a DKG, is that it lets us focus on the VUF security and avoid any nuances due to the key generation phase.
An -threshold VUF scheme for a finite message space is a tuple of polynomial time computable algorithms with the following properties:
Setup.
(public parameter) consists of a generator and a random generator . Let be the weights of the participants, be the total weights, and be the threshold.
KeyGen, , .
The key generation algorithm takes the public parameters then outputs:
Unpredictable depends on below:
outcomes aggregated signature, which is the income of VUF output. The procedure takes place in every block's 1st transaction (Block Metadata transaction), which means not any User or Validator could predict randomness in User Transaction of the current block.
Provable depends on below:
Endless PVSS procedure, described as below:
Public Parameter Setup
define groups and , generator , in group and in group ; as numbers of validators, threshold
denote pp (public parameter) = , the following functions will take pp as default input parameter.
The Endless chain has built-in provision for trusted random number generation, which can better enhance the security of services based on this type of service. At the implementation level, Endless utilizes the Weighted Publicly Verifiable Secret Sharing (wPVSS) algorithm, which can be run efficiently by each verifying node and which is aggregatable to help reduce communication overhead. Meanwhile, there is a communication-efficient aggregatable weighted distributed key generation (wDKG) generation protocol on Endless. Finally, Endless has a novel Weighted Verifiable Random Function (wVRF), which the verifying node evaluates each round, with a constant amount of communication per verifying node, rather than a linear relationship with the amount pledged by the verifier.
Endless framework module provide a series of function randomness APIs:
randomness::u8_integer() returns a u8 random number, evenly samples an 8-bit from Endless Randomness
randomness::u16_integer()
...
randomness::u8_range(min_incl: u8, max_excl: u8)
By repeatedly calling these functions, multiple objects can be safely sampled to generate multiple "unbiased" random numbers.
Endless Roll
Here we build a lottery system and pick a random winner from n participants. In the centralized world with a trusted server, the roll algorithm is the backend simply calls a random integer sampling function (random.randint(0, n-1) in Python, or Math.floor(Math.random() * n) in JS).
In Endless chain, we build a lottery move module:
let winner_idx = endless_framework::randomness::u64_range(0, n); is the randomness API call that returns a u64 integer in range [0, n) uniformly at random.
#[randomness] enables the move compiler to check the outermost of the call stack of decide_winner is private.
Functions using randomness should be defined with private, to avoid Test and Abort attacks. decide_winner is an entry function, instead of public entry.
Test and Abort AttackIf decide_winner() were accidentally marked public, malicious players can deploy their own contract to:
call decide_winner();
read the lottery result (assuming the lottery module has some getter functions for the result);
abort if the result is not in their favor. By repeatedly calling their own contract until a txn succeeds, malicious users can bias the uniform distribution of the winner (dApp developerβs initial design).
Under Gas AttackUsers should also put prevention of Undergassed Attack into security considerations when writing business logic.
Imagine such a dApp. It defines a private entry function for a user to:
toss a coin (gas cost: 9), then
get a reward (gas cost: 10) if coin=1, or do some cleanup (gas cost: 100) otherwise.
A malicious user can control its account balance, so it covers at most 108 gas units (or set transaction parameter max_gas=108), and the cleanup branch (total gas cost: 110) will always abort with an out-of-gas error. The user then repeatedly calls the entry function until it gets the reward.
Here are some ideas of how to prevent undergassing attacks generally:
Make your entry function gas independent from the randomness outcome. The simplest example is to not βactβ on the randomness outcome, i.e., read it and store it for later. Note that calling any other functions can have variable gas costs. For example, when calling randomness to decide which player should win, and then depositing the winnings to the winner might seem like a fixed gas cost. But, 0x1::coin::transfer / 0x1::fungible_asset::transfer can have a variable cost based on the userβs on-chain state.
If your dApp involves a trusted admin/admin group, only allow the trusted to execute randomness transactions (i.e., require an admin signer).
Make the path that is most beneficial have the highest gas (as the attacker can only abort paths with gas above a threshold he chooses). NOTE: that this can be tricky to get right, and the gas schedule can change, and is even harder to get right when there are more than 2 possible outcomes.
Please keep in mind that random returned from calling the randomness API is randomness, but not a secret. The original seed in each block lies in 0x1::randomness::PerBlockRandomness which is public on the Endless chain.
Users should not try to do the following:
Use the randomness API to generate an asymmetric key pair, discard the private key, then think the public key is safe.
Use the randomness API to shuffle some opened cards, veil them, and think no one knows the permutation.
Non-reproducible β The process of generating randomness cannot be reproduced unless the original sequence is preserved.
a vector of secret signing keys , using .
is derived from ShamirShare()
a vector of verification keys , where
a vector of public keys equal to verification keys, i.e., .
The public key size of a party in our weighted VUF is proportional to its weight, i.e., the public key of party contains group elements.
ShareSign.
The partial signing algorithm is a possibly randomized algorithm that takes as input a message and a secret key share . It generates a signature share .
ShareVerify.
The signature share verification function is a deterministic algorithm that takes as input a message , a public key share , and a signature share . It outputs 1 (accept) or 0 (reject).
The function asserts
.
The signature share combining algorithm takes as input the public key , a vector of public key shares , a message , and a set of signature shares (with corresponding indices). It outputs either a signature or β₯.
Derive.
The output derivation algorithm is a deterministic algorithm that takes as input a public key , a message , and a signature , and outputs the VUF output and the proof .
Verify.
The signature verification algorithm is a deterministic algorithm that takes as input a public key , a message , and a VUF output and proof .
verify
run Derive of Step 6:
check if , and outputs 1 (accept) or 0 (reject).
ShareVerify = 1] = 1$$
Verify(m, Aggregate
Tamper-proof depends on below:
Unbiased depends on below:
VUF output is unbiased if
derivation algorithm is unbiased
input is unbiased, e.g., use hash56(m) as input in the derivation function.
the key generation protocol generates decryption key , and the corresponding encryption key
validator_{i}, indexed by privately keeps , and publishes
denote
PVSS
leader node takes (secret) as input and generates secret shares
leader node encrypts share with encryption key , outputs encrypted and proof
leader node generates commitment for secret shares
leader node commits transaction trx, taking () as PVSS transcript
commit transaction actually is , Step. 5 will aggregate these separate into one trx
PVSS Verify
assert
assert
check if is a commitment to valid shares of some secret and the values consist of encryptions of valid shares of the same secret
PVSS Agg
public function Aggregate that anyone can use to aggregate two PVSS transcripts into a single transcript.
repeat to call Agg() to aggregate all into one trx, to save nodes broadcast effort, also save space.
PVSS DecShare
take as input aggregated PVSS transcript trx, index , i-th decryption key , outputs
PVSS Recon
each validator node decrypts with owned to get its share , and publishes with corresponding NIZK proof
anyone could verify with proof , and if shares collected combined weight greater than threshold , the original secret could be recovered.
...
randomness::u256_range(min_incl: u256, max_excl: u256)
module module_owner::lottery {
// ...
struct LotteryState {
players: vector<address>,
winner_idx: std::option::Option<u64>,
}
fun load_lottery_state_mut(): &mut Lottery {
// authentication check
// return global borrow_mut LotteryState
// ...
}
#[randomness]
entry fun decide_winner() {
let lottery_state = load_lottery_state_mut();
let n = vector::length(&lottery_state.players);
let winner_idx = endless_framework::randomness::u64_range(0, n);
lottery_state.winner_idx = std::option::some(winner_idx);
}
}