Settle at a signed quote
Ed25519 · Instructions sysvar · SPL Token
Status: Tested locally in LiteSVM against the Token program and the USDC and wrapped SOL mints copied from mainnet, with Solana's Ed25519 precompile, and in Mollusk, a harness that runs Solana programs without a validator; not yet run on devnet or mainnet.
Cost: Ballista's own work took 8,733 of the tested transaction's 8,892compute units; the two token transfers took the rest. Ballista charges no fee; see what it costs.
What it does
Settles a trade at a price a maker signed off chain. The taker pays the signed price, the maker delivers, and no single settlement can go past what the maker signed: the price, the size, the taker, the tokens and the expiry.
The maker, who quotes, signs a quote off chain and sends it to the taker, who accepts it. Settling moves the maker's tokens, so the maker signs the transaction too. Because the template holds the trade to the quote, the service that co-signs for the maker only has to check that the transaction runs this template and nothing else that could spend the maker's accounts.
Three Solana pieces make this work:
- An Ed25519 signature is
64 bytesthat prove the holder of a key, here the maker's wallet key, signed exactly these bytes. - A precompile is a program built into the Solana runtime; the Ed25519 precompile checks the signatures in its instruction, and a bad one fails the whole transaction.
- The Instructions sysvar is a read-only account the runtime fills with the transaction's instructions, so the template can read the Ed25519 instruction before its own.
The quote is 128 bytes: the tag BLSTQT01, then price, maxAmount and expiry as eight-byte little-endian integers, then the 32-byte addresses of taker, baseMint and quoteMint. The maker delivers the base token, and the taker pays in the quote token. Amounts are in base units, a token's smallest unit. price has six decimals, so 1,000,000 means one quote unit per base unit. expiry is the last Unix timestamp at which the quote can settle.
The precompile proves only that some key signed some bytes. The template ties them to the maker and to this trade, checking in order that:
- the instruction directly before the run is the Ed25519 precompile;
- it holds one self-contained signature over
128 bytes: the signature, the key and the message all sit in its own data, so the bytes the template reads are the bytes the precompile checked; - the signing key is
maker's; - the message starts with
BLSTQT01, the tag that separates quotes from everything else the maker signs (a signature covers bytes, not what they mean); - the clock hasn't passed
expiry; - the quote's
takeris the wallet signing astaker; amountis at mostmaxAmount;- the taker pays from an account in
quoteMint, and the maker delivers from one inbaseMint(a token transfer only moves between accounts of one mint, so this pins both sides); - the account the taker pays into belongs to the maker, not one the taker picked.
It then prices the trade at amount × price ÷ 1,000,000, rounded up in the maker's favor, and makes two SPL Token transfers: the taker pays that to the maker, and the maker delivers amount to the taker.
A quote can settle more than once
This template keeps no state, so it can't count settlements. Until a quote expires, it can settle again unless the maker's co-signer refuses a second settlement of the same quote. maxAmount caps each settlement, not the quote as a whole: in a test, settling the same quote twice delivered 3 SOL against a maxAmount of 2 SOL. A template that must refuse replays itself can keep a per-maker nonce in a registry entry.
Template
import {
INSTRUCTIONS_SYSVAR_ADDRESS_BYTES,
TOKEN_PROGRAM_ADDRESS_BYTES,
account,
compileTemplate,
defineTemplate,
ed25519Signature,
expression,
step,
tokenTransfer,
} from '@jac0xb/ballista';
import { TOKEN_ACCOUNT_LENGTH, TOKEN_ACCOUNT_MINT_OFFSET, TOKEN_ACCOUNT_OWNER_OFFSET } from './shared.js';
/** The signed quote. Integers are little-endian; keys are their 32 raw bytes. */
export const QUOTE = {
length: 128,
/** `QUOTE_TAG`, marking the message as a settlement quote. */
tag: 0,
/** Quote-token base units per `PRICE_SCALE` base-token base units. */
price: 8,
/** The most base-token base units the maker delivers in one settlement. The quote can settle
* again until it expires, so this bounds each settlement, not the total. */
maxAmount: 16,
/** The last Unix timestamp at which the quote can settle. */
expiry: 24,
/** The one wallet that can take the quote. */
taker: 32,
/** The mint the maker delivers. */
baseMint: 64,
/** The mint the taker pays in. */
quoteMint: 96,
} as const;
/** The eight bytes every quote starts with. */
export const QUOTE_TAG = new TextEncoder().encode('BLSTQT01');
/** Prices carry six decimals: a price of 1,000,000 is one quote unit per base unit. */
export const PRICE_SCALE = 1_000_000n;
const instructions = account.fixed('instructions');
const taker = account.fixed('taker');
const maker = account.fixed('maker');
/** The maker's signature, in the instruction directly before this template's run. */
export const signedQuote = ed25519Signature({
sysvar: instructions,
index: expression.subtract(expression.currentInstructionIndex(instructions), expression.u64(1)),
signer: expression.accountField(maker, 'key'),
messageLength: QUOTE.length,
name: 'quote',
});
const tokenAccount = { writable: true, owner: TOKEN_PROGRAM_ADDRESS_BYTES, minDataLength: TOKEN_ACCOUNT_LENGTH };
export const signedQuoteSettlement = defineTemplate({
inputs: {
/** Base-token base units to take, up to the quoted maximum. */
amount: { type: 'u64' },
},
accounts: {
instructions: { address: INSTRUCTIONS_SYSVAR_ADDRESS_BYTES },
tokenProgram: { executable: true, address: TOKEN_PROGRAM_ADDRESS_BYTES },
taker: { signer: true },
maker: { signer: true },
/** Pays, in the quote mint. */
takerQuoteAccount: tokenAccount,
/** Is paid, in the quote mint. */
makerQuoteAccount: tokenAccount,
/** Delivers, in the base mint. */
makerBaseAccount: tokenAccount,
/** Receives, in the base mint. */
takerBaseAccount: tokenAccount,
},
steps: [
...signedQuote.steps,
step.require(
expression.equal(
signedQuote.field(QUOTE.tag, 'u64'),
expression.u64(new DataView(QUOTE_TAG.buffer).getBigUint64(0, true)),
),
'quoteIsTagged',
),
step.require(
expression.lessThanOrEqual(expression.clockUnixTimestamp(), signedQuote.field(QUOTE.expiry, 'i64')),
'quoteHasNotExpired',
),
step.require(
expression.equal(signedQuote.field(QUOTE.taker, 'pubkey'), expression.accountField(taker, 'key')),
'quoteIsForThisTaker',
),
step.require(
expression.lessThanOrEqual(expression.input('amount'), signedQuote.field(QUOTE.maxAmount, 'u64')),
'withinTheQuotedSize',
),
// A token `transfer` moves only between two accounts of one mint, so pinning one side of each
// leg pins both.
step.require(
expression.equal(
expression.accountData(account.fixed('takerQuoteAccount'), TOKEN_ACCOUNT_MINT_OFFSET, 'pubkey'),
signedQuote.field(QUOTE.quoteMint, 'pubkey'),
),
'paysInTheQuotedMint',
),
step.require(
expression.equal(
expression.accountData(account.fixed('makerBaseAccount'), TOKEN_ACCOUNT_MINT_OFFSET, 'pubkey'),
signedQuote.field(QUOTE.baseMint, 'pubkey'),
),
'deliversTheQuotedMint',
),
// The payment reaches an account the maker owns, not one the taker picked.
step.require(
expression.equal(
expression.accountData(account.fixed('makerQuoteAccount'), TOKEN_ACCOUNT_OWNER_OFFSET, 'pubkey'),
expression.accountField(maker, 'key'),
),
'paymentReachesTheMaker',
),
step.let(
'payment',
expression.multiplyDivide(
expression.input('amount'),
signedQuote.field(QUOTE.price, 'u64'),
expression.u64(PRICE_SCALE),
'up',
),
'priceTheFill',
),
tokenTransfer({
tokenProgram: account.fixed('tokenProgram'),
source: account.fixed('takerQuoteAccount'),
destination: account.fixed('makerQuoteAccount'),
authority: taker,
amount: expression.variable('payment'),
label: 'takerPays',
}),
tokenTransfer({
tokenProgram: account.fixed('tokenProgram'),
source: account.fixed('makerBaseAccount'),
destination: account.fixed('takerBaseAccount'),
authority: maker,
amount: expression.input('amount'),
label: 'makerDelivers',
}),
],
});
export const compiled = compileTemplate(signedQuoteSettlement);/// The signed quote's layout. Integers are little-endian; keys are their 32 raw bytes.
const QUOTE_LENGTH: u32 = 128;
const QUOTE_TAG_OFFSET: u32 = 0;
const QUOTE_PRICE: u32 = 8;
const QUOTE_MAX_AMOUNT: u32 = 16;
const QUOTE_EXPIRY: u32 = 24;
const QUOTE_TAKER: u32 = 32;
const QUOTE_BASE_MINT: u32 = 64;
const QUOTE_QUOTE_MINT: u32 = 96;
/// The eight bytes every quote starts with.
const QUOTE_TAG: [u8; 8] = *b"BLSTQT01";
/// Prices carry six decimals: a price of 1,000,000 is one quote unit per base unit.
const PRICE_SCALE: u64 = 1_000_000;
/// Settle a trade at a price the maker signed, in the instruction before this run.
pub fn signed_quote_settlement() -> Template {
// The maker's signature, in the instruction directly before this template's run.
let quote = ed25519_signature(
"instructions",
current_instruction_index("instructions") - u64(1),
key("maker"),
QUOTE_LENGTH,
)
.name("quote");
Template::new()
// Base-token base units to take, up to the quoted maximum.
.input("amount", Type::U64)
.account(
"instructions",
account::readonly().address(INSTRUCTIONS_SYSVAR_ID),
)
.account("tokenProgram", account::program(TOKEN_PROGRAM_ID))
.account("taker", account::signer())
.account("maker", account::signer())
// Pays, in the quote mint.
.account("takerQuoteAccount", token_account())
// Is paid, in the quote mint.
.account("makerQuoteAccount", token_account())
// Delivers, in the base mint.
.account("makerBaseAccount", token_account())
// Receives, in the base mint.
.account("takerBaseAccount", token_account())
.steps(quote.steps())
.step(
step::require(
quote
.field(QUOTE_TAG_OFFSET, ReadType::U64)
.eq(u64(u64::from_le_bytes(QUOTE_TAG))),
)
.label("quoteIsTagged"),
)
.step(
step::require(clock_unix_timestamp().lte(quote.field(QUOTE_EXPIRY, ReadType::I64)))
.label("quoteHasNotExpired"),
)
.step(
step::require(quote.field(QUOTE_TAKER, ReadType::Pubkey).eq(key("taker")))
.label("quoteIsForThisTaker"),
)
.step(
step::require(input("amount").lte(quote.field(QUOTE_MAX_AMOUNT, ReadType::U64)))
.label("withinTheQuotedSize"),
)
// A token `transfer` moves only between two accounts of one mint, so pinning one side of
// each leg pins both.
.step(
step::require(
account_data(
"takerQuoteAccount",
TOKEN_ACCOUNT_MINT_OFFSET,
ReadType::Pubkey,
)
.eq(quote.field(QUOTE_QUOTE_MINT, ReadType::Pubkey)),
)
.label("paysInTheQuotedMint"),
)
.step(
step::require(
account_data(
"makerBaseAccount",
TOKEN_ACCOUNT_MINT_OFFSET,
ReadType::Pubkey,
)
.eq(quote.field(QUOTE_BASE_MINT, ReadType::Pubkey)),
)
.label("deliversTheQuotedMint"),
)
// The payment reaches an account the maker owns, not one the taker picked.
.step(
step::require(
account_data(
"makerQuoteAccount",
TOKEN_ACCOUNT_OWNER_OFFSET,
ReadType::Pubkey,
)
.eq(key("maker")),
)
.label("paymentReachesTheMaker"),
)
.step(
step::let_(
"payment",
input("amount")
.mul_div_up(quote.field(QUOTE_PRICE, ReadType::U64), u64(PRICE_SCALE)),
)
.label("priceTheFill"),
)
.step(
token_transfer(
"tokenProgram",
"takerQuoteAccount",
"makerQuoteAccount",
"taker",
var("payment"),
)
.label("takerPays"),
)
.step(
token_transfer(
"tokenProgram",
"makerBaseAccount",
"takerBaseAccount",
"maker",
input("amount"),
)
.label("makerDelivers"),
)
}import { getAddressDecoder, getAddressEncoder, type Address, type Instruction } from '@solana/kit';
import { ED25519_PROGRAM_ADDRESS_BYTES, INSTRUCTIONS_SYSVAR_ADDRESS_BYTES } from '@jac0xb/ballista';
import { buildKitRunInstruction } from '@jac0xb/ballista/kit';
import { QUOTE, QUOTE_TAG, compiled } from '../signed-quote-settlement.js';
import { TOKEN_PROGRAM, at, pinned } from './programs.js';
const ED25519_PROGRAM = getAddressDecoder().decode(ED25519_PROGRAM_ADDRESS_BYTES);
const INSTRUCTIONS_SYSVAR = getAddressDecoder().decode(INSTRUCTIONS_SYSVAR_ADDRESS_BYTES);
/** The quote a maker signs off chain. */
export interface Quote {
/** Quote-token base units per 1,000,000 base-token base units. */
price: bigint;
/** The most base-token base units the maker delivers. */
maxAmount: bigint;
/** The last Unix timestamp at which the quote can settle. */
expiry: bigint;
/** The one wallet that can take the quote. */
taker: Address;
/** The mint the maker delivers, and the mint the taker pays in. */
baseMint: Address;
quoteMint: Address;
}
/** The 128 bytes the maker signs: the tag `BLSTQT01`, then the fields, integers little-endian. */
export function quoteMessage(quote: Quote): Uint8Array {
const encoder = getAddressEncoder();
const message = new Uint8Array(QUOTE.length);
const view = new DataView(message.buffer);
message.set(QUOTE_TAG, QUOTE.tag);
view.setBigUint64(QUOTE.price, quote.price, true);
view.setBigUint64(QUOTE.maxAmount, quote.maxAmount, true);
view.setBigInt64(QUOTE.expiry, quote.expiry, true);
message.set(encoder.encode(quote.taker), QUOTE.taker);
message.set(encoder.encode(quote.baseMint), QUOTE.baseMint);
message.set(encoder.encode(quote.quoteMint), QUOTE.quoteMint);
return message;
}
/**
* The Ed25519 precompile instruction that verifies `signature`, `signer`'s over `message`. It
* holds one signature, with the key, the signature and the message in its own data.
*/
export function buildEd25519Instruction(input: {
signer: Address;
signature: Uint8Array;
message: Uint8Array;
}): Instruction {
if (input.signature.length !== 64) throw new RangeError('An Ed25519 signature is 64 bytes');
// 0xffff as an instruction index: the bytes are in this instruction's own data.
const OWN_DATA = 0xffff;
const keyOffset = 2 + 14;
const signatureOffset = keyOffset + 32;
const messageOffset = signatureOffset + 64;
const data = new Uint8Array(messageOffset + input.message.length);
const view = new DataView(data.buffer);
data[0] = 1; // one signature, then a padding byte
const offsets = [signatureOffset, OWN_DATA, keyOffset, OWN_DATA, messageOffset, input.message.length, OWN_DATA];
offsets.forEach((field, index) => view.setUint16(2 + 2 * index, field, true));
data.set(getAddressEncoder().encode(input.signer), keyOffset);
data.set(input.signature, signatureOffset);
data.set(input.message, messageOffset);
return { programAddress: ED25519_PROGRAM, data };
}
/**
* The transaction's two instructions, in order: the Ed25519 instruction carrying the maker's
* signature over the quote, and the run, which reads the signature from the instruction directly
* before it. The taker and the maker both sign the transaction.
*/
export function buildSignedQuoteRun(input: {
templateAddress: Address;
taker: Address;
maker: Address;
/** Pays, in the quote mint. */
takerQuoteAccount: Address;
/** Is paid, in the quote mint: the maker's own account. */
makerQuoteAccount: Address;
/** Delivers, in the base mint. */
makerBaseAccount: Address;
/** Receives, in the base mint. */
takerBaseAccount: Address;
quote: Quote;
/** The maker's 64-byte Ed25519 signature over `quoteMessage(quote)`. */
signature: Uint8Array;
/** Base-token base units to take, up to the quote's `maxAmount`. */
amount: bigint;
}): [Instruction, Instruction] {
const verify = buildEd25519Instruction({
signer: input.maker,
signature: input.signature,
message: quoteMessage(input.quote),
});
const run = buildKitRunInstruction({
compiled,
templateAddress: input.templateAddress,
inputs: { amount: input.amount },
accounts: {
instructions: pinned(INSTRUCTIONS_SYSVAR),
tokenProgram: pinned(TOKEN_PROGRAM),
taker: at(input.taker),
maker: at(input.maker),
takerQuoteAccount: at(input.takerQuoteAccount),
makerQuoteAccount: at(input.makerQuoteAccount),
makerBaseAccount: at(input.makerBaseAccount),
takerBaseAccount: at(input.takerBaseAccount),
},
});
return [verify, run];
}/// The quote a maker signs off chain, as `signed-quote-settlement.ts` reads it.
pub struct Quote {
/// Quote-token base units per 1,000,000 base-token base units.
pub price: u64,
/// The most base-token base units the maker delivers in one settlement. The quote can settle
/// again until it expires, so this bounds each settlement, not the total.
pub max_amount: u64,
/// The last Unix timestamp at which the quote can settle.
pub expiry: i64,
/// The one wallet that can take the quote.
pub taker: Pubkey,
/// The mint the maker delivers, and the mint the taker pays in.
pub base_mint: Pubkey,
pub quote_mint: Pubkey,
}
impl Quote {
/// The 128 bytes the maker signs: the tag `BLSTQT01`, then the fields, integers
/// little-endian.
pub fn message(&self) -> Vec<u8> {
let mut message = Vec::with_capacity(128);
message.extend_from_slice(b"BLSTQT01");
message.extend_from_slice(&self.price.to_le_bytes());
message.extend_from_slice(&self.max_amount.to_le_bytes());
message.extend_from_slice(&self.expiry.to_le_bytes());
for key in [self.taker, self.base_mint, self.quote_mint] {
message.extend_from_slice(key.as_ref());
}
message
}
}
/// The Ed25519 precompile instruction that verifies `signature`, `signer`'s over `message`. It
/// holds one signature, with the key, the signature and the message in its own data, laid out as
/// `new_ed25519_instruction_with_signature` lays them out.
pub fn ed25519_instruction(signer: &Pubkey, signature: &[u8; 64], message: &[u8]) -> Instruction {
// `u16::MAX` as an instruction index: the bytes are in this instruction's own data.
const OWN_DATA: u16 = u16::MAX;
let key_offset: u16 = 2 + 14;
let signature_offset = key_offset + 32;
let message_offset = signature_offset + 64;
let mut data = vec![1, 0]; // one signature, then a padding byte
for field in [
signature_offset,
OWN_DATA,
key_offset,
OWN_DATA,
message_offset,
message.len() as u16,
OWN_DATA,
] {
data.extend_from_slice(&field.to_le_bytes());
}
data.extend_from_slice(signer.as_ref());
data.extend_from_slice(signature);
data.extend_from_slice(message);
Instruction {
program_id: ED25519_PROGRAM_ID,
accounts: vec![],
data,
}
}
pub struct SignedQuoteAccounts {
pub taker: Pubkey,
pub maker: Pubkey,
/// Pays, in the quote mint.
pub taker_quote_account: Pubkey,
/// Is paid, in the quote mint: the maker's own account.
pub maker_quote_account: Pubkey,
/// Delivers, in the base mint.
pub maker_base_account: Pubkey,
/// Receives, in the base mint.
pub taker_base_account: Pubkey,
}
/// The transaction's two instructions, in order: the Ed25519 instruction carrying `signature`,
/// the maker's over `quote`, and the run, which reads the signature from the instruction directly
/// before it. The taker and the maker both sign the transaction. `amount` is in base-token base
/// units, up to the quote's `max_amount`.
pub fn run_signed_quote(
template: Pubkey,
a: &SignedQuoteAccounts,
quote: &Quote,
signature: &[u8; 64],
amount: u64,
) -> Result<[Instruction; 2], Box<dyn Error>> {
let verify = ed25519_instruction(&a.maker, signature, "e.message());
let run = templates::signed_quote_settlement()
.compile()?
.run(template)
.input("amount", amount)
.account("instructions", INSTRUCTIONS_SYSVAR_ID)
.account("tokenProgram", TOKEN_PROGRAM_ID)
.account("taker", a.taker)
.account("maker", a.maker)
.account("takerQuoteAccount", a.taker_quote_account)
.account("makerQuoteAccount", a.maker_quote_account)
.account("makerBaseAccount", a.maker_base_account)
.account("takerBaseAccount", a.taker_base_account)
.instruction()?;
Ok([verify, run])
}The first steps come from the SDK's ed25519Signature helper (ed25519_signature in Rust), which returns them with a field reader for the signed message. field refuses a read past the message, and a template that uses field without the steps doesn't compile. The Rust template takes its token_account() from the shared helpers.
The helper's signer must be a key the transaction's builder can't choose. With an input, or the key of an account nothing constrains, the builder could sign a quote with a key of their own. Here it is the key of maker, which must also sign the transaction, so a quote settles only if its signer signs the settlement too. The helper refuses an input, but it can't see an account's constraints: those are the template's to get right.
Run it
The maker signs the quote's 128 bytes with its wallet key and sends the quote and the 64-byte signature to the taker. The taker builds one transaction with two instructions, in this order: the Ed25519 instruction, carrying the maker's key, the signature and the quote, then the run. The taker and the maker both sign it.
The Run tabs build both. quoteMessage (TypeScript) and Quote::message (Rust) write the 128 bytes the maker signs. buildEd25519Instruction and ed25519_instruction build the Ed25519 instruction with one signature and each of its three instruction indexes set to u16::MAX (0xffff), which means "this instruction's own data". buildSignedQuoteRun and run_signed_quote return the two instructions in order.
The Run tabs pass the eight declared accounts, instructions, tokenProgram, taker, maker, takerQuoteAccount, makerQuoteAccount, makerBaseAccount and takerBaseAccount, then the input amount. instructions is the Instructions sysvar. The four token accounts are writable, and the SPL Token program must own them, so Token-2022 accounts are rejected.
The Ed25519 instruction goes directly before the run
The template reads the signature from the instruction just before its own. With nothing before the run, it fails at quoteInstructionIndex. With any other instruction in between, even a memo, it fails at quoteIsEd25519. currentInstructionIndex gives the top-level instruction's index, so if another program calls the run through a CPI, the signature must sit directly before that program's instruction.
What has been tested
- In LiteSVM.
tests/protocols/tests/signed_quote.rsruns it against copies of mainnet's Token program and the USDC and wrapped SOL mints, with the Ed25519 precompile. Selling1.5 SOLand a lamport pays225,375,001 USDCunits, rounded up. Settling at exactlyexpirylands and one second later fails; exactlymaxAmountlands and one lamport more fails; USDC named as the base mint fails atdeliversTheQuotedMint. The same quote settled twice in its window lands both times,3SOL against amaxAmountof2 SOL. No failed run moves any balance. A settlement costs8,892compute units, and the transaction is783 byteswith a15,000 lamportfee. The precompile uses no compute units: it adds one signature to the fee and276of the783 bytes. - End to end in Mollusk.
tests/ballista/src/lib.rs(signed_quote_settles_only_as_the_maker_signed) uploads the template as the TypeScript SDK compiles it and runs it in Mollusk after a real Ed25519 instruction. At a price of2,500,000(2.5quote units per base unit), taking3,000,001base units pays the maker7,500,003,rounded up from7,500,002.5, and delivers3,000,001to the taker. - Failures in the precompile. A signature or signed price with one bit flipped fails the Ed25519 instruction with
InvalidSignature, so Ballista never runs. - Failures in the template, each at its own step: nothing before the run (
quoteInstructionIndex); a memo in between (quoteIsEd25519); another key's signature (quoteIsBySigner); two signatures, a127-bytemessage, or any one of the three instruction indexes set to0instead ofu16::MAX(quoteIsOneSelfContainedSignature); a message the maker signed under the tagBLSTQT02(quoteIsTagged); an expiry one second before the clock (quoteHasNotExpired); another taker (quoteIsForThisTaker); one base unit overmaxAmount(withinTheQuotedSize); a quote for another quote mint (paysInTheQuotedMint); a payment account the taker owns (paymentReachesTheMaker). - Not tested. Devnet and mainnet. Both suites build their own Ed25519 instruction and run, in the same layout as the Run tabs (the LiteSVM test checks its copy against Solana's own builder), so no test runs the Run tabs' code against the program. The TypeScript run is only type-checked.
clients/js/src/compiler.test.tschecks theed25519Signaturehelper: its steps and header check, that it refuses an input assigner, and thatfieldstays inside the message.