diff --git a/src/index/bundle_message.rs b/src/index/bundle_message.rs index 6ca37acd37..1d7d8fa5be 100644 --- a/src/index/bundle_message.rs +++ b/src/index/bundle_message.rs @@ -50,7 +50,7 @@ impl BundleMessage { if let Some(SubType::BRC20(operation)) = sub_type { return !matches!( operation, - BRC20Operation::Mint { .. } | BRC20Operation::InscribeTransfer(_) + BRC20Operation::Mint { .. } | BRC20Operation::InscribeTransfer{ .. } ); } } diff --git a/src/index/event.rs b/src/index/event.rs index e1da37f469..36165d62e9 100644 --- a/src/index/event.rs +++ b/src/index/event.rs @@ -49,8 +49,8 @@ pub(crate) enum Action { Created { inscription: Inscription, parents: Vec, - pre_jubilant_curse_reason: Option, charms: u16, + tapscript_pk: [u8; 35], }, Transferred, } diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 7808af6fd4..d780188695 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -1,4 +1,5 @@ use super::*; +use bitcoin::blockdata::opcodes; use crate::index::bundle_message::BundleMessage; use crate::index::event::{Action, OkxInscriptionEvent}; use crate::okx::UtxoAddress; @@ -37,7 +38,7 @@ enum Origin { unbound: bool, vindicated: bool, inscription: Inscription, - pre_jubilant_curse_reason: Option, + tapscript_pk: [u8; 35], }, Old { sequence_number: u32, @@ -155,6 +156,19 @@ impl InscriptionUpdater<'_, '_> { break; } + let mut tapscript_pk = [0u8; 35]; + if let Some(tapscript) = txin.witness.tapscript() { + if tapscript.len() >= 35 { + let script_bytes = tapscript.as_bytes(); + if script_bytes[0] == opcodes::all::OP_PUSHBYTES_32.to_u8() + && script_bytes[33] == opcodes::all::OP_CHECKSIGVERIFY.to_u8() + && script_bytes[34] >= opcodes::all::OP_PUSHNUM_1.to_u8() + && script_bytes[34] <= opcodes::all::OP_PUSHNUM_8.to_u8() { + tapscript_pk.copy_from_slice(&script_bytes[..35]); + } + } + } + let inscription = envelopes.next().unwrap(); let inscription_id = InscriptionId { @@ -232,7 +246,7 @@ impl InscriptionUpdater<'_, '_> { || inscription.payload.unrecognized_even_field, vindicated: curse.is_some() && jubilant, inscription: inscription.payload, - pre_jubilant_curse_reason: curse, + tapscript_pk: tapscript_pk, }, }); @@ -619,13 +633,13 @@ impl InscriptionUpdater<'_, '_> { Origin::New { inscription, parents, - pre_jubilant_curse_reason, + tapscript_pk, .. } => Action::Created { inscription, parents, - pre_jubilant_curse_reason, charms: charms.unwrap(), + tapscript_pk, }, Origin::Old { .. } => Action::Transferred, }, diff --git a/src/okx/brc20.rs b/src/okx/brc20.rs index df82b65049..0c1b698613 100644 --- a/src/okx/brc20.rs +++ b/src/okx/brc20.rs @@ -14,6 +14,7 @@ mod fixed_point; mod operation; mod policies; mod ticker; +mod utils; pub static MAXIMUM_SUPPLY: Lazy = Lazy::new(|| FixedPoint::new_unchecked(u128::from(u64::MAX), 0)); @@ -30,9 +31,13 @@ pub enum BRC20Operation { Deploy(Deploy), Mint { op: Mint, + signer: Option, parent: Option, }, - InscribeTransfer(Transfer), + InscribeTransfer { + signer: Option, + transfer: Transfer, + }, Transfer { ticker: BRC20Ticker, amount: u128, @@ -59,8 +64,8 @@ pub struct CreatedInscription<'a> { pub inscription_number: i32, pub parents: &'a Vec, pub new_satpoint: SatPoint, - pub pre_jubilant_curse_reason: Option<&'a Curse>, pub charms: u16, + pub tapscript_pk: [u8; 35], } impl CreatedInscription<'_> { @@ -75,8 +80,8 @@ impl<'a> From<&'a OkxInscriptionEvent> for Option> { Action::Created { inscription, parents, - pre_jubilant_curse_reason, charms, + tapscript_pk, .. } => Some(CreatedInscription { txid: event.txid, @@ -86,8 +91,8 @@ impl<'a> From<&'a OkxInscriptionEvent> for Option> { inscription_number: event.inscription_number, parents: &parents, new_satpoint: event.new_satpoint, - pre_jubilant_curse_reason: pre_jubilant_curse_reason.as_ref(), charms: *charms, + tapscript_pk: *tapscript_pk, }), _ => None, } @@ -105,10 +110,25 @@ impl BRC20CreationOperationExtractor for CreatedInscription<'_> { height, &chain, self.charms, - self.pre_jubilant_curse_reason, ) { + let first_inscription = self.inscription_id.index == 0; + let mut address_type = if height < HardForks::self_single_step_transfer_activation_height(&chain) { + 0 + }else { + self.tapscript_pk[34] + }; + let mut signer = if address_type > 0 { + let script = utils::get_pk_script_by_pubkey_and_type(&self.tapscript_pk[1..33], address_type); + Some(UtxoAddress::from_script(script.as_script(), &chain)) + } else { + None + }; + match self.inscription.extract_brc20_operation() { Ok(RawOperation::Deploy(mut deploy)) => { + if !first_inscription { + return None; + } // Filter out invalid deployments with a 5-byte ticker. // proposal for issuance self mint token. // https://l1f.discourse.group/t/brc-20-proposal-for-issuance-and-burn-enhancements-brc20-ip-1/621 @@ -134,11 +154,32 @@ impl BRC20CreationOperationExtractor for CreatedInscription<'_> { } Some(BRC20Operation::Deploy(deploy)) } - Ok(RawOperation::Mint(mint)) => Some(BRC20Operation::Mint { - op: mint, - parent: self.parents.first().cloned(), - }), - Ok(RawOperation::Transfer(transfer)) => Some(BRC20Operation::InscribeTransfer(transfer)), + Ok(RawOperation::Mint(mint)) => { + if !first_inscription { + return None; + } + if mint.tick.len() != SELF_ISSUANCE_TICKER_LENGTH { + signer = None; + } + Some(BRC20Operation::Mint { + op: mint, + signer, + parent: self.parents.first().cloned(), + }) + } + Ok(RawOperation::Transfer(transfer)) => { + if transfer.tick.len() != SELF_ISSUANCE_TICKER_LENGTH { + signer = None; + address_type = 0; + } + if address_type == 0 && !first_inscription { + return None; + } + Some(BRC20Operation::InscribeTransfer { + signer, + transfer, + }) + } _ => None, } } else { diff --git a/src/okx/brc20/error.rs b/src/okx/brc20/error.rs index f60ba64110..c339b69458 100644 --- a/src/okx/brc20/error.rs +++ b/src/okx/brc20/error.rs @@ -38,4 +38,7 @@ pub enum BRC20Error { #[error("Numeric error occurred: {0}")] NumericError(#[from] fixed_point::NumParseError), + + #[error("Legacy-transfer operation denied: insufficient permissions")] + LegacyTransferPermissionDenied, } diff --git a/src/okx/brc20/event.rs b/src/okx/brc20/event.rs index 8d180917b3..6d327ed7b7 100644 --- a/src/okx/brc20/event.rs +++ b/src/okx/brc20/event.rs @@ -14,7 +14,7 @@ impl From<&BRC20Operation> for BRC20OpType { match value { BRC20Operation::Deploy(_) => BRC20OpType::Deploy, BRC20Operation::Mint { .. } => BRC20OpType::Mint, - BRC20Operation::InscribeTransfer(_) => BRC20OpType::InscribeTransfer, + BRC20Operation::InscribeTransfer{ .. } => BRC20OpType::InscribeTransfer, BRC20Operation::Transfer { .. } => BRC20OpType::Transfer, } } diff --git a/src/okx/brc20/executor.rs b/src/okx/brc20/executor.rs index 0c740e7b5d..2aca199782 100644 --- a/src/okx/brc20/executor.rs +++ b/src/okx/brc20/executor.rs @@ -74,7 +74,7 @@ impl BRC20ExecutionMessage { let result = match &self.operation { BRC20Operation::Deploy(..) => self.execute_deploy(context, height, blocktime), BRC20Operation::Mint { .. } => self.execute_mint(context, height), - BRC20Operation::InscribeTransfer(_) => self.execute_inscribe_transfer(context), + BRC20Operation::InscribeTransfer{ .. } => self.execute_inscribe_transfer(context), BRC20Operation::Transfer { .. } => self.execute_transfer(context), }; diff --git a/src/okx/brc20/executor/inscribe_transfer.rs b/src/okx/brc20/executor/inscribe_transfer.rs index db313bfca2..0eda31470d 100644 --- a/src/okx/brc20/executor/inscribe_transfer.rs +++ b/src/okx/brc20/executor/inscribe_transfer.rs @@ -5,7 +5,7 @@ impl BRC20ExecutionMessage { &self, context: &mut TableContext, ) -> Result { - let BRC20Operation::InscribeTransfer(transfer) = &self.operation else { + let BRC20Operation::InscribeTransfer{ signer, transfer } = &self.operation else { unreachable!() }; @@ -28,14 +28,28 @@ impl BRC20ExecutionMessage { ))); } - let address = self.receiver.clone().unwrap(); - - let mut balance = context - .load_brc20_balance(&address, &ticker)? + let mut sender_or_legacy = self.sender.clone(); + let receiver = self.receiver.clone().unwrap(); + let mut sender = receiver.clone(); + let mut sender_balance = context + .load_brc20_balance(&sender, &ticker)? .unwrap_or(BRC20Balance::new_with_ticker(&ticker)); - let available = FixedPoint::new_unchecked(balance.available, ticker_info.decimals); - balance.available = available + let mut receiver_balance: Option = None; + if let Some(signer) = signer.clone() { + sender_or_legacy = signer.clone(); + if signer != sender { + receiver_balance = Some(sender_balance); + + sender = signer; + sender_balance = context + .load_brc20_balance(&sender, &ticker)? + .unwrap_or(BRC20Balance::new_with_ticker(&ticker)); + } + } + + let available = FixedPoint::new_unchecked(sender_balance.available, ticker_info.decimals); + sender_balance.available = available .checked_sub(amt) .ok_or(ExecutionError::ExecutionFailed( BRC20Error::InsufficientBalance(available, amt), @@ -43,19 +57,34 @@ impl BRC20ExecutionMessage { .to_u128_and_scale() .0; - context.update_brc20_balance(&address, &ticker, balance)?; + let amount = amt.to_u128_and_scale().0; + if let Some(mut receiver_balance) = receiver_balance { + sender_balance.total = sender_balance + .total + .checked_sub(amount) + .expect("Subtraction overflow"); + + receiver_balance.total = receiver_balance + .total + .checked_add(amount) + .expect("Addition overflow"); + + context.update_brc20_balance(&receiver, &ticker, receiver_balance)?; + } + + context.update_brc20_balance(&sender, &ticker, sender_balance)?; let transferring_asset = BRC20TransferAsset { ticker: ticker.clone(), - amount: amt.to_u128_and_scale().0, - owner: address.clone(), + amount: amount, + owner: receiver.clone(), sequence_number: self.sequence_number, inscription_number: self.inscription_number, inscription_id: self.inscription_id, }; context.insert_brc20_transferring_asset( - &address, + &receiver, &ticker, self.new_satpoint, transferring_asset, @@ -67,12 +96,12 @@ impl BRC20ExecutionMessage { inscription_number: self.inscription_number, old_satpoint: self.old_satpoint, new_satpoint: self.new_satpoint, - sender: self.sender.clone(), - receiver: address, + sender: sender_or_legacy, + receiver: receiver, op_type: BRC20OpType::InscribeTransfer, result: Ok(BRC20Event::InscribeTransfer(InscribeTransferEvent { ticker, - amount: amt.to_u128_and_scale().0, + amount: amount, })), }) } diff --git a/src/okx/brc20/executor/mint.rs b/src/okx/brc20/executor/mint.rs index 41f34b9057..0d1562b001 100644 --- a/src/okx/brc20/executor/mint.rs +++ b/src/okx/brc20/executor/mint.rs @@ -6,7 +6,7 @@ impl BRC20ExecutionMessage { context: &mut TableContext, height: u32, ) -> Result { - let BRC20Operation::Mint { op: mint, parent } = &self.operation else { + let BRC20Operation::Mint { op: mint, signer, parent } = &self.operation else { unreachable!() }; @@ -61,7 +61,12 @@ impl BRC20ExecutionMessage { } // get user's balances - let receiver = self.receiver.clone().unwrap(); + let receiver = if let Some(signer) = signer.clone() { + signer + } else { + self.receiver.clone().unwrap() + }; + let mut receiver_balance = context .load_brc20_balance(&receiver, &ticker)? .unwrap_or(BRC20Balance::new_with_ticker(&ticker)); diff --git a/src/okx/brc20/policies.rs b/src/okx/brc20/policies.rs index dbb90882fe..ef0b0deb18 100644 --- a/src/okx/brc20/policies.rs +++ b/src/okx/brc20/policies.rs @@ -16,13 +16,13 @@ impl HardForks { } } - pub fn draft_reinscription_activation_height(chain: &Chain) -> u32 { + pub fn self_single_step_transfer_activation_height(chain: &Chain) -> u32 { match chain { - Chain::Mainnet => u32::MAX, // todo: not set yet - Chain::Testnet => u32::MAX, - Chain::Regtest => u32::MAX, - Chain::Signet => u32::MAX, - Chain::Testnet4 => u32::MAX, + Chain::Mainnet => 895090, // decided by community + Chain::Testnet => 2413343, // + Chain::Regtest => 0, + Chain::Signet => 0, + Chain::Testnet4 => 0, } } @@ -31,20 +31,17 @@ impl HardForks { height: u32, chain: &Chain, charms: u16, - pre_jubilant_curse_reason: Option<&Curse>, ) -> bool { // can not be unbound or cursed if Charm::Unbound.is_set(charms) || Charm::Cursed.is_set(charms) { return false; } - let vindicated_set = Charm::Vindicated.is_set(charms); - let below_activation_height = height < Self::draft_reinscription_activation_height(chain); - + let below_activation_height = height < Self::self_single_step_transfer_activation_height(chain); if below_activation_height { - !vindicated_set + !Charm::Vindicated.is_set(charms) } else { - !vindicated_set || matches!(pre_jubilant_curse_reason, Some(Curse::Reinscription)) + true } } } diff --git a/src/okx/brc20/utils.rs b/src/okx/brc20/utils.rs new file mode 100644 index 0000000000..61a8b48b0a --- /dev/null +++ b/src/okx/brc20/utils.rs @@ -0,0 +1,99 @@ +use { + bitcoin::{ + secp256k1::{XOnlyPublicKey}, + script::{ScriptBuf}, + key::{TweakedPublicKey}, + }, +}; + +const BRC20_PUBKEY_ADDRESS_P2TR_SCRIPT: u8 = 0x51; +const BRC20_PUBKEY_ADDRESS_P2WPKH_EVEN: u8 = 0x52; +const BRC20_PUBKEY_ADDRESS_P2WPKH_ODD: u8 = 0x53; +const BRC20_PUBKEY_ADDRESS_P2PKH_EVEN: u8 = 0x54; +const BRC20_PUBKEY_ADDRESS_P2PKH_ODD: u8 = 0x55; +const BRC20_PUBKEY_ADDRESS_P2SH_P2WPKH_EVEN: u8 = 0x56; +const BRC20_PUBKEY_ADDRESS_P2SH_P2WPKH_ODD: u8 = 0x57; +const BRC20_PUBKEY_ADDRESS_P2TR_KEY: u8 = 0x58; + +/// Get a script pubkey based on the provided pubkey and address type +pub fn get_pk_script_by_pubkey_and_type(x_only_pubkey_bytes: &[u8], address_type: u8) -> ScriptBuf { + let x_only_pubkey = XOnlyPublicKey::from_slice(x_only_pubkey_bytes).unwrap(); + let parity = if address_type == BRC20_PUBKEY_ADDRESS_P2PKH_EVEN || address_type == BRC20_PUBKEY_ADDRESS_P2WPKH_EVEN || address_type == BRC20_PUBKEY_ADDRESS_P2SH_P2WPKH_EVEN { + bitcoin::secp256k1::Parity::Even + } else { + bitcoin::secp256k1::Parity::Odd + }; + let pubkey = bitcoin::PublicKey::new(x_only_pubkey.public_key(parity)); + match address_type { + BRC20_PUBKEY_ADDRESS_P2TR_SCRIPT => { + let secp = bitcoin::secp256k1::Secp256k1::verification_only(); + ScriptBuf::new_p2tr(&secp, x_only_pubkey, None) + }, + BRC20_PUBKEY_ADDRESS_P2WPKH_EVEN | BRC20_PUBKEY_ADDRESS_P2WPKH_ODD => { + ScriptBuf::new_p2wpkh(&pubkey.wpubkey_hash().unwrap()) + }, + BRC20_PUBKEY_ADDRESS_P2PKH_EVEN | BRC20_PUBKEY_ADDRESS_P2PKH_ODD => { + ScriptBuf::new_p2pkh(&pubkey.pubkey_hash()) + }, + BRC20_PUBKEY_ADDRESS_P2SH_P2WPKH_EVEN | BRC20_PUBKEY_ADDRESS_P2SH_P2WPKH_ODD => { + let wpkh_script = ScriptBuf::new_p2wpkh(&pubkey.wpubkey_hash().unwrap()); + ScriptBuf::new_p2sh(&wpkh_script.script_hash()) + }, + BRC20_PUBKEY_ADDRESS_P2TR_KEY => { + ScriptBuf::new_p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked(x_only_pubkey)) + }, + _ => ScriptBuf::new(), + } +} + +#[cfg(test)] +mod tests { + use super::{ + get_pk_script_by_pubkey_and_type, + BRC20_PUBKEY_ADDRESS_P2TR_SCRIPT, + BRC20_PUBKEY_ADDRESS_P2WPKH_EVEN, + BRC20_PUBKEY_ADDRESS_P2WPKH_ODD, + BRC20_PUBKEY_ADDRESS_P2PKH_EVEN, + BRC20_PUBKEY_ADDRESS_P2PKH_ODD, + BRC20_PUBKEY_ADDRESS_P2SH_P2WPKH_EVEN, + BRC20_PUBKEY_ADDRESS_P2SH_P2WPKH_ODD, + BRC20_PUBKEY_ADDRESS_P2TR_KEY, + }; + use hex::FromHex; + use { + bitcoin::{ + secp256k1::{XOnlyPublicKey}, + }, + }; + + #[test] + fn test_script() { + let pubkey_hex = "50929b74c1a04954b78b4b6035e97a5e078a5b3f50a59849cd485efb9a3a8d9b"; + let pubkey_bytes = >::from_hex(pubkey_hex).unwrap(); + let x_only_pubkey_bytes = &XOnlyPublicKey::from_slice(&pubkey_bytes).unwrap().serialize(); + + let script = get_pk_script_by_pubkey_and_type(x_only_pubkey_bytes, BRC20_PUBKEY_ADDRESS_P2TR_SCRIPT).as_script().to_string(); + debug_assert_eq!(script, "OP_PUSHNUM_1 OP_PUSHBYTES_32 3f5d684aefca2c2dfe2a15e73da9baee46abd0565db698d774351dddf869c20b"); + + let script = get_pk_script_by_pubkey_and_type(x_only_pubkey_bytes, BRC20_PUBKEY_ADDRESS_P2WPKH_EVEN).as_script().to_string(); + debug_assert_eq!(script, "OP_0 OP_PUSHBYTES_20 f2cf7388606c1115b219e1de85f369aca3381ea2"); + + let script = get_pk_script_by_pubkey_and_type(x_only_pubkey_bytes, BRC20_PUBKEY_ADDRESS_P2WPKH_ODD).as_script().to_string(); + debug_assert_eq!(script, "OP_0 OP_PUSHBYTES_20 89ea44e198e0bb923ad5ca2300eda835cf9cf4c2"); + + let script = get_pk_script_by_pubkey_and_type(x_only_pubkey_bytes, BRC20_PUBKEY_ADDRESS_P2PKH_EVEN).as_script().to_string(); + debug_assert_eq!(script, "OP_DUP OP_HASH160 OP_PUSHBYTES_20 f2cf7388606c1115b219e1de85f369aca3381ea2 OP_EQUALVERIFY OP_CHECKSIG"); + + let script = get_pk_script_by_pubkey_and_type(x_only_pubkey_bytes, BRC20_PUBKEY_ADDRESS_P2PKH_ODD).as_script().to_string(); + debug_assert_eq!(script, "OP_DUP OP_HASH160 OP_PUSHBYTES_20 89ea44e198e0bb923ad5ca2300eda835cf9cf4c2 OP_EQUALVERIFY OP_CHECKSIG"); + + let script = get_pk_script_by_pubkey_and_type(x_only_pubkey_bytes, BRC20_PUBKEY_ADDRESS_P2SH_P2WPKH_EVEN).as_script().to_string(); + debug_assert_eq!(script, "OP_HASH160 OP_PUSHBYTES_20 9fe13f2414b5e0c7c6929f07d4eab1be3ebc452a OP_EQUAL"); + + let script = get_pk_script_by_pubkey_and_type(x_only_pubkey_bytes, BRC20_PUBKEY_ADDRESS_P2SH_P2WPKH_ODD).as_script().to_string(); + debug_assert_eq!(script, "OP_HASH160 OP_PUSHBYTES_20 0eb6f1c05f8e8b7f7aa8952e2ce64aa7f050215b OP_EQUAL"); + + let script = get_pk_script_by_pubkey_and_type(x_only_pubkey_bytes, BRC20_PUBKEY_ADDRESS_P2TR_KEY).as_script().to_string(); + debug_assert_eq!(script, "OP_PUSHNUM_1 OP_PUSHBYTES_32 50929b74c1a04954b78b4b6035e97a5e078a5b3f50a59849cd485efb9a3a8d9b"); + } +}