Skip to content

Commit

Permalink
chore(utxo-coredao): op_return
Browse files Browse the repository at this point in the history
TICKET: BTC-1578
  • Loading branch information
davidkaplanbitgo committed Oct 25, 2024
1 parent b079df4 commit 2b713f7
Show file tree
Hide file tree
Showing 3 changed files with 305 additions and 0 deletions.
1 change: 1 addition & 0 deletions modules/utxo-coredao/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './descriptor';
export * from './transaction';
121 changes: 121 additions & 0 deletions modules/utxo-coredao/src/transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Source: https://docs.coredao.org/docs/Learn/products/btc-staking/design
export const CORE_DAO_TESTNET_CHAIN_ID = Buffer.alloc(2, 0x1115);
export const CORE_DAO_MAINNET_CHAIN_ID = Buffer.alloc(2, 0x1116);
export const CORE_DAO_SATOSHI_PLUS_IDENTIFIER = Buffer.alloc(4, 0x5341542b);

/** As of v2, this is the construction of the OP_RETURN:
* Source: https://docs.coredao.org/docs/Learn/products/btc-staking/design#op_return-output
*
* The OP_RETURN output should contain all staking information in order, and be composed in the following format:
*
* OP_RETURN: identifier 0x6a
* LENGTH: which represents the total byte length after the OP_RETURN opcode. Note that all data has to be pushed with its appropriate size byte(s). [1]
* Satoshi Plus Identifier: (SAT+) 4 bytes
* Version: (0x01) 1 byte
* Chain ID: (0x1115 for Core Testnet and 0x1116 for Core Mainnet) 2 bytes
* Delegator: The Core address to receive rewards, 20 bytes
* Validator: The Core validator address to stake to, 20 bytes
* Fee: Fee for relayer, 1 byte, range [0,255], measured in CORE
* (Optional) RedeemScript
* (Optional) Timelock: 4 bytes
*
* [1] Any bytes bigger than or equal to 0x4c is pushed by using 0x4c (ie. OP_PUSHDATA)
* followed by the length followed by the data (byte[80] -> OP_PUSHDATA + 80 + byte[80])
*
* Either RedeemScript or Timelock must be available, the purpose is to allow relayer to
* obtain the RedeemScript and submit transactions on Core. If a RedeemScript is provided,
* relayer will use it directly. Otherwise, relayer will construct the redeem script based
* on the timelock and the information in the transaction inputs.
*
* @returns Buffer OP_RETURN buffer
*/
export function createCoreDaoOpReturnBuffer({
version,
chainId,
delegator,
validator,
fee,
redeemScript,
timelock,
}: {
version: number;
chainId: Buffer;
delegator: Buffer;
validator: Buffer;
fee: number;
redeemScript?: Buffer;
timelock?: number;
}): Buffer {
if (version < 0 || version > 255) {
throw new Error('Invalid version - out of range');
}
const versionBuffer = Buffer.alloc(1, version);

if (!(chainId.equals(CORE_DAO_TESTNET_CHAIN_ID) || chainId.equals(CORE_DAO_MAINNET_CHAIN_ID))) {
throw new Error('Invalid chain ID');
}

if (delegator.length !== 20) {
throw new Error('Invalid delegator address');
}

if (validator.length !== 20) {
throw new Error('Invalid validator address');
}

if (fee < 0 || fee > 255) {
throw new Error('Invalid fee - out of range');
}
const feeBuffer = Buffer.alloc(1, fee);

if (feeBuffer.length !== 1) {
throw new Error('Invalid fee');
}

if (!redeemScript && !timelock) {
throw new Error('Either redeemScript or timelock must be provided');
}
const redeemScriptBuffer = redeemScript ?? Buffer.from([]);
if (timelock && (timelock < 0 || timelock > 4294967295)) {
throw new Error('Invalid timelock - out of range');
}
const timelockBuffer = timelock ? Buffer.alloc(4, timelock).reverse() : Buffer.from([]);

const totalLength =
CORE_DAO_SATOSHI_PLUS_IDENTIFIER.length +
versionBuffer.length +
chainId.length +
delegator.length +
validator.length +
feeBuffer.length +
redeemScriptBuffer.length +
timelockBuffer.length +
// This is to account for the LENGTH byte
1;

// If the length is >= 0x4c (76), we need to use the OP_PUSHDATA (0x4c) opcode and then the length
const totalLengthBuffer =
totalLength >= 76
? Buffer.concat([
Buffer.from([0x4c]),
Buffer.alloc(
1,
// This is to account for the extra OP_PUSHDATA byte
totalLength + 1
),
])
: Buffer.alloc(1, totalLength);

return Buffer.concat([
Buffer.from([0x6a]),
totalLengthBuffer,
CORE_DAO_SATOSHI_PLUS_IDENTIFIER,
versionBuffer,
chainId,
delegator,
validator,
feeBuffer,
redeemScriptBuffer,
timelockBuffer,
]);
}
183 changes: 183 additions & 0 deletions modules/utxo-coredao/test/unit/transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import * as assert from 'assert';
import { CORE_DAO_MAINNET_CHAIN_ID, CORE_DAO_SATOSHI_PLUS_IDENTIFIER, createCoreDaoOpReturnBuffer } from '../../src';
import { testutil } from '@bitgo/utxo-lib';

describe('OP_RETURN', function () {
const validVersion = 2;
const validChainId = CORE_DAO_MAINNET_CHAIN_ID;
// random 20 byte buffers
const validDelegator = Buffer.alloc(20, testutil.getKey('wasm-possum').publicKey);
const validValidator = Buffer.alloc(20, testutil.getKey('possum-wasm').publicKey);
const validFee = 1;
// p2sh 2-3 script
const validRedeemScript = Buffer.from(
'522103a8295453660d5e212d4aaf82e8254e27e4c6752b2afa36e648537b644a6ca2702103099e28dd8bcb345e655b5312db0a574dded8f740eeef636cf45317bf010452982102a45b464fed0167d175d89bbc31d7ba1c288b52f64270c7eddaafa38803c5a6b553ae',
'hex'
);
const validTimelock = 800800;
it('should throw if invalid parameters are passed', function () {
assert.throws(() =>
createCoreDaoOpReturnBuffer({
version: 292,
chainId: validChainId,
delegator: validDelegator,
validator: validValidator,
fee: validFee,
timelock: validTimelock,
})
);
assert.throws(() =>
createCoreDaoOpReturnBuffer({
version: validVersion,
chainId: Buffer.alloc(32, 0),
delegator: validDelegator,
validator: validValidator,
fee: validFee,
timelock: validTimelock,
})
);
assert.throws(() =>
createCoreDaoOpReturnBuffer({
version: validVersion,
chainId: validChainId,
delegator: Buffer.alloc(19, 0),
validator: validValidator,
fee: validFee,
timelock: validTimelock,
})
);
assert.throws(() =>
createCoreDaoOpReturnBuffer({
version: validVersion,
chainId: validChainId,
delegator: validDelegator,
validator: Buffer.alloc(19, 0),
fee: validFee,
timelock: validTimelock,
})
);
assert.throws(() =>
createCoreDaoOpReturnBuffer({
version: validVersion,
chainId: validChainId,
delegator: validDelegator,
validator: validValidator,
fee: 256,
timelock: validTimelock,
})
);
assert.throws(() =>
createCoreDaoOpReturnBuffer({
version: validVersion,
chainId: validChainId,
delegator: validDelegator,
validator: validValidator,
fee: validFee,
timelock: -1,
})
);
assert.throws(() =>
createCoreDaoOpReturnBuffer({
version: validVersion,
chainId: validChainId,
delegator: validDelegator,
validator: validValidator,
fee: validFee,
})
);
});

it('should return a buffer with the correct length', function () {
const script = createCoreDaoOpReturnBuffer({
version: validVersion,
chainId: validChainId,
delegator: validDelegator,
validator: validValidator,
fee: validFee,
timelock: validTimelock,
});
// Make sure that the first byte is the OP_RETURN opcode
assert.strictEqual(script[0], 0x6a);
// Make sure that the length of the script matches what is in the buffer
assert.strictEqual(
// We do not count the OP_RETURN opcode
script.length - 1,
script[1]
);
});

it('should have the correct placement of the values provided with a redeem script', function () {
// This should produce an Op_RETURN that needs the extra push bytes for the length
const script = createCoreDaoOpReturnBuffer({
version: validVersion,
chainId: validChainId,
delegator: validDelegator,
validator: validValidator,
fee: validFee,
redeemScript: validRedeemScript,
});
// Make sure that the first byte is the OP_RETURN opcode
assert.strictEqual(script[0], 0x6a);
// Make sure that the length of the script matches what is in the buffer
assert.strictEqual(script[1], 0x4c);
assert.strictEqual(
// We do not count the OP_RETURN opcode
script.length - 1,
script[2]
);
// Satoshi plus identifier
assert.deepStrictEqual(script.subarray(3, 7).toString('hex'), CORE_DAO_SATOSHI_PLUS_IDENTIFIER.toString('hex'));
// Make sure that the version is correct
assert.strictEqual(script[7], validVersion);
// Make sure that the chainId is correct
assert.deepStrictEqual(script.subarray(8, 10).toString('hex'), validChainId.toString('hex'));
// Make sure that the delegator is correct
assert.deepStrictEqual(script.subarray(10, 30).toString('hex'), validDelegator.toString('hex'));
// Make sure that the validator is correct
assert.deepStrictEqual(script.subarray(30, 50).toString('hex'), validValidator.toString('hex'));
// Make sure that the fee is correct
assert.strictEqual(script[50], validFee);
// Make sure that the redeemScript is correct
assert.deepStrictEqual(
script.subarray(51, 51 + validRedeemScript.length).toString('hex'),
validRedeemScript.toString('hex')
);
});

it('should have the correct placement of the values provided with a timelock', function () {
// This should produce an Op_RETURN that needs the extra push bytes for the length
const script = createCoreDaoOpReturnBuffer({
version: validVersion,
chainId: validChainId,
delegator: validDelegator,
validator: validValidator,
fee: validFee,
timelock: validTimelock,
});
// Make sure that the first byte is the OP_RETURN opcode
assert.strictEqual(script[0], 0x6a);
// Make sure that the length of the script matches what is in the buffer
assert.strictEqual(
// We do not count the OP_RETURN opcode
script.length - 1,
script[1]
);
// Satoshi plus identifier
assert.deepStrictEqual(script.subarray(2, 6).toString('hex'), CORE_DAO_SATOSHI_PLUS_IDENTIFIER.toString('hex'));
// Make sure that the version is correct
assert.strictEqual(script[6], validVersion);
// Make sure that the chainId is correct
assert.deepStrictEqual(script.subarray(7, 9).toString('hex'), validChainId.toString('hex'));
// Make sure that the delegator is correct
assert.deepStrictEqual(script.subarray(9, 29).toString('hex'), validDelegator.toString('hex'));
// Make sure that the validator is correct
assert.deepStrictEqual(script.subarray(29, 49).toString('hex'), validValidator.toString('hex'));
// Make sure that the fee is correct
assert.strictEqual(script[49], validFee);
// Make sure that the redeemScript is correct
assert.deepStrictEqual(
script.subarray(50, 54).reverse().toString('hex'),
Buffer.alloc(4, validTimelock).toString('hex')
);
});
});

0 comments on commit 2b713f7

Please sign in to comment.